Add: #8 サークル投稿の転送 (#294)

* Add: `conversations`テーブルに`ancestor_status`プロパティ

* Fix test

* Fix test more

* Add: `limited_visibility`に`Reply`を追加、`context`のURI

* Add: 外部からの`context`受信処理

* Fix test

* Add: 公開範囲「返信」

* Fix test

* Fix: 返信に返信以外の公開範囲を設定できない問題

* Add: ローカル投稿時にメンション追加・他サーバーへの転送

* Fix test

* Fix test

* Test: ローカルスレッドへの返信投稿の転送

* Test: 未知のアカウントからのメンション

* Add: 編集・削除の連合に対応

* Remove: 重複テスト

* Fix: 改善

* Add: 編集削除の転送処理・返信なのにsilentなメンションでの通知

* Fix: リプライが第三者に届かない問題

* Add: `always_sign_unsafe`

* Add: Subject

* Remove space

* Fix: 他人のスレッドの送信先一覧を非表示

* Fix: おかしいコード
This commit is contained in:
KMY(雪あすか) 2023-11-30 09:29:24 +09:00 committed by GitHub
parent a52a8ce214
commit a88349af55
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
42 changed files with 1115 additions and 77 deletions

View file

@ -61,16 +61,6 @@ export const defaultMediaVisibility = (status) => {
};
const messages = defineMessages({
public_short: { id: 'privacy.public.short', defaultMessage: 'Public' },
unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' },
public_unlisted_short: { id: 'privacy.public_unlisted.short', defaultMessage: 'Public unlisted' },
login_short: { id: 'privacy.login.short', defaultMessage: 'Login only' },
private_short: { id: 'privacy.private.short', defaultMessage: 'Followers only' },
limited_short: { id: 'privacy.limited.short', defaultMessage: 'Limited menbers only' },
mutual_short: { id: 'privacy.mutual.short', defaultMessage: 'Mutual followers only' },
circle_short: { id: 'privacy.circle.short', defaultMessage: 'Circle members only' },
personal_short: { id: 'privacy.personal.short', defaultMessage: 'Yourself only' },
direct_short: { id: 'privacy.direct.short', defaultMessage: 'Mentioned people only' },
edited: { id: 'status.edited', defaultMessage: 'Edited {date}' },
});

View file

@ -76,16 +76,6 @@ export const defaultMediaVisibility = (status) => {
};
const messages = defineMessages({
public_short: { id: 'privacy.public.short', defaultMessage: 'Public' },
unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' },
public_unlisted_short: { id: 'privacy.public_unlisted.short', defaultMessage: 'Public unlisted' },
login_short: { id: 'privacy.login.short', defaultMessage: 'Login only' },
private_short: { id: 'privacy.private.short', defaultMessage: 'Followers only' },
limited_short: { id: 'privacy.limited.short', defaultMessage: 'Limited menbers only' },
mutual_short: { id: 'privacy.mutual.short', defaultMessage: 'Mutual followers only' },
circle_short: { id: 'privacy.circle.short', defaultMessage: 'Circle members only' },
personal_short: { id: 'privacy.personal.short', defaultMessage: 'Yourself only' },
direct_short: { id: 'privacy.direct.short', defaultMessage: 'Mentioned people only' },
edited: { id: 'status.edited', defaultMessage: 'Edited {date}' },
});

View file

@ -336,7 +336,7 @@ class StatusActionBar extends ImmutablePureComponent {
}
if (signedIn) {
if (writtenByMe) {
if (writtenByMe && status.get('limited_scope') !== 'reply') {
menu.push({ text: intl.formatMessage(messages.mentions), action: this.handleOpenMentions });
}

View file

@ -8,6 +8,7 @@ import { ReactComponent as LoginIcon } from '@material-symbols/svg-600/outlined/
import { ReactComponent as LockIcon } from '@material-symbols/svg-600/outlined/lock.svg';
import { ReactComponent as LockOpenIcon } from '@material-symbols/svg-600/outlined/no_encryption.svg';
import { ReactComponent as PublicIcon } from '@material-symbols/svg-600/outlined/public.svg';
import { ReactComponent as ReplyIcon } from '@material-symbols/svg-600/outlined/reply.svg';
import { ReactComponent as LimitedIcon } from '@material-symbols/svg-600/outlined/shield.svg';
import { ReactComponent as PersonalIcon } from '@material-symbols/svg-600/outlined/sticky_note.svg';
@ -23,6 +24,7 @@ type Visibility =
| 'mutual'
| 'circle'
| 'personal'
| 'reply'
| 'limited';
const messages = defineMessages({
@ -49,6 +51,10 @@ const messages = defineMessages({
id: 'privacy.circle.short',
defaultMessage: 'Circle members only',
},
reply_short: {
id: 'privacy.reply.short',
defaultMessage: 'Reply',
},
personal_short: {
id: 'privacy.personal.short',
defaultMessage: 'Yourself only',
@ -105,6 +111,11 @@ export const VisibilityIcon: React.FC<{ visibility: Visibility }> = ({
iconComponent: CircleIcon,
text: intl.formatMessage(messages.circle_short),
},
reply: {
icon: 'reply',
iconComponent: ReplyIcon,
text: intl.formatMessage(messages.reply_short),
},
personal: {
icon: 'sticky-note-o',
iconComponent: PersonalIcon,

View file

@ -14,6 +14,7 @@ import { ReactComponent as LoginIcon } from '@material-symbols/svg-600/outlined/
import { ReactComponent as LockIcon } from '@material-symbols/svg-600/outlined/lock.svg';
import { ReactComponent as LockOpenIcon } from '@material-symbols/svg-600/outlined/no_encryption.svg';
import { ReactComponent as PublicIcon } from '@material-symbols/svg-600/outlined/public.svg';
import { ReactComponent as ReplyIcon } from '@material-symbols/svg-600/outlined/reply.svg';
import { supportsPassiveEvents } from 'detect-passive-events';
import Overlay from 'react-overlays/Overlay';
@ -38,6 +39,8 @@ const messages = defineMessages({
mutual_long: { id: 'privacy.mutual.long', defaultMessage: 'Mutual follows only' },
circle_short: { id: 'privacy.circle.short', defaultMessage: 'Circle' },
circle_long: { id: 'privacy.circle.long', defaultMessage: 'Circle members only' },
reply_short: { id: 'privacy.reply.short', defaultMessage: 'Reply' },
reply_long: { id: 'privacy.reply.long', defaultMessage: 'Reply to limited post' },
direct_short: { id: 'privacy.direct.short', defaultMessage: 'Mentioned people only' },
direct_long: { id: 'privacy.direct.long', defaultMessage: 'Visible for mentioned users only' },
change_privacy: { id: 'privacy.change', defaultMessage: 'Adjust status privacy' },
@ -166,6 +169,7 @@ class PrivacyDropdown extends PureComponent {
value: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
noDirect: PropTypes.bool,
replyToLimited: PropTypes.bool,
container: PropTypes.func,
disabled: PropTypes.bool,
intl: PropTypes.object.isRequired,
@ -280,10 +284,22 @@ class PrivacyDropdown extends PureComponent {
};
render () {
const { value, container, disabled, intl } = this.props;
const { value, container, disabled, intl, replyToLimited } = this.props;
const { open, placement } = this.state;
const valueOption = this.options.find(item => item.value === value) || this.options[0];
if (replyToLimited) {
if (!this.selectableOptions.some((op) => op.value === 'reply')) {
this.selectableOptions.unshift(
{ icon: 'reply', iconComponent: ReplyIcon, value: 'reply', text: intl.formatMessage(messages.reply_short), meta: intl.formatMessage(messages.reply_long) },
);
}
} else {
if (this.selectableOptions.some((op) => op.value === 'reply')) {
this.selectableOptions = this.selectableOptions.filter((op) => op.value !== 'reply');
}
}
const valueOption = this.selectableOptions.find(item => item.value === value) || this.selectableOptions[0];
return (
<div ref={this.setTargetRef} onKeyDown={this.handleKeyDown}>

View file

@ -7,6 +7,7 @@ import PrivacyDropdown from '../components/privacy_dropdown';
const mapStateToProps = state => ({
value: state.getIn(['compose', 'privacy']),
replyToLimited: state.getIn(['compose', 'reply_to_limited']),
});
const mapDispatchToProps = dispatch => ({

View file

@ -287,7 +287,10 @@ class ActionBar extends PureComponent {
menu.push(null);
}
menu.push({ text: intl.formatMessage(messages.mentions), action: this.handleOpenMentions });
if (status.get('limited_scope') !== 'reply') {
menu.push({ text: intl.formatMessage(messages.mentions), action: this.handleOpenMentions });
}
menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
menu.push(null);
menu.push({ text: intl.formatMessage(messages.edit), action: this.handleEditClick });

View file

@ -77,6 +77,7 @@ const initialState = ImmutableMap({
caretPosition: null,
preselectDate: null,
in_reply_to: null,
reply_to_limited: false,
is_composing: false,
is_submitting: false,
is_changing_upload: false,
@ -114,6 +115,10 @@ const initialPoll = ImmutableMap({
});
function statusToTextMentions(state, status) {
if (status.get('visibility_ex') === 'limited') {
return '';
}
let set = ImmutableOrderedSet([]);
if (status.getIn(['account', 'id']) !== me) {
@ -144,6 +149,7 @@ function clearAll(state) {
if (!state.get('in_reply_to')) {
map.set('posted_on_this_session', true);
}
map.set('reply_to_limited', false);
map.set('limited_scope', null);
map.set('id', null);
map.set('in_reply_to', null);
@ -411,7 +417,12 @@ export default function compose(state = initialState, action) {
map.set('id', null);
map.set('in_reply_to', action.status.get('id'));
map.set('text', statusToTextMentions(state, action.status));
map.set('privacy', privacyPreference(action.status.get('visibility_ex'), state.get('default_privacy')));
map.set('reply_to_limited', action.status.get('visibility_ex') === 'limited');
if (action.status.get('visibility_ex') === 'limited') {
map.set('privacy', 'reply');
} else {
map.set('privacy', privacyPreference(action.status.get('visibility_ex'), state.get('default_privacy')));
}
map.set('limited_scope', null);
map.set('searchability', privacyPreference(action.status.get('searchability'), state.get('default_searchability')));
map.set('focusDate', new Date());
@ -521,7 +532,11 @@ export default function compose(state = initialState, action) {
return state.set('tagHistory', fromJS(action.tags));
case TIMELINE_DELETE:
if (action.id === state.get('in_reply_to')) {
return state.set('in_reply_to', null);
if (state.get('privacy') === 'reply') {
return state.set('in_reply_to', null).set('privacy', 'circle');
} else {
return state.set('in_reply_to', null);
}
} else if (action.id === state.get('id')) {
return state.set('id', null);
} else {
@ -549,6 +564,7 @@ export default function compose(state = initialState, action) {
map.set('text', action.raw_text || unescapeHTML(expandMentions(action.status)));
map.set('in_reply_to', action.status.get('in_reply_to_id'));
map.set('privacy', action.status.get('visibility_ex'));
map.set('reply_to_limited', action.status.get('limited_scope') === 'reply');
map.set('limited_scope', null);
map.set('media_attachments', action.status.get('media_attachments').map((media) => media.set('unattached', true)));
map.set('focusDate', new Date());
@ -583,8 +599,9 @@ export default function compose(state = initialState, action) {
if (action.status.get('visibility_ex') !== 'limited') {
map.set('privacy', action.status.get('visibility_ex'));
} else {
map.set('privacy', action.status.get('limited_scope') === 'mutual' ? 'mutual' : 'circle');
map.set('privacy', action.status.get('limited_scope') || 'circle');
}
map.set('reply_to_limited', action.status.get('limited_scope') === 'reply');
map.set('limited_scope', action.status.get('limited_scope'));
map.set('media_attachments', action.status.get('media_attachments'));
map.set('focusDate', new Date());