diff --git a/app/soapbox/components/sidebar_menu.js b/app/soapbox/components/sidebar_menu.js index 7c20b8d2b..caabe7f0c 100644 --- a/app/soapbox/components/sidebar_menu.js +++ b/app/soapbox/components/sidebar_menu.js @@ -13,11 +13,11 @@ import Icon from './icon'; import DisplayName from './display_name'; import { closeSidebar } from '../actions/sidebar'; import { isStaff } from '../utils/accounts'; -import { makeGetAccount } from '../selectors'; +import { makeGetAccount, makeGetOtherAccounts } from '../selectors'; import { logOut, switchAccount } from 'soapbox/actions/auth'; import ThemeToggle from '../features/ui/components/theme_toggle_container'; import { fetchOwnAccounts } from 'soapbox/actions/auth'; -import { List as ImmutableList, is as ImmutableIs } from 'immutable'; +import { is as ImmutableIs } from 'immutable'; import { getSoapboxConfig } from 'soapbox/actions/soapbox'; const messages = defineMessages({ @@ -45,29 +45,29 @@ const messages = defineMessages({ add_account: { id: 'profile_dropdown.add_account', defaultMessage: 'Add an existing account' }, }); -const mapStateToProps = state => { - const me = state.get('me'); +const makeMapStateToProps = () => { const getAccount = makeGetAccount(); - const soapbox = getSoapboxConfig(state); + const getOtherAccounts = makeGetOtherAccounts(); - const otherAccounts = - state - .getIn(['auth', 'users']) - .keySeq() - .reduce((list, id) => { - if (id === me) return list; - const account = state.getIn(['accounts', id]); - return account ? list.push(account) : list; - }, ImmutableList()); + const mapStateToProps = state => { + const me = state.get('me'); + const soapbox = getSoapboxConfig(state); - return { - account: getAccount(state, me), - sidebarOpen: state.get('sidebar').sidebarOpen, - donateUrl: state.getIn(['patron', 'instance', 'url']), - hasCrypto: typeof soapbox.getIn(['cryptoAddresses', 0, 'ticker']) === 'string', - isStaff: isStaff(state.getIn(['accounts', me])), - otherAccounts, + const accounts = state.get('accounts'); + const authUsers = state.getIn(['auth', 'users']); + const otherAccounts = getOtherAccounts(accounts, authUsers, me); + + return { + account: getAccount(state, me), + sidebarOpen: state.get('sidebar').sidebarOpen, + donateUrl: state.getIn(['patron', 'instance', 'url']), + hasCrypto: typeof soapbox.getIn(['cryptoAddresses', 0, 'ticker']) === 'string', + isStaff: isStaff(state.getIn(['accounts', me])), + otherAccounts, + }; }; + + return mapStateToProps; }; const mapDispatchToProps = (dispatch, { intl }) => ({ @@ -86,7 +86,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ }, }); -export default @connect(mapStateToProps, mapDispatchToProps) +export default @connect(makeMapStateToProps, mapDispatchToProps) @injectIntl class SidebarMenu extends ImmutablePureComponent { diff --git a/app/soapbox/features/chats/components/audio_toggle.js b/app/soapbox/features/chats/components/audio_toggle.js index 9b207273a..27441e15d 100644 --- a/app/soapbox/features/chats/components/audio_toggle.js +++ b/app/soapbox/features/chats/components/audio_toggle.js @@ -2,19 +2,18 @@ import React from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { injectIntl, defineMessages } from 'react-intl'; -import ImmutablePropTypes from 'react-immutable-proptypes'; import Icon from 'soapbox/components/icon'; import { changeSetting, getSettings } from 'soapbox/actions/settings'; -import SettingToggle from 'soapbox/features/notifications/components/setting_toggle'; +import Toggle from 'react-toggle'; const messages = defineMessages({ - switchToOn: { id: 'chats.audio_toggle_on', defaultMessage: 'Audio notification on' }, - switchToOff: { id: 'chats.audio_toggle_off', defaultMessage: 'Audio notification off' }, + switchOn: { id: 'chats.audio_toggle_on', defaultMessage: 'Audio notification on' }, + switchOff: { id: 'chats.audio_toggle_off', defaultMessage: 'Audio notification off' }, }); const mapStateToProps = state => { return { - settings: getSettings(state), + checked: getSettings(state).getIn(['chats', 'sound'], false), }; }; @@ -30,30 +29,32 @@ class AudioToggle extends React.PureComponent { static propTypes = { intl: PropTypes.object.isRequired, - settings: ImmutablePropTypes.map.isRequired, + checked: PropTypes.bool.isRequired, toggleAudio: PropTypes.func, showLabel: PropTypes.bool, }; handleToggleAudio = () => { - this.props.toggleAudio(this.props.settings.getIn(['chats', 'sound']) === true ? false : true); + this.props.toggleAudio(!this.props.checked); } render() { - const { intl, settings, showLabel } = this.props; - let toggle = ( - , unchecked: }} ariaLabel={settings.get('chats', 'sound') === true ? intl.formatMessage(messages.switchToOff) : intl.formatMessage(messages.switchToOn)} /> - ); - - if (showLabel) { - toggle = ( - , unchecked: }} label={settings.get('chats', 'sound') === true ? intl.formatMessage(messages.switchToOff) : intl.formatMessage(messages.switchToOn)} /> - ); - } + const { intl, checked, showLabel } = this.props; + const id ='chats-audio-toggle'; + const label = intl.formatMessage(checked ? messages.switchOff : messages.switchOn); return (
- {toggle} +
+ , unchecked: }} + onKeyDown={this.onKeyDown} + /> + {showLabel && ()} +
); } diff --git a/app/soapbox/features/chats/components/chat.js b/app/soapbox/features/chats/components/chat.js index 1a09edd47..8db6ddb62 100644 --- a/app/soapbox/features/chats/components/chat.js +++ b/app/soapbox/features/chats/components/chat.js @@ -1,4 +1,5 @@ import React from 'react'; +import { connect } from 'react-redux'; import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; import Avatar from '../../../components/avatar'; @@ -6,11 +7,29 @@ import DisplayName from '../../../components/display_name'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { shortNumberFormat } from 'soapbox/utils/numbers'; import emojify from 'soapbox/features/emoji/emoji'; +import { makeGetChat } from 'soapbox/selectors'; -export default class Chat extends ImmutablePureComponent { + +const makeMapStateToProps = () => { + const getChat = makeGetChat(); + + const mapStateToProps = (state, { chatId }) => { + const chat = state.getIn(['chats', chatId]); + + return { + chat: chat ? getChat(state, chat.toJS()) : undefined, + }; + }; + + return mapStateToProps; +}; + +export default @connect(makeMapStateToProps) +class Chat extends ImmutablePureComponent { static propTypes = { - chat: ImmutablePropTypes.map.isRequired, + chatId: PropTypes.string.isRequired, + chat: ImmutablePropTypes.map, onClick: PropTypes.func, }; diff --git a/app/soapbox/features/chats/components/chat_list.js b/app/soapbox/features/chats/components/chat_list.js index e24d31ab9..f89115363 100644 --- a/app/soapbox/features/chats/components/chat_list.js +++ b/app/soapbox/features/chats/components/chat_list.js @@ -1,10 +1,18 @@ import React from 'react'; import { connect } from 'react-redux'; import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; import { injectIntl } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; import Chat from './chat'; -import { makeGetChat } from 'soapbox/selectors'; +import { createSelector } from 'reselect'; + +const getSortedChatIds = chats => ( + chats + .toList() + .sort(chatDateComparator) + .map(chat => chat.get('id')) +); const chatDateComparator = (chatA, chatB) => { // Sort most recently updated chats at the top @@ -17,43 +25,44 @@ const chatDateComparator = (chatA, chatB) => { return 0; }; -const mapStateToProps = state => { - const getChat = makeGetChat(); +const makeMapStateToProps = () => { + const sortedChatIdsSelector = createSelector( + [getSortedChatIds], + chats => chats, + ); - const chats = state.get('chats') - .map(chat => getChat(state, chat.toJS())) - .toList() - .sort(chatDateComparator); + const mapStateToProps = state => ({ + chatIds: sortedChatIdsSelector(state.get('chats')), + }); - return { - chats, - }; + return mapStateToProps; }; -export default @connect(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, emptyMessage: PropTypes.node, }; render() { - const { chats, emptyMessage } = this.props; + const { chatIds, emptyMessage } = this.props; return (
- {chats.count() === 0 && + {chatIds.count() === 0 &&
{emptyMessage}
} - {chats.map(chat => ( -
+ {chatIds.map(chatId => ( +
diff --git a/app/soapbox/features/chats/components/chat_message_list.js b/app/soapbox/features/chats/components/chat_message_list.js index b51915ae6..426075504 100644 --- a/app/soapbox/features/chats/components/chat_message_list.js +++ b/app/soapbox/features/chats/components/chat_message_list.js @@ -14,6 +14,7 @@ import { MediaGallery } from 'soapbox/features/ui/util/async-components'; import Bundle from 'soapbox/features/ui/components/bundle'; import DropdownMenuContainer from 'soapbox/containers/dropdown_menu_container'; import { initReportById } from 'soapbox/actions/reports'; +import { createSelector } from 'reselect'; const messages = defineMessages({ today: { id: 'chats.dividers.today', defaultMessage: 'Today' }, @@ -38,15 +39,34 @@ const makeEmojiMap = record => record.get('emojis', ImmutableList()).reduce((map return map.set(`:${emoji.get('shortcode')}:`, emoji); }, ImmutableMap()); -const mapStateToProps = (state, { chatMessageIds }) => ({ - me: state.get('me'), - chatMessages: chatMessageIds.reduce((acc, curr) => { - const chatMessage = state.getIn(['chat_messages', curr]); - return chatMessage ? acc.push(chatMessage) : acc; - }, ImmutableList()), -}); +const makeGetChatMessages = () => { + return createSelector( + [(chatMessages, chatMessageIds) => ( + chatMessageIds.reduce((acc, curr) => { + const chatMessage = chatMessages.get(curr); + return chatMessage ? acc.push(chatMessage) : acc; + }, ImmutableList()) + )], + chatMessages => chatMessages, + ); +}; -export default @connect(mapStateToProps) +const makeMapStateToProps = () => { + const getChatMessages = makeGetChatMessages(); + + const mapStateToProps = (state, { chatMessageIds }) => { + const chatMessages = state.get('chat_messages'); + + return { + me: state.get('me'), + chatMessages: getChatMessages(chatMessages, chatMessageIds), + }; + }; + + return mapStateToProps; +}; + +export default @connect(makeMapStateToProps) @injectIntl class ChatMessageList extends ImmutablePureComponent { diff --git a/app/soapbox/features/chats/components/chat_panes.js b/app/soapbox/features/chats/components/chat_panes.js index 46e3e4260..b1672c127 100644 --- a/app/soapbox/features/chats/components/chat_panes.js +++ b/app/soapbox/features/chats/components/chat_panes.js @@ -2,47 +2,32 @@ import React from 'react'; import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; -import { injectIntl } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { getSettings } from 'soapbox/actions/settings'; import ChatList from './chat_list'; import { FormattedMessage } from 'react-intl'; -import { makeGetChat } from 'soapbox/selectors'; import { openChat, toggleMainWindow } from 'soapbox/actions/chats'; import ChatWindow from './chat_window'; import { shortNumberFormat } from 'soapbox/utils/numbers'; import AudioToggle from 'soapbox/features/chats/components/audio_toggle'; -import { List as ImmutableList } from 'immutable'; - -const addChatsToPanes = (state, panesData) => { - const getChat = makeGetChat(); - - const newPanes = panesData.get('panes').reduce((acc, pane) => { - const chat = getChat(state, { id: pane.get('chat_id') }); - if (!chat) return acc; - return acc.push(pane.set('chat', chat)); - }, ImmutableList()); - - return panesData.set('panes', newPanes); -}; const mapStateToProps = state => { - const panesData = getSettings(state).get('chats'); + const settings = getSettings(state); return { - panesData: addChatsToPanes(state, panesData), + panes: settings.getIn(['chats', 'panes']), + mainWindowState: settings.getIn(['chats', 'mainWindow']), unreadCount: state.get('chats').reduce((acc, curr) => acc + Math.min(curr.get('unread', 0), 1), 0), }; }; export default @connect(mapStateToProps) -@injectIntl class ChatPanes extends ImmutablePureComponent { static propTypes = { dispatch: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - panesData: ImmutablePropTypes.map, + mainWindowState: PropTypes.string, + panes: ImmutablePropTypes.list, } handleClickChat = (chat) => { @@ -54,12 +39,11 @@ class ChatPanes extends ImmutablePureComponent { } render() { - const { panesData, unreadCount } = this.props; - const panes = panesData.get('panes'); - const mainWindow = panesData.get('mainWindow'); + const { panes, mainWindowState, unreadCount } = this.props; + const open = mainWindowState === 'open'; const mainWindowPane = ( -
+
{unreadCount > 0 && {shortNumberFormat(unreadCount)}}
- } - /> + />}
); @@ -79,9 +63,14 @@ class ChatPanes extends ImmutablePureComponent { return (
{mainWindowPane} - {panes.map((pane, i) => - , - )} + {panes.map((pane, i) => ( + + ))}
); } diff --git a/app/soapbox/features/chats/components/chat_window.js b/app/soapbox/features/chats/components/chat_window.js index d23c27cad..95ba86e64 100644 --- a/app/soapbox/features/chats/components/chat_window.js +++ b/app/soapbox/features/chats/components/chat_window.js @@ -16,21 +16,33 @@ import ChatBox from './chat_box'; import { shortNumberFormat } from 'soapbox/utils/numbers'; import { displayFqn } from 'soapbox/utils/state'; import HoverRefWrapper from 'soapbox/components/hover_ref_wrapper'; +import { makeGetChat } from 'soapbox/selectors'; -const mapStateToProps = (state, { pane }) => ({ - me: state.get('me'), - chat: state.getIn(['chats', pane.get('chat_id')]), - displayFqn: displayFqn(state), -}); +const makeMapStateToProps = () => { + const getChat = makeGetChat(); -export default @connect(mapStateToProps) + const mapStateToProps = (state, { chatId }) => { + const chat = state.getIn(['chats', chatId]); + + return { + me: state.get('me'), + chat: chat ? getChat(state, chat.toJS()) : undefined, + displayFqn: displayFqn(state), + }; + }; + + return mapStateToProps; +}; + +export default @connect(makeMapStateToProps) @injectIntl class ChatWindow extends ImmutablePureComponent { static propTypes = { dispatch: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, - pane: ImmutablePropTypes.map.isRequired, + chatId: PropTypes.string.isRequired, + windowState: PropTypes.string.isRequired, idx: PropTypes.number, chat: ImmutablePropTypes.map, me: PropTypes.node, @@ -68,17 +80,17 @@ class ChatWindow extends ImmutablePureComponent { } componentDidUpdate(prevProps) { - const oldState = prevProps.pane.get('state'); - const newState = this.props.pane.get('state'); + const oldState = prevProps.windowState; + const newState = this.props.windowState; if (oldState !== newState && newState === 'open') this.focusInput(); } render() { - const { pane, idx, chat, displayFqn } = this.props; - const account = pane.getIn(['chat', 'account']); - if (!chat || !account) return null; + const { windowState, idx, chat, displayFqn } = this.props; + if (!chat) return null; + const account = chat.get('account'); const right = (285 * (idx + 1)) + 20; const unreadCount = chat.get('unread'); @@ -98,7 +110,7 @@ class ChatWindow extends ImmutablePureComponent { ); return ( -
+
{unreadCount > 0 ? unreadIcon : avatar }