From 5a30509fa6d2f732080750d2341852ffaf0e415e Mon Sep 17 00:00:00 2001 From: Chewbacca Date: Thu, 8 Dec 2022 14:37:04 -0500 Subject: [PATCH 1/8] Add support for pagination in Chat Search --- .../chat-page/components/chat-page-new.tsx | 40 ++----- .../chats/components/chat-pane/chat-pane.tsx | 9 +- .../chats/components/chat-search-input.tsx | 1 + .../components/chat-search/chat-search.tsx | 107 +++++++----------- .../chats/components/chat-search/results.tsx | 103 +++++++++++------ .../headers/chat-search-header.tsx | 46 ++++++++ app/soapbox/queries/search.ts | 45 ++++++-- 7 files changed, 210 insertions(+), 141 deletions(-) create mode 100644 app/soapbox/features/chats/components/chat-widget/headers/chat-search-header.tsx diff --git a/app/soapbox/features/chats/components/chat-page/components/chat-page-new.tsx b/app/soapbox/features/chats/components/chat-page/components/chat-page-new.tsx index 5b441026e..c362a4159 100644 --- a/app/soapbox/features/chats/components/chat-page/components/chat-page-new.tsx +++ b/app/soapbox/features/chats/components/chat-page/components/chat-page-new.tsx @@ -1,11 +1,9 @@ import React from 'react'; -import { FormattedMessage } from 'react-intl'; import { useHistory } from 'react-router-dom'; -import AccountSearch from 'soapbox/components/account-search'; -import { CardTitle, HStack, IconButton, Stack, Text } from 'soapbox/components/ui'; -import { ChatKeys, useChats } from 'soapbox/queries/chats'; -import { queryClient } from 'soapbox/queries/client'; +import { CardTitle, HStack, IconButton, Stack } from 'soapbox/components/ui'; + +import ChatSearch from '../../chat-search/chat-search'; interface IChatPageNew { } @@ -13,17 +11,10 @@ interface IChatPageNew { /** New message form to create a chat. */ const ChatPageNew: React.FC = () => { const history = useHistory(); - const { getOrCreateChatByAccountId } = useChats(); - - const handleAccountSelected = async (accountId: string) => { - const { data } = await getOrCreateChatByAccountId(accountId); - history.push(`/chats/${data.id}`); - queryClient.invalidateQueries(ChatKeys.chatSearch()); - }; return ( - - + + = () => { - - - - - - - - + + ); }; diff --git a/app/soapbox/features/chats/components/chat-pane/chat-pane.tsx b/app/soapbox/features/chats/components/chat-pane/chat-pane.tsx index ad0dd8e9e..59953b54c 100644 --- a/app/soapbox/features/chats/components/chat-pane/chat-pane.tsx +++ b/app/soapbox/features/chats/components/chat-pane/chat-pane.tsx @@ -13,6 +13,7 @@ import ChatSearch from '../chat-search/chat-search'; import EmptyResultsBlankslate from '../chat-search/empty-results-blankslate'; import ChatPaneHeader from '../chat-widget/chat-pane-header'; import ChatWindow from '../chat-widget/chat-window'; +import ChatSearchHeader from '../chat-widget/headers/chat-search-header'; import { Pane } from '../ui'; import Blankslate from './blankslate'; @@ -86,7 +87,13 @@ const ChatPane = () => { } if (screen === ChatWidgetScreens.SEARCH) { - return ; + return ( + + + + {isOpen ? : null} + + ); } return ( diff --git a/app/soapbox/features/chats/components/chat-search-input.tsx b/app/soapbox/features/chats/components/chat-search-input.tsx index 2474ac6cb..bc8644aab 100644 --- a/app/soapbox/features/chats/components/chat-search-input.tsx +++ b/app/soapbox/features/chats/components/chat-search-input.tsx @@ -29,6 +29,7 @@ const ChatSearchInput: React.FC = ({ value, onChange, onClear className='rounded-full' value={value} onChange={onChange} + outerClassName='mt-0' theme='search' append={ + } + /> + - - {intl.formatMessage(messages.title)} - - - } - isOpen={isOpen} - isToggleable={false} - onToggle={toggleChatPane} - /> - - {isOpen ? ( - -
- setValue(event.target.value)} - theme='search' - append={ - - } - /> -
- - - {renderBody()} - -
- ) : null} - + + {renderBody()} + +
); }; diff --git a/app/soapbox/features/chats/components/chat-search/results.tsx b/app/soapbox/features/chats/components/chat-search/results.tsx index 26127a20b..48909f67e 100644 --- a/app/soapbox/features/chats/components/chat-search/results.tsx +++ b/app/soapbox/features/chats/components/chat-search/results.tsx @@ -1,43 +1,80 @@ -import React from 'react'; +import classNames from 'clsx'; +import React, { useCallback, useState } from 'react'; +import { Virtuoso } from 'react-virtuoso'; import { Avatar, HStack, Stack, Text } from 'soapbox/components/ui'; import VerificationBadge from 'soapbox/components/verification-badge'; +import useAccountSearch from 'soapbox/queries/search'; interface IResults { - accounts: { - display_name: string - acct: string - id: string - avatar: string - verified: boolean - }[] + accountSearchResult: ReturnType onSelect(id: string): void } -const Results = ({ accounts, onSelect }: IResults) => ( - <> - {(accounts || []).map((account: any) => ( - - ))} - -); + const [isNearBottom, setNearBottom] = useState(false); + const [isNearTop, setNearTop] = useState(true); -export default Results; \ No newline at end of file + const handleLoadMore = () => { + if (hasNextPage && !isFetching) { + fetchNextPage(); + } + }; + + const renderAccount = useCallback((_index, account) => ( + + ), []); + + return ( +
+ ( +
+ {renderAccount(index, chat)} +
+ )} + endReached={handleLoadMore} + atTopStateChange={(atTop) => setNearTop(atTop)} + atBottomStateChange={(atBottom) => setNearBottom(atBottom)} + /> + + <> +
+
+ +
+ ); +}; + +export default Results; diff --git a/app/soapbox/features/chats/components/chat-widget/headers/chat-search-header.tsx b/app/soapbox/features/chats/components/chat-widget/headers/chat-search-header.tsx new file mode 100644 index 000000000..d0fd40921 --- /dev/null +++ b/app/soapbox/features/chats/components/chat-widget/headers/chat-search-header.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import { defineMessages, useIntl } from 'react-intl'; + +import { HStack, Icon, Text } from 'soapbox/components/ui'; +import { ChatWidgetScreens, useChatContext } from 'soapbox/contexts/chat-context'; + +import ChatPaneHeader from '../chat-pane-header'; + +const messages = defineMessages({ + title: { id: 'chat_search.title', defaultMessage: 'Messages' }, +}); + +const ChatSearchHeader = () => { + const intl = useIntl(); + + const { changeScreen, isOpen, toggleChatPane } = useChatContext(); + + return ( + + + + + {intl.formatMessage(messages.title)} + + + } + isOpen={isOpen} + isToggleable={false} + onToggle={toggleChatPane} + /> + ); +}; + +export default ChatSearchHeader; \ No newline at end of file diff --git a/app/soapbox/queries/search.ts b/app/soapbox/queries/search.ts index 2b3fd1e70..40b78a015 100644 --- a/app/soapbox/queries/search.ts +++ b/app/soapbox/queries/search.ts @@ -1,27 +1,54 @@ -import { useQuery } from '@tanstack/react-query'; +import { useInfiniteQuery } from '@tanstack/react-query'; +import { getNextLink } from 'soapbox/api'; import { useApi } from 'soapbox/hooks'; +import { Account } from 'soapbox/types/entities'; +import { PaginatedResult } from 'soapbox/utils/queries'; export default function useAccountSearch(q: string) { const api = useApi(); - const getAccountSearch = async(q: string) => { - if (typeof q === 'undefined') { - return null; - } + const getAccountSearch = async(q: string, pageParam: { link?: string }): Promise> => { + const nextPageLink = pageParam?.link; + const uri = nextPageLink || '/api/v1/accounts/search'; - const { data } = await api.get('/api/v1/accounts/search', { + const response = await api.get(uri, { params: { q, + limit: 10, followers: true, }, }); + const { data } = response; - return data; + const link = getNextLink(response); + const hasMore = !!link; + + return { + result: data, + link, + hasMore, + }; }; - return useQuery(['search', 'accounts', q], () => getAccountSearch(q), { + const queryInfo = useInfiniteQuery(['search', 'accounts', q], ({ pageParam }) => getAccountSearch(q, pageParam), { keepPreviousData: true, - placeholderData: [], + getNextPageParam: (config) => { + if (config.hasMore) { + return { link: config.link }; + } + + return undefined; + }, }); + + const data = queryInfo.data?.pages.reduce( + (prev: Account[], curr) => [...prev, ...curr.result], + [], + ); + + return { + ...queryInfo, + data, + }; } From 9657598d334490bbdaa6b762d5e945fa79cd6592 Mon Sep 17 00:00:00 2001 From: Chewbacca Date: Thu, 8 Dec 2022 14:41:26 -0500 Subject: [PATCH 2/8] Remove unneeded code from AccountSearch --- app/soapbox/components/account-search.tsx | 49 +++++++------------ .../components/autosuggest-account-input.tsx | 5 +- app/soapbox/components/autosuggest-input.tsx | 12 ++--- 3 files changed, 22 insertions(+), 44 deletions(-) diff --git a/app/soapbox/components/account-search.tsx b/app/soapbox/components/account-search.tsx index 8b3f50b20..c519b0243 100644 --- a/app/soapbox/components/account-search.tsx +++ b/app/soapbox/components/account-search.tsx @@ -5,7 +5,6 @@ import { defineMessages, useIntl } from 'react-intl'; import AutosuggestAccountInput from 'soapbox/components/autosuggest-account-input'; import SvgIcon from './ui/icon/svg-icon'; -import { InputThemes } from './ui/input/input'; const messages = defineMessages({ placeholder: { id: 'account_search.placeholder', defaultMessage: 'Search for an account' }, @@ -16,20 +15,10 @@ interface IAccountSearch { onSelected: (accountId: string) => void, /** Override the default placeholder of the input. */ placeholder?: string, - /** Position of results relative to the input. */ - resultsPosition?: 'above' | 'below', - /** Optional class for the input */ - className?: string, - autoFocus?: boolean, - hidePortal?: boolean, - theme?: InputThemes, - showButtons?: boolean, - /** Search only among people who follow you (TruthSocial). */ - followers?: boolean, } /** Input to search for accounts. */ -const AccountSearch: React.FC = ({ onSelected, className, showButtons = true, ...rest }) => { +const AccountSearch: React.FC = ({ onSelected, ...rest }) => { const intl = useIntl(); const [value, setValue] = useState(''); @@ -71,7 +60,7 @@ const AccountSearch: React.FC = ({ onSelected, className, showBu
= ({ onSelected, className, showBu {...rest} /> - {showButtons && ( -
- +
+ - -
- )} + +
); diff --git a/app/soapbox/components/autosuggest-account-input.tsx b/app/soapbox/components/autosuggest-account-input.tsx index 352be593b..b2a205e3c 100644 --- a/app/soapbox/components/autosuggest-account-input.tsx +++ b/app/soapbox/components/autosuggest-account-input.tsx @@ -22,8 +22,6 @@ interface IAutosuggestAccountInput { menu?: Menu, onKeyDown?: React.KeyboardEventHandler, theme?: InputThemes, - /** Search only among people who follow you (TruthSocial). */ - followers?: boolean, } const AutosuggestAccountInput: React.FC = ({ @@ -31,7 +29,6 @@ const AutosuggestAccountInput: React.FC = ({ onSelected, value = '', limit = 4, - followers = false, ...rest }) => { const dispatch = useAppDispatch(); @@ -48,7 +45,7 @@ const AutosuggestAccountInput: React.FC = ({ }; const handleAccountSearch = useCallback(throttle(q => { - const params = { q, limit, followers, resolve: false }; + const params = { q, limit, resolve: false }; dispatch(accountSearch(params, controller.current.signal)) .then((accounts: { id: string }[]) => { diff --git a/app/soapbox/components/autosuggest-input.tsx b/app/soapbox/components/autosuggest-input.tsx index 87f1c41f5..35460131a 100644 --- a/app/soapbox/components/autosuggest-input.tsx +++ b/app/soapbox/components/autosuggest-input.tsx @@ -31,7 +31,6 @@ export interface IAutosuggestInput extends Pick, hidePortal?: boolean, theme?: InputThemes, @@ -43,7 +42,6 @@ export default class AutosuggestInput extends ImmutablePureComponent { @@ -260,19 +258,15 @@ export default class AutosuggestInput extends ImmutablePureComponent Date: Thu, 8 Dec 2022 14:42:03 -0500 Subject: [PATCH 3/8] Reduce size of ellipsis icon --- .../features/chats/components/chat-list-item.tsx | 16 +++++++++------- .../features/chats/components/chat-list.tsx | 3 +-- .../chats/components/chat-message-list.tsx | 15 +++++++++------ 3 files changed, 19 insertions(+), 15 deletions(-) diff --git a/app/soapbox/features/chats/components/chat-list-item.tsx b/app/soapbox/features/chats/components/chat-list-item.tsx index 72584e2df..6099955e7 100644 --- a/app/soapbox/features/chats/components/chat-list-item.tsx +++ b/app/soapbox/features/chats/components/chat-list-item.tsx @@ -4,7 +4,7 @@ import { useHistory } from 'react-router-dom'; import { openModal } from 'soapbox/actions/modals'; import RelativeTimestamp from 'soapbox/components/relative-timestamp'; -import { Avatar, HStack, Stack, Text } from 'soapbox/components/ui'; +import { Avatar, HStack, IconButton, Stack, Text } from 'soapbox/components/ui'; import VerificationBadge from 'soapbox/components/verification-badge'; import DropdownMenuContainer from 'soapbox/containers/dropdown-menu-container'; import { useChatContext } from 'soapbox/contexts/chat-context'; @@ -115,12 +115,14 @@ const ChatListItem: React.FC = ({ chat, onClick }) => { {features.chatsDelete && (
- {/* TODO: fix nested buttons here */} - + + +
)} diff --git a/app/soapbox/features/chats/components/chat-list.tsx b/app/soapbox/features/chats/components/chat-list.tsx index 9de6d5c2d..65629ce14 100644 --- a/app/soapbox/features/chats/components/chat-list.tsx +++ b/app/soapbox/features/chats/components/chat-list.tsx @@ -63,8 +63,7 @@ const ChatList: React.FC = ({ onClickChat, useWindowScroll = false, s
- ) - } + )} components={{ ScrollSeekPlaceholder: () => , Footer: () => hasNextPage ? : null, diff --git a/app/soapbox/features/chats/components/chat-message-list.tsx b/app/soapbox/features/chats/components/chat-message-list.tsx index 1f6fd6adb..dcb9d61d0 100644 --- a/app/soapbox/features/chats/components/chat-message-list.tsx +++ b/app/soapbox/features/chats/components/chat-message-list.tsx @@ -8,7 +8,7 @@ import { Components, Virtuoso, VirtuosoHandle } from 'react-virtuoso'; import { openModal } from 'soapbox/actions/modals'; import { initReport } from 'soapbox/actions/reports'; -import { Avatar, Button, Divider, HStack, Icon, Spinner, Stack, Text } from 'soapbox/components/ui'; +import { Avatar, Button, Divider, HStack, Icon, IconButton, Spinner, Stack, Text } from 'soapbox/components/ui'; import DropdownMenuContainer from 'soapbox/containers/dropdown-menu-container'; import emojify from 'soapbox/features/emoji/emoji'; import PlaceholderChatMessage from 'soapbox/features/placeholder/components/placeholder-chat-message'; @@ -286,11 +286,14 @@ const ChatMessageList: React.FC = ({ chat }) => { })} data-testid='chat-message-menu' > - + + +
)} From 15594df64456df199dfb5c0e213f4f443d7e3246 Mon Sep 17 00:00:00 2001 From: Chewbacca Date: Thu, 8 Dec 2022 15:09:58 -0500 Subject: [PATCH 4/8] Fix tests --- .../__tests__/chat-search.test.tsx | 52 ++++++++++--------- .../chat-search/empty-results-blankslate.tsx | 2 +- 2 files changed, 29 insertions(+), 25 deletions(-) diff --git a/app/soapbox/features/chats/components/chat-search/__tests__/chat-search.test.tsx b/app/soapbox/features/chats/components/chat-search/__tests__/chat-search.test.tsx index 5a628054e..e7ea8d739 100644 --- a/app/soapbox/features/chats/components/chat-search/__tests__/chat-search.test.tsx +++ b/app/soapbox/features/chats/components/chat-search/__tests__/chat-search.test.tsx @@ -1,5 +1,6 @@ import userEvent from '@testing-library/user-event'; import React from 'react'; +import { VirtuosoMockContext } from 'react-virtuoso'; import { __stub } from 'soapbox/api'; import { ChatProvider } from 'soapbox/contexts/chat-context'; @@ -8,36 +9,24 @@ import { render, screen, waitFor } from '../../../../../jest/test-helpers'; import ChatSearch from '../chat-search'; const renderComponent = () => render( - - - , + + + + , + , ); describe('', () => { - it('renders correctly', () => { + beforeEach(async() => { renderComponent(); - - expect(screen.getByTestId('pane-header')).toHaveTextContent('Messages'); }); - describe('when the pane is closed', () => { - it('does not render the search input', () => { - renderComponent(); - expect(screen.queryAllByTestId('search')).toHaveLength(0); - }); + it('renders the search input', () => { + expect(screen.getByTestId('search')).toBeInTheDocument(); }); - describe('when the pane is open', () => { - beforeEach(async() => { - renderComponent(); - await userEvent.click(screen.getByTestId('icon-button')); - }); - - it('renders the search input', () => { - expect(screen.getByTestId('search')).toBeInTheDocument(); - }); - - describe('when searching', () => { + describe('when searching', () => { + describe('with results', () => { beforeEach(() => { __stub((mock) => { mock.onGet('/api/v1/accounts/search').reply(200, [{ @@ -51,8 +40,6 @@ describe('', () => { }); it('renders accounts', async() => { - renderComponent(); - const user = userEvent.setup(); await user.type(screen.getByTestId('search'), 'ste'); @@ -61,5 +48,22 @@ describe('', () => { }); }); }); + + describe('without results', () => { + beforeEach(() => { + __stub((mock) => { + mock.onGet('/api/v1/accounts/search').reply(200, []); + }); + }); + + it('renders accounts', async() => { + const user = userEvent.setup(); + await user.type(screen.getByTestId('search'), 'ste'); + + await waitFor(() => { + expect(screen.getByTestId('no-results')).toBeInTheDocument(); + }); + }); + }); }); }); diff --git a/app/soapbox/features/chats/components/chat-search/empty-results-blankslate.tsx b/app/soapbox/features/chats/components/chat-search/empty-results-blankslate.tsx index 15daef888..3e360347e 100644 --- a/app/soapbox/features/chats/components/chat-search/empty-results-blankslate.tsx +++ b/app/soapbox/features/chats/components/chat-search/empty-results-blankslate.tsx @@ -13,7 +13,7 @@ const EmptyResultsBlankslate = () => { return ( - + {intl.formatMessage(messages.title)} From 502f9d9f47309dc30125d6d20c1e05fede99ef66 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 12 Dec 2022 12:10:39 -0600 Subject: [PATCH 5/8] queries/search: use flattenPages util --- app/soapbox/queries/search.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/app/soapbox/queries/search.ts b/app/soapbox/queries/search.ts index 40b78a015..41c4b7fb3 100644 --- a/app/soapbox/queries/search.ts +++ b/app/soapbox/queries/search.ts @@ -3,7 +3,7 @@ import { useInfiniteQuery } from '@tanstack/react-query'; import { getNextLink } from 'soapbox/api'; import { useApi } from 'soapbox/hooks'; import { Account } from 'soapbox/types/entities'; -import { PaginatedResult } from 'soapbox/utils/queries'; +import { flattenPages, PaginatedResult } from 'soapbox/utils/queries'; export default function useAccountSearch(q: string) { const api = useApi(); @@ -42,10 +42,7 @@ export default function useAccountSearch(q: string) { }, }); - const data = queryInfo.data?.pages.reduce( - (prev: Account[], curr) => [...prev, ...curr.result], - [], - ); + const data = flattenPages(queryInfo.data); return { ...queryInfo, From af19c1bd44f0be76f4a39e07f231adf6ea444759 Mon Sep 17 00:00:00 2001 From: Chewbacca Date: Fri, 9 Dec 2022 11:48:29 -0500 Subject: [PATCH 6/8] Fix spacing bug --- app/soapbox/features/chats/components/chat-composer.tsx | 3 +++ app/soapbox/features/chats/components/chat-message-list.tsx | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/app/soapbox/features/chats/components/chat-composer.tsx b/app/soapbox/features/chats/components/chat-composer.tsx index 20db9636d..358fbb6f3 100644 --- a/app/soapbox/features/chats/components/chat-composer.tsx +++ b/app/soapbox/features/chats/components/chat-composer.tsx @@ -151,6 +151,9 @@ const ChatComposer = React.forwardRef return (
+ {/* Spacer */} +
+ {features.chatsMedia && ( diff --git a/app/soapbox/features/chats/components/chat-message-list.tsx b/app/soapbox/features/chats/components/chat-message-list.tsx index dcb9d61d0..983a0d6f2 100644 --- a/app/soapbox/features/chats/components/chat-message-list.tsx +++ b/app/soapbox/features/chats/components/chat-message-list.tsx @@ -450,7 +450,7 @@ const ChatMessageList: React.FC = ({ chat }) => { return (
-
+
Date: Mon, 5 Dec 2022 08:39:28 -0500 Subject: [PATCH 7/8] Remove 2 minute expiration --- .../chats/components/chat-page/components/chat-page-main.tsx | 5 ----- app/soapbox/queries/chats.ts | 3 +-- 2 files changed, 1 insertion(+), 7 deletions(-) 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 8950c4e3e..638a7fea1 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 @@ -183,11 +183,6 @@ const ChatPageMain = () => { label={intl.formatMessage(messages.autoDeleteLabel)} hint={intl.formatMessage(messages.autoDeleteHint)} /> - handleUpdateChat(MessageExpirationValues.TWO_MINUTES)} - isSelected={chat.message_expiration === MessageExpirationValues.TWO_MINUTES} - /> handleUpdateChat(MessageExpirationValues.SEVEN)} diff --git a/app/soapbox/queries/chats.ts b/app/soapbox/queries/chats.ts index 68fa7fc21..a20859e85 100644 --- a/app/soapbox/queries/chats.ts +++ b/app/soapbox/queries/chats.ts @@ -15,10 +15,9 @@ import { useFetchRelationships } from './relationships'; import type { IAccount } from './accounts'; -export const messageExpirationOptions = [120, 604800, 1209600, 2592000, 7776000]; +export const messageExpirationOptions = [604800, 1209600, 2592000, 7776000]; export enum MessageExpirationValues { - 'TWO_MINUTES' = messageExpirationOptions[0], 'SEVEN' = messageExpirationOptions[1], 'FOURTEEN' = messageExpirationOptions[2], 'THIRTY' = messageExpirationOptions[3], From 61cd1d2227b9f36fb7f97ddb27e08860934088d8 Mon Sep 17 00:00:00 2001 From: Chewbacca Date: Wed, 7 Dec 2022 10:44:16 -0500 Subject: [PATCH 8/8] Fix expiration values --- app/soapbox/queries/chats.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/soapbox/queries/chats.ts b/app/soapbox/queries/chats.ts index a20859e85..9a119f5f1 100644 --- a/app/soapbox/queries/chats.ts +++ b/app/soapbox/queries/chats.ts @@ -18,10 +18,10 @@ import type { IAccount } from './accounts'; export const messageExpirationOptions = [604800, 1209600, 2592000, 7776000]; export enum MessageExpirationValues { - 'SEVEN' = messageExpirationOptions[1], - 'FOURTEEN' = messageExpirationOptions[2], - 'THIRTY' = messageExpirationOptions[3], - 'NINETY' = messageExpirationOptions[4] + 'SEVEN' = messageExpirationOptions[0], + 'FOURTEEN' = messageExpirationOptions[1], + 'THIRTY' = messageExpirationOptions[2], + 'NINETY' = messageExpirationOptions[3] } export interface IChat {