Merge remote-tracking branch 'origin/chats' into chats

This commit is contained in:
Chewbacca 2022-11-03 12:13:59 -04:00
commit 7451a52fc2
16 changed files with 180 additions and 112 deletions

View file

@ -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(
<VirtuosoMockContext.Provider value={{ viewportHeight: 300, itemHeight: 100 }}>

View file

@ -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<IChatListItemInterface> = ({ 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<IChatListItemInterface> = ({ chat, onClick }) => {
</HStack>
<HStack alignItems='center' space={2}>
<div className='text-gray-600 hidden group-hover:block hover:text-gray-100'>
{/* TODO: fix nested buttons here */}
<DropdownMenuContainer
items={menu}
src={require('@tabler/icons/dots.svg')}
title='Settings'
/>
</div>
{features.chatsDelete && (
<div className='text-gray-600 hidden group-hover:block hover:text-gray-100'>
{/* TODO: fix nested buttons here */}
<DropdownMenuContainer
items={menu}
src={require('@tabler/icons/dots.svg')}
title='Settings'
/>
</div>
)}
{chat.last_message && (
<>

View file

@ -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 = () => {
) : (
<HStack justifyContent='center' alignItems='center' space={1} className='flex-shrink-0'>
<Icon src={require('@tabler/icons/clock.svg')} className='text-gray-600 w-4 h-4' />
<Text size='sm' theme='muted'>
{intl.formatMessage(messages.messageLifespan, { day: secondsToDays(chat.message_expiration) })}
</Text>
{chat.message_expiration && (
<Text size='sm' theme='muted'>
{intl.formatMessage(messages.messageLifespan, { day: secondsToDays(chat.message_expiration) })}
</Text>
)}
</HStack>
)}
</Stack>

View file

@ -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<IChatMessageList> = ({ 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<IChatMessageList> = ({ 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),

View file

@ -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 && <VerificationBadge />}
</div>
<Text
align='left'
size='sm'
weight='medium'
theme='primary'
truncate
className='w-full'
>
{intl.formatMessage(messages.autoDeleteMessage, { day: secondsToDays(chat.message_expiration) })}
</Text>
{chat.message_expiration && (
<Text
align='left'
size='sm'
weight='medium'
theme='primary'
truncate
className='w-full'
>
{intl.formatMessage(messages.autoDeleteMessage, { day: secondsToDays(chat.message_expiration) })}
</Text>
)}
</Stack>
</HStack>
@ -140,32 +143,34 @@ const ChatPageMain = () => {
</Stack>
</HStack>
<List>
<ListItem
label={intl.formatMessage(messages.autoDeleteLabel)}
hint={intl.formatMessage(messages.autoDeleteHint)}
/>
<ListItem
label={intl.formatMessage(messages.autoDelete7Days)}
onSelect={() => handleUpdateChat(MessageExpirationValues.SEVEN)}
isSelected={chat.message_expiration === MessageExpirationValues.SEVEN}
/>
<ListItem
label={intl.formatMessage(messages.autoDelete14Days)}
onSelect={() => handleUpdateChat(MessageExpirationValues.FOURTEEN)}
isSelected={chat.message_expiration === MessageExpirationValues.FOURTEEN}
/>
<ListItem
label={intl.formatMessage(messages.autoDelete30Days)}
onSelect={() => handleUpdateChat(MessageExpirationValues.THIRTY)}
isSelected={chat.message_expiration === MessageExpirationValues.THIRTY}
/>
<ListItem
label={intl.formatMessage(messages.autoDelete90Days)}
onSelect={() => handleUpdateChat(MessageExpirationValues.NINETY)}
isSelected={chat.message_expiration === MessageExpirationValues.NINETY}
/>
</List>
{features.chatsExpiration && (
<List>
<ListItem
label={intl.formatMessage(messages.autoDeleteLabel)}
hint={intl.formatMessage(messages.autoDeleteHint)}
/>
<ListItem
label={intl.formatMessage(messages.autoDelete7Days)}
onSelect={() => handleUpdateChat(MessageExpirationValues.SEVEN)}
isSelected={chat.message_expiration === MessageExpirationValues.SEVEN}
/>
<ListItem
label={intl.formatMessage(messages.autoDelete14Days)}
onSelect={() => handleUpdateChat(MessageExpirationValues.FOURTEEN)}
isSelected={chat.message_expiration === MessageExpirationValues.FOURTEEN}
/>
<ListItem
label={intl.formatMessage(messages.autoDelete30Days)}
onSelect={() => handleUpdateChat(MessageExpirationValues.THIRTY)}
isSelected={chat.message_expiration === MessageExpirationValues.THIRTY}
/>
<ListItem
label={intl.formatMessage(messages.autoDelete90Days)}
onSelect={() => handleUpdateChat(MessageExpirationValues.NINETY)}
isSelected={chat.message_expiration === MessageExpirationValues.NINETY}
/>
</List>
)}
<Stack space={2}>
<MenuItem
@ -179,16 +184,18 @@ const ChatPageMain = () => {
</div>
</MenuItem>
<MenuItem
as='button'
onSelect={handleLeaveChat}
className='!px-0 hover:!bg-transparent'
>
<div className='w-full flex items-center space-x-2 font-bold text-sm text-danger-600 dark:text-danger-500'>
<Icon src={require('@tabler/icons/logout.svg')} className='w-5 h-5' />
<span>{intl.formatMessage(messages.leaveChat)}</span>
</div>
</MenuItem>
{features.chatsDelete && (
<MenuItem
as='button'
onSelect={handleLeaveChat}
className='!px-0 hover:!bg-transparent'
>
<div className='w-full flex items-center space-x-2 font-bold text-sm text-danger-600 dark:text-danger-500'>
<Icon src={require('@tabler/icons/logout.svg')} className='w-5 h-5' />
<span>{intl.formatMessage(messages.leaveChat)}</span>
</div>
</MenuItem>
)}
</Stack>
</Stack>
</MenuList>

View file

@ -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<FormData>({
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)}
>
<Toggle
checked={data.accepting_messages}
onChange={(event) => setData((prevData) => ({ ...prevData, accepting_messages: event.target.checked }))}
checked={data.accepts_chat_messages}
onChange={(event) => setData((prevData) => ({ ...prevData, accepts_chat_messages: event.target.checked }))}
/>
</ListItem>
</List>

View file

@ -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<FormData>({
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)}
>
<Toggle
checked={data.accepting_messages}
onChange={(event) => setData((prevData) => ({ ...prevData, accepting_messages: event.target.checked }))}
checked={data.accepts_chat_messages}
onChange={(event) => setData((prevData) => ({ ...prevData, accepts_chat_messages: event.target.checked }))}
/>
</ListItem>
</List>

View file

@ -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 = () => {
</Stack>
</HStack>
<List>
<ListItem label={intl.formatMessage(messages.autoDeleteLabel)}>
<Select defaultValue={chat.message_expiration} onChange={(event) => handleUpdateChat(Number(event.target.value))}>
{messageExpirationOptions.map((duration) => {
const inDays = secondsToDays(duration);
{features.chatsExpiration && (
<List>
<ListItem label={intl.formatMessage(messages.autoDeleteLabel)}>
<Select defaultValue={chat.message_expiration} onChange={(event) => handleUpdateChat(Number(event.target.value))}>
{messageExpirationOptions.map((duration) => {
const inDays = secondsToDays(duration);
return (
<option key={duration} value={duration}>
{intl.formatMessage(messages.autoDeleteDays, { day: inDays })}
</option>
);
})}
</Select>
</ListItem>
</List>
return (
<option key={duration} value={duration}>
{intl.formatMessage(messages.autoDeleteDays, { day: inDays })}
</option>
);
})}
</Select>
</ListItem>
</List>
)}
<Stack space={5}>
<button onClick={isBlocking ? handleUnblockUser : handleBlockUser} className='w-full flex items-center space-x-2 font-bold text-sm text-primary-600 dark:text-accent-blue'>
@ -137,10 +140,12 @@ const ChatSettings = () => {
<span>{intl.formatMessage(isBlocking ? messages.unblockUser : messages.blockUser, { acct: chat.account.acct })}</span>
</button>
<button onClick={handleLeaveChat} className='w-full flex items-center space-x-2 font-bold text-sm text-danger-600'>
<Icon src={require('@tabler/icons/logout.svg')} className='w-5 h-5' />
<span>{intl.formatMessage(messages.leaveChat)}</span>
</button>
{features.chatsDelete && (
<button onClick={handleLeaveChat} className='w-full flex items-center space-x-2 font-bold text-sm text-danger-600'>
<Icon src={require('@tabler/icons/logout.svg')} className='w-5 h-5' />
<span>{intl.formatMessage(messages.leaveChat)}</span>
</button>
)}
</Stack>
</Stack>
</>

View file

@ -90,9 +90,11 @@ const ChatWindow = () => {
<Text size='sm' weight='bold' truncate>{chat.account.display_name}</Text>
{chat.account.verified && <VerificationBadge />}
</div>
<Text size='sm' weight='medium' theme='primary' truncate>
{intl.formatMessage(messages.autoDeleteMessage, { day: secondsToDays(chat.message_expiration) })}
</Text>
{chat.message_expiration && (
<Text size='sm' weight='medium' theme='primary' truncate>
{intl.formatMessage(messages.autoDeleteMessage, { day: secondsToDays(chat.message_expiration) })}
</Text>
)}
</Stack>
</LinkWrapper>
</HStack>

View file

@ -17,7 +17,7 @@ const MessagesSettings = () => {
const updateCredentials = useUpdateCredentials();
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
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)}
>
<Toggle
checked={account.accepting_messages}
checked={account.accepts_chat_messages}
onChange={handleChange}
/>
</ListItem>

View file

@ -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<string, any>) => {
return account.set('discoverable', discoverable);
};
/** Normalize message acceptance between Pleroma and Truth Social. */
const normalizeMessageAcceptance = (account: ImmutableMap<string, any>) => {
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<string, any>) => {
const birthday = account.get('birthday');
@ -285,6 +291,7 @@ export const normalizeAccount = (account: Record<string, any>) => {
normalizeFqn(account);
normalizeFavicon(account);
normalizeDiscoverable(account);
normalizeMessageAcceptance(account);
addDomain(account);
addStaffFields(account);
fixUsername(account);

View file

@ -15,13 +15,13 @@ export const ChatMessageRecord = ImmutableRecord({
card: null as Card | null,
chat_id: '',
content: '',
created_at: new Date(),
created_at: '',
emojis: ImmutableList<Emoji>(),
id: '',
unread: false,
deleting: false,
pending: false,
pending: false as boolean | undefined,
});
const normalizeMedia = (status: ImmutableMap<string, any>) => {

View file

@ -30,7 +30,7 @@ export type IAccount = {
}
type UpdateCredentialsData = {
accepting_messages?: boolean
accepts_chat_messages?: boolean
chats_onboarded?: boolean
}

View file

@ -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,

View file

@ -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<IPolicy>('/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,
});
}

View file

@ -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.
*/