diff --git a/app/soapbox/features/chats/components/audio_toggle.js b/app/soapbox/features/chats/components/audio_toggle.js
deleted file mode 100644
index f0068d1f3..000000000
--- a/app/soapbox/features/chats/components/audio_toggle.js
+++ /dev/null
@@ -1,61 +0,0 @@
-import PropTypes from 'prop-types';
-import React from 'react';
-import { injectIntl, defineMessages } from 'react-intl';
-import { connect } from 'react-redux';
-import Toggle from 'react-toggle';
-
-import { changeSetting, getSettings } from 'soapbox/actions/settings';
-
-const messages = defineMessages({
- switchOn: { id: 'chats.audio_toggle_on', defaultMessage: 'Audio notification on' },
- switchOff: { id: 'chats.audio_toggle_off', defaultMessage: 'Audio notification off' },
-});
-
-const mapStateToProps = state => {
- return {
- checked: getSettings(state).getIn(['chats', 'sound'], false),
- };
-};
-
-const mapDispatchToProps = (dispatch) => ({
- toggleAudio(setting) {
- dispatch(changeSetting(['chats', 'sound'], setting));
- },
-});
-
-export default @connect(mapStateToProps, mapDispatchToProps)
-@injectIntl
-class AudioToggle extends React.PureComponent {
-
- static propTypes = {
- intl: PropTypes.object.isRequired,
- checked: PropTypes.bool.isRequired,
- toggleAudio: PropTypes.func,
- showLabel: PropTypes.bool,
- };
-
- handleToggleAudio = () => {
- this.props.toggleAudio(!this.props.checked);
- }
-
- render() {
- const { intl, checked, showLabel } = this.props;
- const id = 'chats-audio-toggle';
- const label = intl.formatMessage(checked ? messages.switchOff : messages.switchOn);
-
- return (
-
-
-
- {showLabel && ()}
-
-
- );
- }
-
-}
diff --git a/app/soapbox/features/chats/components/audio_toggle.tsx b/app/soapbox/features/chats/components/audio_toggle.tsx
new file mode 100644
index 000000000..96ddf6211
--- /dev/null
+++ b/app/soapbox/features/chats/components/audio_toggle.tsx
@@ -0,0 +1,46 @@
+import React from 'react';
+import { defineMessages, useIntl } from 'react-intl';
+import { useDispatch } from 'react-redux';
+import Toggle from 'react-toggle';
+
+import { changeSetting, getSettings } from 'soapbox/actions/settings';
+import { useAppSelector } from 'soapbox/hooks';
+
+const messages = defineMessages({
+ switchOn: { id: 'chats.audio_toggle_on', defaultMessage: 'Audio notification on' },
+ switchOff: { id: 'chats.audio_toggle_off', defaultMessage: 'Audio notification off' },
+});
+
+interface IAudioToggle {
+ showLabel?: boolean
+}
+
+const AudioToggle: React.FC = ({ showLabel }) => {
+ const dispatch = useDispatch();
+ const intl = useIntl();
+
+ const checked = useAppSelector(state => !!getSettings(state).getIn(['chats', 'sound']));
+
+ const handleToggleAudio = () => {
+ dispatch(changeSetting(['chats', 'sound'], !checked));
+ };
+
+ const id = 'chats-audio-toggle';
+ const label = intl.formatMessage(checked ? messages.switchOff : messages.switchOn);
+
+ return (
+
+
+
+ {showLabel && ()}
+
+
+ );
+};
+
+export default AudioToggle;
diff --git a/app/soapbox/features/chats/components/chat.js b/app/soapbox/features/chats/components/chat.js
deleted file mode 100644
index f19190bed..000000000
--- a/app/soapbox/features/chats/components/chat.js
+++ /dev/null
@@ -1,87 +0,0 @@
-import PropTypes from 'prop-types';
-import React from 'react';
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import ImmutablePureComponent from 'react-immutable-pure-component';
-import { FormattedMessage } from 'react-intl';
-import { connect } from 'react-redux';
-
-import Icon from 'soapbox/components/icon';
-import emojify from 'soapbox/features/emoji/emoji';
-import { makeGetChat } from 'soapbox/selectors';
-import { shortNumberFormat } from 'soapbox/utils/numbers';
-
-import Avatar from '../../../components/avatar';
-import DisplayName from '../../../components/display_name';
-
-const makeMapStateToProps = () => {
- const getChat = makeGetChat();
-
- const mapStateToProps = (state, { chatId }) => {
- const chat = state.getIn(['chats', 'items', chatId]);
-
- return {
- chat: chat ? getChat(state, chat.toJS()) : undefined,
- };
- };
-
- return mapStateToProps;
-};
-
-export default @connect(makeMapStateToProps)
-class Chat extends ImmutablePureComponent {
-
- static propTypes = {
- chatId: PropTypes.string.isRequired,
- chat: ImmutablePropTypes.map,
- onClick: PropTypes.func,
- };
-
- handleClick = () => {
- this.props.onClick(this.props.chat);
- }
-
- render() {
- const { chat } = this.props;
- if (!chat) return null;
- const account = chat.get('account');
- const unreadCount = chat.get('unread');
- const content = chat.getIn(['last_message', 'content']);
- const attachment = chat.getIn(['last_message', 'attachment']);
- const image = attachment && attachment.getIn(['pleroma', 'mime_type'], '').startsWith('image/');
- const parsedContent = content ? emojify(content) : '';
-
- return (
-
-
-
-
-
-
- {attachment && (
-
- )}
- {content ? (
-
- ) : attachment && (
-
- {image ? : }
-
- )}
- {unreadCount > 0 &&
{shortNumberFormat(unreadCount)}}
-
-
-
- );
- }
-
-}
diff --git a/app/soapbox/features/chats/components/chat.tsx b/app/soapbox/features/chats/components/chat.tsx
new file mode 100644
index 000000000..afb1d9df8
--- /dev/null
+++ b/app/soapbox/features/chats/components/chat.tsx
@@ -0,0 +1,69 @@
+import React from 'react';
+import { FormattedMessage } from 'react-intl';
+
+import Avatar from 'soapbox/components/avatar';
+import DisplayName from 'soapbox/components/display_name';
+import Icon from 'soapbox/components/icon';
+import emojify from 'soapbox/features/emoji/emoji';
+import { useAppSelector } from 'soapbox/hooks';
+import { makeGetChat } from 'soapbox/selectors';
+import { shortNumberFormat } from 'soapbox/utils/numbers';
+
+import type { Account as AccountEntity, Chat as ChatEntity } from 'soapbox/types/entities';
+
+const getChat = makeGetChat();
+
+interface IChat {
+ chatId: string,
+ onClick: (chat: any) => void,
+}
+
+const Chat: React.FC = ({ chatId, onClick }) => {
+ const chat = useAppSelector((state) => {
+ const chat = state.chats.getIn(['items', chatId]);
+ return chat ? getChat(state, (chat as any).toJS()) : undefined;
+ }) as ChatEntity;
+
+ const account = chat.account as AccountEntity;
+ if (!chat || !account) return null;
+ const unreadCount = chat.unread;
+ const content = chat.getIn(['last_message', 'content']);
+ const attachment = chat.getIn(['last_message', 'attachment']);
+ const image = attachment && (attachment as any).getIn(['pleroma', 'mime_type'], '').startsWith('image/');
+ const parsedContent = content ? emojify(content) : '';
+
+ return (
+
+
+ );
+};
+
+export default Chat;
diff --git a/app/soapbox/features/chats/components/chat_list.js b/app/soapbox/features/chats/components/chat_list.js
deleted file mode 100644
index ffb0c1720..000000000
--- a/app/soapbox/features/chats/components/chat_list.js
+++ /dev/null
@@ -1,99 +0,0 @@
-import { debounce } from 'lodash';
-import PropTypes from 'prop-types';
-import React from 'react';
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import ImmutablePureComponent from 'react-immutable-pure-component';
-import { defineMessages, injectIntl } from 'react-intl';
-import { connect } from 'react-redux';
-import { createSelector } from 'reselect';
-
-import { expandChats } from 'soapbox/actions/chats';
-import ScrollableList from 'soapbox/components/scrollable_list';
-import PlaceholderChat from 'soapbox/features/placeholder/components/placeholder_chat';
-
-import Chat from './chat';
-
-const messages = defineMessages({
- emptyMessage: { id: 'chat_panels.main_window.empty', defaultMessage: 'No chats found. To start a chat, visit a user\'s profile' },
-});
-
-const getSortedChatIds = chats => (
- chats
- .toList()
- .sort(chatDateComparator)
- .map(chat => chat.get('id'))
-);
-
-const chatDateComparator = (chatA, chatB) => {
- // Sort most recently updated chats at the top
- const a = new Date(chatA.get('updated_at'));
- const b = new Date(chatB.get('updated_at'));
-
- if (a === b) return 0;
- if (a > b) return -1;
- if (a < b) return 1;
- return 0;
-};
-
-const sortedChatIdsSelector = createSelector(
- [getSortedChatIds],
- chats => chats,
-);
-
-const makeMapStateToProps = () => {
- const mapStateToProps = state => ({
- chatIds: sortedChatIdsSelector(state.getIn(['chats', 'items'])),
- hasMore: !!state.getIn(['chats', 'next']),
- isLoading: state.getIn(['chats', 'loading']),
- });
-
- return mapStateToProps;
-};
-
-export default @connect(makeMapStateToProps)
-@injectIntl
-class ChatList extends ImmutablePureComponent {
-
- static propTypes = {
- dispatch: PropTypes.func.isRequired,
- intl: PropTypes.object.isRequired,
- chatIds: ImmutablePropTypes.list,
- onClickChat: PropTypes.func,
- onRefresh: PropTypes.func,
- hasMore: PropTypes.func,
- isLoading: PropTypes.bool,
- };
-
- handleLoadMore = debounce(() => {
- this.props.dispatch(expandChats());
- }, 300, { leading: true });
-
- render() {
- const { intl, chatIds, hasMore, isLoading } = this.props;
-
- return (
-
- {chatIds.map(chatId => (
-
-
-
- ))}
-
- );
- }
-
-}
diff --git a/app/soapbox/features/chats/components/chat_list.tsx b/app/soapbox/features/chats/components/chat_list.tsx
new file mode 100644
index 000000000..874c71041
--- /dev/null
+++ b/app/soapbox/features/chats/components/chat_list.tsx
@@ -0,0 +1,84 @@
+import { Map as ImmutableMap } from 'immutable';
+import { debounce } from 'lodash';
+import React from 'react';
+import { defineMessages, useIntl } from 'react-intl';
+import { useDispatch } from 'react-redux';
+import { createSelector } from 'reselect';
+
+import { expandChats } from 'soapbox/actions/chats';
+import ScrollableList from 'soapbox/components/scrollable_list';
+import PlaceholderChat from 'soapbox/features/placeholder/components/placeholder_chat';
+import { useAppSelector } from 'soapbox/hooks';
+
+import Chat from './chat';
+
+const messages = defineMessages({
+ emptyMessage: { id: 'chat_panels.main_window.empty', defaultMessage: 'No chats found. To start a chat, visit a user\'s profile' },
+});
+
+const handleLoadMore = debounce((dispatch) => {
+ dispatch(expandChats());
+}, 300, { leading: true });
+
+const getSortedChatIds = (chats: ImmutableMap) => (
+ chats
+ .toList()
+ .sort(chatDateComparator)
+ .map(chat => chat.id)
+);
+
+const chatDateComparator = (chatA: { updated_at: string }, chatB: { updated_at: string }) => {
+ // Sort most recently updated chats at the top
+ const a = new Date(chatA.updated_at);
+ const b = new Date(chatB.updated_at);
+
+ if (a === b) return 0;
+ if (a > b) return -1;
+ if (a < b) return 1;
+ return 0;
+};
+
+const sortedChatIdsSelector = createSelector(
+ [getSortedChatIds],
+ chats => chats,
+);
+
+interface IChatList {
+ onClickChat: (chat: any) => void,
+ onRefresh: () => void,
+}
+
+const ChatList: React.FC = ({ onClickChat, onRefresh }) => {
+ const dispatch = useDispatch();
+ const intl = useIntl();
+
+ const chatIds = useAppSelector(state => sortedChatIdsSelector(state.chats.get('items')));
+ const hasMore = useAppSelector(state => !!state.chats.get('next'));
+ const isLoading = useAppSelector(state => state.chats.get('isLoading'));
+
+ return (
+ handleLoadMore(dispatch)}
+ onRefresh={onRefresh}
+ placeholderComponent={PlaceholderChat}
+ placeholderCount={20}
+ >
+ {chatIds.map((chatId: string) => (
+
+
+
+ ))}
+
+ );
+};
+
+export default ChatList;
diff --git a/app/soapbox/features/chats/index.js b/app/soapbox/features/chats/index.js
deleted file mode 100644
index ed8c35e60..000000000
--- a/app/soapbox/features/chats/index.js
+++ /dev/null
@@ -1,66 +0,0 @@
-import PropTypes from 'prop-types';
-import React from 'react';
-import { defineMessages, injectIntl } from 'react-intl';
-import { connect } from 'react-redux';
-import { withRouter } from 'react-router-dom';
-
-import { fetchChats, launchChat } from 'soapbox/actions/chats';
-import AccountSearch from 'soapbox/components/account_search';
-import AudioToggle from 'soapbox/features/chats/components/audio_toggle';
-
-import { Column } from '../../components/ui';
-
-import ChatList from './components/chat_list';
-
-const messages = defineMessages({
- title: { id: 'column.chats', defaultMessage: 'Chats' },
- searchPlaceholder: { id: 'chats.search_placeholder', defaultMessage: 'Start a chat with…' },
-});
-
-export default @connect()
-@injectIntl
-@withRouter
-class ChatIndex extends React.PureComponent {
-
- static propTypes = {
- intl: PropTypes.object.isRequired,
- dispatch: PropTypes.func.isRequired,
- history: PropTypes.object,
- };
-
- handleSuggestion = accountId => {
- this.props.dispatch(launchChat(accountId, this.props.history, true));
- }
-
- handleClickChat = (chat) => {
- this.props.history.push(`/chats/${chat.get('id')}`);
- }
-
- handleRefresh = () => {
- const { dispatch } = this.props;
- return dispatch(fetchChats());
- }
-
- render() {
- const { intl } = this.props;
-
- return (
-
-
-
-
-
-
-
- );
- }
-
-}
diff --git a/app/soapbox/features/chats/index.tsx b/app/soapbox/features/chats/index.tsx
new file mode 100644
index 000000000..c13335ff3
--- /dev/null
+++ b/app/soapbox/features/chats/index.tsx
@@ -0,0 +1,55 @@
+import React from 'react';
+import { defineMessages, useIntl } from 'react-intl';
+import { useDispatch } from 'react-redux';
+import { useHistory } from 'react-router-dom';
+
+import { fetchChats, launchChat } from 'soapbox/actions/chats';
+import AccountSearch from 'soapbox/components/account_search';
+import AudioToggle from 'soapbox/features/chats/components/audio_toggle';
+
+import { Column } from '../../components/ui';
+
+import ChatList from './components/chat_list';
+
+const messages = defineMessages({
+ title: { id: 'column.chats', defaultMessage: 'Chats' },
+ searchPlaceholder: { id: 'chats.search_placeholder', defaultMessage: 'Start a chat with…' },
+});
+
+const ChatIndex: React.FC = () => {
+ const intl = useIntl();
+ const dispatch = useDispatch();
+ const history = useHistory();
+
+ const handleSuggestion = (accountId: string) => {
+ dispatch(launchChat(accountId, history, true));
+ };
+
+ const handleClickChat = (chat: { id: string }) => {
+ history.push(`/chats/${chat.id}`);
+ };
+
+ const handleRefresh = () => {
+ return dispatch(fetchChats());
+ };
+
+ return (
+
+
+
+
+
+
+
+ );
+};
+
+export default ChatIndex;
diff --git a/app/soapbox/features/compose/containers/upload_container.js b/app/soapbox/features/compose/containers/upload_container.js
index 9c77c6df7..332f206ee 100644
--- a/app/soapbox/features/compose/containers/upload_container.js
+++ b/app/soapbox/features/compose/containers/upload_container.js
@@ -26,7 +26,7 @@ const mapDispatchToProps = dispatch => ({
},
onOpenModal: media => {
- dispatch(openModal('MEDIA', { media: ImmutableList.of(media), index: 0, onClose: console.log }));
+ dispatch(openModal('MEDIA', { media: ImmutableList.of(media), index: 0 }));
},
onSubmit(router) {
diff --git a/app/soapbox/normalizers/chat.ts b/app/soapbox/normalizers/chat.ts
new file mode 100644
index 000000000..04149c682
--- /dev/null
+++ b/app/soapbox/normalizers/chat.ts
@@ -0,0 +1,18 @@
+import { Map as ImmutableMap, Record as ImmutableRecord, fromJS } from 'immutable';
+
+import type { ReducerAccount } from 'soapbox/reducers/accounts';
+import type { Account, EmbeddedEntity } from 'soapbox/types/entities';
+
+export const ChatRecord = ImmutableRecord({
+ account: null as EmbeddedEntity,
+ id: '',
+ unread: 0,
+ last_message: '' as string || null,
+ updated_at: new Date(),
+});
+
+export const normalizeChat = (chat: Record) => {
+ return ChatRecord(
+ ImmutableMap(fromJS(chat)),
+ );
+};
diff --git a/app/soapbox/normalizers/chat_message.ts b/app/soapbox/normalizers/chat_message.ts
new file mode 100644
index 000000000..71536acb5
--- /dev/null
+++ b/app/soapbox/normalizers/chat_message.ts
@@ -0,0 +1,29 @@
+import {
+ List as ImmutableList,
+ Map as ImmutableMap,
+ Record as ImmutableRecord,
+ fromJS,
+} from 'immutable';
+
+import type { Attachment, Card, Emoji } from 'soapbox/types/entities';
+
+export const ChatMessageRecord = ImmutableRecord({
+ account_id: '',
+ attachment: null as Attachment | null,
+ card: null as Card | null,
+ chat_id: '',
+ content: '',
+ created_at: new Date(),
+ emojis: ImmutableList(),
+ id: '',
+ unread: false,
+
+ deleting: false,
+ pending: false,
+});
+
+export const normalizeChatMessage = (chatMessage: Record) => {
+ return ChatMessageRecord(
+ ImmutableMap(fromJS(chatMessage)),
+ );
+};
diff --git a/app/soapbox/normalizers/index.ts b/app/soapbox/normalizers/index.ts
index 613de5331..3251669de 100644
--- a/app/soapbox/normalizers/index.ts
+++ b/app/soapbox/normalizers/index.ts
@@ -1,6 +1,8 @@
export { AccountRecord, FieldRecord, normalizeAccount } from './account';
export { AttachmentRecord, normalizeAttachment } from './attachment';
export { CardRecord, normalizeCard } from './card';
+export { ChatRecord, normalizeChat } from './chat';
+export { ChatMessageRecord, normalizeChatMessage } from './chat_message';
export { EmojiRecord, normalizeEmoji } from './emoji';
export { InstanceRecord, normalizeInstance } from './instance';
export { MentionRecord, normalizeMention } from './mention';
diff --git a/app/soapbox/reducers/chat_message_lists.js b/app/soapbox/reducers/chat_message_lists.ts
similarity index 63%
rename from app/soapbox/reducers/chat_message_lists.js
rename to app/soapbox/reducers/chat_message_lists.ts
index 9939885e6..79a595801 100644
--- a/app/soapbox/reducers/chat_message_lists.js
+++ b/app/soapbox/reducers/chat_message_lists.ts
@@ -1,4 +1,5 @@
import { Map as ImmutableMap, OrderedSet as ImmutableOrderedSet } from 'immutable';
+import { AnyAction } from 'redux';
import {
CHATS_FETCH_SUCCESS,
@@ -10,41 +11,46 @@ import {
} from 'soapbox/actions/chats';
import { STREAMING_CHAT_UPDATE } from 'soapbox/actions/streaming';
-const initialState = ImmutableMap();
+type APIEntity = Record;
+type APIEntities = Array;
-const idComparator = (a, b) => {
+type State = ImmutableMap>;
+
+const initialState: State = ImmutableMap();
+
+const idComparator = (a: string, b: string) => {
if (a < b) return -1;
if (a > b) return 1;
return 0;
};
-const updateList = (state, chatId, messageIds) => {
+const updateList = (state: State, chatId: string, messageIds: string[]) => {
const ids = state.get(chatId, ImmutableOrderedSet());
- const newIds = ids.union(messageIds).sort(idComparator);
+ const newIds = (ids.union(messageIds) as ImmutableOrderedSet).sort(idComparator);
return state.set(chatId, newIds);
};
-const importMessage = (state, chatMessage) => {
+const importMessage = (state: State, chatMessage: APIEntity) => {
return updateList(state, chatMessage.chat_id, [chatMessage.id]);
};
-const importMessages = (state, chatMessages) => (
+const importMessages = (state: State, chatMessages: APIEntities) => (
state.withMutations(map =>
chatMessages.forEach(chatMessage =>
importMessage(map, chatMessage)))
);
-const importLastMessages = (state, chats) =>
+const importLastMessages = (state: State, chats: APIEntities) =>
state.withMutations(mutable =>
chats.forEach(chat => {
if (chat.last_message) importMessage(mutable, chat.last_message);
}));
-const replaceMessage = (state, chatId, oldId, newId) => {
- return state.update(chatId, chat => chat.delete(oldId).add(newId).sort(idComparator));
+const replaceMessage = (state: State, chatId: string, oldId: string, newId: string) => {
+ return state.update(chatId, chat => chat!.delete(oldId).add(newId).sort(idComparator));
};
-export default function chatMessageLists(state = initialState, action) {
+export default function chatMessageLists(state = initialState, action: AnyAction) {
switch(action.type) {
case CHAT_MESSAGE_SEND_REQUEST:
return updateList(state, action.chatId, [action.uuid]);
@@ -58,11 +64,11 @@ export default function chatMessageLists(state = initialState, action) {
else
return state;
case CHAT_MESSAGES_FETCH_SUCCESS:
- return updateList(state, action.chatId, action.chatMessages.map(chat => chat.id));
+ return updateList(state, action.chatId, action.chatMessages.map((chat: APIEntity) => chat.id));
case CHAT_MESSAGE_SEND_SUCCESS:
return replaceMessage(state, action.chatId, action.uuid, action.chatMessage.id);
case CHAT_MESSAGE_DELETE_SUCCESS:
- return state.update(action.chatId, chat => chat.delete(action.messageId));
+ return state.update(action.chatId, chat => chat!.delete(action.messageId));
default:
return state;
}
diff --git a/app/soapbox/reducers/chat_messages.js b/app/soapbox/reducers/chat_messages.ts
similarity index 60%
rename from app/soapbox/reducers/chat_messages.js
rename to app/soapbox/reducers/chat_messages.ts
index a0787d077..d6b4f4fb9 100644
--- a/app/soapbox/reducers/chat_messages.js
+++ b/app/soapbox/reducers/chat_messages.ts
@@ -1,4 +1,5 @@
import { Map as ImmutableMap, fromJS } from 'immutable';
+import { AnyAction } from 'redux';
import {
CHATS_FETCH_SUCCESS,
@@ -10,25 +11,32 @@ import {
CHAT_MESSAGE_DELETE_SUCCESS,
} from 'soapbox/actions/chats';
import { STREAMING_CHAT_UPDATE } from 'soapbox/actions/streaming';
+import { normalizeChatMessage } from 'soapbox/normalizers';
-const initialState = ImmutableMap();
+type ChatMessageRecord = ReturnType;
+type APIEntity = Record;
+type APIEntities = Array;
-const importMessage = (state, message) => {
- return state.set(message.get('id'), message);
+type State = ImmutableMap;
+
+const importMessage = (state: State, message: APIEntity) => {
+ return state.set(message.id, normalizeChatMessage(message));
};
-const importMessages = (state, messages) =>
+const importMessages = (state: State, messages: APIEntities) =>
state.withMutations(mutable =>
messages.forEach(message => importMessage(mutable, message)));
-const importLastMessages = (state, chats) =>
+const importLastMessages = (state: State, chats: APIEntities) =>
state.withMutations(mutable =>
chats.forEach(chat => {
- if (chat.get('last_message'))
- importMessage(mutable, chat.get('last_message'));
+ if (chat.last_message)
+ importMessage(mutable, chat.last_message);
}));
-export default function chatMessages(state = initialState, action) {
+const initialState: State = ImmutableMap();
+
+export default function chatMessages(state = initialState, action: AnyAction) {
switch(action.type) {
case CHAT_MESSAGE_SEND_REQUEST:
return importMessage(state, fromJS({
@@ -41,16 +49,16 @@ export default function chatMessages(state = initialState, action) {
}));
case CHATS_FETCH_SUCCESS:
case CHATS_EXPAND_SUCCESS:
- return importLastMessages(state, fromJS(action.chats));
+ return importLastMessages(state, action.chats);
case CHAT_MESSAGES_FETCH_SUCCESS:
- return importMessages(state, fromJS(action.chatMessages));
+ return importMessages(state, action.chatMessages);
case CHAT_MESSAGE_SEND_SUCCESS:
return importMessage(state, fromJS(action.chatMessage)).delete(action.uuid);
case STREAMING_CHAT_UPDATE:
- return importLastMessages(state, fromJS([action.chat]));
+ return importLastMessages(state, [action.chat]);
case CHAT_MESSAGE_DELETE_REQUEST:
return state.update(action.messageId, chatMessage =>
- chatMessage.set('pending', true).set('deleting', true));
+ chatMessage!.set('pending', true).set('deleting', true));
case CHAT_MESSAGE_DELETE_SUCCESS:
return state.delete(action.messageId);
default:
diff --git a/app/soapbox/reducers/chats.js b/app/soapbox/reducers/chats.js
deleted file mode 100644
index 430251210..000000000
--- a/app/soapbox/reducers/chats.js
+++ /dev/null
@@ -1,58 +0,0 @@
-import { Map as ImmutableMap, fromJS } from 'immutable';
-
-import {
- CHATS_FETCH_SUCCESS,
- CHATS_FETCH_REQUEST,
- CHATS_EXPAND_SUCCESS,
- CHATS_EXPAND_REQUEST,
- CHAT_FETCH_SUCCESS,
- CHAT_READ_SUCCESS,
- CHAT_READ_REQUEST,
-} from 'soapbox/actions/chats';
-import { STREAMING_CHAT_UPDATE } from 'soapbox/actions/streaming';
-
-const normalizeChat = (chat, normalOldChat) => {
- const normalChat = { ...chat };
- const { account, last_message: lastMessage } = chat;
-
- if (account) normalChat.account = account.id;
- if (lastMessage) normalChat.last_message = lastMessage.id;
-
- return normalChat;
-};
-
-const importChat = (state, chat) => state.setIn(['items', chat.id], fromJS(normalizeChat(chat)));
-
-const importChats = (state, chats, next) =>
- state.withMutations(mutable => {
- if (next !== undefined) mutable.set('next', next);
- chats.forEach(chat => importChat(mutable, chat));
- mutable.set('loading', false);
- });
-
-const initialState = ImmutableMap({
- next: null,
- isLoading: false,
- items: ImmutableMap({}),
-});
-
-export default function chats(state = initialState, action) {
- switch(action.type) {
- case CHATS_FETCH_REQUEST:
- case CHATS_EXPAND_REQUEST:
- return state.set('loading', true);
- case CHATS_FETCH_SUCCESS:
- case CHATS_EXPAND_SUCCESS:
- return importChats(state, action.chats, action.next);
- case STREAMING_CHAT_UPDATE:
- return importChats(state, [action.chat]);
- case CHAT_FETCH_SUCCESS:
- return importChats(state, [action.chat]);
- case CHAT_READ_REQUEST:
- return state.setIn([action.chatId, 'unread'], 0);
- case CHAT_READ_SUCCESS:
- return importChats(state, [action.chat]);
- default:
- return state;
- }
-}
diff --git a/app/soapbox/reducers/chats.ts b/app/soapbox/reducers/chats.ts
new file mode 100644
index 000000000..956da3c39
--- /dev/null
+++ b/app/soapbox/reducers/chats.ts
@@ -0,0 +1,76 @@
+import { Map as ImmutableMap, Record as ImmutableRecord } from 'immutable';
+
+import {
+ CHATS_FETCH_SUCCESS,
+ CHATS_FETCH_REQUEST,
+ CHATS_EXPAND_SUCCESS,
+ CHATS_EXPAND_REQUEST,
+ CHAT_FETCH_SUCCESS,
+ CHAT_READ_SUCCESS,
+ CHAT_READ_REQUEST,
+} from 'soapbox/actions/chats';
+import { STREAMING_CHAT_UPDATE } from 'soapbox/actions/streaming';
+import { normalizeChat } from 'soapbox/normalizers';
+import { normalizeId } from 'soapbox/utils/normalizers';
+
+import type { AnyAction } from 'redux';
+
+type ChatRecord = ReturnType;
+type APIEntity = Record;
+type APIEntities = Array;
+
+export interface ReducerChat extends ChatRecord {
+ account: string | null,
+ last_message: string | null,
+}
+
+const ReducerRecord = ImmutableRecord({
+ next: null as string | null,
+ isLoading: false,
+ items: ImmutableMap({}),
+});
+
+type State = ReturnType;
+
+const minifyChat = (chat: ChatRecord): ReducerChat => {
+ return chat.mergeWith((o, n) => n || o, {
+ account: normalizeId(chat.getIn(['account', 'id'])),
+ last_message: normalizeId(chat.getIn(['last_message', 'id'])),
+ }) as ReducerChat;
+};
+
+const fixChat = (chat: APIEntity): ReducerChat => {
+ return normalizeChat(chat).withMutations(chat => {
+ minifyChat(chat);
+ }) as ReducerChat;
+};
+
+const importChat = (state: State, chat: APIEntity) => state.setIn(['items', chat.id], fixChat(chat));
+
+const importChats = (state: State, chats: APIEntities, next?: string) =>
+ state.withMutations(mutable => {
+ if (next !== undefined) mutable.set('next', next);
+ chats.forEach(chat => importChat(mutable, chat));
+ mutable.set('isLoading', false);
+ });
+
+export default function chats(state: State = ReducerRecord(), action: AnyAction): State {
+ switch(action.type) {
+ case CHATS_FETCH_REQUEST:
+ case CHATS_EXPAND_REQUEST:
+ return state.set('isLoading', true);
+ case CHATS_FETCH_SUCCESS:
+ case CHATS_EXPAND_SUCCESS:
+ return importChats(state, action.chats, action.next);
+ case STREAMING_CHAT_UPDATE:
+ return importChats(state, [action.chat]);
+ case CHAT_FETCH_SUCCESS:
+ return importChats(state, [action.chat]);
+ case CHAT_READ_REQUEST:
+ return state.setIn([action.chatId, 'unread'], 0);
+ case CHAT_READ_SUCCESS:
+ return importChats(state, [action.chat]);
+ default:
+ return state;
+ }
+}
diff --git a/app/soapbox/selectors/index.ts b/app/soapbox/selectors/index.ts
index ddac93b24..40f8105e3 100644
--- a/app/soapbox/selectors/index.ts
+++ b/app/soapbox/selectors/index.ts
@@ -12,6 +12,7 @@ import { validId } from 'soapbox/utils/auth';
import ConfigDB from 'soapbox/utils/config_db';
import { shouldFilter } from 'soapbox/utils/timelines';
+import type { ReducerChat } from 'soapbox/reducers/chats';
import type { RootState } from 'soapbox/store';
import type { Notification } from 'soapbox/types/entities';
@@ -241,16 +242,18 @@ type APIChat = { id: string, last_message: string };
export const makeGetChat = () => {
return createSelector(
[
- (state: RootState, { id }: APIChat) => state.chats.getIn(['items', id]),
+ (state: RootState, { id }: APIChat) => state.chats.getIn(['items', id]) as ReducerChat,
(state: RootState, { id }: APIChat) => state.accounts.get(state.chats.getIn(['items', id, 'account'])),
(state: RootState, { last_message }: APIChat) => state.chat_messages.get(last_message),
],
- (chat, account, lastMessage: string) => {
- if (!chat) return null;
+ (chat, account, lastMessage) => {
+ if (!chat || !account) return null;
- return chat.withMutations((map: ImmutableMap) => {
+ return chat.withMutations((map) => {
+ // @ts-ignore
map.set('account', account);
+ // @ts-ignore
map.set('last_message', lastMessage);
});
},
diff --git a/app/soapbox/types/entities.ts b/app/soapbox/types/entities.ts
index 942e5f4f8..80caf1ea1 100644
--- a/app/soapbox/types/entities.ts
+++ b/app/soapbox/types/entities.ts
@@ -2,6 +2,8 @@ import {
AccountRecord,
AttachmentRecord,
CardRecord,
+ ChatRecord,
+ ChatMessageRecord,
EmojiRecord,
FieldRecord,
InstanceRecord,
@@ -16,6 +18,8 @@ import type { Record as ImmutableRecord } from 'immutable';
type Attachment = ReturnType;
type Card = ReturnType;
+type Chat = ReturnType;
+type ChatMessage = ReturnType;
type Emoji = ReturnType;
type Field = ReturnType;
type Instance = ReturnType;
@@ -44,6 +48,8 @@ export {
Account,
Attachment,
Card,
+ Chat,
+ ChatMessage,
Emoji,
Field,
Instance,