mirror of
https://github.com/mastodon/mastodon.git
synced 2026-09-12 15:16:40 -05:00
Refactor Messages/Private Mentions page to TS (#40347)
This commit is contained in:
@@ -39,6 +39,10 @@ const randomUpTo = max =>
|
||||
* @typedef {(dispatch: Dispatch, getState: GetState) => Promise<void>} FallbackFunction
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {(dispatch: Dispatch, getState: GetState) => () => void} StreamThunk
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {string} timelineId
|
||||
* @param {string} channelName
|
||||
@@ -47,7 +51,7 @@ const randomUpTo = max =>
|
||||
* @param {FallbackFunction} [options.fallback]
|
||||
* @param {() => UnknownAction} [options.fillGaps]
|
||||
* @param {(status: object) => boolean} [options.accept]
|
||||
* @returns {() => void}
|
||||
* @returns {StreamThunk}
|
||||
*/
|
||||
export const connectTimelineStream = (timelineId, channelName, params = {}, options = {}) => {
|
||||
const { messages } = getLocale();
|
||||
@@ -159,7 +163,7 @@ async function refreshHomeTimelineAndNotification(dispatch) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {() => void}
|
||||
* @returns {StreamThunk}
|
||||
*/
|
||||
export const connectUserStream = () =>
|
||||
connectTimelineStream('home', 'user', {}, {
|
||||
@@ -171,7 +175,7 @@ export const connectUserStream = () =>
|
||||
/**
|
||||
* @param {Object} options
|
||||
* @param {boolean} [options.onlyMedia]
|
||||
* @returns {() => void}
|
||||
* @returns {StreamThunk}
|
||||
*/
|
||||
export const connectCommunityStream = ({ onlyMedia } = {}) =>
|
||||
connectTimelineStream(`community${onlyMedia ? ':media' : ''}`, `public:local${onlyMedia ? ':media' : ''}`, {}, {
|
||||
@@ -183,7 +187,7 @@ export const connectCommunityStream = ({ onlyMedia } = {}) =>
|
||||
* @param {Object} options
|
||||
* @param {boolean} [options.onlyMedia]
|
||||
* @param {boolean} [options.onlyRemote]
|
||||
* @returns {() => void}
|
||||
* @returns {StreamThunk}
|
||||
*/
|
||||
export const connectPublicStream = ({ onlyMedia, onlyRemote } = {}) =>
|
||||
connectTimelineStream(`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`, `public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`, {}, {
|
||||
@@ -196,20 +200,20 @@ export const connectPublicStream = ({ onlyMedia, onlyRemote } = {}) =>
|
||||
* @param {string} tagName
|
||||
* @param {boolean} onlyLocal
|
||||
* @param {(status: object) => boolean} accept
|
||||
* @returns {() => void}
|
||||
* @returns {StreamThunk}
|
||||
*/
|
||||
export const connectHashtagStream = (columnId, tagName, onlyLocal, accept) =>
|
||||
connectTimelineStream(`hashtag:${columnId}${onlyLocal ? ':local' : ''}`, `hashtag${onlyLocal ? ':local' : ''}`, { tag: tagName }, { accept });
|
||||
|
||||
/**
|
||||
* @returns {() => void}
|
||||
* @returns {StreamThunk}
|
||||
*/
|
||||
export const connectDirectStream = () =>
|
||||
connectTimelineStream('direct', 'direct');
|
||||
|
||||
/**
|
||||
* @param {string} listId
|
||||
* @returns {() => void}
|
||||
* @returns {StreamThunk}
|
||||
*/
|
||||
export const connectListStream = listId =>
|
||||
connectTimelineStream(`list:${listId}`, 'list', { list: listId }, {
|
||||
|
||||
@@ -1,68 +1,114 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { Link, useHistory } from 'react-router-dom';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import type {
|
||||
Map as ImmutableMap,
|
||||
List as ImmutableList,
|
||||
Record as ImmutableRecord,
|
||||
} from 'immutable';
|
||||
|
||||
import { LinkedDisplayName } from '@/mastodon/components/display_name';
|
||||
import { AnimateEmojiProvider } from '@/mastodon/components/emoji/context';
|
||||
import type { Account } from '@/mastodon/models/account';
|
||||
import type { StatusShape } from '@/mastodon/models/status';
|
||||
import {
|
||||
createAppSelector,
|
||||
useAppDispatch,
|
||||
useAppSelector,
|
||||
} from '@/mastodon/store';
|
||||
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
|
||||
import ReplyIcon from '@/material-icons/400-24px/reply.svg?react';
|
||||
import { replyCompose } from 'mastodon/actions/compose';
|
||||
import { markConversationRead, deleteConversation } from 'mastodon/actions/conversations';
|
||||
import {
|
||||
markConversationRead,
|
||||
deleteConversation,
|
||||
} from 'mastodon/actions/conversations';
|
||||
import { openModal } from 'mastodon/actions/modal';
|
||||
import { muteStatus, unmuteStatus, toggleStatusSpoilers } from 'mastodon/actions/statuses';
|
||||
import { Hotkeys } from 'mastodon/components/hotkeys';
|
||||
import {
|
||||
muteStatus,
|
||||
unmuteStatus,
|
||||
toggleStatusSpoilers,
|
||||
} from 'mastodon/actions/statuses';
|
||||
import AttachmentList from 'mastodon/components/attachment_list';
|
||||
import AvatarComposite from 'mastodon/components/avatar_composite';
|
||||
import { Dropdown } from 'mastodon/components/dropdown_menu';
|
||||
import { Hotkeys } from 'mastodon/components/hotkeys';
|
||||
import { IconButton } from 'mastodon/components/icon_button';
|
||||
import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
|
||||
import StatusContent from 'mastodon/components/status_content';
|
||||
import { Dropdown } from 'mastodon/components/dropdown_menu';
|
||||
import { makeGetStatus } from 'mastodon/selectors';
|
||||
import { LinkedDisplayName } from '@/mastodon/components/display_name';
|
||||
import { AnimateEmojiProvider } from '@/mastodon/components/emoji/context';
|
||||
|
||||
const messages = defineMessages({
|
||||
more: { id: 'status.more', defaultMessage: 'More' },
|
||||
open: { id: 'conversation.open', defaultMessage: 'View conversation' },
|
||||
reply: { id: 'status.reply', defaultMessage: 'Reply' },
|
||||
markAsRead: { id: 'conversation.mark_as_read', defaultMessage: 'Mark as read' },
|
||||
markAsRead: {
|
||||
id: 'conversation.mark_as_read',
|
||||
defaultMessage: 'Mark as read',
|
||||
},
|
||||
delete: { id: 'conversation.delete', defaultMessage: 'Delete conversation' },
|
||||
muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' },
|
||||
unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' },
|
||||
muteConversation: {
|
||||
id: 'status.mute_conversation',
|
||||
defaultMessage: 'Mute conversation',
|
||||
},
|
||||
unmuteConversation: {
|
||||
id: 'status.unmute_conversation',
|
||||
defaultMessage: 'Unmute conversation',
|
||||
},
|
||||
});
|
||||
|
||||
const getAccounts = createSelector(
|
||||
(state) => state.get('accounts'),
|
||||
(_, accountIds) => accountIds,
|
||||
(accounts, accountIds) =>
|
||||
accountIds.map(id => accounts.get(id))
|
||||
const getAccounts = createAppSelector(
|
||||
(state) => state.accounts,
|
||||
(_, accountIds: ImmutableList<string>) => accountIds,
|
||||
(
|
||||
accounts: ImmutableMap<string, Account>,
|
||||
accountIds: ImmutableList<string>,
|
||||
) => accountIds.map((id) => accounts.get(id)),
|
||||
);
|
||||
|
||||
const getStatus = makeGetStatus();
|
||||
|
||||
export const Conversation = ({ conversation, scrollKey }) => {
|
||||
interface Conversation {
|
||||
id: string;
|
||||
unread: boolean;
|
||||
accounts: ImmutableList<string>;
|
||||
last_status: string | null;
|
||||
}
|
||||
|
||||
export const Conversation: React.FC<{
|
||||
conversation: ImmutableRecord<Conversation>;
|
||||
scrollKey: string;
|
||||
}> = ({ conversation, scrollKey }) => {
|
||||
const id = conversation.get('id');
|
||||
const unread = conversation.get('unread');
|
||||
const lastStatusId = conversation.get('last_status');
|
||||
const accountIds = conversation.get('accounts');
|
||||
const intl = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
const dispatch = useAppDispatch();
|
||||
const history = useHistory();
|
||||
const lastStatus = useSelector(state => getStatus(state, { id: lastStatusId }));
|
||||
const accounts = useSelector(state => getAccounts(state, accountIds));
|
||||
const lastStatus = useAppSelector(
|
||||
(state) =>
|
||||
// @ts-expect-error getStatus isn't properly typed yet
|
||||
getStatus(state, { id: lastStatusId }) as
|
||||
| ImmutableRecord<StatusShape>
|
||||
| undefined,
|
||||
);
|
||||
const accounts = useAppSelector((state) => getAccounts(state, accountIds));
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
if (unread) {
|
||||
dispatch(markConversationRead(id));
|
||||
}
|
||||
|
||||
history.push(`/@${lastStatus.getIn(['account', 'acct'])}/${lastStatus.get('id')}`);
|
||||
if (lastStatus) {
|
||||
history.push(
|
||||
`/@${lastStatus.getIn(['account', 'acct']) as string}/${lastStatus.get('id')}`,
|
||||
);
|
||||
}
|
||||
}, [dispatch, history, unread, id, lastStatus]);
|
||||
|
||||
const handleMarkAsRead = useCallback(() => {
|
||||
@@ -71,10 +117,17 @@ export const Conversation = ({ conversation, scrollKey }) => {
|
||||
|
||||
const handleReply = useCallback(() => {
|
||||
dispatch((_, getState) => {
|
||||
let state = getState();
|
||||
const state = getState();
|
||||
const composeText = state.compose.get('text');
|
||||
const text = typeof composeText === 'string' ? composeText.trim() : '';
|
||||
|
||||
if (state.getIn(['compose', 'text']).trim().length !== 0) {
|
||||
dispatch(openModal({ modalType: 'CONFIRM_REPLY', modalProps: { status: lastStatus } }));
|
||||
if (text.length !== 0) {
|
||||
dispatch(
|
||||
openModal({
|
||||
modalType: 'CONFIRM_REPLY',
|
||||
modalProps: { status: lastStatus },
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
dispatch(replyCompose(lastStatus));
|
||||
}
|
||||
@@ -86,6 +139,8 @@ export const Conversation = ({ conversation, scrollKey }) => {
|
||||
}, [dispatch, id]);
|
||||
|
||||
const handleConversationMute = useCallback(() => {
|
||||
if (!lastStatus) return;
|
||||
|
||||
if (lastStatus.get('muted')) {
|
||||
dispatch(unmuteStatus(lastStatus.get('id')));
|
||||
} else {
|
||||
@@ -94,7 +149,9 @@ export const Conversation = ({ conversation, scrollKey }) => {
|
||||
}, [dispatch, lastStatus]);
|
||||
|
||||
const handleShowMore = useCallback(() => {
|
||||
dispatch(toggleStatusSpoilers(lastStatus.get('id')));
|
||||
if (lastStatus) {
|
||||
dispatch(toggleStatusSpoilers(lastStatus.get('id')));
|
||||
}
|
||||
}, [dispatch, lastStatus]);
|
||||
|
||||
if (!lastStatus) {
|
||||
@@ -104,19 +161,40 @@ export const Conversation = ({ conversation, scrollKey }) => {
|
||||
const menu = [
|
||||
{ text: intl.formatMessage(messages.open), action: handleClick },
|
||||
null,
|
||||
{ text: intl.formatMessage(lastStatus.get('muted') ? messages.unmuteConversation : messages.muteConversation), action: handleConversationMute },
|
||||
{
|
||||
text: intl.formatMessage(
|
||||
lastStatus.get('muted')
|
||||
? messages.unmuteConversation
|
||||
: messages.muteConversation,
|
||||
),
|
||||
action: handleConversationMute,
|
||||
},
|
||||
];
|
||||
|
||||
if (unread) {
|
||||
menu.push({ text: intl.formatMessage(messages.markAsRead), action: handleMarkAsRead });
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.markAsRead),
|
||||
action: handleMarkAsRead,
|
||||
});
|
||||
menu.push(null);
|
||||
}
|
||||
|
||||
menu.push({ text: intl.formatMessage(messages.delete), action: handleDelete });
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.delete),
|
||||
action: handleDelete,
|
||||
});
|
||||
|
||||
const names = accounts.map((account) => (
|
||||
<LinkedDisplayName displayProps={{account, variant: 'simple'}} key={account.get('id')} />
|
||||
)).reduce((prev, cur) => [prev, ', ', cur]);
|
||||
const names: React.ReactNode = accounts
|
||||
.map((account) =>
|
||||
account ? (
|
||||
<LinkedDisplayName
|
||||
displayProps={{ account, variant: 'simple' }}
|
||||
key={account.get('id')}
|
||||
/>
|
||||
) : null,
|
||||
)
|
||||
.filter(Boolean)
|
||||
.reduce((prev, cur) => [prev, ', ', cur]);
|
||||
|
||||
const handlers = {
|
||||
reply: handleReply,
|
||||
@@ -126,23 +204,37 @@ export const Conversation = ({ conversation, scrollKey }) => {
|
||||
|
||||
return (
|
||||
<Hotkeys handlers={handlers}>
|
||||
<div className={classNames('conversation focusable muted', { unread })} tabIndex={0}>
|
||||
<div className='conversation__avatar' onClick={handleClick} role='presentation'>
|
||||
<div
|
||||
className={classNames('conversation focusable muted', { unread })}
|
||||
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
|
||||
tabIndex={0}
|
||||
>
|
||||
<div
|
||||
className='conversation__avatar'
|
||||
onClick={handleClick}
|
||||
role='presentation'
|
||||
>
|
||||
<AvatarComposite accounts={accounts} size={48} />
|
||||
</div>
|
||||
|
||||
<div className='conversation__content'>
|
||||
<div className='conversation__content__info'>
|
||||
<div className='conversation__content__relative-time'>
|
||||
{unread && <span className='conversation__unread' />} <RelativeTimestamp timestamp={lastStatus.get('created_at')} />
|
||||
{unread && <span className='conversation__unread' />}{' '}
|
||||
<RelativeTimestamp timestamp={lastStatus.get('created_at')} />
|
||||
</div>
|
||||
|
||||
<AnimateEmojiProvider className='conversation__content__names'>
|
||||
<FormattedMessage id='conversation.with' defaultMessage='With {names}' values={{ names: <span>{names}</span> }} />
|
||||
<FormattedMessage
|
||||
id='conversation.with'
|
||||
defaultMessage='With {names}'
|
||||
values={{ names: <span>{names}</span> }}
|
||||
/>
|
||||
</AnimateEmojiProvider>
|
||||
</div>
|
||||
|
||||
<StatusContent
|
||||
// @ts-expect-error StatusContent isn't typed yet
|
||||
status={lastStatus}
|
||||
onClick={handleClick}
|
||||
expanded={!lastStatus.get('hidden')}
|
||||
@@ -150,7 +242,9 @@ export const Conversation = ({ conversation, scrollKey }) => {
|
||||
collapsible
|
||||
/>
|
||||
|
||||
{lastStatus.get('media_attachments').size > 0 && (
|
||||
{lastStatus
|
||||
// @ts-expect-error Status isn't properly typed yet
|
||||
.get('media_attachments').size > 0 && (
|
||||
<AttachmentList
|
||||
compact
|
||||
media={lastStatus.get('media_attachments')}
|
||||
@@ -158,11 +252,18 @@ export const Conversation = ({ conversation, scrollKey }) => {
|
||||
)}
|
||||
|
||||
<div className='status__action-bar'>
|
||||
<IconButton className='status__action-bar-button' title={intl.formatMessage(messages.reply)} icon='reply' iconComponent={ReplyIcon} onClick={handleReply} />
|
||||
<IconButton
|
||||
className='status__action-bar-button'
|
||||
title={intl.formatMessage(messages.reply)}
|
||||
icon='reply'
|
||||
iconComponent={ReplyIcon}
|
||||
onClick={handleReply}
|
||||
/>
|
||||
|
||||
<div className='status__action-bar-dropdown'>
|
||||
<Dropdown
|
||||
scrollKey={scrollKey}
|
||||
// @ts-expect-error Dropdown status prop isn't properly typed
|
||||
status={lastStatus}
|
||||
items={menu}
|
||||
icon='ellipsis-h'
|
||||
@@ -178,8 +279,3 @@ export const Conversation = ({ conversation, scrollKey }) => {
|
||||
</Hotkeys>
|
||||
);
|
||||
};
|
||||
|
||||
Conversation.propTypes = {
|
||||
conversation: ImmutablePropTypes.map.isRequired,
|
||||
scrollKey: PropTypes.string,
|
||||
};
|
||||
@@ -1,18 +1,20 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
|
||||
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
|
||||
|
||||
import { Helmet } from '@unhead/react/helmet';
|
||||
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react';
|
||||
import { addColumn, removeColumn, moveColumn } from 'mastodon/actions/columns';
|
||||
import { mountConversations, unmountConversations, expandConversations } from 'mastodon/actions/conversations';
|
||||
import { connectDirectStream } from 'mastodon/actions/streaming';
|
||||
import { Column } from '@/mastodon/components/column';
|
||||
import { ColumnHeader } from '@/mastodon/components/column/header';
|
||||
import { useAppDispatch } from '@/mastodon/store';
|
||||
import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react';
|
||||
import { addColumn, removeColumn, moveColumn } from 'mastodon/actions/columns';
|
||||
import {
|
||||
mountConversations,
|
||||
unmountConversations,
|
||||
expandConversations,
|
||||
} from 'mastodon/actions/conversations';
|
||||
import { connectDirectStream } from 'mastodon/actions/streaming';
|
||||
|
||||
import { ConversationsList } from './components/conversations_list';
|
||||
|
||||
@@ -20,9 +22,14 @@ const messages = defineMessages({
|
||||
title: { id: 'column.direct', defaultMessage: 'Private mentions' },
|
||||
});
|
||||
|
||||
const DirectTimeline = ({ columnId, multiColumn }) => {
|
||||
interface ColumnBase {
|
||||
columnId?: string;
|
||||
multiColumn?: boolean;
|
||||
}
|
||||
|
||||
const DirectTimeline: React.FC<ColumnBase> = ({ columnId, multiColumn }) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
const dispatch = useAppDispatch();
|
||||
const pinned = !!columnId;
|
||||
|
||||
const handlePin = useCallback(() => {
|
||||
@@ -33,9 +40,12 @@ const DirectTimeline = ({ columnId, multiColumn }) => {
|
||||
}
|
||||
}, [dispatch, columnId]);
|
||||
|
||||
const handleMove = useCallback((dir) => {
|
||||
dispatch(moveColumn(columnId, dir));
|
||||
}, [dispatch, columnId]);
|
||||
const handleMove = useCallback(
|
||||
(dir: number) => {
|
||||
dispatch(moveColumn(columnId, dir));
|
||||
},
|
||||
[dispatch, columnId],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(mountConversations());
|
||||
@@ -50,7 +60,10 @@ const DirectTimeline = ({ columnId, multiColumn }) => {
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<Column bindToDocument={!multiColumn} label={intl.formatMessage(messages.title)}>
|
||||
<Column
|
||||
bindToDocument={!multiColumn}
|
||||
label={intl.formatMessage(messages.title)}
|
||||
>
|
||||
<ColumnHeader
|
||||
icon='at'
|
||||
iconComponent={AlternateEmailIcon}
|
||||
@@ -65,9 +78,33 @@ const DirectTimeline = ({ columnId, multiColumn }) => {
|
||||
<ConversationsList
|
||||
trackScroll={!pinned}
|
||||
scrollKey={`direct_timeline-${columnId}`}
|
||||
emptyMessage={<FormattedMessage id='empty_column.direct' defaultMessage="You don't have any private mentions yet. When you send or receive one, it will show up here." />}
|
||||
emptyMessage={
|
||||
<FormattedMessage
|
||||
id='empty_column.direct'
|
||||
defaultMessage="You don't have any private mentions yet. When you send or receive one, it will show up here."
|
||||
/>
|
||||
}
|
||||
bindToDocument={!multiColumn}
|
||||
prepend={<div className='follow_requests-unlocked_explanation'><span><FormattedMessage id='compose_form.encryption_warning' defaultMessage='Posts on Mastodon are not end-to-end encrypted. Do not share any dangerous information over Mastodon.' /> <a href='https://docs.joinmastodon.org/user/posting/#private' rel='noreferrer' target='_blank'><FormattedMessage id='compose_form.direct_message_warning_learn_more' defaultMessage='Learn more' /></a></span></div>}
|
||||
prepend={
|
||||
<div className='follow_requests-unlocked_explanation'>
|
||||
<span>
|
||||
<FormattedMessage
|
||||
id='compose_form.encryption_warning'
|
||||
defaultMessage='Posts on Mastodon are not end-to-end encrypted. Do not share any dangerous information over Mastodon.'
|
||||
/>{' '}
|
||||
<a
|
||||
href='https://docs.joinmastodon.org/user/posting/#private'
|
||||
rel='noreferrer'
|
||||
target='_blank'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='compose_form.direct_message_warning_learn_more'
|
||||
defaultMessage='Learn more'
|
||||
/>
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
alwaysPrepend
|
||||
/>
|
||||
|
||||
@@ -79,9 +116,5 @@ const DirectTimeline = ({ columnId, multiColumn }) => {
|
||||
);
|
||||
};
|
||||
|
||||
DirectTimeline.propTypes = {
|
||||
columnId: PropTypes.string,
|
||||
multiColumn: PropTypes.bool,
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default DirectTimeline;
|
||||
@@ -147,11 +147,10 @@ const channelNameWithInlineParams = (channelName, params) => {
|
||||
* @param {string} channelName
|
||||
* @param {Object.<string, string>} params
|
||||
* @param {(dispatch: Dispatch, getState: GetState) => { onConnect: () => void, onReceive: (event: StreamEvent) => void, onDisconnect: () => void }} callbacks
|
||||
* @returns {() => void}
|
||||
* @returns {(dispatch: Dispatch, getState: GetState) => () => void}
|
||||
*/
|
||||
// @ts-expect-error
|
||||
export const connectStream = (channelName, params, callbacks) => (dispatch, getState) => {
|
||||
const streamingAPIBaseURL = getState().getIn(['meta', 'streaming_api_base_url']);
|
||||
const streamingAPIBaseURL = getState().meta.get('streaming_api_base_url');
|
||||
const accessToken = getAccessToken();
|
||||
const { onConnect, onReceive, onDisconnect } = callbacks(dispatch, getState);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user