diff --git a/app/soapbox/features/chats/components/__tests__/chat-message-list.test.tsx b/app/soapbox/features/chats/components/__tests__/chat-message-list.test.tsx index 063302523..0ab2c5f7c 100644 --- a/app/soapbox/features/chats/components/__tests__/chat-message-list.test.tsx +++ b/app/soapbox/features/chats/components/__tests__/chat-message-list.test.tsx @@ -3,6 +3,7 @@ import React from 'react'; import { VirtuosoMockContext } from 'react-virtuoso'; import { ChatContext } from 'soapbox/contexts/chat-context'; +import { normalizeInstance } from 'soapbox/normalizers'; import { IAccount } from 'soapbox/queries/accounts'; import { __stub } from '../../../../api'; @@ -52,7 +53,9 @@ Object.assign(navigator, { }, }); -const store = rootState.set('me', '1'); +const store = rootState + .set('me', '1') + .set('instance', normalizeInstance({ version: '3.4.1 (compatible; TruthSocial 1.0.0)' })); const renderComponentWithChatContext = () => render( diff --git a/app/soapbox/features/chats/components/chat-list-item.tsx b/app/soapbox/features/chats/components/chat-list-item.tsx index 756d7f173..7b2a229a2 100644 --- a/app/soapbox/features/chats/components/chat-list-item.tsx +++ b/app/soapbox/features/chats/components/chat-list-item.tsx @@ -6,7 +6,7 @@ import RelativeTimestamp from 'soapbox/components/relative-timestamp'; import { Avatar, HStack, Stack, Text } from 'soapbox/components/ui'; import VerificationBadge from 'soapbox/components/verification_badge'; import DropdownMenuContainer from 'soapbox/containers/dropdown_menu_container'; -import { useAppDispatch } from 'soapbox/hooks'; +import { useAppDispatch, useFeatures } from 'soapbox/hooks'; import { IChat, useChatActions } from 'soapbox/queries/chats'; import type { Menu } from 'soapbox/components/dropdown_menu'; @@ -26,6 +26,7 @@ interface IChatListItemInterface { const ChatListItem: React.FC = ({ chat, onClick }) => { const dispatch = useAppDispatch(); const intl = useIntl(); + const features = useFeatures(); const { deleteChat } = useChatActions(chat?.id as string); @@ -80,14 +81,16 @@ const ChatListItem: React.FC = ({ chat, onClick }) => { -
- {/* TODO: fix nested buttons here */} - -
+ {features.chatsDelete && ( +
+ {/* TODO: fix nested buttons here */} + +
+ )} {chat.last_message && ( <> diff --git a/app/soapbox/features/chats/components/chat-message-list-intro.tsx b/app/soapbox/features/chats/components/chat-message-list-intro.tsx index dc6ddfd47..2386f24f4 100644 --- a/app/soapbox/features/chats/components/chat-message-list-intro.tsx +++ b/app/soapbox/features/chats/components/chat-message-list-intro.tsx @@ -6,7 +6,7 @@ import { openModal } from 'soapbox/actions/modals'; import Link from 'soapbox/components/link'; import { Avatar, Button, HStack, Icon, Stack, Text } from 'soapbox/components/ui'; import { useChatContext } from 'soapbox/contexts/chat-context'; -import { useAppDispatch } from 'soapbox/hooks'; +import { useAppDispatch, useFeatures } from 'soapbox/hooks'; import { useChatActions } from 'soapbox/queries/chats'; import { secondsToDays } from 'soapbox/utils/numbers'; @@ -24,6 +24,7 @@ const messages = defineMessages({ const ChatMessageListIntro = () => { const dispatch = useAppDispatch(); const intl = useIntl(); + const features = useFeatures(); const { chat, needsAcceptance } = useChatContext(); const { acceptChat, deleteChat } = useChatActions(chat?.id as string); @@ -38,7 +39,7 @@ const ChatMessageListIntro = () => { })); }; - if (!chat) { + if (!chat || !features.chatAcceptance) { return null; } @@ -97,9 +98,11 @@ const ChatMessageListIntro = () => { ) : ( - - {intl.formatMessage(messages.messageLifespan, { day: secondsToDays(chat.message_expiration) })} - + {chat.message_expiration && ( + + {intl.formatMessage(messages.messageLifespan, { day: secondsToDays(chat.message_expiration) })} + + )} )} diff --git a/app/soapbox/features/chats/components/chat-message-list.tsx b/app/soapbox/features/chats/components/chat-message-list.tsx index 674416a31..4e85468e9 100644 --- a/app/soapbox/features/chats/components/chat-message-list.tsx +++ b/app/soapbox/features/chats/components/chat-message-list.tsx @@ -14,7 +14,7 @@ import DropdownMenuContainer from 'soapbox/containers/dropdown_menu_container'; import PlaceholderChatMessage from 'soapbox/features/placeholder/components/placeholder-chat-message'; import Bundle from 'soapbox/features/ui/components/bundle'; import { MediaGallery } from 'soapbox/features/ui/util/async-components'; -import { useAppSelector, useAppDispatch, useOwnAccount } from 'soapbox/hooks'; +import { useAppSelector, useAppDispatch, useOwnAccount, useFeatures } from 'soapbox/hooks'; import { normalizeAccount } from 'soapbox/normalizers'; import { ChatKeys, IChat, IChatMessage, useChatActions, useChatMessages } from 'soapbox/queries/chats'; import { queryClient } from 'soapbox/queries/client'; @@ -73,6 +73,8 @@ const ChatMessageList: React.FC = ({ chat, autosize }) => { const intl = useIntl(); const dispatch = useAppDispatch(); const account = useOwnAccount(); + const features = useFeatures(); + const lastReadMessageDateString = chat.latest_read_message_by_account.find((latest) => latest.id === chat.account.id)?.date; const lastReadMessageTimestamp = lastReadMessageDateString ? new Date(lastReadMessageDateString) : null; @@ -245,11 +247,13 @@ const ChatMessageList: React.FC = ({ chat, autosize }) => { destructive: true, }); } else { - menu.push({ - text: intl.formatMessage(messages.report), - action: () => dispatch(initReport(normalizeAccount(chat.account) as any, { chatMessage } as any)), - icon: require('@tabler/icons/flag.svg'), - }); + if (features.reportChats) { + menu.push({ + text: intl.formatMessage(messages.report), + action: () => dispatch(initReport(normalizeAccount(chat.account) as any, { chatMessage } as any)), + icon: require('@tabler/icons/flag.svg'), + }); + } menu.push({ text: intl.formatMessage(messages.deleteForMe), action: () => handleDeleteMessage.mutate(chatMessage.id), diff --git a/app/soapbox/features/chats/components/chat-page/components/chat-page-main.tsx b/app/soapbox/features/chats/components/chat-page/components/chat-page-main.tsx index ba897b350..bf0d729f0 100644 --- a/app/soapbox/features/chats/components/chat-page/components/chat-page-main.tsx +++ b/app/soapbox/features/chats/components/chat-page/components/chat-page-main.tsx @@ -7,7 +7,7 @@ import { openModal } from 'soapbox/actions/modals'; import List, { ListItem } from 'soapbox/components/list'; import { Avatar, HStack, Icon, IconButton, Menu, MenuButton, MenuItem, MenuList, Stack, Text } from 'soapbox/components/ui'; import VerificationBadge from 'soapbox/components/verification_badge'; -import { useAppDispatch, useAppSelector } from 'soapbox/hooks'; +import { useAppDispatch, useAppSelector, useFeatures } from 'soapbox/hooks'; import { MessageExpirationValues, useChat, useChatActions } from 'soapbox/queries/chats'; import { secondsToDays } from 'soapbox/utils/numbers'; @@ -41,6 +41,7 @@ const messages = defineMessages({ const ChatPageMain = () => { const dispatch = useAppDispatch(); const intl = useIntl(); + const features = useFeatures(); const history = useHistory(); const { chatId } = useParams<{ chatId: string }>(); @@ -109,16 +110,18 @@ const ChatPageMain = () => { {chat.account?.verified && } - - {intl.formatMessage(messages.autoDeleteMessage, { day: secondsToDays(chat.message_expiration) })} - + {chat.message_expiration && ( + + {intl.formatMessage(messages.autoDeleteMessage, { day: secondsToDays(chat.message_expiration) })} + + )}
@@ -140,32 +143,34 @@ const ChatPageMain = () => { - - - handleUpdateChat(MessageExpirationValues.SEVEN)} - isSelected={chat.message_expiration === MessageExpirationValues.SEVEN} - /> - handleUpdateChat(MessageExpirationValues.FOURTEEN)} - isSelected={chat.message_expiration === MessageExpirationValues.FOURTEEN} - /> - handleUpdateChat(MessageExpirationValues.THIRTY)} - isSelected={chat.message_expiration === MessageExpirationValues.THIRTY} - /> - handleUpdateChat(MessageExpirationValues.NINETY)} - isSelected={chat.message_expiration === MessageExpirationValues.NINETY} - /> - + {features.chatsExpiration && ( + + + handleUpdateChat(MessageExpirationValues.SEVEN)} + isSelected={chat.message_expiration === MessageExpirationValues.SEVEN} + /> + handleUpdateChat(MessageExpirationValues.FOURTEEN)} + isSelected={chat.message_expiration === MessageExpirationValues.FOURTEEN} + /> + handleUpdateChat(MessageExpirationValues.THIRTY)} + isSelected={chat.message_expiration === MessageExpirationValues.THIRTY} + /> + handleUpdateChat(MessageExpirationValues.NINETY)} + isSelected={chat.message_expiration === MessageExpirationValues.NINETY} + /> + + )} { - -
- - {intl.formatMessage(messages.leaveChat)} -
-
+ {features.chatsDelete && ( + +
+ + {intl.formatMessage(messages.leaveChat)} +
+
+ )}
diff --git a/app/soapbox/features/chats/components/chat-page/components/chat-page-settings.tsx b/app/soapbox/features/chats/components/chat-page/components/chat-page-settings.tsx index 4c214ff07..e63aeee45 100644 --- a/app/soapbox/features/chats/components/chat-page/components/chat-page-settings.tsx +++ b/app/soapbox/features/chats/components/chat-page/components/chat-page-settings.tsx @@ -7,7 +7,7 @@ import { useOwnAccount } from 'soapbox/hooks'; import { useUpdateCredentials } from 'soapbox/queries/accounts'; type FormData = { - accepting_messages?: boolean + accepts_chat_messages?: boolean chats_onboarded: boolean } @@ -26,7 +26,7 @@ const ChatPageSettings = () => { const [data, setData] = useState({ chats_onboarded: true, - accepting_messages: account?.accepting_messages, + accepts_chat_messages: account?.accepts_chat_messages, }); const handleSubmit = (event: React.FormEvent) => { @@ -49,8 +49,8 @@ const ChatPageSettings = () => { hint={intl.formatMessage(messages.acceptingMessageHint)} > setData((prevData) => ({ ...prevData, accepting_messages: event.target.checked }))} + checked={data.accepts_chat_messages} + onChange={(event) => setData((prevData) => ({ ...prevData, accepts_chat_messages: event.target.checked }))} /> diff --git a/app/soapbox/features/chats/components/chat-page/components/welcome.tsx b/app/soapbox/features/chats/components/chat-page/components/welcome.tsx index d70cf7d93..3d2c3156f 100644 --- a/app/soapbox/features/chats/components/chat-page/components/welcome.tsx +++ b/app/soapbox/features/chats/components/chat-page/components/welcome.tsx @@ -7,7 +7,7 @@ import { useOwnAccount } from 'soapbox/hooks'; import { useUpdateCredentials } from 'soapbox/queries/accounts'; type FormData = { - accepting_messages?: boolean + accepts_chat_messages?: boolean chats_onboarded: boolean } @@ -27,7 +27,7 @@ const Welcome = () => { const [data, setData] = useState({ chats_onboarded: true, - accepting_messages: account?.accepting_messages, + accepts_chat_messages: account?.accepts_chat_messages, }); const handleSubmit = (event: React.FormEvent) => { @@ -65,8 +65,8 @@ const Welcome = () => { hint={intl.formatMessage(messages.acceptingMessageHint)} > setData((prevData) => ({ ...prevData, accepting_messages: event.target.checked }))} + checked={data.accepts_chat_messages} + onChange={(event) => setData((prevData) => ({ ...prevData, accepts_chat_messages: event.target.checked }))} /> diff --git a/app/soapbox/features/chats/components/chat-widget/chat-settings.tsx b/app/soapbox/features/chats/components/chat-widget/chat-settings.tsx index e67b28269..b00c4dac8 100644 --- a/app/soapbox/features/chats/components/chat-widget/chat-settings.tsx +++ b/app/soapbox/features/chats/components/chat-widget/chat-settings.tsx @@ -6,7 +6,7 @@ import { openModal } from 'soapbox/actions/modals'; import List, { ListItem } from 'soapbox/components/list'; import { Avatar, HStack, Icon, Select, Stack, Text } from 'soapbox/components/ui'; import { ChatWidgetScreens, useChatContext } from 'soapbox/contexts/chat-context'; -import { useAppDispatch, useAppSelector } from 'soapbox/hooks'; +import { useAppDispatch, useAppSelector, useFeatures } from 'soapbox/hooks'; import { messageExpirationOptions, MessageExpirationValues, useChatActions } from 'soapbox/queries/chats'; import { secondsToDays } from 'soapbox/utils/numbers'; @@ -33,6 +33,7 @@ const messages = defineMessages({ const ChatSettings = () => { const dispatch = useAppDispatch(); const intl = useIntl(); + const features = useFeatures(); const { chat, changeScreen, toggleChatPane } = useChatContext(); const { deleteChat, updateChat } = useChatActions(chat?.id as string); @@ -115,21 +116,23 @@ const ChatSettings = () => { - - - handleUpdateChat(Number(event.target.value))}> + {messageExpirationOptions.map((duration) => { + const inDays = secondsToDays(duration); - return ( - - ); - })} - - - + return ( + + ); + })} + + + + )} - + {features.chatsDelete && ( + + )} diff --git a/app/soapbox/features/chats/components/chat-widget/chat-window.tsx b/app/soapbox/features/chats/components/chat-widget/chat-window.tsx index ddf869b13..ca8f4f499 100644 --- a/app/soapbox/features/chats/components/chat-widget/chat-window.tsx +++ b/app/soapbox/features/chats/components/chat-widget/chat-window.tsx @@ -90,9 +90,11 @@ const ChatWindow = () => { {chat.account.display_name} {chat.account.verified && } - - {intl.formatMessage(messages.autoDeleteMessage, { day: secondsToDays(chat.message_expiration) })} - + {chat.message_expiration && ( + + {intl.formatMessage(messages.autoDeleteMessage, { day: secondsToDays(chat.message_expiration) })} + + )} diff --git a/app/soapbox/features/settings/components/messages-settings.tsx b/app/soapbox/features/settings/components/messages-settings.tsx index c96dc1468..f5706f058 100644 --- a/app/soapbox/features/settings/components/messages-settings.tsx +++ b/app/soapbox/features/settings/components/messages-settings.tsx @@ -17,7 +17,7 @@ const MessagesSettings = () => { const updateCredentials = useUpdateCredentials(); const handleChange = (event: React.ChangeEvent) => { - updateCredentials.mutate({ accepting_messages: event.target.checked }); + updateCredentials.mutate({ accepts_chat_messages: event.target.checked }); }; if (!account) { @@ -31,7 +31,7 @@ const MessagesSettings = () => { hint={intl.formatMessage(messages.hint)} > diff --git a/app/soapbox/normalizers/account.ts b/app/soapbox/normalizers/account.ts index 63ad77c48..30cfd512d 100644 --- a/app/soapbox/normalizers/account.ts +++ b/app/soapbox/normalizers/account.ts @@ -21,7 +21,7 @@ import type { Emoji, Field, EmbeddedEntity, Relationship } from 'soapbox/types/e // https://docs.joinmastodon.org/entities/account/ export const AccountRecord = ImmutableRecord({ - accepting_messages: false, + accepts_chat_messages: false, acct: '', avatar: '', avatar_static: '', @@ -264,6 +264,12 @@ const normalizeDiscoverable = (account: ImmutableMap) => { return account.set('discoverable', discoverable); }; +/** Normalize message acceptance between Pleroma and Truth Social. */ +const normalizeMessageAcceptance = (account: ImmutableMap) => { + const acceptance = Boolean(account.getIn(['pleroma', 'accepts_chat_messages']) || account.get('accepting_messages')); + return account.set('accepts_chat_messages', acceptance); +}; + /** Normalize undefined/null birthday to empty string. */ const fixBirthday = (account: ImmutableMap) => { const birthday = account.get('birthday'); @@ -285,6 +291,7 @@ export const normalizeAccount = (account: Record) => { normalizeFqn(account); normalizeFavicon(account); normalizeDiscoverable(account); + normalizeMessageAcceptance(account); addDomain(account); addStaffFields(account); fixUsername(account); diff --git a/app/soapbox/normalizers/chat_message.ts b/app/soapbox/normalizers/chat_message.ts index bdf2653a9..393f950af 100644 --- a/app/soapbox/normalizers/chat_message.ts +++ b/app/soapbox/normalizers/chat_message.ts @@ -15,13 +15,13 @@ export const ChatMessageRecord = ImmutableRecord({ card: null as Card | null, chat_id: '', content: '', - created_at: new Date(), + created_at: '', emojis: ImmutableList(), id: '', unread: false, deleting: false, - pending: false, + pending: false as boolean | undefined, }); const normalizeMedia = (status: ImmutableMap) => { diff --git a/app/soapbox/queries/accounts.ts b/app/soapbox/queries/accounts.ts index 228c9a2b1..75d1193f6 100644 --- a/app/soapbox/queries/accounts.ts +++ b/app/soapbox/queries/accounts.ts @@ -30,7 +30,7 @@ export type IAccount = { } type UpdateCredentialsData = { - accepting_messages?: boolean + accepts_chat_messages?: boolean chats_onboarded?: boolean } diff --git a/app/soapbox/queries/chats.ts b/app/soapbox/queries/chats.ts index d41185682..34bd0f532 100644 --- a/app/soapbox/queries/chats.ts +++ b/app/soapbox/queries/chats.ts @@ -9,6 +9,7 @@ import compareId from 'soapbox/compare_id'; import { ChatWidgetScreens, useChatContext } from 'soapbox/contexts/chat-context'; import { useStatContext } from 'soapbox/contexts/stat-context'; import { useApi, useAppDispatch, useAppSelector, useFeatures } from 'soapbox/hooks'; +import { normalizeChatMessage } from 'soapbox/normalizers'; import { flattenPages, PaginatedResult, updatePageItem } from 'soapbox/utils/queries'; import { queryClient } from './client'; @@ -45,7 +46,7 @@ export interface IChat { date: string }[] latest_read_message_created_at: null | string - message_expiration: MessageExpirationValues + message_expiration?: MessageExpirationValues unread: number } @@ -92,7 +93,7 @@ const useChatMessages = (chat: IChat) => { const link = getNextLink(response); const hasMore = !!link; - const result = data.sort(reverseOrder); + const result = data.sort(reverseOrder).map(normalizeChatMessage); return { result, diff --git a/app/soapbox/queries/policies.ts b/app/soapbox/queries/policies.ts index 908064e7e..f7c97ff53 100644 --- a/app/soapbox/queries/policies.ts +++ b/app/soapbox/queries/policies.ts @@ -1,6 +1,6 @@ import { useMutation, useQuery } from '@tanstack/react-query'; -import { useApi, useOwnAccount } from 'soapbox/hooks'; +import { useApi, useFeatures, useOwnAccount } from 'soapbox/hooks'; import { queryClient } from './client'; @@ -15,6 +15,7 @@ const PolicyKeys = { function usePendingPolicy() { const api = useApi(); const account = useOwnAccount(); + const features = useFeatures(); const getPolicy = async() => { const { data } = await api.get('/api/v1/truth/policies/pending'); @@ -27,7 +28,7 @@ function usePendingPolicy() { refetchOnWindowFocus: true, staleTime: 60000, // 1 minute cacheTime: Infinity, - enabled: !!account, + enabled: !!account && features.truthPolicies, }); } diff --git a/app/soapbox/utils/features.ts b/app/soapbox/utils/features.ts index 211027efb..a245987bd 100644 --- a/app/soapbox/utils/features.ts +++ b/app/soapbox/utils/features.ts @@ -198,12 +198,30 @@ const getInstanceFeatures = (instance: Instance) => { v.software === PLEROMA, ]), + /** + * Ability to accept a chat. + * POST /api/v1/pleroma/chats/:id/accept + */ + chatAcceptance: v.software === TRUTHSOCIAL, + /** * Pleroma chats API. * @see {@link https://docs.pleroma.social/backend/development/API/chats/} */ chats: v.software === TRUTHSOCIAL || (v.software === PLEROMA && gte(v.version, '2.1.0')), + /** + * Ability to delete a chat. + * @see DELETE /api/v1/pleroma/chats/:id + */ + chatsDelete: v.software === TRUTHSOCIAL, + + /** + * Ability to set disappearing messages on chats. + * @see PATCH /api/v1/pleroma/chats/:id + */ + chatsExpiration: v.software === TRUTHSOCIAL, + /** * Ability to search among chats. * @see GET /api/v1/pleroma/chats @@ -514,10 +532,17 @@ const getInstanceFeatures = (instance: Instance) => { v.software === PLEROMA && v.build === SOAPBOX && gte(v.version, '2.4.50'), ]), - reportMultipleStatuses: any([ - v.software === MASTODON, - v.software === PLEROMA, - ]), + /** + * Ability to report chat messages. + * @see POST /api/v1/reports + */ + reportChats: v.software === TRUTHSOCIAL, + + /** + * Ability to select more than one status when reporting. + * @see POST /api/v1/reports + */ + reportMultipleStatuses: v.software !== TRUTHSOCIAL, /** * Can request a password reset email through the API. @@ -635,6 +660,13 @@ const getInstanceFeatures = (instance: Instance) => { v.software === TRUTHSOCIAL, ]), + /** + * Truth Social policies. + * @see GET /api/v1/truth/policies/pending + * @see PATCH /api/v1/truth/policies/:policyId/accept + */ + truthPolicies: v.software === TRUTHSOCIAL, + /** * Supports Truth suggestions. */