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_DOMAIN=cb6e6126.ngrok.io
LOCAL_HTTPS=true LOCAL_HTTPS=true
# Elasticsearch # Elasticsearch
ES_ENABLED=true ES_ENABLED=false
ES_HOST=localhost ES_HOST=localhost
ES_PORT=9200 ES_PORT=9200
ES_PREFIX=test 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`本番サーバーで動作確認を行うためのコードが含まれている場合があります。ブランチの最新コミットではなく最新タグを取り込むことを強くおすすめします - 特に最新コミットでは、デバッグ用コードや、`kmy.blue`本番サーバーで動作確認を行うためのコードが含まれている場合があります。ブランチの最新コミットではなく最新タグを取り込むことを強くおすすめします
Mastodonの最新バージョンでは、`dist`フォルダに`mastodon-streaming@.service`が追加されています。これは現在の一般的な手順書には存在しません。各サービスファイルをコピーするとき、`mastodon-streaming@.service`をコピーし忘れないようにしてください。
### ElasticSearchを使用する場合 ### ElasticSearchを使用する場合
kmyblueでは、sudachiの使用を前提としています。 kmyblueでは、sudachiの使用を前提としています。
@ -56,5 +58,33 @@ RAILS_ENV=production bin/rails assets:precompile
# ElasticSearchを使用する場合 # ElasticSearchを使用する場合
RAILS_ENV=production bin/tootctl search deploy 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を参照してください。 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の強み ## kmyblueの強み
### 本家Mastodonへの積極的追従 ### 本家Mastodonへの積極的追従

View file

@ -15,6 +15,10 @@ class Api::V1::Statuses::EmojiReactionedByAccountsController < Api::BaseControll
private private
def load_accounts 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 = default_accounts
scope = scope.where.not(account_id: current_account.excluded_from_timeline_account_ids) unless current_account.nil? scope = scope.where.not(account_id: current_account.excluded_from_timeline_account_ids) unless current_account.nil?
scope.merge(paginated_emoji_reactions).to_a scope.merge(paginated_emoji_reactions).to_a

View file

@ -42,6 +42,7 @@ class Api::V1::Statuses::EmojiReactionsController < Api::BaseController
def create_private(emoji) def create_private(emoji)
count = EmojiReaction.where(account: current_account, status: @status).count 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.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) EmojiReactService.new.call(current_account, @status, emoji)
render json: @status, serializer: REST::StatusSerializer 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 // to use the progress bar to show download progress
import Bundle from '../features/ui/components/bundle'; import Bundle from '../features/ui/components/bundle';
import { MediaGallery, Video, Audio } from '../features/ui/util/async-components'; 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 { Avatar } from './avatar';
import { AvatarOverlay } from './avatar_overlay'; import { AvatarOverlay } from './avatar_overlay';
@ -577,8 +577,8 @@ class Status extends ImmutablePureComponent {
let emojiReactionsBar = null; let emojiReactionsBar = null;
if (!this.props.withoutEmojiReactions && status.get('emoji_reactions')) { if (!this.props.withoutEmojiReactions && status.get('emoji_reactions')) {
const emojiReactions = status.get('emoji_reactions'); const emojiReactions = status.get('emoji_reactions');
if (emojiReactions.size > 0) { if (emojiReactions.size > 0 && enableEmojiReaction) {
emojiReactionsBar = <StatusEmojiReactionsBar emojiReactions={emojiReactions} status={status} onEmojiReact={this.props.onEmojiReact} onUnEmojiReact={this.props.onUnEmojiReact} />; 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} {(!isCardMediaWithSensitive || !status.get('hidden')) && media}
{expanded && hashtagBar} {(!status.get('spoiler_text') || expanded) && hashtagBar}
{emojiReactionsBar} {emojiReactionsBar}

View file

@ -10,9 +10,10 @@ import { connect } from 'react-redux';
import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'mastodon/permissions'; import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'mastodon/permissions';
import DropdownMenuContainer from '../containers/dropdown_menu_container'; import DropdownMenuContainer from '../containers/dropdown_menu_container';
import EmojiPickerDropdown from '../features/compose/containers/emoji_picker_dropdown_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'; 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} /> <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 emojiReactionPolicy = account.getIn(['other_settings', 'emoji_reaction_policy']) || 'allow';
const followed = !account.getIn(['other_settings', 'emoji_reaction_must_following']) || (relationship && relationship.get('followed_by')); const following = emojiReactionPolicy !== 'following_only' || (relationship && relationship.get('following'));
const denyFromAll = !account.getIn(['other_settings', 'emoji_reaction_deny_from_all']); 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 = ( const emojiPickerButton = (
<IconButton className='status__action-bar__button' title={intl.formatMessage(messages.emojiReaction)} icon='smile-o' onClick={this.handleEmojiPickInnerButton} /> <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} /> <EmojiPickerDropdown onPickEmoji={this.handleEmojiPick} button={emojiPickerButton} />
); );

View file

@ -60,6 +60,7 @@ class StatusEmojiReactionsBar extends PureComponent {
status: ImmutablePropTypes.map, status: ImmutablePropTypes.map,
onEmojiReact: PropTypes.func, onEmojiReact: PropTypes.func,
onUnEmojiReact: PropTypes.func, onUnEmojiReact: PropTypes.func,
myReactionOnly: PropTypes.bool,
}; };
onEmojiReact = (name) => { onEmojiReact = (name) => {
@ -73,20 +74,23 @@ class StatusEmojiReactionsBar extends PureComponent {
}; };
render () { 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)
<EmojiReactionButton .filter(emoji => emoji.get('count') !== 0)
key={index} .filter(emoji => !myReactionOnly || emoji.get('me'))
name={emoji.get('name')} .map((emoji, index) => (
count={emoji.get('count')} <EmojiReactionButton
me={emoji.get('me')} key={index}
url={emoji.get('url')} name={emoji.get('name')}
staticUrl={emoji.get('static_url')} count={myReactionOnly ? 1 : emoji.get('count')}
domain={emoji.get('domain')} me={emoji.get('me')}
onEmojiReact={this.onEmojiReact} url={emoji.get('url')}
onUnEmojiReact={this.onUnEmojiReact} staticUrl={emoji.get('static_url')}
/>)); domain={emoji.get('domain')}
onEmojiReact={this.onEmojiReact}
onUnEmojiReact={this.onUnEmojiReact}
/>));
return ( return (
<div className='status__emoji-reactions-bar'> <div className='status__emoji-reactions-bar'>

View file

@ -10,9 +10,10 @@ import { connect } from 'react-redux';
import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'mastodon/permissions'; import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'mastodon/permissions';
import { IconButton } from '../../../components/icon_button'; import { IconButton } from '../../../components/icon_button';
import DropdownMenuContainer from '../../../containers/dropdown_menu_container'; 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'; import EmojiPickerDropdown from '../../compose/containers/emoji_picker_dropdown_container';
const messages = defineMessages({ 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; let replyIcon;
if (status.get('in_reply_to_id', null) === null) { if (status.get('in_reply_to_id', null) === null) {
replyIcon = 'reply'; replyIcon = 'reply';
@ -329,13 +326,26 @@ class ActionBar extends PureComponent {
reblogTitle = intl.formatMessage(messages.cannot_reblog); 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 ( return (
<div className='detailed-status__action-bar'> <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 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={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='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'><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'> <div className='detailed-status__action-bar-dropdown'>
<DropdownMenuContainer size={18} icon='ellipsis-h' status={status} items={menu} direction='left' title={intl.formatMessage(messages.more)} /> <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 { getHashtagBarForStatus } from 'mastodon/components/hashtag_bar';
import { Icon } from 'mastodon/components/icon'; import { Icon } from 'mastodon/components/icon';
import PictureInPicturePlaceholder from 'mastodon/components/picture_in_picture_placeholder'; import PictureInPicturePlaceholder from 'mastodon/components/picture_in_picture_placeholder';
import { enableEmojiReaction } from 'mastodon/initial_state';
import { Avatar } from '../../../components/avatar'; import { Avatar } from '../../../components/avatar';
import { DisplayName } from '../../../components/display_name'; import { DisplayName } from '../../../components/display_name';
@ -240,7 +241,8 @@ class DetailedStatus extends ImmutablePureComponent {
let emojiReactionsBar = null; let emojiReactionsBar = null;
if (status.get('emoji_reactions')) { if (status.get('emoji_reactions')) {
const emojiReactions = 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} />; 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} {(!isCardMediaWithSensitive || !status.get('hidden')) && media}
{expanded && hashtagBar} {(!status.get('spoiler_text') || expanded) && hashtagBar}
{emojiReactionsBar} {emojiReactionsBar}

View file

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

View file

@ -32,7 +32,11 @@ textarea {
color: $primary-text-color; 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; color: $ui-base-color;
} }

View file

@ -7,7 +7,7 @@ class ActivityPub::Activity::Like < ActivityPub::Activity
def perform def perform
@original_status = status_from_uri(object_uri) @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? if shortcode.nil?
process_favourite process_favourite
@ -32,6 +32,8 @@ class ActivityPub::Activity::Like < ActivityPub::Activity
end end
def process_emoji_reaction def process_emoji_reaction
return if !@original_status.account.local? && !Setting.receive_other_servers_emoji_reaction
if emoji_tag.present? if emoji_tag.present?
return if emoji_tag['id'].blank? || emoji_tag['name'].blank? || emoji_tag['icon'].blank? || emoji_tag['icon']['url'].blank? 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 end
def write_stream(emoji_reaction) 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) } .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 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 end
def render_emoji_reaction(emoji_group) 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 = { '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 emoji_group['domain'] = emoji_reaction.custom_emoji.domain if emoji_reaction.custom_emoji
end 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 end
def render_emoji_reaction(emoji_group) def render_emoji_reaction(emoji_group)

View file

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

View file

@ -6,6 +6,8 @@ class StatusCacheHydrator
end end
def hydrate(account_id) def hydrate(account_id)
account = Account.find(account_id)
# The cache of the serialized hash is generated by the fan-out-on-write service # 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) } 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][: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][: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][:filtered] = payload[:filtered]
payload[:reblog][:emoji_reactions] = @status.emoji_reactions_grouped_by_name(account)
if payload[:reblog][:poll] if payload[:reblog][:poll]
if @status.reblog.account_id == account_id if @status.reblog.account_id == account_id
@ -56,6 +59,7 @@ class StatusCacheHydrator
payload[:filtered] = CustomFilter payload[:filtered] = CustomFilter
.apply_cached_filters(CustomFilter.cached_filters_for(account_id), @status, following?(account_id)) .apply_cached_filters(CustomFilter.cached_filters_for(account_id), @status, following?(account_id))
.map { |filter| serialized_filter(filter) } .map { |filter| serialized_filter(filter) }
payload[:emoji_reactions] = @status.emoji_reactions_grouped_by_name(account)
if payload[:poll] if payload[:poll]
payload[:poll][:voted] = @status.account_id == account_id payload[:poll][:voted] = @status.account_id == account_id

View file

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

View file

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

View file

@ -43,6 +43,14 @@ module HasUserSettings
settings['web.hide_recent_emojis'] settings['web.hide_recent_emojis']
end 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 def setting_default_sensitive
settings['default_sensitive'] settings['default_sensitive']
end end
@ -75,6 +83,10 @@ module HasUserSettings
false false
end end
def setting_emoji_reaction_policy
settings['emoji_reaction_policy']
end
def setting_unfollow_modal def setting_unfollow_modal
settings['web.unfollow_modal'] settings['web.unfollow_modal']
end end
@ -204,7 +216,15 @@ module HasUserSettings
end end
def setting_default_searchability 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 end
def setting_disallow_unlisted_public_searchability def setting_disallow_unlisted_public_searchability

View file

@ -18,7 +18,7 @@ class EmojiReaction < ApplicationRecord
include Paginable include Paginable
EMOJI_REACTION_LIMIT = 32_767 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) update_index('statuses', :status)

View file

@ -37,12 +37,14 @@ class Form::AdminSettings
status_page_url status_page_url
captcha_enabled captcha_enabled
ng_words ng_words
enable_block_emoji_reaction_settings
hide_local_users_for_anonymous hide_local_users_for_anonymous
post_hash_tags_max post_hash_tags_max
sensitive_words sensitive_words
sensitive_words_for_full sensitive_words_for_full
authorized_fetch authorized_fetch
receive_other_servers_emoji_reaction
streaming_other_servers_emoji_reaction
enable_emoji_reaction
).freeze ).freeze
INTEGER_KEYS = %i( INTEGER_KEYS = %i(
@ -64,9 +66,11 @@ class Form::AdminSettings
noindex noindex
require_invite_text require_invite_text
captcha_enabled captcha_enabled
enable_block_emoji_reaction_settings
hide_local_users_for_anonymous hide_local_users_for_anonymous
authorized_fetch authorized_fetch
receive_other_servers_emoji_reaction
streaming_other_servers_emoji_reaction
enable_emoji_reaction
).freeze ).freeze
UPLOAD_KEYS = %i( 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) update_status_stat!(status_referred_by_count: [public_send(:status_referred_by_count) + diff, 0].max)
end 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| (Oj.load(status_stat&.emoji_reactions || '', mode: :strict) || []).tap do |emoji_reactions|
if account.present? if account.present?
remove_emoji_reactions = []
emoji_reactions.each do |emoji_reaction| emoji_reactions.each do |emoji_reaction|
emoji_reaction['me'] = emoji_reaction['account_ids'].include?(account.id.to_s) 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) 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 emoji_reaction['count'] = emoji_reaction['account_ids'].size
remove_emoji_reactions << emoji_reaction if emoji_reaction['count'] <= 0
end end
emoji_reactions - remove_emoji_reactions
end end
end end
end end

View file

@ -12,7 +12,7 @@ class Trends::Tags < Trends::Base
} }
def register(status, at_time = Time.now.utc) 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| status.tags.each do |tag|
add(tag, status.account_id, at_time) if tag.usable? add(tag, status.account_id, at_time) if tag.usable?

View file

@ -26,6 +26,8 @@ class UserSettings
setting :stay_privacy, default: false setting :stay_privacy, default: false
setting :default_reblog_privacy, default: nil setting :default_reblog_privacy, default: nil
setting :default_searchability, default: :direct, in: %w(public private direct limited) 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 :disallow_unlisted_public_searchability, default: false
setting :public_post_to_unlisted, default: false setting :public_post_to_unlisted, default: false
setting :reject_public_unlisted_subscription, default: false setting :reject_public_unlisted_subscription, default: false
@ -34,6 +36,7 @@ class UserSettings
setting :reaction_deck, default: nil setting :reaction_deck, default: nil
setting :stop_emoji_reaction_streaming, default: false setting :stop_emoji_reaction_streaming, default: false
setting :emoji_reaction_streaming_notify_impl2, 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 :unsafe_limited_distribution, default: false
setting :dtl_force_with_tag, default: :none, in: %w(full searchability none) setting :dtl_force_with_tag, default: :none, in: %w(full searchability none)
setting :dtl_force_subscribable, default: false setting :dtl_force_subscribable, default: false
@ -52,6 +55,8 @@ class UserSettings
setting :enable_login_privacy, default: false setting :enable_login_privacy, default: false
setting :enable_dtl_menu, default: false setting :enable_dtl_menu, default: false
setting :hide_recent_emojis, 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 :reblog_modal, default: false
setting :unfollow_modal, default: true setting :unfollow_modal, default: true
setting :reduce_motion, default: false setting :reduce_motion, default: false
@ -80,12 +85,6 @@ class UserSettings
setting :must_be_following_dm, default: false setting :must_be_following_dm, default: false
end 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) def initialize(original_hash)
@original_hash = original_hash || {} @original_hash = original_hash || {}
end end

View file

@ -48,6 +48,8 @@ class InitialStateSerializer < ActiveModel::Serializer
store[:display_media] = object.current_account.user.setting_display_media store[:display_media] = object.current_account.user.setting_display_media
store[:display_media_expand] = object.current_account.user.setting_display_media_expand store[:display_media_expand] = object.current_account.user.setting_display_media_expand
store[:expand_spoilers] = object.current_account.user.setting_expand_spoilers 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_login_privacy] = object.current_account.user.setting_enable_login_privacy
store[:enable_dtl_menu] = object.current_account.user.setting_enable_dtl_menu store[:enable_dtl_menu] = object.current_account.user.setting_enable_dtl_menu
store[:hide_recent_emojis] = object.current_account.user.setting_hide_recent_emojis 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[:display_media] = Setting.display_media
store[:reduce_motion] = Setting.reduce_motion store[:reduce_motion] = Setting.reduce_motion
store[:use_blurhash] = Setting.use_blurhash store[:use_blurhash] = Setting.use_blurhash
store[:enable_emoji_reaction] = Setting.enable_emoji_reaction
end end
store[:disabled_account_id] = object.disabled_account.id.to_s if object.disabled_account 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 end
def other_settings def other_settings
object.suspended? ? {} : object.public_settings object.suspended? ? {} : object.public_settings_for_local
end end
end end

View file

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

View file

@ -125,6 +125,16 @@ class REST::StatusSerializer < ActiveModel::Serializer
object.emoji_reactions_grouped_by_name(current_user&.account) object.emoji_reactions_grouped_by_name(current_user&.account)
end 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 def reactions
emoji_reactions.tap do |rs| emoji_reactions.tap do |rs|
rs.each do |emoji_reaction| rs.each do |emoji_reaction|

View file

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

View file

@ -46,8 +46,10 @@ class EmojiReactService < BaseService
status = emoji_reaction.status status = emoji_reaction.status
if status.account.local? if status.account.local?
LocalNotificationWorker.perform_async(status.account_id, emoji_reaction.id, 'EmojiReaction', 'reaction') if status.account.user&.setting_emoji_reaction_streaming_notify_impl2 if status.account.user&.setting_enable_emoji_reaction
LocalNotificationWorker.perform_async(status.account_id, emoji_reaction.id, 'EmojiReaction', '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? elsif status.account.activitypub?
ActivityPub::DeliveryWorker.perform_async(build_json(emoji_reaction), emoji_reaction.account_id, status.account.inbox_url) ActivityPub::DeliveryWorker.perform_async(build_json(emoji_reaction), emoji_reaction.account_id, status.account.inbox_url)
end end
@ -63,7 +65,7 @@ class EmojiReactService < BaseService
end end
def write_stream(emoji_reaction) 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) } .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 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) 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 update
poll poll
emoji_reaction emoji_reaction
status
warning warning
).freeze ).freeze
@ -53,18 +52,6 @@ class NotifyService < BaseService
@recipient.user.settings['interactions.must_be_following'] && !following_sender? @recipient.user.settings['interactions.must_be_following'] && !following_sender?
end 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? def message?
@notification.type == :mention @notification.type == :mention
end end
@ -134,8 +121,6 @@ class NotifyService < BaseService
blocked ||= optional_non_follower? blocked ||= optional_non_follower?
blocked ||= optional_non_following? blocked ||= optional_non_following?
blocked ||= optional_non_following_and_direct? blocked ||= optional_non_following_and_direct?
blocked ||= optional_non_follower_emoji_reaction?
blocked ||= optional_non_following_emoji_reaction?
blocked ||= conversation_muted? blocked ||= conversation_muted?
blocked ||= blocked_mention? if @notification.type == :mention blocked ||= blocked_mention? if @notification.type == :mention
blocked blocked

View file

@ -11,7 +11,7 @@ class SearchService < BaseService
@offset = options[:type].blank? ? 0 : options[:offset].to_i @offset = options[:type].blank? ? 0 : options[:offset].to_i
@resolve = options[:resolve] || false @resolve = options[:resolve] || false
@following = options[:following] || 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| default_results.tap do |results|
next if @query.blank? || @limit.zero? next if @query.blank? || @limit.zero?

View file

@ -22,22 +22,6 @@ class EmojiReactionValidator < ActiveModel::Validator
end end
def deny_emoji_reactions?(emoji_reaction) def deny_emoji_reactions?(emoji_reaction)
return false unless Setting.enable_block_emoji_reaction_settings !emoji_reaction.status.account.allow_emoji_reaction?(emoji_reaction.account)
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)
end end
end end

View file

@ -13,9 +13,6 @@
.fields-group .fields-group
= f.input :post_hash_tags_max, wrapper: :with_label, as: :integer, label: t('admin.ng_words.post_hash_tags_max') = 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 .fields-group
= f.input :hide_local_users_for_anonymous, wrapper: :with_label, as: :boolean, label: t('admin.ng_words.hide_local_users_for_anonymous') = 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 .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') = 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') %h4= t('admin.settings.discovery.publish_statistics')
.fields-group .fields-group

View file

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

View file

@ -39,6 +39,9 @@
.fields-group .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 = 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 .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') = 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_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', 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') = 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')
= f.simple_fields_for :settings, current_user.settings do |ff| - if Setting.enable_emoji_reaction
.fields-group = f.simple_fields_for :settings, current_user.settings do |ff|
= 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') .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' %h4= t 'preferences.posting_defaults'
.fields-row .fields-row
.fields-group.fields-row__column.fields-row__column-6 .fields-group.fields-row__column.fields-row__column-12
= 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
= 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') = 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 .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') = 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 - if Setting.enable_emoji_reaction
= ff.input :'web.enable_login_privacy', wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_enable_login_privacy'), hint: false .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 - 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 Lockable
include AccountScope include AccountScope
def perform(payload_json, status_id, _my_account_id = nil) def perform(payload_json, status_id, reacted_account_id)
status = Status.find(status_id.to_i) return unless Setting.enable_emoji_reaction
status = Status.find(status_id)
reacted_account = Account.find(reacted_account_id)
if status.present? if status.present?
scope_status(status).includes(:user).find_each do |account| scope = scope_status(status)
redis.publish("timeline:#{account.id}", payload_json) if (account.user.nil? || !account.user&.setting_stop_emoji_reaction_streaming) && redis.exists?("subscribed:timeline:#{account.id}")
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
end end
@ -19,4 +31,21 @@ class DeliveryEmojiReactionWorker
rescue ActiveRecord::RecordNotFound rescue ActiveRecord::RecordNotFound
true true
end 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 end

View file

@ -597,7 +597,6 @@ en:
media_attachments: media_attachments:
title: Media attachments title: Media attachments
ng_words: 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 hide_local_users_for_anonymous: Hide timeline local user posts from anonymous
keywords: Reject keywords keywords: Reject keywords
keywords_hint: The first character of the line is "?". to use regular expressions 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 desc_html: Affects all users who have not changed this setting themselves
title: Opt users out of search engine indexing by default title: Opt users out of search engine indexing by default
discovery: discovery:
emoji_reactions: Stamp
follow_recommendations: Follow recommendations 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. 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 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>. 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 remove: Unlink alias
antennas: antennas:
beta: This function is in beta.
contexts: contexts:
account: Accounts account: Accounts
domain: Domains domain: Domains
@ -1649,6 +1648,10 @@ en:
other: Other other: Other
posting_defaults: Posting defaults posting_defaults: Posting defaults
public_timelines: Public timelines public_timelines: Public timelines
reaching: Visibility and search
search: Search
searchability: Searchability of your post
visibility: Visibility
privacy: 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." 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 privacy: Privacy
@ -1671,6 +1674,7 @@ en:
reactions: reactions:
errors: errors:
banned: Banned reaction from the user banned: Banned reaction from the user
disabled: Stamp is disabled on this server
duplication: Cannot react same things duplication: Cannot react same things
limit_reached: Limit of different reactions reached limit_reached: Limit of different reactions reached
unrecognized_emoji: is not a recognized emoji unrecognized_emoji: is not a recognized emoji
@ -1817,12 +1821,16 @@ en:
searchabilities: searchabilities:
direct: Reactionners direct: Reactionners
direct_long: Reacter of this post can find direct_long: Reacter of this post can find
direct_search_long: You can search you reacted posts only
limited: Self only limited: Self only
limited_long: Nobody can find, but you can limited_long: Nobody can find, but you can
limited_search_long: You can search your posts only
private: Followers and reactionners private: Followers and reactionners
private_long: Your followers and reactionners can find private_long: Your followers and reactionners can find
private_search_long: You can search you are following or reacted posts only
public: Public public: Public
public_long: Anyone can find public_long: Anyone can find
public_search_long: You can search all posts permitted to search
show_more: Show more show_more: Show more
show_newer: Show newer show_newer: Show newer
show_older: Show older show_older: Show older

View file

@ -595,7 +595,6 @@ ja:
media_attachments: media_attachments:
title: 投稿された画像 title: 投稿された画像
ng_words: ng_words:
enable_block_emoji_reaction_settings: 各ユーザーにスタンプ機能のブロック設定項目を解放する
hide_local_users_for_anonymous: ログインしていない状態でローカルユーザーの投稿をタイムラインから取得できないようにする hide_local_users_for_anonymous: ログインしていない状態でローカルユーザーの投稿をタイムラインから取得できないようにする
keywords: 投稿できないキーワード keywords: 投稿できないキーワード
keywords_hint: 行を「?」で始めると、正規表現が使えます keywords_hint: 行を「?」で始めると、正規表現が使えます
@ -801,6 +800,7 @@ ja:
desc_html: この設定を自分で変更していない全ユーザーに影響します desc_html: この設定を自分で変更していない全ユーザーに影響します
title: デフォルトで検索エンジンによるインデックスを拒否する title: デフォルトで検索エンジンによるインデックスを拒否する
discovery: discovery:
emoji_reactions: スタンプ
follow_recommendations: おすすめフォロー follow_recommendations: おすすめフォロー
preamble: Mastodon を知らないユーザーを取り込むには、興味深いコンテンツを浮上させることが重要です。サーバー上で様々なディスカバリー機能がどのように機能するかを制御します。 preamble: Mastodon を知らないユーザーを取り込むには、興味深いコンテンツを浮上させることが重要です。サーバー上で様々なディスカバリー機能がどのように機能するかを制御します。
profile_directory: ディレクトリ profile_directory: ディレクトリ
@ -1017,7 +1017,6 @@ ja:
hint_html: 他のアカウントからこのアカウントにフォロワーを引き継いで引っ越したい場合、ここでエイリアスを作成しておく必要があります。エイリアス自体は<strong>無害で、取り消す</strong>ことができます。<strong>引っ越しは以前のアカウント側から開始する必要があります</strong>。 hint_html: 他のアカウントからこのアカウントにフォロワーを引き継いで引っ越したい場合、ここでエイリアスを作成しておく必要があります。エイリアス自体は<strong>無害で、取り消す</strong>ことができます。<strong>引っ越しは以前のアカウント側から開始する必要があります</strong>。
remove: エイリアスを削除 remove: エイリアスを削除
antennas: antennas:
beta: アンテナ機能はベータ版です。今後、予告なく全データリセット・機能削除を行う場合があります。
contexts: contexts:
account: アカウント account: アカウント
domain: ドメイン domain: ドメイン
@ -1589,6 +1588,10 @@ ja:
other: その他 other: その他
posting_defaults: デフォルトの投稿設定 posting_defaults: デフォルトの投稿設定
public_timelines: 公開タイムライン public_timelines: 公開タイムライン
reaching: 公開範囲と検索
search: 検索
searchability: あなたの投稿の検索許可
visibility: 公開範囲
privacy: 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." 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 privacy: Privacy
@ -1610,6 +1613,7 @@ ja:
reactions: reactions:
errors: errors:
banned: 指定ユーザーからのリアクションは禁止されています banned: 指定ユーザーからのリアクションは禁止されています
disabled: このサーバーではスタンプ機能は無効になっています
duplication: 同じリアクションを複数行おうとしました duplication: 同じリアクションを複数行おうとしました
limit_reached: リアクションの種類が上限に達しました limit_reached: リアクションの種類が上限に達しました
unrecognized_emoji: は絵文字として認識されていません unrecognized_emoji: は絵文字として認識されていません
@ -1750,12 +1754,16 @@ ja:
searchabilities: searchabilities:
direct: 反応者 direct: 反応者
direct_long: この投稿に反応した人しか検索できません direct_long: この投稿に反応した人しか検索できません
direct_search_long: あなたが反応した投稿のみが検索されます
limited: 自分のみ limited: 自分のみ
limited_long: この投稿はあなたしか検索できません limited_long: この投稿はあなたしか検索できません
limited_search_long: あなたの投稿のみが検索されます
private: フォロワーと反応者 private: フォロワーと反応者
private_long: この投稿はフォロワーと反応者のみが検索できます private_long: この投稿はフォロワーと反応者のみが検索できます
private_search_long: あなたのフォロー相手またはあなたが反応した投稿のみが検索されます
public: 全て public: 全て
public_long: この投稿は誰でも検索できます public_long: この投稿は誰でも検索できます
public_search_long: 検索が許可された全ての投稿が検索できます
show_more: もっと見る show_more: もっと見る
show_newer: 新しいものを表示 show_newer: 新しいものを表示
show_older: 古いものを表示 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_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_force_with_tag: "With using #%{tag} tag, your post settings will be changed forcibly"
setting_dtl_menu: Show DTL menu on web 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_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 setting_use_pending_items: Hide timeline updates behind a click instead of automatically scrolling the feed
username: You can use letters, numbers, and underscores 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. 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. 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. 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 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_email: How people can reach you for legal or support inquiries.
site_contact_username: How people can reach you on Mastodon. 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_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. 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 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. theme: Theme that logged out visitors and new users see.
thumbnail: A roughly 2:1 image displayed alongside your server information. 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. 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_language: Posting language
setting_default_privacy: Posting privacy setting_default_privacy: Posting privacy
setting_default_reblog_privacy: Reblogging 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_default_sensitive: Always mark media as sensitive
setting_delete_modal: Show confirmation dialog before deleting a post setting_delete_modal: Show confirmation dialog before deleting a post
setting_disable_swiping: Disable swiping motions 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_subscribable: Ignore your dissubscribable setting when using the DTL tag
setting_dtl_force_with_tag: Post with 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_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_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_expand_spoilers: Always expand posts marked with content warnings
setting_hide_followers_count: Hide followers count setting_hide_followers_count: Hide followers count
setting_hide_following_count: Hide following 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_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_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_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_stay_privacy: Not change privacy after post
setting_stop_emoji_reaction_streaming: Disable stamp streamings setting_stop_emoji_reaction_streaming: Disable stamp streamings
setting_system_font_ui: Use system's default font 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_unsafe_limited_distribution: Send limit posts with unsafe way to other servers
setting_use_blurhash: Show colorful gradients for hidden media setting_use_blurhash: Show colorful gradients for hidden media
setting_use_pending_items: Slow mode setting_use_pending_items: Slow mode
setting_use_public_index: Include permitted accounts post to results of search
severity: Severity severity: Severity
sign_in_token_attempt: Security code sign_in_token_attempt: Security code
title: Title title: Title
@ -298,10 +314,12 @@ en:
closed_registrations_message: Custom message when sign-ups are not available closed_registrations_message: Custom message when sign-ups are not available
content_cache_retention_period: Content cache retention period content_cache_retention_period: Content cache retention period
custom_css: Custom CSS custom_css: Custom CSS
enable_emoji_reaction: Enable stamp function
mascot: Custom mascot (legacy) mascot: Custom mascot (legacy)
media_cache_retention_period: Media cache retention period media_cache_retention_period: Media cache retention period
peers_api_enabled: Publish list of discovered servers in the API peers_api_enabled: Publish list of discovered servers in the API
profile_directory: Enable profile directory profile_directory: Enable profile directory
receive_other_servers_emoji_reaction: Receive stamp between other server users
registrations_mode: Who can sign-up registrations_mode: Who can sign-up
require_invite_text: Require a reason to join require_invite_text: Require a reason to join
show_domain_blocks: Show domain blocks show_domain_blocks: Show domain blocks
@ -313,6 +331,7 @@ en:
site_terms: Privacy Policy site_terms: Privacy Policy
site_title: Server name site_title: Server name
status_page_url: Status page URL status_page_url: Status page URL
streaming_other_servers_emoji_reaction: Streaming stamp between other server users
theme: Default theme theme: Default theme
thumbnail: Server thumbnail thumbnail: Server thumbnail
timeline_preview: Allow unauthenticated access to public timelines timeline_preview: Allow unauthenticated access to public timelines

View file

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

View file

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

View file

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

View file

@ -12,6 +12,7 @@ require_relative 'feeds'
require_relative 'ip_blocks' require_relative 'ip_blocks'
require_relative 'maintenance' require_relative 'maintenance'
require_relative 'media' require_relative 'media'
require_relative 'ohagi'
require_relative 'preview_cards' require_relative 'preview_cards'
require_relative 'search' require_relative 'search'
require_relative 'settings' require_relative 'settings'
@ -65,6 +66,9 @@ module Mastodon::CLI
desc 'maintenance SUBCOMMAND ...ARGS', 'Various maintenance utilities' desc 'maintenance SUBCOMMAND ...ARGS', 'Various maintenance utilities'
subcommand 'maintenance', Maintenance subcommand 'maintenance', Maintenance
desc 'ohagi SUBCOMMAND ...ARGS', 'Ohagis'
subcommand 'ohagi', Ohagi
option :dry_run, type: :boolean option :dry_run, type: :boolean
desc 'self-destruct', 'Erase the server from the federation' desc 'self-destruct', 'Erase the server from the federation'
long_desc <<~LONG_DESC 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' require 'rails_helper'
describe SearchQueryTransformer do 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(:account) { Fabricate(:account) }
let(:parser) { SearchQueryParser.new.parse(query) } 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