diff --git a/packages/pl-api/lib/features.ts b/packages/pl-api/lib/features.ts index 006c578d3..a59b99975 100644 --- a/packages/pl-api/lib/features.ts +++ b/packages/pl-api/lib/features.ts @@ -571,6 +571,12 @@ const getFeatures = (instance: Instance) => { v.software === PLEROMA, ]), + /** + * Ability to post statuses to the recipients of parent post. + * @see POST /api/v1/statuses + */ + createStatusConversationScope: v.software === MITRA, + /** * @see POST /api/v1/statuses */ diff --git a/packages/pl-api/lib/params/statuses.ts b/packages/pl-api/lib/params/statuses.ts index bfb7105e3..b2688cba9 100644 --- a/packages/pl-api/lib/params/statuses.ts +++ b/packages/pl-api/lib/params/statuses.ts @@ -49,6 +49,7 @@ interface CreateStatusOptionalParams { * `local` — Requires features{@link Features['createStatusLocalScope']}. * `mutuals_only` — Requires features{@link Features['createStatusMutualsOnlyScope']}. * `subscribers` — Requires features{@link Features['createStatusSubscribersScope']}. + * `conversation` — Requires features{@link Features['createStatusConversationScope']}. * `list:LIST_ID` — Requires features{@link Features['createStatusListScope']}. * `circle:LIST_ID` — Requires features{@link Features['circles']}. */ diff --git a/packages/pl-fe/src/actions/compose.ts b/packages/pl-fe/src/actions/compose.ts index d7edbe89b..83a01d83b 100644 --- a/packages/pl-fe/src/actions/compose.ts +++ b/packages/pl-fe/src/actions/compose.ts @@ -183,6 +183,7 @@ interface ComposeReplyAction { preserveSpoilers: boolean; rebloggedBy?: Pick; approvalRequired?: boolean; + conversationScope: boolean; } const replyCompose = ( @@ -208,6 +209,7 @@ const replyCompose = ( preserveSpoilers, rebloggedBy, approvalRequired, + conversationScope: features.createStatusConversationScope, }); useModalsStore.getState().openModal('COMPOSE'); }; @@ -223,13 +225,15 @@ interface ComposeQuoteAction { status: Pick; account: Pick | undefined; explicitAddressing: boolean; + conversationScope: boolean; } const quoteCompose = (status: ComposeQuoteAction['status']) => (dispatch: AppDispatch, getState: () => RootState) => { const state = getState(); const { forceImplicitAddressing } = useSettingsStore.getState().settings; - const explicitAddressing = state.auth.client.features.createStatusExplicitAddressing && !forceImplicitAddressing; + const { createStatusConversationScope, createStatusExplicitAddressing } = state.auth.client.features; + const explicitAddressing = createStatusExplicitAddressing && !forceImplicitAddressing; dispatch({ type: COMPOSE_QUOTE, @@ -237,6 +241,7 @@ const quoteCompose = (status: ComposeQuoteAction['status']) => status, account: selectOwnAccount(state), explicitAddressing, + conversationScope: createStatusConversationScope, }); useModalsStore.getState().openModal('COMPOSE'); }; diff --git a/packages/pl-fe/src/features/compose/components/privacy-dropdown.tsx b/packages/pl-fe/src/features/compose/components/privacy-dropdown.tsx index 7731720d8..37446a7b1 100644 --- a/packages/pl-fe/src/features/compose/components/privacy-dropdown.tsx +++ b/packages/pl-fe/src/features/compose/components/privacy-dropdown.tsx @@ -20,6 +20,8 @@ const messages = defineMessages({ unlisted_long: { id: 'privacy.unlisted.long', defaultMessage: 'Do not post to public timelines' }, private_short: { id: 'privacy.private.short', defaultMessage: 'Followers-only' }, private_long: { id: 'privacy.private.long', defaultMessage: 'Post to followers only' }, + conversation_short: { id: 'privacy.conversation.short', defaultMessage: 'Conversation' }, + conversation_long: { id: 'privacy.conversation.long', defaultMessage: 'Post to recipients of the parent post' }, mutuals_only_short: { id: 'privacy.mutuals_only.short', defaultMessage: 'Mutuals-only' }, mutuals_only_long: { id: 'privacy.mutuals_only.long', defaultMessage: 'Post to mutually followed users only' }, direct_short: { id: 'privacy.direct.short', defaultMessage: 'Direct' }, @@ -45,7 +47,7 @@ interface Option { items?: Array>; } -const getItems = (features: Features, lists: ReturnType, circles: Array, intl: IntlShape) => [ +const getItems = (features: Features, lists: ReturnType, circles: Array, isReply: boolean, intl: IntlShape) => [ { icon: require('@phosphor-icons/core/regular/globe.svg'), value: 'public', @@ -64,6 +66,12 @@ const getItems = (features: Features, lists: ReturnType, text: intl.formatMessage(messages.private_short), meta: intl.formatMessage(messages.private_long), }, + isReply && features.createStatusConversationScope ? { + icon: require('@phosphor-icons/core/regular/chats-circle.svg'), + value: 'conversation', + text: intl.formatMessage(messages.conversation_short), + meta: intl.formatMessage(messages.conversation_long), + } : undefined, features.createStatusMutualsOnlyScope ? { icon: require('@phosphor-icons/core/regular/users-three.svg'), value: 'mutuals_only', @@ -127,13 +135,15 @@ const PrivacyDropdown: React.FC = ({ const { data: lists = [] } = useLists(getOrderedLists); const { data: circles = [] } = useCircles(getOrderedLists); + const isReply = !!compose.in_reply_to; + const value = compose.privacy; const unavailable = compose.id; const onChange = (value: string) => value && dispatch(changeComposeVisibility(composeId, value)); - const options = useMemo(() => getItems(features, lists, circles, intl), [features, lists, circles]); + const options = useMemo(() => getItems(features, lists, circles, isReply, intl), [features, lists, circles, isReply]); const items: Array = options.map(item => ({ ...item, action: item.value ? () => onChange(item.value) : undefined, diff --git a/packages/pl-fe/src/locales/en.json b/packages/pl-fe/src/locales/en.json index 257e8e669..3decb407e 100644 --- a/packages/pl-fe/src/locales/en.json +++ b/packages/pl-fe/src/locales/en.json @@ -1456,6 +1456,8 @@ "privacy.change": "Adjust post privacy", "privacy.circle.long": "Visible to members of a circle", "privacy.circle.short": "Circle only", + "privacy.conversation.long": "Post to recipients of the parent post", + "privacy.conversation.short": "Conversation", "privacy.direct.long": "Post to mentioned users only", "privacy.direct.short": "Direct", "privacy.list.long": "Visible to members of a list", diff --git a/packages/pl-fe/src/reducers/compose.ts b/packages/pl-fe/src/reducers/compose.ts index 9774eb10c..d8b92a370 100644 --- a/packages/pl-fe/src/reducers/compose.ts +++ b/packages/pl-fe/src/reducers/compose.ts @@ -271,7 +271,9 @@ const updateSuggestionTags = (compose: Compose, token: string, tags: Tag[]) => { compose.suggestion_token = token; }; -const privacyPreference = (a: string, b: string, list_id: number | null) => { +const privacyPreference = (a: string, b: string, list_id: number | null, conversationScope = false) => { + if (['private', 'subscribers'].includes(a) && conversationScope) return 'conversation'; + const order = ['public', 'unlisted', 'mutuals_only', 'private', 'direct', 'local']; if (a === 'group') return a; @@ -403,7 +405,7 @@ const compose = (state = initialState, action: ComposeAction | EventsAction | In compose.to = to; compose.parent_reblogged_by = action.rebloggedBy?.id || null; compose.text = !action.explicitAddressing ? statusToTextMentions(action.status, action.account) : ''; - compose.privacy = privacyPreference(action.status.visibility, defaultCompose.privacy, action.status.list_id); + compose.privacy = privacyPreference(action.status.visibility, defaultCompose.privacy, action.status.list_id, action.conversationScope); compose.federated = action.status.local_only !== true; compose.focusDate = new Date(); compose.caretPosition = null;