Merge branch 'kb_development' into kb_migration

This commit is contained in:
KMY 2023-09-13 22:07:59 +09:00
commit 74d4062f9d
60 changed files with 647 additions and 514 deletions

View file

@ -4,7 +4,7 @@ NODE_ENV=tests
LOCAL_DOMAIN=cb6e6126.ngrok.io
LOCAL_HTTPS=true
# Elasticsearch
ES_ENABLED=true
ES_ENABLED=false
ES_HOST=localhost
ES_PORT=9200
ES_PREFIX=test

View file

@ -1,96 +0,0 @@
on:
workflow_call:
inputs:
platforms:
required: true
type: string
use_native_arm64_builder:
type: boolean
push_to_images:
type: string
version_prerelease:
type: string
version_metadata:
type: string
flavor:
type: string
tags:
type: string
labels:
type: string
jobs:
build-image:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/setup-qemu-action@v2
if: contains(inputs.platforms, 'linux/arm64') && !inputs.use_native_arm64_builder
- uses: docker/setup-buildx-action@v2
id: buildx
if: ${{ !(inputs.use_native_arm64_builder && contains(inputs.platforms, 'linux/arm64')) }}
- name: Start a local Docker Builder
if: inputs.use_native_arm64_builder && contains(inputs.platforms, 'linux/arm64')
run: |
docker run --rm -d --name buildkitd -p 1234:1234 --privileged moby/buildkit:latest --addr tcp://0.0.0.0:1234
- uses: docker/setup-buildx-action@v2
id: buildx-native
if: inputs.use_native_arm64_builder && contains(inputs.platforms, 'linux/arm64')
with:
driver: remote
endpoint: tcp://localhost:1234
platforms: linux/amd64
append: |
- endpoint: tcp://${{ vars.DOCKER_BUILDER_HETZNER_ARM64_01_HOST }}:13865
platforms: linux/arm64
name: mastodon-docker-builder-arm64-01
driver-opts:
- servername=mastodon-docker-builder-arm64-01
env:
BUILDER_NODE_1_AUTH_TLS_CACERT: ${{ secrets.DOCKER_BUILDER_HETZNER_ARM64_01_CACERT }}
BUILDER_NODE_1_AUTH_TLS_CERT: ${{ secrets.DOCKER_BUILDER_HETZNER_ARM64_01_CERT }}
BUILDER_NODE_1_AUTH_TLS_KEY: ${{ secrets.DOCKER_BUILDER_HETZNER_ARM64_01_KEY }}
- name: Log in to Docker Hub
if: contains(inputs.push_to_images, 'kmycode/mastodon')
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Log in to the Github Container registry
if: contains(inputs.push_to_images, 'ghcr.io')
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/metadata-action@v4
id: meta
if: ${{ inputs.push_to_images != '' }}
with:
images: ${{ inputs.push_to_images }}
flavor: ${{ inputs.flavor }}
tags: ${{ inputs.tags }}
labels: ${{ inputs.labels }}
- uses: docker/build-push-action@v4
with:
context: .
build-args: |
MASTODON_VERSION_PRERELEASE=${{ inputs.version_prerelease }}
MASTODON_VERSION_METADATA=${{ inputs.version_metadata }}
platforms: ${{ inputs.platforms }}
provenance: false
builder: ${{ steps.buildx.outputs.name || steps.buildx-native.outputs.name }}
push: ${{ inputs.push_to_images != '' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

View file

@ -1,41 +0,0 @@
name: Build nightly container image
on:
workflow_dispatch:
schedule:
- cron: '0 2 * * *' # run at 2 AM UTC
permissions:
contents: read
packages: write
jobs:
compute-suffix:
runs-on: ubuntu-latest
steps:
- id: version_vars
env:
TZ: Etc/UTC
run: |
echo mastodon_version_prerelease=nightly.$(date +'%Y-%m-%d')>> $GITHUB_OUTPUT
outputs:
prerelease: ${{ steps.version_vars.outputs.mastodon_version_prerelease }}
build-image:
needs: compute-suffix
uses: ./.github/workflows/build-container-image.yml
with:
platforms: linux/amd64,linux/arm64
use_native_arm64_builder: true
push_to_images: |
kmycode/mastodon
ghcr.io/kmycode/mastodon
version_prerelease: ${{ needs.compute-suffix.outputs.prerelease }}
labels: |
org.opencontainers.image.description=Nightly build image used for testing purposes
flavor: |
latest=auto
tags: |
type=raw,value=edge
type=raw,value=nightly
type=schedule,pattern=${{ needs.compute-suffix.outputs.prerelease }}
secrets: inherit

View file

@ -1,41 +0,0 @@
name: Build container image for PR
on:
pull_request:
types: [labeled, synchronize, reopened, ready_for_review, opened]
permissions:
contents: read
packages: write
jobs:
compute-suffix:
runs-on: ubuntu-latest
# This is only allowed to run if:
# - the PR branch is in the `mastodon/mastodon` repository
# - the PR is not a draft
# - the PR has the "build-image" label
if: ${{ github.event.pull_request.head.repo.full_name == github.repository && !github.event.pull_request.draft && contains(github.event.pull_request.labels.*.name, 'build-image') }}
steps:
# Repository needs to be cloned so `git rev-parse` below works
- name: Clone repository
uses: actions/checkout@v4
- id: version_vars
run: |
echo mastodon_version_metadata=pr-${{ github.event.pull_request.number }}-$(git rev-parse --short HEAD) >> $GITHUB_OUTPUT
outputs:
metadata: ${{ steps.version_vars.outputs.mastodon_version_metadata }}
build-image:
needs: compute-suffix
uses: ./.github/workflows/build-container-image.yml
with:
platforms: linux/amd64,linux/arm64
use_native_arm64_builder: true
push_to_images: |
ghcr.io/mastodon/mastodon
version_metadata: ${{ needs.compute-suffix.outputs.metadata }}
flavor: |
latest=auto
tags: |
type=ref,event=pr
secrets: inherit

View file

@ -1,27 +0,0 @@
name: Build container release images
on:
push:
tags:
- '*'
permissions:
contents: read
packages: write
jobs:
build-image:
uses: ./.github/workflows/build-container-image.yml
with:
platforms: linux/amd64,linux/arm64
use_native_arm64_builder: true
push_to_images: |
tootsuite/mastodon
ghcr.io/mastodon/mastodon
# Only tag with latest when ran against the latest stable branch
# This needs to be updated after each minor version release
flavor: |
latest=${{ startsWith(github.ref, 'refs/tags/v4.1.') }}
tags: |
type=pep440,pattern={{raw}}
type=pep440,pattern=v{{major}}.{{minor}}
secrets: inherit

View file

@ -1,76 +0,0 @@
name: Crowdin / Download translations
on:
schedule:
- cron: '17 4 * * *' # Every day
workflow_dispatch:
permissions:
contents: write
pull-requests: write
jobs:
download-translations:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Increase Git http.postBuffer
# This is needed due to a bug in Ubuntu's cURL version?
# See https://github.com/orgs/community/discussions/55820
run: |
git config --global http.version HTTP/1.1
git config --global http.postBuffer 157286400
# Download the translation files from Crowdin
- name: crowdin action
uses: crowdin/github-action@v1
with:
upload_sources: false
upload_translations: false
download_translations: true
crowdin_branch_name: main
push_translations: false
create_pull_request: false
env:
CROWDIN_PROJECT_ID: ${{ vars.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
# As the files are extracted from a Docker container, they belong to root:root
# We need to fix this before the next steps
- name: Fix file permissions
run: sudo chown -R runner:docker .
# This is needed to run the normalize step
- name: Install native Ruby dependencies
run: sudo apt-get install -y libicu-dev libidn11-dev
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: .ruby-version
bundler-cache: true
- name: Run i18n normalize task
run: bundle exec i18n-tasks normalize
# Create or update the pull request
- name: Create Pull Request
uses: peter-evans/create-pull-request@v5.0.2
with:
commit-message: 'New Crowdin translations'
title: 'New Crowdin Translations (automated)'
author: 'GitHub Actions <noreply@github.com>'
body: |
New Crowdin translations, automated with Github Actions
See `.github/workflows/crowdin-download.yml`
This PR will be updated every day with new translations.
Due to a limitation in Github Actions, checks are not running on this PR without manual action.
If you want to run the checks, then close and re-open it.
branch: i18n/crowdin/translations
base: main
labels: i18n

View file

@ -1,35 +0,0 @@
name: Crowdin / Upload translations
on:
push:
branches:
- main
paths:
- crowdin.yml
- app/javascript/mastodon/locales/en.json
- config/locales/en.yml
- config/locales/simple_form.en.yml
- config/locales/activerecord.en.yml
- config/locales/devise.en.yml
- config/locales/doorkeeper.en.yml
- .github/workflows/crowdin-upload.yml
jobs:
upload-translations:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: crowdin action
uses: crowdin/github-action@v1
with:
upload_sources: true
upload_translations: false
download_translations: false
crowdin_branch_name: main
env:
CROWDIN_PROJECT_ID: ${{ vars.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}

View file

@ -1,21 +0,0 @@
name: Test container image build
on:
pull_request:
paths:
- .github/workflows/build-nightly.yml
- .github/workflows/build-push-pr.yml
- .github/workflows/build-releases.yml
- .github/workflows/test-image-build.yml
- Dockerfile
permissions:
contents: read
jobs:
build-image:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
uses: ./.github/workflows/build-container-image.yml
with:
platforms: linux/amd64 # Testing only on native platform so it is performant

View file

@ -14,6 +14,8 @@ kmyblueは頻繁にバージョンアップを行います。
- バグが含まれていることがあります
- 特に最新コミットでは、デバッグ用コードや、`kmy.blue`本番サーバーで動作確認を行うためのコードが含まれている場合があります。ブランチの最新コミットではなく最新タグを取り込むことを強くおすすめします
Mastodonの最新バージョンでは、`dist`フォルダに`mastodon-streaming@.service`が追加されています。これは現在の一般的な手順書には存在しません。各サービスファイルをコピーするとき、`mastodon-streaming@.service`をコピーし忘れないようにしてください。
### ElasticSearchを使用する場合
kmyblueでは、sudachiの使用を前提としています。
@ -56,5 +58,33 @@ RAILS_ENV=production bin/rails assets:precompile
# ElasticSearchを使用する場合
RAILS_ENV=production bin/tootctl search deploy
sudo systemctl start mastodon-web mastodon-streaming@4000 mastodon-sidekiq
RAILS_ENV=production bin/tootctl cache clear
sudo systemctl start mastodon-web mastodon-streaming mastodon-sidekiq
```
## kmyblueのバージョンをアップデートする
リリースノートを参照して、自分に必要な作業を特定してください。面倒な場合は毎回全部実行してしまっても問題ありません。(プリコンパイルが失敗する可能性があるのでご注意ください)
```
# Rubyパッケージアップデート
bundle intall
# JSパッケージアップデート
yarn install
# DBマイグレーション
RAILS_ENV=production bin/rails db:migrate
# プリコンパイル
# うまくいかない場合エラーは出ないのにWeb表示が崩れるはclobberしてからprecompile
# それでもうまくいかない場合はsudo systemctl stop mastodon-webしてから試す
# それでもうまくいかない場合はサーバーOSを再起動してから試す
RAILS_ENV=production bin/rails assets:clobber # プリコンパイルがうまくいかない場合
RAILS_ENV=production bin/rails assets:precompile
# サーバー再起動
sudo systemctl restart mastodon-web
sudo systemctl restart mastodon-streaming
sudo systemctl restart mastodon-sidekiq
```

View file

@ -19,6 +19,20 @@ INSTALL.mdを参照してください。
CONTRIBUTING.mdを参照してください。
## テスト
```
# デバッグ実行(以下のいずれか)
foreman start
DB_USER=postgres DB_PASS=password foreman start
# 一部を除く全てのテストを行う
RAILS_ENV=test bundle exec rspec spec
# ElasticSearch連携テストを行う
RAILS_ENV=test ES_ENABLED=true RUN_SEARCH_SPECS=true bundle exec rspec spec/search
```
## kmyblueの強み
### 本家Mastodonへの積極的追従

View file

@ -15,6 +15,10 @@ class Api::V1::Statuses::EmojiReactionedByAccountsController < Api::BaseControll
private
def load_accounts
return [] unless Setting.enable_emoji_reaction
return [] if current_account.nil? && @status.account.emoji_reaction_policy != :allow
return [] if current_account.present? && !@status.account.show_emoji_reaction?(current_account)
scope = default_accounts
scope = scope.where.not(account_id: current_account.excluded_from_timeline_account_ids) unless current_account.nil?
scope.merge(paginated_emoji_reactions).to_a

View file

@ -42,6 +42,7 @@ class Api::V1::Statuses::EmojiReactionsController < Api::BaseController
def create_private(emoji)
count = EmojiReaction.where(account: current_account, status: @status).count
raise Mastodon::ValidationError, I18n.t('reactions.errors.limit_reached') if count >= EmojiReaction::EMOJI_REACTION_PER_ACCOUNT_LIMIT
raise Mastodon::ValidationError, I18n.t('reactions.errors.disabled') unless Setting.enable_emoji_reaction
EmojiReactService.new.call(current_account, @status, emoji)
render json: @status, serializer: REST::StatusSerializer

View file

@ -0,0 +1,9 @@
# frozen_string_literal: true
class Settings::Preferences::ReachingController < Settings::Preferences::BaseController
private
def after_update_redirect_path
settings_preferences_reaching_path
end
end

View file

@ -18,7 +18,7 @@ import Card from '../features/status/components/card';
// to use the progress bar to show download progress
import Bundle from '../features/ui/components/bundle';
import { MediaGallery, Video, Audio } from '../features/ui/util/async-components';
import { displayMedia } from '../initial_state';
import { displayMedia, enableEmojiReaction, showEmojiReactionOnTimeline } from '../initial_state';
import { Avatar } from './avatar';
import { AvatarOverlay } from './avatar_overlay';
@ -577,8 +577,8 @@ class Status extends ImmutablePureComponent {
let emojiReactionsBar = null;
if (!this.props.withoutEmojiReactions && status.get('emoji_reactions')) {
const emojiReactions = status.get('emoji_reactions');
if (emojiReactions.size > 0) {
emojiReactionsBar = <StatusEmojiReactionsBar emojiReactions={emojiReactions} status={status} onEmojiReact={this.props.onEmojiReact} onUnEmojiReact={this.props.onUnEmojiReact} />;
if (emojiReactions.size > 0 && enableEmojiReaction) {
emojiReactionsBar = <StatusEmojiReactionsBar emojiReactions={emojiReactions} myReactionOnly={!showEmojiReactionOnTimeline} status={status} onEmojiReact={this.props.onEmojiReact} onUnEmojiReact={this.props.onUnEmojiReact} />;
}
}
@ -622,7 +622,7 @@ class Status extends ImmutablePureComponent {
{(!isCardMediaWithSensitive || !status.get('hidden')) && media}
{expanded && hashtagBar}
{(!status.get('spoiler_text') || expanded) && hashtagBar}
{emojiReactionsBar}

View file

@ -10,9 +10,10 @@ import { connect } from 'react-redux';
import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'mastodon/permissions';
import DropdownMenuContainer from '../containers/dropdown_menu_container';
import EmojiPickerDropdown from '../features/compose/containers/emoji_picker_dropdown_container';
import { bookmarkCategoryNeeded, me } from '../initial_state';
import { enableEmojiReaction , bookmarkCategoryNeeded, me } from '../initial_state';
import { IconButton } from './icon_button';
@ -409,13 +410,16 @@ class StatusActionBar extends ImmutablePureComponent {
<IconButton className='status__action-bar__button' title={intl.formatMessage(messages.hide)} icon='eye' onClick={this.handleHideClick} />
);
const following = !account.getIn(['other_settings', 'emoji_reaction_must_follower']) || (relationship && relationship.get('following'));
const followed = !account.getIn(['other_settings', 'emoji_reaction_must_following']) || (relationship && relationship.get('followed_by'));
const denyFromAll = !account.getIn(['other_settings', 'emoji_reaction_deny_from_all']);
const emojiReactionPolicy = account.getIn(['other_settings', 'emoji_reaction_policy']) || 'allow';
const following = emojiReactionPolicy !== 'following_only' || (relationship && relationship.get('following'));
const followed = emojiReactionPolicy !== 'followers_only' || (relationship && relationship.get('followed_by'));
const mutual = emojiReactionPolicy !== 'mutuals_only' || (relationship && relationship.get('following') && relationship.get('followed_by'));
const outside = emojiReactionPolicy !== 'outside_only' || (relationship && (relationship.get('following') || relationship.get('followed_by')));
const denyFromAll = emojiReactionPolicy !== 'block' && emojiReactionPolicy !== 'block';
const emojiPickerButton = (
<IconButton className='status__action-bar__button' title={intl.formatMessage(messages.emojiReaction)} icon='smile-o' onClick={this.handleEmojiPickInnerButton} />
);
const emojiPickerDropdown = (writtenByMe || ((denyFromAll) && (following) && (followed))) && (
const emojiPickerDropdown = enableEmojiReaction && denyFromAll && (writtenByMe || (following && followed && mutual && outside)) && (
<EmojiPickerDropdown onPickEmoji={this.handleEmojiPick} button={emojiPickerButton} />
);

View file

@ -60,6 +60,7 @@ class StatusEmojiReactionsBar extends PureComponent {
status: ImmutablePropTypes.map,
onEmojiReact: PropTypes.func,
onUnEmojiReact: PropTypes.func,
myReactionOnly: PropTypes.bool,
};
onEmojiReact = (name) => {
@ -73,13 +74,16 @@ class StatusEmojiReactionsBar extends PureComponent {
};
render () {
const { emojiReactions } = this.props;
const { emojiReactions, myReactionOnly } = this.props;
const emojiButtons = Array.from(emojiReactions).filter(emoji => emoji.get('count') !== 0).map((emoji, index) => (
const emojiButtons = Array.from(emojiReactions)
.filter(emoji => emoji.get('count') !== 0)
.filter(emoji => !myReactionOnly || emoji.get('me'))
.map((emoji, index) => (
<EmojiReactionButton
key={index}
name={emoji.get('name')}
count={emoji.get('count')}
count={myReactionOnly ? 1 : emoji.get('count')}
me={emoji.get('me')}
url={emoji.get('url')}
staticUrl={emoji.get('static_url')}

View file

@ -10,9 +10,10 @@ import { connect } from 'react-redux';
import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'mastodon/permissions';
import { IconButton } from '../../../components/icon_button';
import DropdownMenuContainer from '../../../containers/dropdown_menu_container';
import { bookmarkCategoryNeeded, me } from '../../../initial_state';
import { enableEmojiReaction , bookmarkCategoryNeeded, me } from '../../../initial_state';
import EmojiPickerDropdown from '../../compose/containers/emoji_picker_dropdown_container';
const messages = defineMessages({
@ -305,10 +306,6 @@ class ActionBar extends PureComponent {
}
}
const emojiPickerButton = (
<IconButton icon='smile-o' onClick={this.handleEmojiPickInnerButton} title={intl.formatMessage(messages.pickEmoji)} />
);
let replyIcon;
if (status.get('in_reply_to_id', null) === null) {
replyIcon = 'reply';
@ -329,13 +326,26 @@ class ActionBar extends PureComponent {
reblogTitle = intl.formatMessage(messages.cannot_reblog);
}
const emojiReactionPolicy = account.getIn(['other_settings', 'emoji_reaction_policy']) || 'allow';
const following = emojiReactionPolicy !== 'following_only' || (relationship && relationship.get('following'));
const followed = emojiReactionPolicy !== 'followers_only' || (relationship && relationship.get('followed_by'));
const mutual = emojiReactionPolicy !== 'mutuals_only' || (relationship && relationship.get('following') && relationship.get('followed_by'));
const outside = emojiReactionPolicy !== 'outside_only' || (relationship && (relationship.get('following') || relationship.get('followed_by')));
const denyFromAll = emojiReactionPolicy !== 'block' && emojiReactionPolicy !== 'block';
const emojiPickerButton = (
<IconButton icon='smile-o' onClick={this.handleEmojiPickInnerButton} title={intl.formatMessage(messages.pickEmoji)} />
);
const emojiPickerDropdown = enableEmojiReaction && denyFromAll && (writtenByMe || (following && followed && mutual && outside)) && (
<div className='detailed-status__button'><EmojiPickerDropdown onPickEmoji={this.handleEmojiPick} button={emojiPickerButton} /></div>
);
return (
<div className='detailed-status__action-bar'>
<div className='detailed-status__button'><IconButton title={intl.formatMessage(messages.reply)} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} /></div>
<div className='detailed-status__button'><IconButton className={classNames({ reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon='retweet' onClick={this.handleReblogClick} /></div>
<div className='detailed-status__button'><IconButton className='star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} /></div>
<div className='detailed-status__button'><IconButton className='bookmark-icon' disabled={!signedIn} active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} /></div>
<div className='detailed-status__button'><EmojiPickerDropdown onPickEmoji={this.handleEmojiPick} button={emojiPickerButton} /></div>
{emojiPickerDropdown}
<div className='detailed-status__action-bar-dropdown'>
<DropdownMenuContainer size={18} icon='ellipsis-h' status={status} items={menu} direction='left' title={intl.formatMessage(messages.more)} />

View file

@ -13,6 +13,7 @@ import EditedTimestamp from 'mastodon/components/edited_timestamp';
import { getHashtagBarForStatus } from 'mastodon/components/hashtag_bar';
import { Icon } from 'mastodon/components/icon';
import PictureInPicturePlaceholder from 'mastodon/components/picture_in_picture_placeholder';
import { enableEmojiReaction } from 'mastodon/initial_state';
import { Avatar } from '../../../components/avatar';
import { DisplayName } from '../../../components/display_name';
@ -240,7 +241,8 @@ class DetailedStatus extends ImmutablePureComponent {
let emojiReactionsBar = null;
if (status.get('emoji_reactions')) {
const emojiReactions = status.get('emoji_reactions');
if (emojiReactions.size > 0) {
const emojiReactionPolicy = status.getIn(['account', 'other_settings', 'emoji_reaction_policy']) || 'allow';
if (emojiReactions.size > 0 && enableEmojiReaction && emojiReactionPolicy !== 'block') {
emojiReactionsBar = <StatusEmojiReactionsBar emojiReactions={emojiReactions} status={status} onEmojiReact={this.props.onEmojiReact} onUnEmojiReact={this.props.onUnEmojiReact} />;
}
}
@ -398,7 +400,7 @@ class DetailedStatus extends ImmutablePureComponent {
{(!isCardMediaWithSensitive || !status.get('hidden')) && media}
{expanded && hashtagBar}
{(!status.get('spoiler_text') || expanded) && hashtagBar}
{emojiReactionsBar}

View file

@ -59,6 +59,7 @@
* @property {boolean} display_media_expand
* @property {string} domain
* @property {string} dtl_tag
* @property {boolean} enable_emoji_reaction
* @property {boolean} enable_login_privacy
* @property {boolean} enable_dtl_menu
* @property {boolean=} expand_spoilers
@ -75,6 +76,7 @@
* @property {string} repository
* @property {boolean} search_enabled
* @property {boolean} trends_enabled
* @property {boolean} show_emoji_reaction_on_timeline
* @property {boolean} single_user_mode
* @property {string} source_url
* @property {string} streaming_api_base_url
@ -126,6 +128,7 @@ export const displayMedia = getMeta('display_media');
export const displayMediaExpand = getMeta('display_media_expand');
export const domain = getMeta('domain');
export const dtlTag = getMeta('dtl_tag');
export const enableEmojiReaction = getMeta('enable_emoji_reaction');
export const enableLoginPrivacy = getMeta('enable_login_privacy');
export const enableDtlMenu = getMeta('enable_dtl_menu');
export const expandSpoilers = getMeta('expand_spoilers');
@ -142,6 +145,7 @@ export const registrationsOpen = getMeta('registrations_open');
export const repository = getMeta('repository');
export const searchEnabled = getMeta('search_enabled');
export const trendsEnabled = getMeta('trends_enabled');
export const showEmojiReactionOnTimeline = getMeta('show_emoji_reaction_on_timeline');
export const showTrends = getMeta('show_trends');
export const singleUserMode = getMeta('single_user_mode');
export const source_url = getMeta('source_url');

View file

@ -32,7 +32,11 @@ textarea {
color: $primary-text-color;
}
.compose-form .compose-form__warning {
.compose-form .compose-form__warning,
.reply-indicator__content,
.reply-indicator__display-name,
.reply-indicator__cancel,
.autosuggest-textarea__suggestions__item {
color: $ui-base-color;
}

View file

@ -7,7 +7,7 @@ class ActivityPub::Activity::Like < ActivityPub::Activity
def perform
@original_status = status_from_uri(object_uri)
return if @original_status.nil? || !@original_status.account.local? || delete_arrived_first?(@json['id']) || reject_favourite?
return if @original_status.nil? || delete_arrived_first?(@json['id']) || reject_favourite?
if shortcode.nil?
process_favourite
@ -32,6 +32,8 @@ class ActivityPub::Activity::Like < ActivityPub::Activity
end
def process_emoji_reaction
return if !@original_status.account.local? && !Setting.receive_other_servers_emoji_reaction
if emoji_tag.present?
return if emoji_tag['id'].blank? || emoji_tag['name'].blank? || emoji_tag['icon'].blank? || emoji_tag['icon']['url'].blank?
@ -106,10 +108,10 @@ class ActivityPub::Activity::Like < ActivityPub::Activity
end
def write_stream(emoji_reaction)
emoji_group = @original_status.emoji_reactions_grouped_by_name
emoji_group = @original_status.emoji_reactions_grouped_by_name(nil, force: true)
.find { |reaction_group| reaction_group['name'] == emoji_reaction.name && (!reaction_group.key?(:domain) || reaction_group['domain'] == emoji_reaction.custom_emoji&.domain) }
emoji_group['status_id'] = @original_status.id.to_s
DeliveryEmojiReactionWorker.perform_async(render_emoji_reaction(emoji_group), @original_status.id, emoji_reaction.account_id)
DeliveryEmojiReactionWorker.perform_async(render_emoji_reaction(emoji_group), @original_status.id, emoji_reaction.account_id) if @original_status.local? || Setting.streaming_other_servers_emoji_reaction
end
def render_emoji_reaction(emoji_group)

View file

@ -149,7 +149,7 @@ class ActivityPub::Activity::Undo < ActivityPub::Activity
emoji_group = { 'name' => emoji_reaction.name, 'count' => 0, 'account_ids' => [], 'status_id' => @original_status.id.to_s }
emoji_group['domain'] = emoji_reaction.custom_emoji.domain if emoji_reaction.custom_emoji
end
DeliveryEmojiReactionWorker.perform_async(render_emoji_reaction(emoji_group), @original_status.id, emoji_reaction.account_id)
DeliveryEmojiReactionWorker.perform_async(render_emoji_reaction(emoji_group), @original_status.id, emoji_reaction.account_id) if @original_status.local? || Setting.streaming_other_servers_emoji_reaction
end
def render_emoji_reaction(emoji_group)

View file

@ -61,19 +61,19 @@ class SearchQueryTransformer < Parslet::Transform
when 'library'
[StatusesIndex]
else
[PublicStatusesIndex, StatusesIndex]
@options[:current_account].user&.setting_use_public_index ? [PublicStatusesIndex, StatusesIndex] : [StatusesIndex]
end
end
def default_filter
definition_should = [
default_should1,
default_should2,
non_publicly_searchable,
public_index,
searchability_limited,
]
definition_should << searchability_public if %i(public).include?(@searchability)
definition_should << searchability_private if %i(public unlisted private).include?(@searchability)
definition_should << searchable_by_me if %i(public unlisted private direct).include?(@searchability)
definition_should << self_posts if %i(public unlisted private direct).exclude?(@searchability)
{
bool: {
@ -83,7 +83,7 @@ class SearchQueryTransformer < Parslet::Transform
}
end
def default_should1
def public_index
{
term: {
_index: PublicStatusesIndex.index_name,
@ -91,24 +91,7 @@ class SearchQueryTransformer < Parslet::Transform
}
end
def default_should2
{
bool: {
must: [
{
term: { _index: StatusesIndex.index_name },
},
{
term: {
searchable_by: @options[:current_account].id,
},
},
],
},
}
end
def non_publicly_searchable
def searchable_by_me
{
bool: {
must: [
@ -128,6 +111,21 @@ class SearchQueryTransformer < Parslet::Transform
}
end
def self_posts
{
bool: {
must: [
{
term: { _index: StatusesIndex.index_name },
},
{
term: { account_id: @options[:current_account].id },
},
],
},
}
end
def searchability_public
{
bool: {
@ -349,6 +347,6 @@ class SearchQueryTransformer < Parslet::Transform
end
rule(query: sequence(:clauses)) do
Query.new(clauses, current_account: current_account)
Query.new(clauses, current_account: current_account, searchability: searchability)
end
end

View file

@ -6,6 +6,8 @@ class StatusCacheHydrator
end
def hydrate(account_id)
account = Account.find(account_id)
# The cache of the serialized hash is generated by the fan-out-on-write service
payload = Rails.cache.fetch("fan-out/#{@status.id}") { InlineRenderer.render(@status, nil, :status) }
@ -33,6 +35,7 @@ class StatusCacheHydrator
payload[:reblog][:bookmarked] = Bookmark.where(account_id: account_id, status_id: @status.reblog_of_id).exists?
payload[:reblog][:pinned] = StatusPin.where(account_id: account_id, status_id: @status.reblog_of_id).exists? if @status.reblog.account_id == account_id
payload[:reblog][:filtered] = payload[:filtered]
payload[:reblog][:emoji_reactions] = @status.emoji_reactions_grouped_by_name(account)
if payload[:reblog][:poll]
if @status.reblog.account_id == account_id
@ -56,6 +59,7 @@ class StatusCacheHydrator
payload[:filtered] = CustomFilter
.apply_cached_filters(CustomFilter.cached_filters_for(account_id), @status, following?(account_id))
.map { |filter| serialized_filter(filter) }
payload[:emoji_reactions] = @status.emoji_reactions_grouped_by_name(account)
if payload[:poll]
payload[:poll][:voted] = @status.account_id == account_id

View file

@ -363,28 +363,37 @@ class Account < ApplicationRecord
false
end
def emoji_reactions_must_following?
return false unless Setting.enable_block_emoji_reaction_settings
return user&.settings&.[]('emoji_reactions.must_be_following') || false if user.present?
return settings['emoji_reactions_must_be_following'] || false if settings.present?
def emoji_reaction_policy
return settings['emoji_reaction_policy']&.to_sym || :allow if settings.present? && user.nil?
return :allow if user.nil?
return :block if local? && !Setting.enable_emoji_reaction
false
user.setting_emoji_reaction_policy&.to_sym
end
def emoji_reactions_must_follower?
return false unless Setting.enable_block_emoji_reaction_settings
return user&.settings&.[]('emoji_reactions.must_be_follower') || false if user.present?
return settings['emoji_reaction_must_be_follower'] || false if settings.present?
def show_emoji_reaction?(account)
return false unless Setting.enable_emoji_reaction
case emoji_reaction_policy
when :block
false
when :following_only
account.present? && (id == account.id || following?(account))
when :followers_only
account.present? && (id == account.id || followed_by?(account))
when :mutuals_only
account.present? && (id == account.id || mutual?(account))
when :outside_only
account.present? && (id == account.id || following?(account) || followed_by?(account))
else
true
end
end
def emoji_reactions_deny_from_all?
return false unless Setting.enable_block_emoji_reaction_settings
return user&.settings&.[]('emoji_reactions.deny_from_all') || false if user.present?
return settings['emoji_reaction_deny_from_all'] || false if settings.present?
def allow_emoji_reaction?(account)
return false if account.nil?
false
show_emoji_reaction?(account)
end
def public_settings
@ -398,17 +407,27 @@ class Account < ApplicationRecord
'translatable_private' => translatable_private?,
'link_preview' => link_preview?,
}
if Setting.enable_block_emoji_reaction_settings
if Setting.enable_emoji_reaction
config = config.merge({
'emoji_reaction_must_following' => emoji_reactions_must_following?,
'emoji_reaction_must_follower' => emoji_reactions_must_follower?,
'emoji_reaction_deny_from_all' => emoji_reactions_deny_from_all?,
'emoji_reaction_policy' => emoji_reaction_policy,
})
end
config = config.merge(settings) if settings.present?
config
end
def public_settings_for_local
config = public_settings
unless Setting.enable_emoji_reaction
config = config.merge({
'emoji_reaction_policy' => :block,
})
end
config
end
def previous_strikes_count
strikes.where(overruled_at: nil).count
end

View file

@ -211,6 +211,10 @@ module AccountInteractions
other_account.following?(self)
end
def mutual?(other_account)
following?(other_account) && followed_by?(other_account)
end
def blocking?(other_account)
block_relationships.where(target_account: other_account).exists?
end

View file

@ -43,6 +43,14 @@ module HasUserSettings
settings['web.hide_recent_emojis']
end
def setting_enable_emoji_reaction
settings['web.enable_emoji_reaction']
end
def setting_show_emoji_reaction_on_timeline
settings['web.show_emoji_reaction_on_timeline']
end
def setting_default_sensitive
settings['default_sensitive']
end
@ -75,6 +83,10 @@ module HasUserSettings
false
end
def setting_emoji_reaction_policy
settings['emoji_reaction_policy']
end
def setting_unfollow_modal
settings['web.unfollow_modal']
end
@ -204,7 +216,15 @@ module HasUserSettings
end
def setting_default_searchability
settings['default_searchability'] || 'private'
settings['default_searchability'] || 'direct'
end
def setting_default_searchability_of_search
settings['default_searchability_of_search']
end
def setting_use_public_index
settings['use_public_index']
end
def setting_disallow_unlisted_public_searchability

View file

@ -18,7 +18,7 @@ class EmojiReaction < ApplicationRecord
include Paginable
EMOJI_REACTION_LIMIT = 32_767
EMOJI_REACTION_PER_ACCOUNT_LIMIT = 3
EMOJI_REACTION_PER_ACCOUNT_LIMIT = ENV.fetch('EMOJI_REACTION_PER_ACCOUNT_LIMIT', 3)
update_index('statuses', :status)

View file

@ -37,12 +37,14 @@ class Form::AdminSettings
status_page_url
captcha_enabled
ng_words
enable_block_emoji_reaction_settings
hide_local_users_for_anonymous
post_hash_tags_max
sensitive_words
sensitive_words_for_full
authorized_fetch
receive_other_servers_emoji_reaction
streaming_other_servers_emoji_reaction
enable_emoji_reaction
).freeze
INTEGER_KEYS = %i(
@ -64,9 +66,11 @@ class Form::AdminSettings
noindex
require_invite_text
captcha_enabled
enable_block_emoji_reaction_settings
hide_local_users_for_anonymous
authorized_fetch
receive_other_servers_emoji_reaction
streaming_other_servers_emoji_reaction
enable_emoji_reaction
).freeze
UPLOAD_KEYS = %i(

View file

@ -349,14 +349,25 @@ class Status < ApplicationRecord
update_status_stat!(status_referred_by_count: [public_send(:status_referred_by_count) + diff, 0].max)
end
def emoji_reactions_grouped_by_name(account = nil)
def emoji_reactions_grouped_by_name(account = nil, **options)
return [] if account.present? && !self.account.show_emoji_reaction?(account)
return [] if account.nil? && !options[:force] && self.account.emoji_reaction_policy != :allow
(Oj.load(status_stat&.emoji_reactions || '', mode: :strict) || []).tap do |emoji_reactions|
if account.present?
remove_emoji_reactions = []
emoji_reactions.each do |emoji_reaction|
emoji_reaction['me'] = emoji_reaction['account_ids'].include?(account.id.to_s)
emoji_reaction['account_ids'] -= account.excluded_from_timeline_account_ids.map(&:to_s)
accounts = Account.where(id: emoji_reaction['account_ids'], silenced_at: nil, suspended_at: nil)
accounts = accounts.where('domain IS NULL OR domain NOT IN (?)', account.excluded_from_timeline_domains) if account.excluded_from_timeline_domains.size.positive?
emoji_reaction['account_ids'] = accounts.pluck(:id).map(&:to_s)
emoji_reaction['count'] = emoji_reaction['account_ids'].size
remove_emoji_reactions << emoji_reaction if emoji_reaction['count'] <= 0
end
emoji_reactions - remove_emoji_reactions
end
end
end

View file

@ -12,7 +12,7 @@ class Trends::Tags < Trends::Base
}
def register(status, at_time = Time.now.utc)
return unless !status.reblog? && status.public_visibility? && !status.account.silenced?
return unless !status.reblog? && %i(public public_unlisted login).include?(status.visibility.to_sym) && !status.account.silenced?
status.tags.each do |tag|
add(tag, status.account_id, at_time) if tag.usable?

View file

@ -26,6 +26,8 @@ class UserSettings
setting :stay_privacy, default: false
setting :default_reblog_privacy, default: nil
setting :default_searchability, default: :direct, in: %w(public private direct limited)
setting :default_searchability_of_search, default: :public, in: %w(public private direct limited)
setting :use_public_index, default: true
setting :disallow_unlisted_public_searchability, default: false
setting :public_post_to_unlisted, default: false
setting :reject_public_unlisted_subscription, default: false
@ -34,6 +36,7 @@ class UserSettings
setting :reaction_deck, default: nil
setting :stop_emoji_reaction_streaming, default: false
setting :emoji_reaction_streaming_notify_impl2, default: false
setting :emoji_reaction_policy, default: :allow, in: %w(allow outside_only followers_only following_only mutuals_only block)
setting :unsafe_limited_distribution, default: false
setting :dtl_force_with_tag, default: :none, in: %w(full searchability none)
setting :dtl_force_subscribable, default: false
@ -52,6 +55,8 @@ class UserSettings
setting :enable_login_privacy, default: false
setting :enable_dtl_menu, default: false
setting :hide_recent_emojis, default: false
setting :enable_emoji_reaction, default: true
setting :show_emoji_reaction_on_timeline, default: true
setting :reblog_modal, default: false
setting :unfollow_modal, default: true
setting :reduce_motion, default: false
@ -80,12 +85,6 @@ class UserSettings
setting :must_be_following_dm, default: false
end
namespace :emoji_reactions do
setting :must_be_follower, default: false
setting :must_be_following, default: false
setting :deny_from_all, default: false
end
def initialize(original_hash)
@original_hash = original_hash || {}
end

View file

@ -48,6 +48,8 @@ class InitialStateSerializer < ActiveModel::Serializer
store[:display_media] = object.current_account.user.setting_display_media
store[:display_media_expand] = object.current_account.user.setting_display_media_expand
store[:expand_spoilers] = object.current_account.user.setting_expand_spoilers
store[:enable_emoji_reaction] = object.current_account.user.setting_enable_emoji_reaction
store[:show_emoji_reaction_on_timeline] = object.current_account.user.setting_show_emoji_reaction_on_timeline
store[:enable_login_privacy] = object.current_account.user.setting_enable_login_privacy
store[:enable_dtl_menu] = object.current_account.user.setting_enable_dtl_menu
store[:hide_recent_emojis] = object.current_account.user.setting_hide_recent_emojis
@ -63,6 +65,7 @@ class InitialStateSerializer < ActiveModel::Serializer
store[:display_media] = Setting.display_media
store[:reduce_motion] = Setting.reduce_motion
store[:use_blurhash] = Setting.use_blurhash
store[:enable_emoji_reaction] = Setting.enable_emoji_reaction
end
store[:disabled_account_id] = object.disabled_account.id.to_s if object.disabled_account

View file

@ -168,6 +168,6 @@ class REST::AccountSerializer < ActiveModel::Serializer
end
def other_settings
object.suspended? ? {} : object.public_settings
object.suspended? ? {} : object.public_settings_for_local
end
end

View file

@ -108,7 +108,6 @@ class REST::InstanceSerializer < ActiveModel::Serializer
# for third party apps
def fedibird_capabilities
capabilities = [
:emoji_reaction,
:kmyblue_visibility_public_unlisted,
:enable_wide_emoji,
:enable_wide_emoji_reaction,
@ -126,6 +125,7 @@ class REST::InstanceSerializer < ActiveModel::Serializer
]
capabilities << :profile_search unless Chewy.enabled?
capabilities << :emoji_reaction if Setting.enable_emoji_reaction
capabilities
end

View file

@ -125,6 +125,16 @@ class REST::StatusSerializer < ActiveModel::Serializer
object.emoji_reactions_grouped_by_name(current_user&.account)
end
def emoji_reactions_count
if current_user&.account.nil?
return 0 unless Setting.enable_emoji_reaction
object.account.emoji_reaction_policy == :allow ? object.emoji_reactions_count : 0
else
object.account.show_emoji_reaction?(current_user.account) ? object.emoji_reactions_count : 0
end
end
def reactions
emoji_reactions.tap do |rs|
rs.each do |emoji_reaction|

View file

@ -117,7 +117,6 @@ class REST::V1::InstanceSerializer < ActiveModel::Serializer
# for third party apps
def fedibird_capabilities
capabilities = [
:emoji_reaction,
:kmyblue_visibility_public_unlisted,
:enable_wide_emoji,
:enable_wide_emoji_reaction,
@ -135,6 +134,7 @@ class REST::V1::InstanceSerializer < ActiveModel::Serializer
]
capabilities << :profile_search unless Chewy.enabled?
capabilities << :emoji_reaction if Setting.enable_emoji_reaction
capabilities
end

View file

@ -46,8 +46,10 @@ class EmojiReactService < BaseService
status = emoji_reaction.status
if status.account.local?
if status.account.user&.setting_enable_emoji_reaction
LocalNotificationWorker.perform_async(status.account_id, emoji_reaction.id, 'EmojiReaction', 'reaction') if status.account.user&.setting_emoji_reaction_streaming_notify_impl2
LocalNotificationWorker.perform_async(status.account_id, emoji_reaction.id, 'EmojiReaction', 'emoji_reaction')
end
elsif status.account.activitypub?
ActivityPub::DeliveryWorker.perform_async(build_json(emoji_reaction), emoji_reaction.account_id, status.account.inbox_url)
end
@ -63,7 +65,7 @@ class EmojiReactService < BaseService
end
def write_stream(emoji_reaction)
emoji_group = emoji_reaction.status.emoji_reactions_grouped_by_name
emoji_group = emoji_reaction.status.emoji_reactions_grouped_by_name(nil, force: true)
.find { |reaction_group| reaction_group['name'] == emoji_reaction.name && (!reaction_group.key?(:domain) || reaction_group['domain'] == emoji_reaction.custom_emoji&.domain) }
emoji_group['status_id'] = emoji_reaction.status_id.to_s
DeliveryEmojiReactionWorker.perform_async(render_emoji_reaction(emoji_group), emoji_reaction.status_id, emoji_reaction.account_id)

View file

@ -9,7 +9,6 @@ class NotifyService < BaseService
update
poll
emoji_reaction
status
warning
).freeze
@ -53,18 +52,6 @@ class NotifyService < BaseService
@recipient.user.settings['interactions.must_be_following'] && !following_sender?
end
def optional_non_follower_emoji_reaction?
emoji_reaction? && @recipient.user.settings['emoji_reactions.must_be_follower'] && !@notification.from_account.following?(@recipient)
end
def optional_non_following_emoji_reaction?
emoji_reaction? && @recipient.user.settings['emoji_reactions.must_be_following'] && !following_sender?
end
def emoji_reaction?
@notification.type == :emoji_reaction
end
def message?
@notification.type == :mention
end
@ -134,8 +121,6 @@ class NotifyService < BaseService
blocked ||= optional_non_follower?
blocked ||= optional_non_following?
blocked ||= optional_non_following_and_direct?
blocked ||= optional_non_follower_emoji_reaction?
blocked ||= optional_non_following_emoji_reaction?
blocked ||= conversation_muted?
blocked ||= blocked_mention? if @notification.type == :mention
blocked

View file

@ -11,7 +11,7 @@ class SearchService < BaseService
@offset = options[:type].blank? ? 0 : options[:offset].to_i
@resolve = options[:resolve] || false
@following = options[:following] || false
@searchability = options[:searchability] || 'public'
@searchability = options[:searchability] || account&.user&.setting_default_searchability_of_search.to_s || 'public'
default_results.tap do |results|
next if @query.blank? || @limit.zero?

View file

@ -22,22 +22,6 @@ class EmojiReactionValidator < ActiveModel::Validator
end
def deny_emoji_reactions?(emoji_reaction)
return false unless Setting.enable_block_emoji_reaction_settings
return false if emoji_reaction.status.account.user.nil?
return false if emoji_reaction.status.account_id == emoji_reaction.account_id
deny_from_all?(emoji_reaction) || non_follower?(emoji_reaction) || non_following?(emoji_reaction)
end
def deny_from_all?(emoji_reaction)
emoji_reaction.status.account.user.settings['emoji_reactions.deny_from_all']
end
def non_following?(emoji_reaction)
emoji_reaction.status.account.user.settings['emoji_reactions.must_be_following'] && !emoji_reaction.status.account.following?(emoji_reaction.account)
end
def non_follower?(emoji_reaction)
emoji_reaction.status.account.user.settings['emoji_reactions.must_be_follower'] && !emoji_reaction.account.following?(emoji_reaction.status.account)
!emoji_reaction.status.account.allow_emoji_reaction?(emoji_reaction.account)
end
end

View file

@ -13,9 +13,6 @@
.fields-group
= f.input :post_hash_tags_max, wrapper: :with_label, as: :integer, label: t('admin.ng_words.post_hash_tags_max')
.fields-group
= f.input :enable_block_emoji_reaction_settings, wrapper: :with_label, as: :boolean, label: t('admin.ng_words.enable_block_emoji_reaction_settings')
.fields-group
= f.input :hide_local_users_for_anonymous, wrapper: :with_label, as: :boolean, label: t('admin.ng_words.hide_local_users_for_anonymous')

View file

@ -29,6 +29,17 @@
.fields-group
= f.input :noindex, as: :boolean, wrapper: :with_label, label: t('admin.settings.default_noindex.title'), hint: t('admin.settings.default_noindex.desc_html')
%h4= t('admin.settings.discovery.emoji_reactions')
.fields-group
= f.input :enable_emoji_reaction, as: :boolean, wrapper: :with_label, kmyblue: true, hint: false
.fields-group
= f.input :receive_other_servers_emoji_reaction, as: :boolean, wrapper: :with_label, kmyblue: true
.fields-group
= f.input :streaming_other_servers_emoji_reaction, as: :boolean, wrapper: :with_label, kmyblue: true
%h4= t('admin.settings.discovery.publish_statistics')
.fields-group

View file

@ -4,9 +4,6 @@
- content_for :heading_actions do
= link_to t('antennas.new.title'), new_antenna_path, class: 'button'
.flash-message.alert
%strong= t('antennas.beta')
- if @antennas.empty?
.muted-hint.center-text= t 'antennas.index.empty'
- else

View file

@ -39,6 +39,9 @@
.fields-group
= ff.input :'web.hide_recent_emojis', wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_hide_recent_emojis'), hint: false
- if Setting.enable_emoji_reaction
= ff.input :'web.enable_emoji_reaction', wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_enable_emoji_reaction'), hint: I18n.t('simple_form.hints.defaults.setting_enable_emoji_reaction')
= ff.input :'web.show_emoji_reaction_on_timeline', wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_show_emoji_reaction_on_timeline')
.fields-group
= ff.input :'web.bookmark_category_needed', wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_bookmark_category_needed'), hint: I18n.t('simple_form.hints.defaults.setting_bookmark_category_needed')

View file

@ -42,11 +42,8 @@
= ff.input :'interactions.must_be_follower', wrapper: :with_label, label: I18n.t('simple_form.labels.interactions.must_be_follower')
= ff.input :'interactions.must_be_following', wrapper: :with_label, label: I18n.t('simple_form.labels.interactions.must_be_following')
= ff.input :'interactions.must_be_following_dm', wrapper: :with_label, label: I18n.t('simple_form.labels.interactions.must_be_following_dm')
- if Setting.enable_block_emoji_reaction_settings
= ff.input :'emoji_reactions.must_be_follower', kmyblue: true, wrapper: :with_label, label: I18n.t('simple_form.labels.emoji_reactions.must_be_follower')
= ff.input :'emoji_reactions.must_be_following', kmyblue: true, wrapper: :with_label, label: I18n.t('simple_form.labels.emoji_reactions.must_be_following')
= ff.input :'emoji_reactions.deny_from_all', kmyblue: true, wrapper: :with_label, label: I18n.t('simple_form.labels.emoji_reactions.deny_from_all')
- if Setting.enable_emoji_reaction
= f.simple_fields_for :settings, current_user.settings do |ff|
.fields-group
= ff.input :stop_emoji_reaction_streaming, as: :boolean, wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_stop_emoji_reaction_streaming'), hint: I18n.t('simple_form.hints.defaults.setting_stop_emoji_reaction_streaming')

View file

@ -14,33 +14,16 @@
%h4= t 'preferences.posting_defaults'
.fields-row
.fields-group.fields-row__column.fields-row__column-6
= ff.input :default_privacy, collection: Status.selectable_visibilities, wrapper: :with_label, include_blank: false, label_method: ->(visibility) { safe_join([I18n.t("statuses.visibilities.#{visibility}"), I18n.t("statuses.visibilities.#{visibility}_long")], ' - ') }, required: false, hint: false, label: I18n.t('simple_form.labels.defaults.setting_default_privacy')
.fields-group.fields-row__column.fields-row__column-6
= ff.input :default_reblog_privacy, collection: Status.selectable_reblog_visibilities, wrapper: :with_label, kmyblue: true, include_blank: false, label_method: lambda { |visibility| safe_join([I18n.t("statuses.visibilities.#{visibility}"), I18n.t("statuses.visibilities.#{visibility}_long")], ' - ') }, required: false, hint: false, label: I18n.t('simple_form.labels.defaults.setting_default_reblog_privacy')
.fields-row
.fields-group.fields-row__column.fields-row__column-6
= ff.input :default_searchability, collection: Status.selectable_searchabilities, wrapper: :with_label, kmyblue: true, include_blank: false, label_method: lambda { |searchability| safe_join([I18n.t("statuses.searchabilities.#{searchability}"), I18n.t("statuses.searchabilities.#{searchability}_long")], ' - ') }, required: false, hint: false, label: I18n.t('simple_form.labels.defaults.setting_default_searchability')
.fields-group.fields-row__column.fields-row__column-6
.fields-group.fields-row__column.fields-row__column-12
= ff.input :default_language, collection: [nil] + filterable_languages, wrapper: :with_label, label_method: ->(locale) { locale.nil? ? I18n.t('statuses.default_language') : native_locale_name(locale) }, required: false, include_blank: false, hint: false, label: I18n.t('simple_form.labels.defaults.setting_default_language')
.fields-group
= ff.input :stay_privacy, wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_stay_privacy')
.fields-group
= ff.input :public_post_to_unlisted, wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_public_post_to_unlisted'), hint: I18n.t('simple_form.hints.defaults.setting_public_post_to_unlisted')
.fields-group
= ff.input :disallow_unlisted_public_searchability, wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_disallow_unlisted_public_searchability'), hint: I18n.t('simple_form.hints.defaults.setting_disallow_unlisted_public_searchability')
.fields-group
= ff.input :default_sensitive, wrapper: :with_label, label: I18n.t('simple_form.labels.defaults.setting_default_sensitive'), hint: I18n.t('simple_form.hints.defaults.setting_default_sensitive')
.fields-group
= ff.input :'web.enable_login_privacy', wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_enable_login_privacy'), hint: false
- if Setting.enable_emoji_reaction
.fields-row
.fields-group.fields-row__column.fields-row__column-12
= ff.input :emoji_reaction_policy, kmyblue: true, collection: ['allow', 'outside_only', 'followers_only', 'following_only', 'mutuals_only', 'block'], label_method: lambda { |item| safe_join([t("simple_form.labels.defaults.setting_emoji_reaction_policy_items.#{item}")]) }, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li', include_blank: false, wrapper: :with_label, label: I18n.t('simple_form.labels.defaults.setting_emoji_reaction_policy'), hint: false, warning_hint: I18n.t('simple_form.hints.defaults.setting_emoji_reaction_policy')
- if @dtl_enabled

View file

@ -0,0 +1,49 @@
- content_for :page_title do
= t('settings.preferences')
- content_for :heading_actions do
= button_tag t('generic.save_changes'), class: 'button', form: 'edit_preferences'
= simple_form_for current_user, url: settings_preferences_reaching_path, html: { method: :put, id: 'edit_preferences' } do |f|
= render 'shared/error_messages', object: current_user
= f.simple_fields_for :settings, current_user.settings do |ff|
%h4= t 'preferences.visibility'
.fields-row
.fields-group.fields-row__column.fields-row__column-6
= ff.input :default_privacy, collection: Status.selectable_visibilities, wrapper: :with_label, include_blank: false, label_method: ->(visibility) { safe_join([I18n.t("statuses.visibilities.#{visibility}"), I18n.t("statuses.visibilities.#{visibility}_long")], ' - ') }, required: false, hint: false, label: I18n.t('simple_form.labels.defaults.setting_default_privacy')
.fields-group.fields-row__column.fields-row__column-6
= ff.input :default_reblog_privacy, collection: Status.selectable_reblog_visibilities, wrapper: :with_label, kmyblue: true, include_blank: false, label_method: lambda { |visibility| safe_join([I18n.t("statuses.visibilities.#{visibility}"), I18n.t("statuses.visibilities.#{visibility}_long")], ' - ') }, required: false, hint: false, label: I18n.t('simple_form.labels.defaults.setting_default_reblog_privacy')
.fields-group
= ff.input :stay_privacy, wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_stay_privacy')
.fields-group
= ff.input :public_post_to_unlisted, wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_public_post_to_unlisted'), hint: I18n.t('simple_form.hints.defaults.setting_public_post_to_unlisted')
.fields-group
= ff.input :'web.enable_login_privacy', wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_enable_login_privacy'), hint: false
%h4= t 'preferences.searchability'
.fields-row
.fields-group.fields-row__column.fields-row__column-12
= ff.input :default_searchability, collection: Status.selectable_searchabilities, wrapper: :with_label, kmyblue: true, include_blank: false, label_method: lambda { |searchability| safe_join([I18n.t("statuses.searchabilities.#{searchability}"), I18n.t("statuses.searchabilities.#{searchability}_long")], ' - ') }, required: false, hint: false, label: I18n.t('simple_form.labels.defaults.setting_default_searchability')
.fields-group
= ff.input :disallow_unlisted_public_searchability, wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_disallow_unlisted_public_searchability'), hint: I18n.t('simple_form.hints.defaults.setting_disallow_unlisted_public_searchability')
%h4= t 'preferences.search'
.fields-row
.fields-group.fields-row__column.fields-row__column-12
= ff.input :default_searchability_of_search, collection: Status.selectable_searchabilities, wrapper: :with_label, kmyblue: true, include_blank: false, label_method: lambda { |searchability| safe_join([I18n.t("statuses.searchabilities.#{searchability}"), I18n.t("statuses.searchabilities.#{searchability}_search_long")], ' - ') }, required: false, hint: false, label: I18n.t('simple_form.labels.defaults.setting_default_searchability_of_search')
.fields-group
= ff.input :use_public_index, wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_use_public_index')
.actions
= f.button :button, t('generic.save_changes'), type: :submit

View file

@ -6,12 +6,24 @@ class DeliveryEmojiReactionWorker
include Lockable
include AccountScope
def perform(payload_json, status_id, _my_account_id = nil)
status = Status.find(status_id.to_i)
def perform(payload_json, status_id, reacted_account_id)
return unless Setting.enable_emoji_reaction
status = Status.find(status_id)
reacted_account = Account.find(reacted_account_id)
if status.present?
scope_status(status).includes(:user).find_each do |account|
redis.publish("timeline:#{account.id}", payload_json) if (account.user.nil? || !account.user&.setting_stop_emoji_reaction_streaming) && redis.exists?("subscribed:timeline:#{account.id}")
scope = scope_status(status)
policy = status.account.emoji_reaction_policy
return if policy == :block
scope.select(:id).merge(policy_scope(status.account, policy)).includes(:user).find_each do |account|
next if account.user.present? && (account.user.setting_stop_emoji_reaction_streaming || !account.user.setting_enable_emoji_reaction)
next unless redis.exists?("subscribed:timeline:#{account.id}")
next if !reacted_account.local? && account.excluded_from_timeline_domains.include?(reacted_account.domain)
redis.publish("timeline:#{account.id}", payload_json)
end
end
@ -19,4 +31,21 @@ class DeliveryEmojiReactionWorker
rescue ActiveRecord::RecordNotFound
true
end
def policy_scope(account, policy)
case policy
when :block
Account.where(id: 0)
when :mutuals_only
account.mutuals.local.or(Account.where(id: account))
when :following_only
account.following.local.or(Account.where(id: account))
when :followers_only
account.followers.local.or(Account.where(id: account))
when :outside_only
account.followers.local.or(Account.where(id: account.following.local)).or(Account.where(id: account))
else
Account.local
end
end
end

View file

@ -597,7 +597,6 @@ en:
media_attachments:
title: Media attachments
ng_words:
enable_block_emoji_reaction_settings: Enable block emoji reactions settings for users
hide_local_users_for_anonymous: Hide timeline local user posts from anonymous
keywords: Reject keywords
keywords_hint: The first character of the line is "?". to use regular expressions
@ -805,6 +804,7 @@ en:
desc_html: Affects all users who have not changed this setting themselves
title: Opt users out of search engine indexing by default
discovery:
emoji_reactions: Stamp
follow_recommendations: Follow recommendations
preamble: Surfacing interesting content is instrumental in onboarding new users who may not know anyone Mastodon. Control how various discovery features work on your server.
profile_directory: Profile directory
@ -1068,7 +1068,6 @@ en:
hint_html: If you want to move from another account to this one, here you can create an alias, which is required before you can proceed with moving followers from the old account to this one. This action by itself is <strong>harmless and reversible</strong>. <strong>The account migration is initiated from the old account</strong>.
remove: Unlink alias
antennas:
beta: This function is in beta.
contexts:
account: Accounts
domain: Domains
@ -1649,6 +1648,10 @@ en:
other: Other
posting_defaults: Posting defaults
public_timelines: Public timelines
reaching: Visibility and search
search: Search
searchability: Searchability of your post
visibility: Visibility
privacy:
hint_html: "<strong>Customize how you want your profile and your posts to be found.</strong> A variety of features in Mastodon can help you reach a wider audience when enabled. Take a moment to review these settings to make sure they fit your use case."
privacy: Privacy
@ -1671,6 +1674,7 @@ en:
reactions:
errors:
banned: Banned reaction from the user
disabled: Stamp is disabled on this server
duplication: Cannot react same things
limit_reached: Limit of different reactions reached
unrecognized_emoji: is not a recognized emoji
@ -1817,12 +1821,16 @@ en:
searchabilities:
direct: Reactionners
direct_long: Reacter of this post can find
direct_search_long: You can search you reacted posts only
limited: Self only
limited_long: Nobody can find, but you can
limited_search_long: You can search your posts only
private: Followers and reactionners
private_long: Your followers and reactionners can find
private_search_long: You can search you are following or reacted posts only
public: Public
public_long: Anyone can find
public_search_long: You can search all posts permitted to search
show_more: Show more
show_newer: Show newer
show_older: Show older

View file

@ -595,7 +595,6 @@ ja:
media_attachments:
title: 投稿された画像
ng_words:
enable_block_emoji_reaction_settings: 各ユーザーにスタンプ機能のブロック設定項目を解放する
hide_local_users_for_anonymous: ログインしていない状態でローカルユーザーの投稿をタイムラインから取得できないようにする
keywords: 投稿できないキーワード
keywords_hint: 行を「?」で始めると、正規表現が使えます
@ -801,6 +800,7 @@ ja:
desc_html: この設定を自分で変更していない全ユーザーに影響します
title: デフォルトで検索エンジンによるインデックスを拒否する
discovery:
emoji_reactions: スタンプ
follow_recommendations: おすすめフォロー
preamble: Mastodon を知らないユーザーを取り込むには、興味深いコンテンツを浮上させることが重要です。サーバー上で様々なディスカバリー機能がどのように機能するかを制御します。
profile_directory: ディレクトリ
@ -1017,7 +1017,6 @@ ja:
hint_html: 他のアカウントからこのアカウントにフォロワーを引き継いで引っ越したい場合、ここでエイリアスを作成しておく必要があります。エイリアス自体は<strong>無害で、取り消す</strong>ことができます。<strong>引っ越しは以前のアカウント側から開始する必要があります</strong>。
remove: エイリアスを削除
antennas:
beta: アンテナ機能はベータ版です。今後、予告なく全データリセット・機能削除を行う場合があります。
contexts:
account: アカウント
domain: ドメイン
@ -1589,6 +1588,10 @@ ja:
other: その他
posting_defaults: デフォルトの投稿設定
public_timelines: 公開タイムライン
reaching: 公開範囲と検索
search: 検索
searchability: あなたの投稿の検索許可
visibility: 公開範囲
privacy:
hint_html: "<strong>Customize how you want your profile and your posts to be found.</strong> A variety of features in Mastodon can help you reach a wider audience when enabled. Take a moment to review these settings to make sure they fit your use case."
privacy: Privacy
@ -1610,6 +1613,7 @@ ja:
reactions:
errors:
banned: 指定ユーザーからのリアクションは禁止されています
disabled: このサーバーではスタンプ機能は無効になっています
duplication: 同じリアクションを複数行おうとしました
limit_reached: リアクションの種類が上限に達しました
unrecognized_emoji: は絵文字として認識されていません
@ -1750,12 +1754,16 @@ ja:
searchabilities:
direct: 反応者
direct_long: この投稿に反応した人しか検索できません
direct_search_long: あなたが反応した投稿のみが検索されます
limited: 自分のみ
limited_long: この投稿はあなたしか検索できません
limited_search_long: あなたの投稿のみが検索されます
private: フォロワーと反応者
private_long: この投稿はフォロワーと反応者のみが検索できます
private_search_long: あなたのフォロー相手またはあなたが反応した投稿のみが検索されます
public: 全て
public_long: この投稿は誰でも検索できます
public_search_long: 検索が許可された全ての投稿が検索できます
show_more: もっと見る
show_newer: 新しいものを表示
show_older: 古いものを表示

View file

@ -69,6 +69,8 @@ en:
setting_dtl_force_subscribable: Your post can be detected local user's antenna to subscribe deep timeline
setting_dtl_force_with_tag: "With using #%{tag} tag, your post settings will be changed forcibly"
setting_dtl_menu: Show DTL menu on web
setting_emoji_reaction_policy: Even with this setting, users on other servers are free to put their stamp on the post and share it within the same server. If you simply want to remove the stamp from your own screen, you can disable it from the appearance settings
setting_enable_emoji_reaction: If turn off, other users still can react your posts
setting_use_blurhash: Gradients are based on the colors of the hidden visuals but obfuscate any details
setting_use_pending_items: Hide timeline updates behind a click instead of automatically scrolling the feed
username: You can use letters, numbers, and underscores
@ -96,6 +98,7 @@ en:
media_cache_retention_period: Downloaded media files will be deleted after the specified number of days when set to a positive value, and re-downloaded on demand.
peers_api_enabled: A list of domain names this server has encountered in the fediverse. No data is included here about whether you federate with a given server, just that your server knows about it. This is used by services that collect statistics on federation in a general sense.
profile_directory: The profile directory lists all users who have opted-in to be discoverable.
receive_other_servers_emoji_reaction: It can cause load. It is recommended to enable it only when there are few people.
require_invite_text: When sign-ups require manual approval, make the “Why do you want to join?” text input mandatory rather than optional
site_contact_email: How people can reach you for legal or support inquiries.
site_contact_username: How people can reach you on Mastodon.
@ -104,6 +107,7 @@ en:
site_terms: Use your own privacy policy or leave blank to use the default. Can be structured with Markdown syntax.
site_title: How people may refer to your server besides its domain name.
status_page_url: URL of a page where people can see the status of this server during an outage
streaming_other_servers_emoji_reaction: Check your server network capacity. A lot of data streaming.
theme: Theme that logged out visitors and new users see.
thumbnail: A roughly 2:1 image displayed alongside your server information.
timeline_preview: Logged out visitors will be able to browse the most recent public posts available on the server.
@ -227,7 +231,8 @@ en:
setting_default_language: Posting language
setting_default_privacy: Posting privacy
setting_default_reblog_privacy: Reblogging privacy
setting_default_searchability: Searchability
setting_default_searchability: Searchability of your own post
setting_default_searchability_of_search: Your search setting
setting_default_sensitive: Always mark media as sensitive
setting_delete_modal: Show confirmation dialog before deleting a post
setting_disable_swiping: Disable swiping motions
@ -240,7 +245,16 @@ en:
setting_dtl_force_subscribable: Ignore your dissubscribable setting when using the DTL tag
setting_dtl_force_with_tag: Post with DTL tag
setting_emoji_reaction_streaming_notify_impl2: Enable stamp notification compat with Nyastodon, Catstodon, glitch-soc
setting_enable_emoji_reaction: Use stamp function
setting_enable_login_privacy: Enable login visibility
setting_emoji_reaction_policy: Stamp receive/display policy
setting_emoji_reaction_policy_items:
allow: Allow all
block: Block
followers_only: Followers only
following_only: Followings only
mutuals_only: Mutuals only
outside_only: Followings or followers only
setting_expand_spoilers: Always expand posts marked with content warnings
setting_hide_followers_count: Hide followers count
setting_hide_following_count: Hide following count
@ -255,6 +269,7 @@ en:
setting_reject_unlisted_subscription: Reject sending unlisted posts to Misskey, Calckey
setting_send_without_domain_blocks: Send your post to all server with administrator set as rejecting-post-server for protect you [DEPRECATED]
setting_show_application: Disclose application used to send posts
setting_show_emoji_reaction_on_timeline: Show all stamps on timeline
setting_stay_privacy: Not change privacy after post
setting_stop_emoji_reaction_streaming: Disable stamp streamings
setting_system_font_ui: Use system's default font
@ -265,6 +280,7 @@ en:
setting_unsafe_limited_distribution: Send limit posts with unsafe way to other servers
setting_use_blurhash: Show colorful gradients for hidden media
setting_use_pending_items: Slow mode
setting_use_public_index: Include permitted accounts post to results of search
severity: Severity
sign_in_token_attempt: Security code
title: Title
@ -298,10 +314,12 @@ en:
closed_registrations_message: Custom message when sign-ups are not available
content_cache_retention_period: Content cache retention period
custom_css: Custom CSS
enable_emoji_reaction: Enable stamp function
mascot: Custom mascot (legacy)
media_cache_retention_period: Media cache retention period
peers_api_enabled: Publish list of discovered servers in the API
profile_directory: Enable profile directory
receive_other_servers_emoji_reaction: Receive stamp between other server users
registrations_mode: Who can sign-up
require_invite_text: Require a reason to join
show_domain_blocks: Show domain blocks
@ -313,6 +331,7 @@ en:
site_terms: Privacy Policy
site_title: Server name
status_page_url: Status page URL
streaming_other_servers_emoji_reaction: Streaming stamp between other server users
theme: Default theme
thumbnail: Server thumbnail
timeline_preview: Allow unauthenticated access to public timelines

View file

@ -70,7 +70,9 @@ ja:
setting_display_media_show_all: メディアを常に表示する
setting_dtl_force_subscribable: 購読拒否設定に関係なく、ディープタイムラインに向けた投稿はアンテナに掲載されます。ディープタイムラインをアンテナ経由で閲覧している人にあなたの発言が届きます
setting_dtl_force_with_tag: "ハッシュタグ #%{tag} をつけて投稿するとき、公開範囲と検索許可を強制的に置き換えるかを設定します"
setting_emoji_reaction_policy: この設定をしても他のサーバーのユーザーはその投稿に自由にスタンプをつけ、同じサーバーの中で共有できます。単にあなた自身の画面からスタンプを除去したいだけなら、外観設定からスタンプを無効にすることができます
setting_emoji_reaction_streaming_notify_impl2: 当該サーバーの独自機能に対応したアプリを利用時に、スタンプ機能を利用できます。動作確認していないため(そもそもそのようなアプリ自体を確認できていないため)正しく動かない場合があります
setting_enable_emoji_reaction: この機能を無効にしても、他の人はあなたの投稿にスタンプをつけられます
setting_hide_network: フォローとフォロワーの情報がプロフィールページで見られないようにします
setting_link_preview: プレビュー生成を停止することは、センシティブなサイトへのリンクを頻繁に投稿する人にも有効かもしれません
setting_noai: AI学習への利用を禁止するメタタグをプロフィールページに追加します。ただし実効性があるとは限りません
@ -106,6 +108,7 @@ ja:
media_cache_retention_period: 正の値に設定されている場合、ダウンロードされたメディアファイルは指定された日数の後に削除され、リクエストに応じて再ダウンロードされます。
peers_api_enabled: このサーバーが Fediverse で遭遇したドメイン名のリストです。このサーバーが知っているだけで、特定のサーバーと連合しているかのデータは含まれません。これは一般的に Fediverse に関する統計情報を収集するサービスによって使用されます。
profile_directory: ディレクトリには、掲載する設定をしたすべてのユーザーが一覧表示されます。
receive_other_servers_emoji_reaction: 負荷の原因になります。人が少ない場合にのみ有効にすることをおすすめします。
require_invite_text: アカウント登録が承認制の場合、登録の際の申請事由の入力を必須にします
site_contact_email: 法律またはサポートに関する問い合わせ先
site_contact_username: マストドンでの連絡方法
@ -114,6 +117,7 @@ ja:
site_terms: 独自のプライバシーポリシーを使用するか空白にしてデフォルトのプライバシーポリシーを使用します。Markdownが使えます。
site_title: ドメイン名以外でサーバーを参照する方法
status_page_url: 障害発生時などにユーザーがサーバーの状態を確認できるページのURL
streaming_other_servers_emoji_reaction: ストリーミングサーバーの通信許容量、および各ユーザーの通信容量を必ず確認してください。大量のデータが配信されます。
theme: ログインしていない人と新規ユーザーに表示されるテーマ。
thumbnail: サーバー情報と共に表示される、アスペクト比が約 2:1 の画像。
timeline_preview: ログインしていないユーザーがサーバー上の最新の公開投稿を閲覧できるようにします。
@ -236,7 +240,8 @@ ja:
setting_default_language: 投稿する言語
setting_default_privacy: 投稿の公開範囲
setting_default_reblog_privacy: BTの公開範囲
setting_default_searchability: 投稿の検索を許可する範囲
setting_default_searchability: 自分の投稿の検索を許可する範囲
setting_default_searchability_of_search: 自分が検索するときの検索許可設定
setting_default_sensitive: メディアを常に閲覧注意としてマークする
setting_delete_modal: 投稿を削除する前に確認ダイアログを表示する
setting_disable_swiping: スワイプでの切り替えを無効にする
@ -250,7 +255,16 @@ ja:
setting_dtl_force_with_tag: DTL参加時の投稿設定
setting_dtl_menu: Webクライアントのメニューにディープタイムラインを追加する
setting_enable_login_privacy: 公開範囲「ログインユーザーのみ」をWeb UIで選択可能にする
setting_emoji_reaction_policy: スタンプ受け入れと表示設定
setting_emoji_reaction_policy_items:
allow: 全員に許可
block: 全員禁止
followers_only: フォロワーのみ許可
following_only: フォロー中の相手のみ許可
mutuals_only: 相互のみ許可
outside_only: フォロー中、またはフォロワーのみに許可
setting_emoji_reaction_streaming_notify_impl2: Nyastodon, Catstodon, glitch-soc互換のスタンプ機能を有効にする
setting_enable_emoji_reaction: スタンプ機能を使用する
setting_expand_spoilers: 閲覧注意としてマークされた投稿を常に展開する
setting_hide_followers_count: フォロワー数を隠す
setting_hide_following_count: フォロー数を隠す
@ -266,6 +280,7 @@ ja:
setting_reject_unlisted_subscription: Misskey系サーバーに「未収載」投稿を「フォロワーのみ」に変換して配送する
setting_send_without_domain_blocks: 管理人の設定した配送停止設定を拒否する (非推奨)
setting_show_application: 送信したアプリを開示する
setting_show_emoji_reaction_on_timeline: タイムライン上に他の人のつけたスタンプを表示する
setting_stay_privacy: 投稿時に公開範囲を保存する
setting_stop_emoji_reaction_streaming: スタンプのストリーミングを停止する
setting_system_font_ui: システムのデフォルトフォントを使う
@ -276,6 +291,7 @@ ja:
setting_unsafe_limited_distribution: 安全でない方法で限定投稿を他サーバーに配信する (非推奨)
setting_use_blurhash: 非表示のメディアを色付きのぼかしで表示する
setting_use_pending_items: 手動更新モード
setting_use_public_index: Mastodonの標準設定によって検索が許可されたアカウントの公開投稿を検索結果に含める
severity: 重大性
sign_in_token_attempt: セキュリティコード
title: タイトル
@ -309,10 +325,12 @@ ja:
closed_registrations_message: アカウント作成を停止している時のカスタムメッセージ
content_cache_retention_period: コンテンツキャッシュの保持期間
custom_css: カスタムCSS
enable_emoji_reaction: スタンプ機能を有効にする
mascot: カスタムマスコット(レガシー)
media_cache_retention_period: メディアキャッシュの保持期間
peers_api_enabled: 発見したサーバーのリストをAPIで公開する
profile_directory: ディレクトリを有効にする
receive_other_servers_emoji_reaction: 他のサーバーのユーザーが他のサーバーの投稿につけたスタンプを受け入れる
registrations_mode: 新規登録が可能な人
require_invite_text: 申請事由の入力を必須にする
show_domain_blocks: ドメインブロックを表示
@ -324,6 +342,7 @@ ja:
site_terms: プライバシーポリシー
site_title: サーバーの名前
status_page_url: ステータスページのURL
streaming_other_servers_emoji_reaction: 他のサーバーのユーザーが他のサーバーの投稿につけたスタンプをストリーミングする
theme: デフォルトテーマ
thumbnail: サーバーのサムネイル
timeline_preview: 公開タイムラインへの未認証のアクセスを許可する

View file

@ -11,6 +11,7 @@ SimpleNavigation::Configuration.run do |navigation|
n.item :preferences, safe_join([fa_icon('cog fw'), t('settings.preferences')]), settings_preferences_path, if: -> { current_user.functional? } do |s|
s.item :appearance, safe_join([fa_icon('desktop fw'), t('settings.appearance')]), settings_preferences_appearance_path
s.item :notifications, safe_join([fa_icon('bell fw'), t('settings.notifications')]), settings_preferences_notifications_path
s.item :reaching, safe_join([fa_icon('search fw'), t('preferences.reaching')]), settings_preferences_reaching_path
s.item :other, safe_join([fa_icon('cog fw'), t('preferences.other')]), settings_preferences_other_path
end

View file

@ -10,6 +10,7 @@ namespace :settings do
namespace :preferences do
resource :appearance, only: [:show, :update], controller: :appearance
resource :notifications, only: [:show, :update]
resource :reaching, only: [:show, :update], controller: :reaching
resource :other, only: [:show, :update], controller: :other
end

View file

@ -38,6 +38,9 @@ defaults: &defaults
require_invite_text: false
backups_retention_period: 7
captcha_enabled: false
receive_other_servers_emoji_reaction: false
streaming_other_servers_emoji_reaction: false
enable_emoji_reaction: true
development:
<<: *defaults

View file

@ -12,6 +12,7 @@ require_relative 'feeds'
require_relative 'ip_blocks'
require_relative 'maintenance'
require_relative 'media'
require_relative 'ohagi'
require_relative 'preview_cards'
require_relative 'search'
require_relative 'settings'
@ -65,6 +66,9 @@ module Mastodon::CLI
desc 'maintenance SUBCOMMAND ...ARGS', 'Various maintenance utilities'
subcommand 'maintenance', Maintenance
desc 'ohagi SUBCOMMAND ...ARGS', 'Ohagis'
subcommand 'ohagi', Ohagi
option :dry_run, type: :boolean
desc 'self-destruct', 'Erase the server from the federation'
long_desc <<~LONG_DESC

33
lib/mastodon/cli/ohagi.rb Normal file
View file

@ -0,0 +1,33 @@
# frozen_string_literal: true
require 'concurrent'
require_relative 'base'
module Mastodon::CLI
class Ohagi < Base
desc 'good', 'Ohagi is good'
def good
say('Thanks!', :green)
end
desc 'bad', 'Ohagi is bad'
def bad
say('Sorry...', :red)
end
desc 'tsubuan', 'Ohagi is tsubuan'
def tsubuan
say('Thanks! You are knight in shining armor!', :green)
end
desc 'koshian', 'Ohagi is koshian'
def koshian
say('Let the WAR begin.', :red)
end
desc 'kokuraan', 'Ohagi is kokuraan'
def kokuraan
say('I hate you.', :yellow)
end
end
end

View file

@ -3,7 +3,7 @@
require 'rails_helper'
describe SearchQueryTransformer do
subject { described_class.new.apply(parser, current_account: account) }
subject { described_class.new.apply(parser, current_account: account, searchability: :public) }
let(:account) { Fabricate(:account) }
let(:parser) { SearchQueryParser.new.parse(query) }

View file

@ -0,0 +1,177 @@
# frozen_string_literal: true
require 'rails_helper'
describe StatusesSearchService do
describe 'a local user posts with searchability' do
subject do
described_class.new.call('ohagi', account, limit: 10, searchability: searchability).map(&:id)
end
let(:alice) { Fabricate(:user).account }
let(:following) { Fabricate(:user).account }
let(:reacted) { Fabricate(:user).account }
let(:other) { Fabricate(:user).account }
let(:account) { nil }
let(:searchability) { :public }
let!(:status) { Fabricate(:status, text: 'Hello, ohagi', account: alice, searchability: searchability) }
before do
alice.update!(indexable: true)
following.follow!(alice)
Fabricate(:favourite, account: reacted, status: status)
PublicStatusesIndex.import!
StatusesIndex.import!
end
context 'when public searchability' do
let(:searchability) { :public }
let(:account) { other }
context 'with other account' do
it 'search status' do
expect(subject.count).to eq 1
expect(subject).to include status.id
end
end
context 'with follower' do
let(:account) { following }
it 'search status' do
expect(subject.count).to eq 1
expect(subject).to include status.id
end
end
context 'with reacted user' do
let(:account) { reacted }
it 'search status' do
expect(subject.count).to eq 1
expect(subject).to include status.id
end
end
context 'with self' do
let(:account) { alice }
it 'search status' do
expect(subject.count).to eq 1
expect(subject).to include status.id
end
end
end
context 'when private searchability' do
let(:searchability) { :private }
let(:account) { other }
context 'with other account' do
it 'search status' do
expect(subject.count).to eq 0
end
end
context 'with follower' do
let(:account) { following }
it 'search status' do
expect(subject.count).to eq 1
expect(subject).to include status.id
end
end
context 'with reacted user' do
let(:account) { reacted }
it 'search status' do
expect(subject.count).to eq 1
expect(subject).to include status.id
end
end
context 'with self' do
let(:account) { alice }
it 'search status' do
expect(subject.count).to eq 1
expect(subject).to include status.id
end
end
end
context 'when direct searchability' do
let(:searchability) { :direct }
let(:account) { other }
context 'with other account' do
it 'search status' do
expect(subject.count).to eq 0
end
end
context 'with follower' do
let(:account) { following }
it 'search status' do
expect(subject.count).to eq 0
end
end
context 'with reacted user' do
let(:account) { reacted }
it 'search status' do
expect(subject.count).to eq 1
expect(subject).to include status.id
end
end
context 'with self' do
let(:account) { alice }
it 'search status' do
expect(subject.count).to eq 1
expect(subject).to include status.id
end
end
end
context 'when limited searchability' do
let(:searchability) { :limited }
let(:account) { other }
context 'with other account' do
it 'search status' do
expect(subject.count).to eq 0
end
end
context 'with follower' do
let(:account) { following }
it 'search status' do
expect(subject.count).to eq 0
end
end
context 'with reacted user' do
let(:account) { reacted }
it 'search status' do
expect(subject.count).to eq 0
end
end
context 'with self' do
let(:account) { alice }
it 'search status' do
expect(subject.count).to eq 1
expect(subject).to include status.id
end
end
end
end
end