Add DM conversations mode similar to upstream
This commit is contained in:
parent
e16c8fbc7a
commit
d61a6271c6
|
@ -0,0 +1,84 @@
|
||||||
|
import api, { getLinks } from 'flavours/glitch/util/api';
|
||||||
|
import {
|
||||||
|
importFetchedAccounts,
|
||||||
|
importFetchedStatuses,
|
||||||
|
importFetchedStatus,
|
||||||
|
} from './importer';
|
||||||
|
|
||||||
|
export const CONVERSATIONS_MOUNT = 'CONVERSATIONS_MOUNT';
|
||||||
|
export const CONVERSATIONS_UNMOUNT = 'CONVERSATIONS_UNMOUNT';
|
||||||
|
|
||||||
|
export const CONVERSATIONS_FETCH_REQUEST = 'CONVERSATIONS_FETCH_REQUEST';
|
||||||
|
export const CONVERSATIONS_FETCH_SUCCESS = 'CONVERSATIONS_FETCH_SUCCESS';
|
||||||
|
export const CONVERSATIONS_FETCH_FAIL = 'CONVERSATIONS_FETCH_FAIL';
|
||||||
|
export const CONVERSATIONS_UPDATE = 'CONVERSATIONS_UPDATE';
|
||||||
|
|
||||||
|
export const CONVERSATIONS_READ = 'CONVERSATIONS_READ';
|
||||||
|
|
||||||
|
export const mountConversations = () => ({
|
||||||
|
type: CONVERSATIONS_MOUNT,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const unmountConversations = () => ({
|
||||||
|
type: CONVERSATIONS_UNMOUNT,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const markConversationRead = conversationId => (dispatch, getState) => {
|
||||||
|
dispatch({
|
||||||
|
type: CONVERSATIONS_READ,
|
||||||
|
id: conversationId,
|
||||||
|
});
|
||||||
|
|
||||||
|
api(getState).post(`/api/v1/conversations/${conversationId}/read`);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const expandConversations = ({ maxId } = {}) => (dispatch, getState) => {
|
||||||
|
dispatch(expandConversationsRequest());
|
||||||
|
|
||||||
|
const params = { max_id: maxId };
|
||||||
|
|
||||||
|
if (!maxId) {
|
||||||
|
params.since_id = getState().getIn(['conversations', 'items', 0, 'last_status']);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isLoadingRecent = !!params.since_id;
|
||||||
|
|
||||||
|
api(getState).get('/api/v1/conversations', { params })
|
||||||
|
.then(response => {
|
||||||
|
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||||
|
|
||||||
|
dispatch(importFetchedAccounts(response.data.reduce((aggr, item) => aggr.concat(item.accounts), [])));
|
||||||
|
dispatch(importFetchedStatuses(response.data.map(item => item.last_status).filter(x => !!x)));
|
||||||
|
dispatch(expandConversationsSuccess(response.data, next ? next.uri : null, isLoadingRecent));
|
||||||
|
})
|
||||||
|
.catch(err => dispatch(expandConversationsFail(err)));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const expandConversationsRequest = () => ({
|
||||||
|
type: CONVERSATIONS_FETCH_REQUEST,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const expandConversationsSuccess = (conversations, next, isLoadingRecent) => ({
|
||||||
|
type: CONVERSATIONS_FETCH_SUCCESS,
|
||||||
|
conversations,
|
||||||
|
next,
|
||||||
|
isLoadingRecent,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const expandConversationsFail = error => ({
|
||||||
|
type: CONVERSATIONS_FETCH_FAIL,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const updateConversations = conversation => dispatch => {
|
||||||
|
dispatch(importFetchedAccounts(conversation.accounts));
|
||||||
|
|
||||||
|
if (conversation.last_status) {
|
||||||
|
dispatch(importFetchedStatus(conversation.last_status));
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: CONVERSATIONS_UPDATE,
|
||||||
|
conversation,
|
||||||
|
});
|
||||||
|
};
|
|
@ -7,6 +7,7 @@ import {
|
||||||
disconnectTimeline,
|
disconnectTimeline,
|
||||||
} from './timelines';
|
} from './timelines';
|
||||||
import { updateNotifications, expandNotifications } from './notifications';
|
import { updateNotifications, expandNotifications } from './notifications';
|
||||||
|
import { updateConversations } from './conversations';
|
||||||
import { fetchFilters } from './filters';
|
import { fetchFilters } from './filters';
|
||||||
import { getLocale } from 'mastodon/locales';
|
import { getLocale } from 'mastodon/locales';
|
||||||
|
|
||||||
|
@ -37,6 +38,9 @@ export function connectTimelineStream (timelineId, path, pollingRefresh = null,
|
||||||
case 'notification':
|
case 'notification':
|
||||||
dispatch(updateNotifications(JSON.parse(data.payload), messages, locale));
|
dispatch(updateNotifications(JSON.parse(data.payload), messages, locale));
|
||||||
break;
|
break;
|
||||||
|
case 'conversation':
|
||||||
|
dispatch(updateConversations(JSON.parse(data.payload)));
|
||||||
|
break;
|
||||||
case 'filters_changed':
|
case 'filters_changed':
|
||||||
dispatch(fetchFilters());
|
dispatch(fetchFilters());
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -0,0 +1,104 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import { autoPlayGif } from 'flavours/glitch/util/initial_state';
|
||||||
|
|
||||||
|
export default class AvatarComposite extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
accounts: ImmutablePropTypes.list.isRequired,
|
||||||
|
animate: PropTypes.bool,
|
||||||
|
size: PropTypes.number.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
animate: autoPlayGif,
|
||||||
|
};
|
||||||
|
|
||||||
|
renderItem (account, size, index) {
|
||||||
|
const { animate } = this.props;
|
||||||
|
|
||||||
|
let width = 50;
|
||||||
|
let height = 100;
|
||||||
|
let top = 'auto';
|
||||||
|
let left = 'auto';
|
||||||
|
let bottom = 'auto';
|
||||||
|
let right = 'auto';
|
||||||
|
|
||||||
|
if (size === 1) {
|
||||||
|
width = 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (size === 4 || (size === 3 && index > 0)) {
|
||||||
|
height = 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (size === 2) {
|
||||||
|
if (index === 0) {
|
||||||
|
right = '2px';
|
||||||
|
} else {
|
||||||
|
left = '2px';
|
||||||
|
}
|
||||||
|
} else if (size === 3) {
|
||||||
|
if (index === 0) {
|
||||||
|
right = '2px';
|
||||||
|
} else if (index > 0) {
|
||||||
|
left = '2px';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index === 1) {
|
||||||
|
bottom = '2px';
|
||||||
|
} else if (index > 1) {
|
||||||
|
top = '2px';
|
||||||
|
}
|
||||||
|
} else if (size === 4) {
|
||||||
|
if (index === 0 || index === 2) {
|
||||||
|
right = '2px';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index === 1 || index === 3) {
|
||||||
|
left = '2px';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index < 2) {
|
||||||
|
bottom = '2px';
|
||||||
|
} else {
|
||||||
|
top = '2px';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const style = {
|
||||||
|
left: left,
|
||||||
|
top: top,
|
||||||
|
right: right,
|
||||||
|
bottom: bottom,
|
||||||
|
width: `${width}%`,
|
||||||
|
height: `${height}%`,
|
||||||
|
backgroundSize: 'cover',
|
||||||
|
backgroundImage: `url(${account.get(animate ? 'avatar' : 'avatar_static')})`,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
href={account.get('url')}
|
||||||
|
target='_blank'
|
||||||
|
onClick={(e) => this.props.onAccountClick(account.get('id'), e)}
|
||||||
|
title={`@${account.get('acct')}`}
|
||||||
|
key={account.get('id')}
|
||||||
|
>
|
||||||
|
<div style={style} data-avatar-of={`@${account.get('acct')}`} />
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { accounts, size } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='account__avatar-composite' style={{ width: `${size}px`, height: `${size}px` }}>
|
||||||
|
{accounts.take(4).map((account, i) => this.renderItem(account, accounts.size, i))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -10,24 +10,56 @@ export default function DisplayName ({
|
||||||
className,
|
className,
|
||||||
inline,
|
inline,
|
||||||
localDomain,
|
localDomain,
|
||||||
|
others,
|
||||||
|
onAccountClick,
|
||||||
}) {
|
}) {
|
||||||
const computedClass = classNames('display-name', { inline }, className);
|
const computedClass = classNames('display-name', { inline }, className);
|
||||||
|
|
||||||
if (!account) return null;
|
if (!account) return null;
|
||||||
|
|
||||||
|
let displayName, suffix;
|
||||||
|
|
||||||
let acct = account.get('acct');
|
let acct = account.get('acct');
|
||||||
|
|
||||||
if (acct.indexOf('@') === -1 && localDomain) {
|
if (acct.indexOf('@') === -1 && localDomain) {
|
||||||
acct = `${acct}@${localDomain}`;
|
acct = `${acct}@${localDomain}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The result.
|
if (others && others.size > 0) {
|
||||||
return account ? (
|
displayName = others.take(2).map(a => (
|
||||||
|
<a
|
||||||
|
href={a.get('url')}
|
||||||
|
target='_blank'
|
||||||
|
onClick={(e) => onAccountClick(a.get('id'), e)}
|
||||||
|
title={`@${a.get('acct')}`}
|
||||||
|
>
|
||||||
|
<bdi key={a.get('id')}>
|
||||||
|
<strong className='display-name__html' dangerouslySetInnerHTML={{ __html: a.get('display_name_html') }} />
|
||||||
|
</bdi>
|
||||||
|
</a>
|
||||||
|
)).reduce((prev, cur) => [prev, ', ', cur]);
|
||||||
|
|
||||||
|
if (others.size - 2 > 0) {
|
||||||
|
displayName.push(` +${others.size - 2}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
suffix = (
|
||||||
|
<a href={account.get('url')} target='_blank' onClick={(e) => onAccountClick(account.get('id'), e)}>
|
||||||
|
<span className='display-name__account'>@{acct}</span>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
displayName = <bdi><strong className='display-name__html' dangerouslySetInnerHTML={{ __html: account.get('display_name_html') }} /></bdi>;
|
||||||
|
suffix = <span className='display-name__account'>@{acct}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
<span className={computedClass}>
|
<span className={computedClass}>
|
||||||
<bdi><strong className='display-name__html' dangerouslySetInnerHTML={{ __html: account.get('display_name_html') }} /></bdi>
|
{displayName}
|
||||||
{inline ? ' ' : null}
|
{inline ? ' ' : null}
|
||||||
<span className='display-name__account'>@{acct}</span>
|
{suffix}
|
||||||
</span>
|
</span>
|
||||||
) : null;
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Props.
|
// Props.
|
||||||
|
@ -36,4 +68,6 @@ DisplayName.propTypes = {
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
inline: PropTypes.bool,
|
inline: PropTypes.bool,
|
||||||
localDomain: PropTypes.string,
|
localDomain: PropTypes.string,
|
||||||
|
others: ImmutablePropTypes.list,
|
||||||
|
handleClick: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
|
@ -66,6 +66,7 @@ export default class Status extends ImmutablePureComponent {
|
||||||
containerId: PropTypes.string,
|
containerId: PropTypes.string,
|
||||||
id: PropTypes.string,
|
id: PropTypes.string,
|
||||||
status: ImmutablePropTypes.map,
|
status: ImmutablePropTypes.map,
|
||||||
|
otherAccounts: ImmutablePropTypes.list,
|
||||||
account: ImmutablePropTypes.map,
|
account: ImmutablePropTypes.map,
|
||||||
onReply: PropTypes.func,
|
onReply: PropTypes.func,
|
||||||
onFavourite: PropTypes.func,
|
onFavourite: PropTypes.func,
|
||||||
|
@ -83,6 +84,7 @@ export default class Status extends ImmutablePureComponent {
|
||||||
muted: PropTypes.bool,
|
muted: PropTypes.bool,
|
||||||
collapse: PropTypes.bool,
|
collapse: PropTypes.bool,
|
||||||
hidden: PropTypes.bool,
|
hidden: PropTypes.bool,
|
||||||
|
unread: PropTypes.bool,
|
||||||
prepend: PropTypes.string,
|
prepend: PropTypes.string,
|
||||||
withDismiss: PropTypes.bool,
|
withDismiss: PropTypes.bool,
|
||||||
onMoveUp: PropTypes.func,
|
onMoveUp: PropTypes.func,
|
||||||
|
@ -93,6 +95,7 @@ export default class Status extends ImmutablePureComponent {
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
cacheMediaWidth: PropTypes.func,
|
cacheMediaWidth: PropTypes.func,
|
||||||
cachedMediaWidth: PropTypes.number,
|
cachedMediaWidth: PropTypes.number,
|
||||||
|
onClick: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
|
@ -321,17 +324,21 @@ export default class Status extends ImmutablePureComponent {
|
||||||
const { status } = this.props;
|
const { status } = this.props;
|
||||||
const { isCollapsed } = this.state;
|
const { isCollapsed } = this.state;
|
||||||
if (!router) return;
|
if (!router) return;
|
||||||
if (destination === undefined) {
|
|
||||||
destination = `/statuses/${
|
|
||||||
status.getIn(['reblog', 'id'], status.get('id'))
|
|
||||||
}`;
|
|
||||||
}
|
|
||||||
if (e.button === 0 && !(e.ctrlKey || e.altKey || e.metaKey)) {
|
if (e.button === 0 && !(e.ctrlKey || e.altKey || e.metaKey)) {
|
||||||
if (isCollapsed) this.setCollapsed(false);
|
if (isCollapsed) this.setCollapsed(false);
|
||||||
else if (e.shiftKey) {
|
else if (e.shiftKey) {
|
||||||
this.setCollapsed(true);
|
this.setCollapsed(true);
|
||||||
document.getSelection().removeAllRanges();
|
document.getSelection().removeAllRanges();
|
||||||
|
} else if (this.props.onClick) {
|
||||||
|
this.props.onClick();
|
||||||
|
return;
|
||||||
} else {
|
} else {
|
||||||
|
if (destination === undefined) {
|
||||||
|
destination = `/statuses/${
|
||||||
|
status.getIn(['reblog', 'id'], status.get('id'))
|
||||||
|
}`;
|
||||||
|
}
|
||||||
let state = {...router.history.location.state};
|
let state = {...router.history.location.state};
|
||||||
state.mastodonBackSteps = (state.mastodonBackSteps || 0) + 1;
|
state.mastodonBackSteps = (state.mastodonBackSteps || 0) + 1;
|
||||||
router.history.push(destination, state);
|
router.history.push(destination, state);
|
||||||
|
@ -441,6 +448,7 @@ export default class Status extends ImmutablePureComponent {
|
||||||
intl,
|
intl,
|
||||||
status,
|
status,
|
||||||
account,
|
account,
|
||||||
|
otherAccounts,
|
||||||
settings,
|
settings,
|
||||||
collapsed,
|
collapsed,
|
||||||
muted,
|
muted,
|
||||||
|
@ -450,6 +458,7 @@ export default class Status extends ImmutablePureComponent {
|
||||||
onOpenMedia,
|
onOpenMedia,
|
||||||
notification,
|
notification,
|
||||||
hidden,
|
hidden,
|
||||||
|
unread,
|
||||||
featured,
|
featured,
|
||||||
...other
|
...other
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
@ -617,6 +626,7 @@ export default class Status extends ImmutablePureComponent {
|
||||||
collapsed: isCollapsed,
|
collapsed: isCollapsed,
|
||||||
'has-background': isCollapsed && background,
|
'has-background': isCollapsed && background,
|
||||||
'status__wrapper-reply': !!status.get('in_reply_to_id'),
|
'status__wrapper-reply': !!status.get('in_reply_to_id'),
|
||||||
|
read: unread === false,
|
||||||
muted,
|
muted,
|
||||||
}, 'focusable');
|
}, 'focusable');
|
||||||
|
|
||||||
|
@ -647,6 +657,7 @@ export default class Status extends ImmutablePureComponent {
|
||||||
friend={account}
|
friend={account}
|
||||||
collapsed={isCollapsed}
|
collapsed={isCollapsed}
|
||||||
parseClick={parseClick}
|
parseClick={parseClick}
|
||||||
|
otherAccounts={otherAccounts}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</span>
|
</span>
|
||||||
|
@ -656,6 +667,7 @@ export default class Status extends ImmutablePureComponent {
|
||||||
collapsible={settings.getIn(['collapsed', 'enabled'])}
|
collapsible={settings.getIn(['collapsed', 'enabled'])}
|
||||||
collapsed={isCollapsed}
|
collapsed={isCollapsed}
|
||||||
setCollapsed={setCollapsed}
|
setCollapsed={setCollapsed}
|
||||||
|
directMessage={!!otherAccounts}
|
||||||
/>
|
/>
|
||||||
</header>
|
</header>
|
||||||
<StatusContent
|
<StatusContent
|
||||||
|
@ -673,6 +685,7 @@ export default class Status extends ImmutablePureComponent {
|
||||||
status={status}
|
status={status}
|
||||||
account={status.get('account')}
|
account={status.get('account')}
|
||||||
showReplyCount={settings.get('show_reply_count')}
|
showReplyCount={settings.get('show_reply_count')}
|
||||||
|
directMessage={!!otherAccounts}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{notification ? (
|
{notification ? (
|
||||||
|
|
|
@ -71,6 +71,7 @@ export default class StatusActionBar extends ImmutablePureComponent {
|
||||||
onBookmark: PropTypes.func,
|
onBookmark: PropTypes.func,
|
||||||
withDismiss: PropTypes.bool,
|
withDismiss: PropTypes.bool,
|
||||||
showReplyCount: PropTypes.bool,
|
showReplyCount: PropTypes.bool,
|
||||||
|
directMessage: PropTypes.bool,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -191,7 +192,7 @@ export default class StatusActionBar extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { status, intl, withDismiss, showReplyCount } = this.props;
|
const { status, intl, withDismiss, showReplyCount, directMessage } = this.props;
|
||||||
|
|
||||||
const mutingConversation = status.get('muted');
|
const mutingConversation = status.get('muted');
|
||||||
const anonymousAccess = !me;
|
const anonymousAccess = !me;
|
||||||
|
@ -282,14 +283,15 @@ export default class StatusActionBar extends ImmutablePureComponent {
|
||||||
return (
|
return (
|
||||||
<div className='status__action-bar'>
|
<div className='status__action-bar'>
|
||||||
{replyButton}
|
{replyButton}
|
||||||
<IconButton className='status__action-bar-button' disabled={reblogDisabled} active={status.get('reblogged')} pressed={status.get('reblogged')} title={reblogDisabled ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(reblogMessage)} icon={reblogIcon} onClick={this.handleReblogClick} />
|
{!directMessage && [
|
||||||
<IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} />
|
<IconButton className='status__action-bar-button' disabled={reblogDisabled} active={status.get('reblogged')} pressed={status.get('reblogged')} title={reblogDisabled ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(reblogMessage)} icon={reblogIcon} onClick={this.handleReblogClick} />,
|
||||||
{shareButton}
|
<IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} />,
|
||||||
<IconButton className='status__action-bar-button bookmark-icon' disabled={anonymousAccess} active={status.get('bookmarked')} pressed={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} />
|
shareButton,
|
||||||
|
<IconButton className='status__action-bar-button bookmark-icon' disabled={anonymousAccess} active={status.get('bookmarked')} pressed={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} />,
|
||||||
<div className='status__action-bar-dropdown'>
|
<div className='status__action-bar-dropdown'>
|
||||||
<DropdownMenuContainer disabled={anonymousAccess} status={status} items={menu} icon='ellipsis-h' size={18} direction='right' ariaLabel={intl.formatMessage(messages.more)} />
|
<DropdownMenuContainer disabled={anonymousAccess} status={status} items={menu} icon='ellipsis-h' size={18} direction='right' ariaLabel={intl.formatMessage(messages.more)} />
|
||||||
</div>
|
</div>,
|
||||||
|
]}
|
||||||
|
|
||||||
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'><RelativeTimestamp timestamp={status.get('created_at')} /></a>
|
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'><RelativeTimestamp timestamp={status.get('created_at')} /></a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -6,6 +6,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
// Mastodon imports.
|
// Mastodon imports.
|
||||||
import Avatar from './avatar';
|
import Avatar from './avatar';
|
||||||
import AvatarOverlay from './avatar_overlay';
|
import AvatarOverlay from './avatar_overlay';
|
||||||
|
import AvatarComposite from './avatar_composite';
|
||||||
import DisplayName from './display_name';
|
import DisplayName from './display_name';
|
||||||
|
|
||||||
export default class StatusHeader extends React.PureComponent {
|
export default class StatusHeader extends React.PureComponent {
|
||||||
|
@ -14,12 +15,18 @@ export default class StatusHeader extends React.PureComponent {
|
||||||
status: ImmutablePropTypes.map.isRequired,
|
status: ImmutablePropTypes.map.isRequired,
|
||||||
friend: ImmutablePropTypes.map,
|
friend: ImmutablePropTypes.map,
|
||||||
parseClick: PropTypes.func.isRequired,
|
parseClick: PropTypes.func.isRequired,
|
||||||
|
otherAccounts: ImmutablePropTypes.list,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handles clicks on account name/image
|
// Handles clicks on account name/image
|
||||||
|
handleClick = (id, e) => {
|
||||||
|
const { parseClick } = this.props;
|
||||||
|
parseClick(e, `/accounts/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
handleAccountClick = (e) => {
|
handleAccountClick = (e) => {
|
||||||
const { status, parseClick } = this.props;
|
const { status } = this.props;
|
||||||
parseClick(e, `/accounts/${status.getIn(['account', 'id'])}`);
|
this.handleClick(status.getIn(['account', 'id']), e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rendering.
|
// Rendering.
|
||||||
|
@ -27,36 +34,55 @@ export default class StatusHeader extends React.PureComponent {
|
||||||
const {
|
const {
|
||||||
status,
|
status,
|
||||||
friend,
|
friend,
|
||||||
|
otherAccounts,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const account = status.get('account');
|
const account = status.get('account');
|
||||||
|
|
||||||
return (
|
let statusAvatar;
|
||||||
<div className='status__info__account' >
|
if (otherAccounts && otherAccounts.size > 0) {
|
||||||
<a
|
statusAvatar = <AvatarComposite accounts={otherAccounts} size={48} onAccountClick={this.handleClick} />;
|
||||||
href={account.get('url')}
|
} else if (friend === undefined || friend === null) {
|
||||||
target='_blank'
|
statusAvatar = <Avatar account={account} size={48} />;
|
||||||
className='status__avatar'
|
} else {
|
||||||
onClick={this.handleAccountClick}
|
statusAvatar = <AvatarOverlay account={account} friend={friend} />;
|
||||||
>
|
}
|
||||||
{
|
|
||||||
friend ? (
|
if (!otherAccounts) {
|
||||||
<AvatarOverlay account={account} friend={friend} />
|
return (
|
||||||
) : (
|
<div className='status__info__account'>
|
||||||
<Avatar account={account} size={48} />
|
<a
|
||||||
)
|
href={account.get('url')}
|
||||||
}
|
target='_blank'
|
||||||
</a>
|
className='status__avatar'
|
||||||
<a
|
onClick={this.handleAccountClick}
|
||||||
href={account.get('url')}
|
>
|
||||||
target='_blank'
|
{statusAvatar}
|
||||||
className='status__display-name'
|
</a>
|
||||||
onClick={this.handleAccountClick}
|
<a
|
||||||
>
|
href={account.get('url')}
|
||||||
<DisplayName account={account} />
|
target='_blank'
|
||||||
</a>
|
className='status__display-name'
|
||||||
</div>
|
onClick={this.handleAccountClick}
|
||||||
);
|
>
|
||||||
|
<DisplayName account={account} others={otherAccounts} />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// This is a DM conversation
|
||||||
|
return (
|
||||||
|
<div className='status__info__account'>
|
||||||
|
<span className='status__avatar'>
|
||||||
|
{statusAvatar}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span className='status__display-name'>
|
||||||
|
<DisplayName account={account} others={otherAccounts} onAccountClick={this.handleClick} />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,7 @@ export default class StatusIcons extends React.PureComponent {
|
||||||
mediaIcon: PropTypes.string,
|
mediaIcon: PropTypes.string,
|
||||||
collapsible: PropTypes.bool,
|
collapsible: PropTypes.bool,
|
||||||
collapsed: PropTypes.bool,
|
collapsed: PropTypes.bool,
|
||||||
|
directMessage: PropTypes.bool,
|
||||||
setCollapsed: PropTypes.func.isRequired,
|
setCollapsed: PropTypes.func.isRequired,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
@ -42,6 +43,7 @@ export default class StatusIcons extends React.PureComponent {
|
||||||
mediaIcon,
|
mediaIcon,
|
||||||
collapsible,
|
collapsible,
|
||||||
collapsed,
|
collapsed,
|
||||||
|
directMessage,
|
||||||
intl,
|
intl,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
@ -59,9 +61,7 @@ export default class StatusIcons extends React.PureComponent {
|
||||||
aria-hidden='true'
|
aria-hidden='true'
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{(
|
{!directMessage && <VisibilityIcon visibility={status.get('visibility')} />}
|
||||||
<VisibilityIcon visibility={status.get('visibility')} />
|
|
||||||
)}
|
|
||||||
{collapsible ? (
|
{collapsible ? (
|
||||||
<IconButton
|
<IconButton
|
||||||
className='status__collapse-button'
|
className='status__collapse-button'
|
||||||
|
|
|
@ -0,0 +1,64 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
import StatusContainer from 'flavours/glitch/containers/status_container';
|
||||||
|
|
||||||
|
export default class Conversation extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
static contextTypes = {
|
||||||
|
router: PropTypes.object,
|
||||||
|
};
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
conversationId: PropTypes.string.isRequired,
|
||||||
|
accounts: ImmutablePropTypes.list.isRequired,
|
||||||
|
lastStatusId: PropTypes.string,
|
||||||
|
unread:PropTypes.bool.isRequired,
|
||||||
|
onMoveUp: PropTypes.func,
|
||||||
|
onMoveDown: PropTypes.func,
|
||||||
|
markRead: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
handleClick = () => {
|
||||||
|
if (!this.context.router) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { lastStatusId, unread, markRead } = this.props;
|
||||||
|
|
||||||
|
if (unread) {
|
||||||
|
markRead();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.context.router.history.push(`/statuses/${lastStatusId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleHotkeyMoveUp = () => {
|
||||||
|
this.props.onMoveUp(this.props.conversationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleHotkeyMoveDown = () => {
|
||||||
|
this.props.onMoveDown(this.props.conversationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { accounts, lastStatusId, unread } = this.props;
|
||||||
|
|
||||||
|
if (lastStatusId === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StatusContainer
|
||||||
|
id={lastStatusId}
|
||||||
|
unread={unread}
|
||||||
|
otherAccounts={accounts}
|
||||||
|
onMoveUp={this.handleHotkeyMoveUp}
|
||||||
|
onMoveDown={this.handleHotkeyMoveDown}
|
||||||
|
onClick={this.handleClick}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,73 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
import ConversationContainer from '../containers/conversation_container';
|
||||||
|
import ScrollableList from 'flavours/glitch/components/scrollable_list';
|
||||||
|
import { debounce } from 'lodash';
|
||||||
|
|
||||||
|
export default class ConversationsList extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
conversations: ImmutablePropTypes.list.isRequired,
|
||||||
|
hasMore: PropTypes.bool,
|
||||||
|
isLoading: PropTypes.bool,
|
||||||
|
onLoadMore: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
getCurrentIndex = id => this.props.conversations.findIndex(x => x.get('id') === id)
|
||||||
|
|
||||||
|
handleMoveUp = id => {
|
||||||
|
const elementIndex = this.getCurrentIndex(id) - 1;
|
||||||
|
this._selectChild(elementIndex, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMoveDown = id => {
|
||||||
|
const elementIndex = this.getCurrentIndex(id) + 1;
|
||||||
|
this._selectChild(elementIndex, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
_selectChild (index, align_top) {
|
||||||
|
const container = this.node.node;
|
||||||
|
const element = container.querySelector(`article:nth-of-type(${index + 1}) .focusable`);
|
||||||
|
|
||||||
|
if (element) {
|
||||||
|
if (align_top && container.scrollTop > element.offsetTop) {
|
||||||
|
element.scrollIntoView(true);
|
||||||
|
} else if (!align_top && container.scrollTop + container.clientHeight < element.offsetTop + element.offsetHeight) {
|
||||||
|
element.scrollIntoView(false);
|
||||||
|
}
|
||||||
|
element.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setRef = c => {
|
||||||
|
this.node = c;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleLoadOlder = debounce(() => {
|
||||||
|
const last = this.props.conversations.last();
|
||||||
|
|
||||||
|
if (last && last.get('last_status')) {
|
||||||
|
this.props.onLoadMore(last.get('last_status'));
|
||||||
|
}
|
||||||
|
}, 300, { leading: true })
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { conversations, onLoadMore, ...other } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ScrollableList {...other} onLoadMore={onLoadMore && this.handleLoadOlder} scrollKey='direct' ref={this.setRef}>
|
||||||
|
{conversations.map(item => (
|
||||||
|
<ConversationContainer
|
||||||
|
key={item.get('id')}
|
||||||
|
conversationId={item.get('id')}
|
||||||
|
onMoveUp={this.handleMoveUp}
|
||||||
|
onMoveDown={this.handleMoveDown}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ScrollableList>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import Conversation from '../components/conversation';
|
||||||
|
import { markConversationRead } from '../../../actions/conversations';
|
||||||
|
|
||||||
|
const mapStateToProps = (state, { conversationId }) => {
|
||||||
|
const conversation = state.getIn(['conversations', 'items']).find(x => x.get('id') === conversationId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
accounts: conversation.get('accounts').map(accountId => state.getIn(['accounts', accountId], null)),
|
||||||
|
unread: conversation.get('unread'),
|
||||||
|
lastStatusId: conversation.get('last_status', null),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch, { conversationId }) => ({
|
||||||
|
markRead: () => dispatch(markConversationRead(conversationId)),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(Conversation);
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import ConversationsList from '../components/conversations_list';
|
||||||
|
import { expandConversations } from 'flavours/glitch/actions/conversations';
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
conversations: state.getIn(['conversations', 'items']),
|
||||||
|
isLoading: state.getIn(['conversations', 'isLoading'], true),
|
||||||
|
hasMore: state.getIn(['conversations', 'hasMore'], false),
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
onLoadMore: maxId => dispatch(expandConversations({ maxId })),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(ConversationsList);
|
|
@ -5,10 +5,13 @@ import StatusListContainer from 'flavours/glitch/features/ui/containers/status_l
|
||||||
import Column from 'flavours/glitch/components/column';
|
import Column from 'flavours/glitch/components/column';
|
||||||
import ColumnHeader from 'flavours/glitch/components/column_header';
|
import ColumnHeader from 'flavours/glitch/components/column_header';
|
||||||
import { expandDirectTimeline } from 'flavours/glitch/actions/timelines';
|
import { expandDirectTimeline } from 'flavours/glitch/actions/timelines';
|
||||||
|
import { mountConversations, unmountConversations, expandConversations } from 'flavours/glitch/actions/conversations';
|
||||||
import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns';
|
import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns';
|
||||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||||
import ColumnSettingsContainer from './containers/column_settings_container';
|
import ColumnSettingsContainer from './containers/column_settings_container';
|
||||||
import { connectDirectStream } from 'flavours/glitch/actions/streaming';
|
import { connectDirectStream } from 'flavours/glitch/actions/streaming';
|
||||||
|
import { changeSetting } from 'flavours/glitch/actions/settings';
|
||||||
|
import ConversationsListContainer from './containers/conversations_list_container';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
title: { id: 'column.direct', defaultMessage: 'Direct messages' },
|
title: { id: 'column.direct', defaultMessage: 'Direct messages' },
|
||||||
|
@ -16,6 +19,7 @@ const messages = defineMessages({
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
hasUnread: state.getIn(['timelines', 'direct', 'unread']) > 0,
|
hasUnread: state.getIn(['timelines', 'direct', 'unread']) > 0,
|
||||||
|
conversationsMode: state.getIn(['settings', 'direct', 'conversations']),
|
||||||
});
|
});
|
||||||
|
|
||||||
@connect(mapStateToProps)
|
@connect(mapStateToProps)
|
||||||
|
@ -28,6 +32,7 @@ export default class DirectTimeline extends React.PureComponent {
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
hasUnread: PropTypes.bool,
|
hasUnread: PropTypes.bool,
|
||||||
multiColumn: PropTypes.bool,
|
multiColumn: PropTypes.bool,
|
||||||
|
conversationsMode: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
handlePin = () => {
|
handlePin = () => {
|
||||||
|
@ -50,13 +55,32 @@ export default class DirectTimeline extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
const { dispatch } = this.props;
|
const { dispatch, conversationsMode } = this.props;
|
||||||
|
|
||||||
|
dispatch(mountConversations());
|
||||||
|
|
||||||
|
if (conversationsMode) {
|
||||||
|
dispatch(expandConversations());
|
||||||
|
} else {
|
||||||
|
dispatch(expandDirectTimeline());
|
||||||
|
}
|
||||||
|
|
||||||
dispatch(expandDirectTimeline());
|
|
||||||
this.disconnect = dispatch(connectDirectStream());
|
this.disconnect = dispatch(connectDirectStream());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
const { dispatch, conversationsMode } = this.props;
|
||||||
|
|
||||||
|
if (prevProps.conversationsMode && !conversationsMode) {
|
||||||
|
dispatch(expandDirectTimeline());
|
||||||
|
} else if (!prevProps.conversationsMode && conversationsMode) {
|
||||||
|
dispatch(expandConversations());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
componentWillUnmount () {
|
componentWillUnmount () {
|
||||||
|
this.props.dispatch(unmountConversations());
|
||||||
|
|
||||||
if (this.disconnect) {
|
if (this.disconnect) {
|
||||||
this.disconnect();
|
this.disconnect();
|
||||||
this.disconnect = null;
|
this.disconnect = null;
|
||||||
|
@ -67,14 +91,49 @@ export default class DirectTimeline extends React.PureComponent {
|
||||||
this.column = c;
|
this.column = c;
|
||||||
}
|
}
|
||||||
|
|
||||||
handleLoadMore = maxId => {
|
handleLoadMoreTimeline = maxId => {
|
||||||
this.props.dispatch(expandDirectTimeline({ maxId }));
|
this.props.dispatch(expandDirectTimeline({ maxId }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleLoadMoreConversations = maxId => {
|
||||||
|
this.props.dispatch(expandConversations({ maxId }));
|
||||||
|
}
|
||||||
|
|
||||||
|
handleTimelineClick = () => {
|
||||||
|
this.props.dispatch(changeSetting(['direct', 'conversations'], false));
|
||||||
|
}
|
||||||
|
|
||||||
|
handleConversationsClick = () => {
|
||||||
|
this.props.dispatch(changeSetting(['direct', 'conversations'], true));
|
||||||
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { intl, hasUnread, columnId, multiColumn } = this.props;
|
const { intl, hasUnread, columnId, multiColumn, conversationsMode } = this.props;
|
||||||
const pinned = !!columnId;
|
const pinned = !!columnId;
|
||||||
|
|
||||||
|
let contents;
|
||||||
|
if (conversationsMode) {
|
||||||
|
contents = (
|
||||||
|
<ConversationsListContainer
|
||||||
|
trackScroll={!pinned}
|
||||||
|
scrollKey={`direct_timeline-${columnId}`}
|
||||||
|
timelineId='direct'
|
||||||
|
onLoadMore={this.handleLoadMore}
|
||||||
|
emptyMessage={<FormattedMessage id='empty_column.direct' defaultMessage="You don't have any direct messages yet. When you send or receive one, it will show up here." />}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
contents = (
|
||||||
|
<StatusListContainer
|
||||||
|
trackScroll={!pinned}
|
||||||
|
scrollKey={`direct_timeline-${columnId}`}
|
||||||
|
timelineId='direct'
|
||||||
|
onLoadMore={this.handleLoadMoreTimeline}
|
||||||
|
emptyMessage={<FormattedMessage id='empty_column.direct' defaultMessage="You don't have any direct messages yet. When you send or receive one, it will show up here." />}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column ref={this.setRef} label={intl.formatMessage(messages.title)}>
|
<Column ref={this.setRef} label={intl.formatMessage(messages.title)}>
|
||||||
<ColumnHeader
|
<ColumnHeader
|
||||||
|
@ -90,13 +149,28 @@ export default class DirectTimeline extends React.PureComponent {
|
||||||
<ColumnSettingsContainer />
|
<ColumnSettingsContainer />
|
||||||
</ColumnHeader>
|
</ColumnHeader>
|
||||||
|
|
||||||
<StatusListContainer
|
<div className='notification__filter-bar'>
|
||||||
trackScroll={!pinned}
|
<button
|
||||||
scrollKey={`direct_timeline-${columnId}`}
|
className={conversationsMode ? 'active' : ''}
|
||||||
timelineId='direct'
|
onClick={this.handleConversationsClick}
|
||||||
onLoadMore={this.handleLoadMore}
|
>
|
||||||
emptyMessage={<FormattedMessage id='empty_column.direct' defaultMessage="You don't have any direct messages yet. When you send or receive one, it will show up here." />}
|
<FormattedMessage
|
||||||
/>
|
id='direct.conversations_mode'
|
||||||
|
defaultMessage='Conversations'
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={conversationsMode ? '' : 'active'}
|
||||||
|
onClick={this.handleTimelineClick}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id='direct.timeline_mode'
|
||||||
|
defaultMessage='Timeline'
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{contents}
|
||||||
</Column>
|
</Column>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,102 @@
|
||||||
|
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
||||||
|
import {
|
||||||
|
CONVERSATIONS_MOUNT,
|
||||||
|
CONVERSATIONS_UNMOUNT,
|
||||||
|
CONVERSATIONS_FETCH_REQUEST,
|
||||||
|
CONVERSATIONS_FETCH_SUCCESS,
|
||||||
|
CONVERSATIONS_FETCH_FAIL,
|
||||||
|
CONVERSATIONS_UPDATE,
|
||||||
|
CONVERSATIONS_READ,
|
||||||
|
} from '../actions/conversations';
|
||||||
|
import compareId from 'flavours/glitch/util/compare_id';
|
||||||
|
|
||||||
|
const initialState = ImmutableMap({
|
||||||
|
items: ImmutableList(),
|
||||||
|
isLoading: false,
|
||||||
|
hasMore: true,
|
||||||
|
mounted: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const conversationToMap = item => ImmutableMap({
|
||||||
|
id: item.id,
|
||||||
|
unread: item.unread,
|
||||||
|
accounts: ImmutableList(item.accounts.map(a => a.id)),
|
||||||
|
last_status: item.last_status ? item.last_status.id : null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateConversation = (state, item) => state.update('items', list => {
|
||||||
|
const index = list.findIndex(x => x.get('id') === item.id);
|
||||||
|
const newItem = conversationToMap(item);
|
||||||
|
|
||||||
|
if (index === -1) {
|
||||||
|
return list.unshift(newItem);
|
||||||
|
} else {
|
||||||
|
return list.set(index, newItem);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const expandNormalizedConversations = (state, conversations, next, isLoadingRecent) => {
|
||||||
|
let items = ImmutableList(conversations.map(conversationToMap));
|
||||||
|
|
||||||
|
return state.withMutations(mutable => {
|
||||||
|
if (!items.isEmpty()) {
|
||||||
|
mutable.update('items', list => {
|
||||||
|
list = list.map(oldItem => {
|
||||||
|
const newItemIndex = items.findIndex(x => x.get('id') === oldItem.get('id'));
|
||||||
|
|
||||||
|
if (newItemIndex === -1) {
|
||||||
|
return oldItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newItem = items.get(newItemIndex);
|
||||||
|
items = items.delete(newItemIndex);
|
||||||
|
|
||||||
|
return newItem;
|
||||||
|
});
|
||||||
|
|
||||||
|
list = list.concat(items);
|
||||||
|
|
||||||
|
return list.sortBy(x => x.get('last_status'), (a, b) => {
|
||||||
|
if(a === null || b === null) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return compareId(a, b) * -1;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!next && !isLoadingRecent) {
|
||||||
|
mutable.set('hasMore', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
mutable.set('isLoading', false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function conversations(state = initialState, action) {
|
||||||
|
switch (action.type) {
|
||||||
|
case CONVERSATIONS_FETCH_REQUEST:
|
||||||
|
return state.set('isLoading', true);
|
||||||
|
case CONVERSATIONS_FETCH_FAIL:
|
||||||
|
return state.set('isLoading', false);
|
||||||
|
case CONVERSATIONS_FETCH_SUCCESS:
|
||||||
|
return expandNormalizedConversations(state, action.conversations, action.next, action.isLoadingRecent);
|
||||||
|
case CONVERSATIONS_UPDATE:
|
||||||
|
return updateConversation(state, action.conversation);
|
||||||
|
case CONVERSATIONS_MOUNT:
|
||||||
|
return state.update('mounted', count => count + 1);
|
||||||
|
case CONVERSATIONS_UNMOUNT:
|
||||||
|
return state.update('mounted', count => count - 1);
|
||||||
|
case CONVERSATIONS_READ:
|
||||||
|
return state.update('items', list => list.map(item => {
|
||||||
|
if (item.get('id') === action.id) {
|
||||||
|
return item.set('unread', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}));
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
|
@ -28,6 +28,7 @@ import lists from './lists';
|
||||||
import listEditor from './list_editor';
|
import listEditor from './list_editor';
|
||||||
import listAdder from './list_adder';
|
import listAdder from './list_adder';
|
||||||
import filters from './filters';
|
import filters from './filters';
|
||||||
|
import conversations from './conversations';
|
||||||
import suggestions from './suggestions';
|
import suggestions from './suggestions';
|
||||||
import pinnedAccountsEditor from './pinned_accounts_editor';
|
import pinnedAccountsEditor from './pinned_accounts_editor';
|
||||||
import polls from './polls';
|
import polls from './polls';
|
||||||
|
@ -64,6 +65,7 @@ const reducers = {
|
||||||
listEditor,
|
listEditor,
|
||||||
listAdder,
|
listAdder,
|
||||||
filters,
|
filters,
|
||||||
|
conversations,
|
||||||
suggestions,
|
suggestions,
|
||||||
pinnedAccountsEditor,
|
pinnedAccountsEditor,
|
||||||
polls,
|
polls,
|
||||||
|
|
|
@ -72,6 +72,7 @@ const initialState = ImmutableMap({
|
||||||
}),
|
}),
|
||||||
|
|
||||||
direct: ImmutableMap({
|
direct: ImmutableMap({
|
||||||
|
conversations: true,
|
||||||
regex: ImmutableMap({
|
regex: ImmutableMap({
|
||||||
body: '',
|
body: '',
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -46,6 +46,18 @@
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&-composite {
|
||||||
|
@include avatar-radius;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
& div {
|
||||||
|
@include avatar-radius;
|
||||||
|
float: left;
|
||||||
|
position: relative;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.account__avatar-overlay {
|
.account__avatar-overlay {
|
||||||
|
|
|
@ -287,8 +287,12 @@
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
strong {
|
strong {
|
||||||
display: block;
|
|
||||||
height: 18px;
|
height: 18px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
@ -308,7 +312,7 @@
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
> a:hover {
|
||||||
strong {
|
strong {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
|
@ -209,7 +209,7 @@
|
||||||
outline: 0;
|
outline: 0;
|
||||||
background: lighten($ui-base-color, 4%);
|
background: lighten($ui-base-color, 4%);
|
||||||
|
|
||||||
.status.status-direct {
|
&.status.status-direct:not(.read) {
|
||||||
background: lighten($ui-base-color, 12%);
|
background: lighten($ui-base-color, 12%);
|
||||||
|
|
||||||
&.muted {
|
&.muted {
|
||||||
|
@ -249,8 +249,9 @@
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.status-direct {
|
&.status-direct:not(.read) {
|
||||||
background: lighten($ui-base-color, 8%);
|
background: lighten($ui-base-color, 8%);
|
||||||
|
border-bottom-color: lighten($ui-base-color, 12%);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.light {
|
&.light {
|
||||||
|
@ -333,7 +334,7 @@
|
||||||
&:focus > .status__content:after {
|
&:focus > .status__content:after {
|
||||||
background: linear-gradient(rgba(lighten($ui-base-color, 4%), 0), rgba(lighten($ui-base-color, 4%), 1));
|
background: linear-gradient(rgba(lighten($ui-base-color, 4%), 0), rgba(lighten($ui-base-color, 4%), 1));
|
||||||
}
|
}
|
||||||
&.status-direct> .status__content:after {
|
&.status-direct:not(.read)> .status__content:after {
|
||||||
background: linear-gradient(rgba(lighten($ui-base-color, 8%), 0), rgba(lighten($ui-base-color, 8%), 1));
|
background: linear-gradient(rgba(lighten($ui-base-color, 8%), 0), rgba(lighten($ui-base-color, 8%), 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -599,7 +600,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.status__display-name,
|
a.status__display-name,
|
||||||
.reply-indicator__display-name,
|
.reply-indicator__display-name,
|
||||||
.detailed-status__display-name,
|
.detailed-status__display-name,
|
||||||
.account__display-name {
|
.account__display-name {
|
||||||
|
|
|
@ -27,15 +27,16 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.status.status-direct {
|
.status.status-direct:not(.read) {
|
||||||
background: darken($ui-base-color, 8%);
|
background: darken($ui-base-color, 8%);
|
||||||
|
border-bottom-color: darken($ui-base-color, 12%);
|
||||||
|
|
||||||
&.collapsed> .status__content:after {
|
&.collapsed> .status__content:after {
|
||||||
background: linear-gradient(rgba(darken($ui-base-color, 8%), 0), rgba(darken($ui-base-color, 8%), 1));
|
background: linear-gradient(rgba(darken($ui-base-color, 8%), 0), rgba(darken($ui-base-color, 8%), 1));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.focusable:focus.status.status-direct {
|
.focusable:focus.status.status-direct:not(.read) {
|
||||||
background: darken($ui-base-color, 4%);
|
background: darken($ui-base-color, 4%);
|
||||||
|
|
||||||
&.collapsed> .status__content:after {
|
&.collapsed> .status__content:after {
|
||||||
|
|
Loading…
Reference in New Issue