Add ability to update deletion duration
This commit is contained in:
@ -14,16 +14,18 @@ const List: React.FC = ({ children }) => (
|
||||
interface IListItem {
|
||||
label: React.ReactNode,
|
||||
hint?: React.ReactNode,
|
||||
onClick?: () => void,
|
||||
onClick?(): void,
|
||||
onSelect?(): void
|
||||
isSelected?: boolean
|
||||
}
|
||||
|
||||
const ListItem: React.FC<IListItem> = ({ label, hint, children, onClick }) => {
|
||||
const ListItem: React.FC<IListItem> = ({ label, hint, children, onClick, onSelect, isSelected }) => {
|
||||
const id = uuidv4();
|
||||
const domId = `list-group-${id}`;
|
||||
|
||||
const Comp = onClick ? 'a' : 'div';
|
||||
const LabelComp = onClick ? 'span' : 'label';
|
||||
const linkProps = onClick ? { onClick } : {};
|
||||
const LabelComp = onClick || onSelect ? 'span' : 'label';
|
||||
const linkProps = onClick || onSelect ? { onClick: onClick || onSelect } : {};
|
||||
|
||||
const renderChildren = React.useCallback(() => {
|
||||
return React.Children.map(children, (child) => {
|
||||
@ -46,7 +48,7 @@ const ListItem: React.FC<IListItem> = ({ label, hint, children, onClick }) => {
|
||||
<Comp
|
||||
className={classNames({
|
||||
'flex items-center justify-between px-3 py-2 first:rounded-t-lg last:rounded-b-lg bg-gradient-to-r from-gradient-start/10 to-gradient-end/10': true,
|
||||
'cursor-pointer hover:from-gradient-start/20 hover:to-gradient-end/20 dark:hover:from-gradient-start/5 dark:hover:to-gradient-end/5': typeof onClick !== 'undefined',
|
||||
'cursor-pointer hover:from-gradient-start/20 hover:to-gradient-end/20 dark:hover:from-gradient-start/5 dark:hover:to-gradient-end/5': typeof onClick !== 'undefined' || typeof onSelect !== 'undefined',
|
||||
})}
|
||||
{...linkProps}
|
||||
>
|
||||
@ -65,6 +67,16 @@ const ListItem: React.FC<IListItem> = ({ label, hint, children, onClick }) => {
|
||||
<Icon src={require('@tabler/icons/chevron-right.svg')} className='ml-1' />
|
||||
</div>
|
||||
) : renderChildren()}
|
||||
|
||||
{onSelect ? (
|
||||
<div className='flex flex-row items-center text-gray-700 dark:text-gray-600'>
|
||||
{children}
|
||||
|
||||
{isSelected ? (
|
||||
<Icon src={require('@tabler/icons/check.svg')} className='ml-1 text-primary-500 dark:text-primary-400' />
|
||||
) : null}
|
||||
</div>
|
||||
) : renderChildren()}
|
||||
</Comp>
|
||||
);
|
||||
};
|
||||
|
||||
@ -3,11 +3,13 @@ import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { blockAccount, unblockAccount } from 'soapbox/actions/accounts';
|
||||
import { openModal } from 'soapbox/actions/modals';
|
||||
import { Avatar, Divider, HStack, Icon, IconButton, Menu, MenuButton, MenuItem, MenuList, Stack, Text } from 'soapbox/components/ui';
|
||||
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 { useChatContext } from 'soapbox/contexts/chat-context';
|
||||
import { useAppDispatch, useAppSelector, useOwnAccount } from 'soapbox/hooks';
|
||||
import { useChatActions } from 'soapbox/queries/chats';
|
||||
import { MessageExpirationValues, useChatActions } from 'soapbox/queries/chats';
|
||||
import { secondsToDays } from 'soapbox/utils/numbers';
|
||||
|
||||
import Chat from '../../chat';
|
||||
|
||||
@ -28,6 +30,13 @@ const messages = defineMessages({
|
||||
unblockUser: { id: 'chat_settings.options.unblock_user', defaultMessage: 'Unblock @{acct}' },
|
||||
reportUser: { id: 'chat_settings.options.report_user', defaultMessage: 'Report @{acct}' },
|
||||
leaveChat: { id: 'chat_settings.options.leave_chat', defaultMessage: 'Leave Chat' },
|
||||
autoDeleteLabel: { id: 'chat_settings.auto_delete.label', defaultMessage: 'Auto-delete messages' },
|
||||
autoDeleteHint: { id: 'chat_settings.auto_delete.hint', defaultMessage: 'Sent messages will auto-delete after the time period selected' },
|
||||
autoDelete7Days: { id: 'chat_settings.auto_delete.7days', defaultMessage: '7 days' },
|
||||
autoDelete14Days: { id: 'chat_settings.auto_delete.14days', defaultMessage: '14 days' },
|
||||
autoDelete30Days: { id: 'chat_settings.auto_delete.30days', defaultMessage: '30 days' },
|
||||
autoDelete90Days: { id: 'chat_settings.auto_delete.90days', defaultMessage: '90 days' },
|
||||
autoDeleteMessage: { id: 'chat_window.auto_delete_label', defaultMessage: 'Auto-delete after {day} days' },
|
||||
});
|
||||
|
||||
const ChatPageMain = () => {
|
||||
@ -38,7 +47,9 @@ const ChatPageMain = () => {
|
||||
const inputRef = useRef<HTMLTextAreaElement | null>(null);
|
||||
|
||||
const { chat, setChat } = useChatContext();
|
||||
const { deleteChat } = useChatActions(chat?.id as string);
|
||||
const { deleteChat, updateChat } = useChatActions(chat?.id as string);
|
||||
|
||||
const handleUpdateChat = (value: MessageExpirationValues) => updateChat.mutate({ message_expiration: value });
|
||||
|
||||
const isBlocking = useAppSelector((state) => state.getIn(['relationships', chat?.account?.id, 'blocking']));
|
||||
|
||||
@ -106,11 +117,11 @@ const ChatPageMain = () => {
|
||||
align='left'
|
||||
size='sm'
|
||||
weight='medium'
|
||||
theme='muted'
|
||||
theme='primary'
|
||||
truncate
|
||||
className='w-full'
|
||||
>
|
||||
{chat.account.acct}
|
||||
{intl.formatMessage(messages.autoDeleteMessage, { day: secondsToDays(chat.message_expiration) })}
|
||||
</Text>
|
||||
</Stack>
|
||||
</HStack>
|
||||
@ -133,7 +144,32 @@ const ChatPageMain = () => {
|
||||
</Stack>
|
||||
</HStack>
|
||||
|
||||
<Divider />
|
||||
<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
|
||||
|
||||
@ -3,10 +3,11 @@ import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { blockAccount, unblockAccount } from 'soapbox/actions/accounts';
|
||||
import { openModal } from 'soapbox/actions/modals';
|
||||
import { Avatar, Divider, HStack, Icon, Stack, Text } from 'soapbox/components/ui';
|
||||
import List, { ListItem } from 'soapbox/components/list';
|
||||
import { Avatar, HStack, Icon, Stack, Text } from 'soapbox/components/ui';
|
||||
import { useChatContext } from 'soapbox/contexts/chat-context';
|
||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||
import { useChatActions } from 'soapbox/queries/chats';
|
||||
import { MessageExpirationValues, useChatActions } from 'soapbox/queries/chats';
|
||||
|
||||
import ChatPaneHeader from './chat-pane-header';
|
||||
|
||||
@ -24,6 +25,12 @@ const messages = defineMessages({
|
||||
blockUser: { id: 'chat_settings.options.block_user', defaultMessage: 'Block @{acct}' },
|
||||
unblockUser: { id: 'chat_settings.options.unblock_user', defaultMessage: 'Unblock @{acct}' },
|
||||
leaveChat: { id: 'chat_settings.options.leave_chat', defaultMessage: 'Leave Chat' },
|
||||
autoDeleteLabel: { id: 'chat_settings.auto_delete.label', defaultMessage: 'Auto-delete messages' },
|
||||
autoDeleteHint: { id: 'chat_settings.auto_delete.hint', defaultMessage: 'Sent messages will auto-delete after the time period selected' },
|
||||
autoDelete7Days: { id: 'chat_settings.auto_delete.7days', defaultMessage: '7 days' },
|
||||
autoDelete14Days: { id: 'chat_settings.auto_delete.14days', defaultMessage: '14 days' },
|
||||
autoDelete30Days: { id: 'chat_settings.auto_delete.30days', defaultMessage: '30 days' },
|
||||
autoDelete90Days: { id: 'chat_settings.auto_delete.90days', defaultMessage: '90 days' },
|
||||
});
|
||||
|
||||
const ChatSettings = () => {
|
||||
@ -31,7 +38,9 @@ const ChatSettings = () => {
|
||||
const intl = useIntl();
|
||||
|
||||
const { chat, setEditing, toggleChatPane } = useChatContext();
|
||||
const { deleteChat } = useChatActions(chat?.id as string);
|
||||
const { deleteChat, updateChat } = useChatActions(chat?.id as string);
|
||||
|
||||
const handleUpdateChat = (value: MessageExpirationValues) => updateChat.mutate({ message_expiration: value });
|
||||
|
||||
const isBlocking = useAppSelector((state) => state.getIn(['relationships', chat?.account?.id, 'blocking']));
|
||||
|
||||
@ -107,7 +116,32 @@ const ChatSettings = () => {
|
||||
</Stack>
|
||||
</HStack>
|
||||
|
||||
<Divider />
|
||||
<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={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'>
|
||||
|
||||
@ -1,15 +1,21 @@
|
||||
import React, { useRef } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { Avatar, HStack, Icon, Stack, Text } from 'soapbox/components/ui';
|
||||
import VerificationBadge from 'soapbox/components/verification_badge';
|
||||
import { useChatContext } from 'soapbox/contexts/chat-context';
|
||||
import { secondsToDays } from 'soapbox/utils/numbers';
|
||||
|
||||
import Chat from '../chat';
|
||||
|
||||
import ChatPaneHeader from './chat-pane-header';
|
||||
import ChatSettings from './chat-settings';
|
||||
|
||||
const messages = defineMessages({
|
||||
autoDeleteMessage: { id: 'chat_window.auto_delete_label', defaultMessage: 'Auto-delete after {day} days' },
|
||||
});
|
||||
|
||||
const LinkWrapper = ({ enabled, to, children }: { enabled: boolean, to: string, children: React.ReactNode }): JSX.Element => {
|
||||
if (!enabled) {
|
||||
return <>{children}</>;
|
||||
@ -24,6 +30,8 @@ const LinkWrapper = ({ enabled, to, children }: { enabled: boolean, to: string,
|
||||
|
||||
/** Floating desktop chat window. */
|
||||
const ChatWindow = () => {
|
||||
const intl = useIntl();
|
||||
|
||||
const { chat, setChat, isOpen, isEditing, needsAcceptance, setEditing, setSearching, toggleChatPane } = useChatContext();
|
||||
|
||||
const inputRef = useRef<HTMLTextAreaElement | null>(null);
|
||||
@ -79,7 +87,9 @@ 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>@{chat.account.acct}</Text>
|
||||
<Text size='sm' weight='medium' theme='primary' truncate>
|
||||
{intl.formatMessage(messages.autoDeleteMessage, { day: secondsToDays(chat.message_expiration) })}
|
||||
</Text>
|
||||
</Stack>
|
||||
</LinkWrapper>
|
||||
</HStack>
|
||||
|
||||
@ -3,6 +3,7 @@ import sumBy from 'lodash/sumBy';
|
||||
|
||||
import { fetchRelationships } from 'soapbox/actions/accounts';
|
||||
import { importFetchedAccount, importFetchedAccounts } from 'soapbox/actions/importer';
|
||||
import snackbar from 'soapbox/actions/snackbar';
|
||||
import { getNextLink } from 'soapbox/api';
|
||||
import compareId from 'soapbox/compare_id';
|
||||
import { useChatContext } from 'soapbox/contexts/chat-context';
|
||||
@ -14,10 +15,20 @@ import { queryClient } from './client';
|
||||
|
||||
import type { IAccount } from './accounts';
|
||||
|
||||
export enum MessageExpirationValues {
|
||||
'SEVEN' = 604800,
|
||||
'FOURTEEN' = 1209600,
|
||||
'THIRTY' = 2592000,
|
||||
'NINETY' = 7776000
|
||||
}
|
||||
|
||||
export interface IChat {
|
||||
id: string
|
||||
unread: number
|
||||
accepted: boolean
|
||||
account: IAccount
|
||||
created_at: Date
|
||||
created_by_account: string
|
||||
discarded_at: null | string
|
||||
id: string
|
||||
last_message: null | {
|
||||
account_id: string
|
||||
chat_id: string
|
||||
@ -27,11 +38,12 @@ export interface IChat {
|
||||
id: string
|
||||
unread: boolean
|
||||
}
|
||||
created_at: Date
|
||||
updated_at: Date
|
||||
accepted: boolean
|
||||
discarded_at: null | string
|
||||
account: IAccount
|
||||
latest_read_message_by_account: {
|
||||
[id: number]: string
|
||||
}[]
|
||||
latest_read_message_created_at: string
|
||||
message_expiration: MessageExpirationValues
|
||||
unread: number
|
||||
}
|
||||
|
||||
export interface IChatMessage {
|
||||
@ -44,6 +56,10 @@ export interface IChatMessage {
|
||||
pending?: boolean
|
||||
}
|
||||
|
||||
type UpdateChatVariables = {
|
||||
message_expiration: MessageExpirationValues
|
||||
}
|
||||
|
||||
const ChatKeys = {
|
||||
chat: (chatId?: string) => ['chats', 'chat', chatId] as const,
|
||||
chatMessages: (chatId: string) => ['chats', 'messages', chatId] as const,
|
||||
@ -171,7 +187,9 @@ const useChat = (chatId?: string) => {
|
||||
|
||||
const useChatActions = (chatId: string) => {
|
||||
const api = useApi();
|
||||
const { setChat, setEditing } = useChatContext();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const { chat, setChat, setEditing } = useChatContext();
|
||||
|
||||
const markChatAsRead = (lastReadId: string) => {
|
||||
api.post<IChat>(`/api/v1/pleroma/chats/${chatId}/read`, { last_read_id: lastReadId })
|
||||
@ -183,6 +201,35 @@ const useChatActions = (chatId: string) => {
|
||||
return api.post<IChat>(`/api/v1/pleroma/chats/${chatId}/messages`, { content });
|
||||
};
|
||||
|
||||
const updateChat = useMutation((data: UpdateChatVariables) => api.patch<IChat>(`/api/v1/pleroma/chats/${chatId}`, data), {
|
||||
onMutate: async (data) => {
|
||||
// Cancel any outgoing refetches (so they don't overwrite our optimistic update)
|
||||
await queryClient.cancelQueries(ChatKeys.chat(chatId));
|
||||
|
||||
// Snapshot the previous value
|
||||
const prevChat = { ...chat };
|
||||
const nextChat = { ...chat, ...data };
|
||||
|
||||
// Optimistically update to the new value
|
||||
queryClient.setQueryData(ChatKeys.chat(chatId), nextChat);
|
||||
setChat(nextChat as IChat);
|
||||
|
||||
// Return a context object with the snapshotted value
|
||||
return { prevChat };
|
||||
},
|
||||
// If the mutation fails, use the context returned from onMutate to roll back
|
||||
onError: (_error: any, _newData: any, context: any) => {
|
||||
setChat(context?.prevChat);
|
||||
queryClient.setQueryData(ChatKeys.chat(chatId), context.prevChat);
|
||||
dispatch(snackbar.error('Chat Settings failed to update.'));
|
||||
},
|
||||
onSuccess(response) {
|
||||
queryClient.invalidateQueries(ChatKeys.chat(chatId));
|
||||
setChat(response.data);
|
||||
dispatch(snackbar.success('Chat Settings updated successfully'));
|
||||
},
|
||||
});
|
||||
|
||||
const deleteChatMessage = (chatMessageId: string) => api.delete<IChat>(`/api/v1/pleroma/chats/${chatId}/messages/${chatMessageId}`);
|
||||
|
||||
const acceptChat = useMutation(() => api.post<IChat>(`/api/v1/pleroma/chats/${chatId}/accept`), {
|
||||
@ -202,7 +249,7 @@ const useChatActions = (chatId: string) => {
|
||||
},
|
||||
});
|
||||
|
||||
return { createChatMessage, markChatAsRead, deleteChatMessage, acceptChat, deleteChat };
|
||||
return { createChatMessage, markChatAsRead, deleteChatMessage, updateChat, acceptChat, deleteChat };
|
||||
};
|
||||
|
||||
export { ChatKeys, useChat, useChatActions, useChats, useChatMessages };
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
import { render, screen } from '../../jest/test-helpers';
|
||||
import { isIntegerId, shortNumberFormat } from '../numbers';
|
||||
import { isIntegerId, secondsToDays, shortNumberFormat } from '../numbers';
|
||||
|
||||
test('isIntegerId()', () => {
|
||||
expect(isIntegerId('0')).toBe(true);
|
||||
@ -14,6 +14,13 @@ test('isIntegerId()', () => {
|
||||
expect(isIntegerId(undefined as any)).toBe(false);
|
||||
});
|
||||
|
||||
test('secondsToDays', () => {
|
||||
expect(secondsToDays(604800)).toEqual(7);
|
||||
expect(secondsToDays(1209600)).toEqual(14);
|
||||
expect(secondsToDays(2592000)).toEqual(30);
|
||||
expect(secondsToDays(7776000)).toEqual(90);
|
||||
});
|
||||
|
||||
describe('shortNumberFormat', () => {
|
||||
test('handles non-numbers', () => {
|
||||
render(<div data-testid='num'>{shortNumberFormat('not-number')}</div>, undefined, null);
|
||||
|
||||
@ -4,6 +4,8 @@ import { FormattedNumber } from 'react-intl';
|
||||
/** Check if a value is REALLY a number. */
|
||||
export const isNumber = (value: unknown): value is number => typeof value === 'number' && !isNaN(value);
|
||||
|
||||
export const secondsToDays = (seconds: number) => Math.floor(seconds / (3600 * 24));
|
||||
|
||||
const roundDown = (num: number) => {
|
||||
if (num >= 100 && num < 1000) {
|
||||
num = Math.floor(num);
|
||||
|
||||
Reference in New Issue
Block a user