Merge branch 'kb_migration' into kb_migration_development

This commit is contained in:
KMY 2023-04-26 11:38:42 +09:00
commit 45f192732f
12 changed files with 169 additions and 35 deletions

View file

@ -31,6 +31,7 @@ class Api::V1::ListsController < Api::BaseController
end
def destroy
raise Mastodon::ValidationError, I18n.t('antennas.errors.remove_list_with_antenna') if Antenna.where(list_id: @list.id).any?
@list.destroy!
render_empty
end

View file

@ -46,6 +46,16 @@ class Follows extends React.PureComponent {
render () {
const { onBack, isLoading, suggestions, account } = this.props;
let loadedContent;
if (isLoading) {
loadedContent = (new Array(8)).fill().map((_, i) => <EmptyAccount key={i} />);
} else if (suggestions.isEmpty()) {
loadedContent = <div className='follow-recommendations__empty'><FormattedMessage id='onboarding.follows.empty' defaultMessage='Unfortunately, no results can be shown right now. You can try using search or browsing the explore page to find people to follow, or try again later.' /></div>;
} else {
loadedContent = suggestions.map(suggestion => <Account id={suggestion.get('account')} key={suggestion.get('account')} />);
}
return (
<Column>
<ColumnBackButton onClick={onBack} />
@ -59,12 +69,10 @@ class Follows extends React.PureComponent {
<ProgressIndicator steps={7} completed={account.get('following_count') * 1} />
<div className='follow-recommendations'>
{isLoading ? (new Array(8)).fill().map((_, i) => <EmptyAccount key={i} />) : suggestions.map(suggestion => (
<Account id={suggestion.get('account')} key={suggestion.get('account')} />
))}
{loadedContent}
</div>
<p className='onboarding__lead'><FormattedHTMLMessage id='onboarding.tips.accounts_from_other_servers' defaultMessage='<strong>Did you know?</strong> Since Mastodon is decentralized, some profiles you come across will be hosted on servers other than yours. And yet you can interact with them seamlessly! Their server is in the second half of their username!' values={{ strong: text => <strong>{text}</strong> }} /></p>
<p className='onboarding__lead'><FormattedHTMLMessage id='onboarding.tips.accounts_from_other_servers' defaultMessage='<strong>Did you know?</strong> Since Mastodon is decentralized, some profiles you come across will be hosted on servers other than yours. And yet you can interact with them seamlessly! Their server is in the second half of their username!' /></p>
<div className='onboarding__footer'>
<button className='link-button' onClick={onBack}><FormattedMessage id='onboarding.actions.back' defaultMessage='Take me back' /></button>

View file

@ -5,14 +5,15 @@ import PropTypes from 'prop-types';
import { me, domain } from 'mastodon/initial_state';
import { connect } from 'react-redux';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { defineMessages, injectIntl, FormattedMessage, FormattedHTMLMessage } from 'react-intl';
import classNames from 'classnames';
import Icon from 'mastodon/components/icon';
import ArrowSmallRight from './components/arrow_small_right';
import { Link } from 'react-router-dom';
import SwipeableViews from 'react-swipeable-views';
const messages = defineMessages({
shareableMessage: { id: 'onboarding.share.message', defaultMessage: 'I\'m {username} on Mastodon! Come follow me at {url}' },
shareableMessage: { id: 'onboarding.share.message', defaultMessage: 'I\'m {username} on #Mastodon! Come follow me at {url}' },
});
const mapStateToProps = state => ({
@ -80,6 +81,60 @@ class CopyPasteText extends React.PureComponent {
}
class TipCarousel extends React.PureComponent {
static propTypes = {
children: PropTypes.node,
};
state = {
index: 0,
};
handleSwipe = index => {
this.setState({ index });
};
handleChangeIndex = e => {
this.setState({ index: Number(e.currentTarget.getAttribute('data-index')) });
};
handleKeyDown = e => {
switch(e.key) {
case 'ArrowLeft':
e.preventDefault();
this.setState(({ index }, { children }) => ({ index: Math.abs(index - 1) % children.length }));
break;
case 'ArrowRight':
e.preventDefault();
this.setState(({ index }, { children }) => ({ index: (index + 1) % children.length }));
break;
}
};
render () {
const { children } = this.props;
const { index } = this.state;
return (
<div className='tip-carousel' tabIndex='0' onKeyDown={this.handleKeyDown}>
<SwipeableViews onChangeIndex={this.handleSwipe} index={index} enableMouseEvents tabIndex='-1'>
{children}
</SwipeableViews>
<div className='media-modal__pagination'>
{children.map((_, i) => (
<button key={i} className={classNames('media-modal__page-dot', { active: i === index })} data-index={i} onClick={this.handleChangeIndex}>
{i + 1}
</button>
))}
</div>
</div>
);
}
}
class Share extends React.PureComponent {
static propTypes = {
@ -105,6 +160,12 @@ class Share extends React.PureComponent {
<CopyPasteText value={intl.formatMessage(messages.shareableMessage, { username: `@${account.get('username')}@${domain}`, url })} />
<TipCarousel>
<div><p className='onboarding__lead'><FormattedHTMLMessage id='onboarding.tips.verification' defaultMessage='<strong>Did you know?</strong> You can verify your account by putting a link to your Mastodon profile on your own website and adding the website to your profile. No fees or documents necessary!' /></p></div>
<div><p className='onboarding__lead'><FormattedHTMLMessage id='onboarding.tips.migration' defaultMessage='<strong>Did you know?</strong> If you feel like {domain} is not a great server choice for you in the future, you can move to another Mastodon server without losing your followers. You can even host your own server!' values={{ domain }} /></p></div>
<div><p className='onboarding__lead'><FormattedHTMLMessage id='onboarding.tips.2fa' defaultMessage='<strong>Did you know?</strong> You can secure your account by setting up two-factor authentication in your account settings. It works with any TOTP app of your choice, no phone number necessary!' /></p></div>
</TipCarousel>
<p className='onboarding__lead'><FormattedMessage id='onboarding.share.next_steps' defaultMessage='Possible next steps:' /></p>
<div className='onboarding__links'>

View file

@ -3186,6 +3186,10 @@
},
{
"descriptors": [
{
"defaultMessage": "Unfortunately, no results can be shown right now. You can try using search or browsing the explore page to find people to follow, or try again later.",
"id": "onboarding.follows.empty"
},
{
"defaultMessage": "Popular on Mastodon",
"id": "onboarding.follows.title"
@ -3265,7 +3269,7 @@
{
"descriptors": [
{
"defaultMessage": "I'm {username} on Mastodon! Come follow me at {url}",
"defaultMessage": "I'm {username} on #Mastodon! Come follow me at {url}",
"id": "onboarding.share.message"
},
{
@ -3284,6 +3288,18 @@
"defaultMessage": "Let people know how they can find you on Mastodon!",
"id": "onboarding.share.lead"
},
{
"defaultMessage": "<strong>Did you know?</strong> You can verify your account by putting a link to your Mastodon profile on your own website and adding the website to your profile. No fees or documents necessary!",
"id": "onboarding.tips.verification"
},
{
"defaultMessage": "<strong>Did you know?</strong> If you feel like {domain} is not a great server choice for you in the future, you can move to another Mastodon server without losing your followers. You can even host your own server!",
"id": "onboarding.tips.migration"
},
{
"defaultMessage": "<strong>Did you know?</strong> You can secure your account by setting up two-factor authentication in your account settings. It works with any TOTP app of your choice, no phone number necessary!",
"id": "onboarding.tips.2fa"
},
{
"defaultMessage": "Possible next steps:",
"id": "onboarding.share.next_steps"

View file

@ -447,10 +447,11 @@
"onboarding.actions.close": "Don't show this screen again",
"onboarding.actions.go_to_explore": "See what's trending",
"onboarding.actions.go_to_home": "Go to your home feed",
"onboarding.follows.empty": "Unfortunately, no results can be shown right now. You can try using search or browsing the explore page to find people to follow, or try again later.",
"onboarding.follows.lead": "You curate your own home feed. The more people you follow, the more active and interesting it will be. These profiles may be a good starting point—you can always unfollow them later!",
"onboarding.follows.title": "Popular on Mastodon",
"onboarding.share.lead": "Let people know how they can find you on Mastodon!",
"onboarding.share.message": "I'm {username} on Mastodon! Come follow me at {url}",
"onboarding.share.message": "I'm {username} on #Mastodon! Come follow me at {url}",
"onboarding.share.next_steps": "Possible next steps:",
"onboarding.share.title": "Share your profile",
"onboarding.start.lead": "Your new Mastodon account is ready to go. Here's how you can make the most of it:",
@ -464,7 +465,10 @@
"onboarding.steps.setup_profile.title": "Customize your profile",
"onboarding.steps.share_profile.body": "Let your friends know how to find you on Mastodon!",
"onboarding.steps.share_profile.title": "Share your profile",
"onboarding.tips.2fa": "<strong>Did you know?</strong> You can secure your account by setting up two-factor authentication in your account settings. It works with any TOTP app of your choice, no phone number necessary!",
"onboarding.tips.accounts_from_other_servers": "<strong>Did you know?</strong> Since Mastodon is decentralized, some profiles you come across will be hosted on servers other than yours. And yet you can interact with them seamlessly! Their server is in the second half of their username!",
"onboarding.tips.migration": "<strong>Did you know?</strong> If you feel like {domain} is not a great server choice for you in the future, you can move to another Mastodon server without losing your followers. You can even host your own server!",
"onboarding.tips.verification": "<strong>Did you know?</strong> You can verify your account by putting a link to your Mastodon profile on your own website and adding the website to your profile. No fees or documents necessary!",
"password_confirmation.exceeds_maxlength": "Password confirmation exceeds the maximum password length",
"password_confirmation.mismatching": "Password confirmation does not match",
"picture_in_picture.restore": "Put it back",

View file

@ -3080,6 +3080,29 @@ $ui-header-height: 55px;
.account:last-child {
border-bottom: 0;
}
&__empty {
text-align: center;
color: $darker-text-color;
font-weight: 500;
padding: 40px;
}
}
.tip-carousel {
border: 1px solid transparent;
border-radius: 8px;
padding: 16px;
margin-bottom: 30px;
&:focus {
outline: 0;
border-color: $highlight-text-color;
}
.media-modal__pagination {
margin-bottom: 0;
}
}
.copy-paste-text {
@ -5617,6 +5640,11 @@ a.status-card.compact:hover {
&.active {
opacity: 1;
}
&:focus {
outline: 0;
background-color: $highlight-text-color;
}
}
.media-modal__close {

View file

@ -17,6 +17,9 @@
# updated_at :datetime not null
# expires_at :datetime
# with_media_only :boolean default(FALSE), not null
# exclude_domains :jsonb
# exclude_accounts :jsonb
# exclude_tags :jsonb
#
class Antenna < ApplicationRecord
include Expireable
@ -86,7 +89,7 @@ class Antenna < ApplicationRecord
def keywords_raw=(raw)
keywords = raw.split(/\R/).filter { |r| r.present? && r.length >= 2 }.uniq
self[:keywords] = keywords
self[:any_keywords] = !keywords.any? && !exclude_keywords&.any?
self[:any_keywords] = !keywords.any?
end
def exclude_keywords_raw
@ -98,7 +101,6 @@ class Antenna < ApplicationRecord
def exclude_keywords_raw=(raw)
exclude_keywords = raw.split(/\R/).filter { |r| r.present? }.uniq
self[:exclude_keywords] = exclude_keywords
self[:any_keywords] = !keywords&.any? && !exclude_keywords.any?
end
def tags_raw
@ -118,18 +120,19 @@ class Antenna < ApplicationRecord
end
def exclude_tags_raw
antenna_tags.where(exclude: true).map(&:tag).map(&:name).join("\n")
return '' if !exclude_tags.present?
Tag.where(id: exclude_tags).map(&:name).join("\n")
end
def exclude_tags_raw=(raw)
return if exclude_tags_raw == raw
tags = []
tag_names = raw.split(/\R/).filter { |r| r.present? }.map { |r| r.start_with?('#') ? r[1..-1] : r }.uniq
antenna_tags.where(exclude: true).destroy_all
Tag.find_or_create_by_names(tag_names).each do |tag|
antenna_tags.create!(tag: tag, exclude: true)
tags << tag.id
end
self[:exclude_tags] = tags
end
def domains_raw
@ -149,18 +152,15 @@ class Antenna < ApplicationRecord
end
def exclude_domains_raw
antenna_domains.where(exclude: true).map(&:name).join("\n")
return '' if !exclude_domains.present?
exclude_domains.join("\n")
end
def exclude_domains_raw=(raw)
return if exclude_domains_raw == raw
domain_names = raw.split(/\R/).filter { |r| r.present? }.uniq
antenna_domains.where(exclude: true).destroy_all
domain_names.each do |domain|
antenna_domains.create!(name: domain, exclude: true)
end
self[:exclude_domains] = domain_names
end
def accounts_raw
@ -186,7 +186,8 @@ class Antenna < ApplicationRecord
end
def exclude_accounts_raw
antenna_accounts.where(exclude: true).map(&:account).map { |account| account.domain ? "@#{account.username}@#{account.domain}" : "@#{account.username}" }.join("\n")
return '' if !exclude_accounts.present?
Account.where(id: exclude_accounts).map { |account| account.domain ? "@#{account.username}@#{account.domain}" : "@#{account.username}" }.join("\n")
end
def exclude_accounts_raw=(raw)
@ -194,16 +195,15 @@ class Antenna < ApplicationRecord
account_names = raw.split(/\R/).filter { |r| r.present? }.map { |r| r.start_with?('@') ? r[1..-1] : r }.uniq
hit = false
antenna_accounts.where(exclude: true).destroy_all
accounts = []
account_names.each do |name|
username, domain = name.split('@')
account = Account.find_by(username: username, domain: domain)
if account.present?
antenna_accounts.create!(account: account, exclude: true)
hit = true
accounts << account.id
end
end
self[:exclude_accounts] = accounts
end
end

View file

@ -118,30 +118,34 @@ class FanOutOnWriteService < BaseService
def deliver_to_antennas!
lists = []
tag_ids = @status.tags.pluck(:id)
domain = @account.domain || Rails.configuration.x.local_domain
antennas = Antenna.availables
antennas = antennas.left_joins(:antenna_accounts).where(any_accounts: true).or(Antenna.availables.left_joins(:antenna_accounts) .where(antenna_accounts: { exclude: false, account: @status.account }))
antennas = antennas.left_joins(:antenna_domains) .where(any_domains: true) .or(Antenna.availables.left_joins(:antenna_accounts).left_joins(:antenna_domains) .where(antenna_domains: { exclude: false, name: @status.account.domain }))
antennas = antennas.left_joins(:antenna_tags) .where(any_tags: true) .or(Antenna.availables.left_joins(:antenna_accounts).left_joins(:antenna_domains).left_joins(:antenna_tags).where(antenna_tags: { exclude: false, tag: @status.tags }))
antennas = antennas.where(account: @status.account.followers) if @status.visibility.to_sym == :unlisted
antennas = antennas.where(with_media_only: false) if !@status.with_media?
antennas = antennas.where.not(account: @status.account.blocking)
antennas = antennas.includes(:antenna_accounts).includes(:antenna_domains).includes(:antenna_tags)
antennas.in_batches do |ans|
ans.each do |antenna|
next if !antenna.enabled?
next if @status.account.blocking?(antenna.account)
next if antenna.keywords.any? && !([nil, :public].include?(@status.searchability&.to_sym))
next if antenna.keywords.any? && !antenna.keywords.any? { |keyword| @status.text.include?(keyword) }
next if antenna.exclude_keywords.any? && antenna.exclude_keywords.any? { |keyword| @status.text.include?(keyword) }
next if antenna.antenna_accounts.where(exclude: true, account: @status.account).any?
next if antenna.antenna_domains.where(exclude: true, name: @status.account.domain).any?
next if antenna.antenna_tags.where(exclude: true, tag: @status.tags).any?
lists << antenna.list
next if antenna.exclude_keywords&.any? { |keyword| @status.text.include?(keyword) }
next if antenna.exclude_accounts&.include?(@status.account_id)
next if antenna.exclude_domains&.include?(domain)
next if antenna.exclude_tags&.any? { |tag_id| tag_ids.include?(tag_id) }
lists << antenna.list_id
end
end
lists = lists.uniq
if lists.any?
FeedInsertWorker.push_bulk(lists) do |list|
[@status.id, list.id, 'list', { 'update' => update? }]
[@status.id, list, 'list', { 'update' => update? }]
end
end
end

View file

@ -994,6 +994,7 @@ en:
empty_contexts: No contexts! You must set any context filters
invalid_context: None or invalid context supplied
invalid_list_owner: This list is not yours
remove_list_with_antenna: Cannot remove list because this list is related to antenna.
index:
contexts: Antennas in %{contexts}
delete: Delete

View file

@ -945,7 +945,7 @@ ja:
hint_html: 他のアカウントからこのアカウントにフォロワーを引き継いで引っ越したい場合、ここでエイリアスを作成しておく必要があります。エイリアス自体は<strong>無害で、取り消す</strong>ことができます。<strong>引っ越しは以前のアカウント側から開始する必要があります</strong>。
remove: エイリアスを削除
antennas:
beta: アンテナ機能はベータ版です。今後、予告なく全データリセット・機能削除を行う場合があります。この機能の存在は外部に積極的に宣伝しないよう、ご協力をお願いします。
beta: アンテナ機能はベータ版です。今後、予告なく全データリセット・機能削除を行う場合があります。
contexts:
account: アカウント
domain: ドメイン
@ -954,12 +954,13 @@ ja:
errors:
empty_contexts: 絞り込み条件が1つも指定されていないため無効です除外条件はカウントされません
invalid_list_owner: これはあなたのリストではありません
remove_list_with_antenna: アンテナが関連付けられているリストは削除できません
edit:
accounts_hint: ローカルアカウントの場合は「@info」、リモートアカウントの場合は「@info@example.com」の形式で指定します。サーバーが認識していないアカウントは保存時に自動的に削除されます。
accounts_raw: 絞り込むアカウント
available: 有効
description: アンテナは、サーバーが認識した全ての公開・ローカル公開投稿のうち、検索許可が「公開」または明示的に設定されていないもの(検索許可システムに対応していないサーバーからの投稿)、かつ購読を拒否していないすべてのアカウントからの投稿が対象です。検出された投稿は、指定したリストに追加されます。
domains_hint: ドメインとは、アカウントIDやサイトのURLのうち「kmy.blue」「example.com」に該当する部分です。自身のサーバーを指定することはできません
domains_hint: ドメインとは、アカウントIDやサイトのURLのうち「kmy.blue」「example.com」に該当する部分です
domains_raw: 絞り込むドメイン
exclude_accounts_raw: 除外するアカウント
exclude_domains_raw: 除外するドメイン

View file

@ -0,0 +1,7 @@
class AddExcludesToAntennas < ActiveRecord::Migration[6.1]
def change
add_column :antennas, :exclude_domains, :jsonb
add_column :antennas, :exclude_accounts, :jsonb
add_column :antennas, :exclude_tags, :jsonb
end
end

View file

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2023_04_23_233429) do
ActiveRecord::Schema.define(version: 2023_04_26_013738) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@ -302,6 +302,9 @@ ActiveRecord::Schema.define(version: 2023_04_23_233429) do
t.datetime "updated_at", null: false
t.datetime "expires_at"
t.boolean "with_media_only", default: false, null: false
t.jsonb "exclude_domains"
t.jsonb "exclude_accounts"
t.jsonb "exclude_tags"
t.index ["account_id"], name: "index_antennas_on_account_id"
t.index ["any_accounts"], name: "index_antennas_on_any_accounts"
t.index ["any_domains"], name: "index_antennas_on_any_domains"