Merge branch 'develop' into 'chat_notifications'

Merge develop into chat_notifications

See merge request soapbox-pub/soapbox-fe!204
better-alerts
Curtis 4 years ago
commit a5ef0f46e5

@ -7,7 +7,7 @@ export const SOAPBOX_CONFIG_REQUEST_FAIL = 'SOAPBOX_CONFIG_REQUEST_FAIL';
export const defaultConfig = ImmutableMap({
logo: '',
banner: '',
brandColor: '#0482d8', // Azure
brandColor: '', // Empty
customCss: ImmutableList(),
promoPanel: ImmutableMap({
items: ImmutableList(),
@ -50,6 +50,9 @@ export function fetchSoapboxJson() {
}
export function importSoapboxConfig(soapboxConfig) {
if (!soapboxConfig.brandColor) {
soapboxConfig.brandColor = '#0482d8';
};
return {
type: SOAPBOX_CONFIG_REQUEST_SUCCESS,
soapboxConfig,

@ -21,6 +21,7 @@ import { NavLink } from 'react-router-dom';
import ProfileHoverCardContainer from '../features/profile_hover_card/profile_hover_card_container';
import { isMobile } from '../../../app/soapbox/is_mobile';
import { debounce } from 'lodash';
import { getDomain } from 'soapbox/utils/accounts';
// We use the component (and not the container) since we do not want
// to use the progress bar to show download progress
@ -455,6 +456,8 @@ class Status extends ImmutablePureComponent {
const statusUrl = `/@${status.getIn(['account', 'acct'])}/posts/${status.get('id')}`;
const { profileCardVisible } = this.state;
const favicon = status.getIn(['account', 'pleroma', 'favicon']);
const domain = getDomain(status.get('account'));
return (
<HotKeys handlers={handlers}>
@ -468,9 +471,9 @@ class Status extends ImmutablePureComponent {
<RelativeTimestamp timestamp={status.get('created_at')} />
</NavLink>
{status.hasIn(['account', 'pleroma', 'favicon']) &&
{favicon &&
<div className='status__favicon'>
<img src={status.getIn(['account', 'pleroma', 'favicon'])} alt='' />
<img src={favicon} alt='' title={domain} />
</div>}
<div className='status__profile' onMouseEnter={this.handleProfileHover} onMouseLeave={this.handleProfileLeave}>

@ -6,18 +6,20 @@ import { injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import Avatar from 'soapbox/components/avatar';
import { acctFull } from 'soapbox/utils/accounts';
import { fetchChat } from 'soapbox/actions/chats';
import { fetchChat, markChatRead } from 'soapbox/actions/chats';
import ChatBox from './components/chat_box';
import Column from 'soapbox/components/column';
import ColumnBackButton from 'soapbox/components/column_back_button';
import { Map as ImmutableMap } from 'immutable';
import { makeGetChat } from 'soapbox/selectors';
const mapStateToProps = (state, { params }) => {
const getChat = makeGetChat();
const chat = state.getIn(['chats', params.chatId], ImmutableMap()).toJS();
return {
me: state.get('me'),
chat: getChat(state, { id: params.chatId }),
chat: getChat(state, chat),
};
};
@ -42,9 +44,26 @@ class ChatRoom extends ImmutablePureComponent {
this.inputElem.focus();
}
markRead = () => {
const { dispatch, chat } = this.props;
if (!chat) return;
dispatch(markChatRead(chat.get('id')));
}
componentDidMount() {
const { dispatch, params } = this.props;
dispatch(fetchChat(params.chatId));
this.markRead();
}
componentDidUpdate(prevProps) {
const markReadConditions = [
() => this.props.chat !== undefined,
() => this.props.chat.get('unread') > 0,
];
if (markReadConditions.every(c => c() === true))
this.markRead();
}
render() {

@ -40,11 +40,24 @@ class ChatBox extends ImmutablePureComponent {
content: '',
}
handleKeyDown = (e) => {
sendMessage = () => {
const { chatId } = this.props;
if (e.key === 'Enter') {
if (this.state.content.length < 1) return;
this.props.dispatch(sendChatMessage(chatId, this.state));
this.setState({ content: '' });
}
insertLine = () => {
const { content } = this.state;
this.setState({ content: content + '\n' });
}
handleKeyDown = (e) => {
if (e.key === 'Enter' && e.shiftKey) {
this.insertLine();
e.preventDefault();
} else if (e.key === 'Enter') {
this.sendMessage();
e.preventDefault();
}
}

@ -3,7 +3,6 @@ import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { fetchChats } from 'soapbox/actions/chats';
import Chat from './chat';
import { makeGetChat } from 'soapbox/selectors';
@ -42,10 +41,6 @@ class ChatList extends ImmutablePureComponent {
emptyMessage: PropTypes.node,
};
componentDidMount() {
this.props.dispatch(fetchChats());
}
render() {
const { chats, emptyMessage } = this.props;

@ -4,9 +4,14 @@ import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { List as ImmutableList } from 'immutable';
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
import emojify from 'soapbox/features/emoji/emoji';
import classNames from 'classnames';
import { escape } from 'lodash';
const makeEmojiMap = record => record.get('emojis', ImmutableList()).reduce((map, emoji) => {
return map.set(`:${emoji.get('shortcode')}:`, emoji);
}, ImmutableMap());
const mapStateToProps = (state, { chatMessageIds }) => ({
me: state.get('me'),
@ -72,6 +77,18 @@ class ChatMessageList extends ImmutablePureComponent {
this.scrollToBottom();
}
parsePendingContent = content => {
return escape(content).replace(/(?:\r\n|\r|\n)/g, '<br>');
}
parseContent = chatMessage => {
const content = chatMessage.get('content') || '';
const pending = chatMessage.get('pending', false);
const formatted = pending ? this.parsePendingContent(content) : content;
const emojiMap = makeEmojiMap(chatMessage);
return emojify(formatted, emojiMap.toJS());
}
render() {
const { chatMessages, me } = this.props;
@ -88,7 +105,7 @@ class ChatMessageList extends ImmutablePureComponent {
<span
title={this.getFormattedTimestamp(chatMessage)}
className='chat-message__bubble'
dangerouslySetInnerHTML={{ __html: emojify(chatMessage.get('content') || '') }}
dangerouslySetInnerHTML={{ __html: this.parseContent(chatMessage) }}
ref={this.setRef}
/>
</div>

@ -34,6 +34,8 @@ const messages = defineMessages({
admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
admin_status: { id: 'status.admin_status', defaultMessage: 'Open this post in the moderation interface' },
copy: { id: 'status.copy', defaultMessage: 'Copy link to post' },
bookmark: { id: 'status.bookmark', defaultMessage: 'Bookmark' },
unbookmark: { id: 'status.unbookmark', defaultMessage: 'Remove bookmark' },
});
const mapStateToProps = state => {
@ -63,6 +65,7 @@ class ActionBar extends React.PureComponent {
onFavourite: PropTypes.func.isRequired,
onEmojiReact: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired,
onBookmark: PropTypes.func,
onDirect: PropTypes.func.isRequired,
onMention: PropTypes.func.isRequired,
onMute: PropTypes.func,
@ -103,6 +106,10 @@ class ActionBar extends React.PureComponent {
}
}
handleBookmarkClick = () => {
this.props.onBookmark(this.props.status);
}
handleFavouriteClick = () => {
const { me } = this.props;
if (me) {
@ -237,9 +244,12 @@ class ActionBar extends React.PureComponent {
if (publicStatus) {
menu.push({ text: intl.formatMessage(messages.copy), action: this.handleCopy });
// menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed });
menu.push(null);
}
menu.push({ text: intl.formatMessage(status.get('bookmarked') ? messages.unbookmark : messages.bookmark), action: this.handleBookmarkClick });
menu.push(null);
if (me === status.getIn(['account', 'id'])) {
if (publicStatus) {
menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });

@ -19,6 +19,7 @@ import { StatusInteractionBar } from './status_interaction_bar';
import ProfileHoverCardContainer from 'soapbox/features/profile_hover_card/profile_hover_card_container';
import { isMobile } from 'soapbox/is_mobile';
import { debounce } from 'lodash';
import { getDomain } from 'soapbox/utils/accounts';
export default class DetailedStatus extends ImmutablePureComponent {
@ -103,6 +104,8 @@ export default class DetailedStatus extends ImmutablePureComponent {
const outerStyle = { boxSizing: 'border-box' };
const { compact } = this.props;
const { profileCardVisible } = this.state;
const favicon = status.getIn(['account', 'pleroma', 'favicon']);
const domain = getDomain(status.get('account'));
if (!status) {
return null;
@ -208,9 +211,9 @@ export default class DetailedStatus extends ImmutablePureComponent {
<div className='detailed-status__meta'>
<StatusInteractionBar status={status} />
<div>
{status.hasIn(['account', 'pleroma', 'favicon']) &&
{favicon &&
<div className='status__favicon'>
<img src={status.getIn(['account', 'pleroma', 'favicon'])} alt='' />
<img src={favicon} alt='' title={domain} />
</div>}
{statusTypeIcon}<a className='detailed-status__datetime' href={status.get('url')} target='_blank' rel='noopener'>

@ -12,6 +12,8 @@ import {
favourite,
unreblog,
unfavourite,
bookmark,
unbookmark,
pin,
unpin,
} from '../../../actions/interactions';
@ -88,6 +90,14 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
});
},
onBookmark(status) {
if (status.get('bookmarked')) {
dispatch(unbookmark(status));
} else {
dispatch(bookmark(status));
}
},
onFavourite(status) {
if (status.get('favourited')) {
dispatch(unfavourite(status));

@ -14,6 +14,8 @@ import {
unfavourite,
reblog,
unreblog,
bookmark,
unbookmark,
pin,
unpin,
} from '../../actions/interactions';
@ -168,6 +170,14 @@ class Status extends ImmutablePureComponent {
}
}
handleBookmark = (status) => {
if (status.get('bookmarked')) {
this.props.dispatch(unbookmark(status));
} else {
this.props.dispatch(bookmark(status));
}
}
handleReplyClick = (status) => {
let { askReplyConfirmation, dispatch, intl } = this.props;
if (askReplyConfirmation) {
@ -507,6 +517,7 @@ class Status extends ImmutablePureComponent {
onBlock={this.handleBlockClick}
onReport={this.handleReport}
onPin={this.handlePin}
onBookmark={this.handleBookmark}
onEmbed={this.handleEmbed}
/>
</div>

@ -18,6 +18,7 @@ import { expandHomeTimeline } from '../../actions/timelines';
import { expandNotifications } from '../../actions/notifications';
import { fetchReports } from '../../actions/admin';
import { fetchFilters } from '../../actions/filters';
import { fetchChats } from 'soapbox/actions/chats';
import { clearHeight } from '../../actions/height_cache';
import { openModal } from '../../actions/modal';
import { WrappedRoute } from './util/react_router_helpers';
@ -433,6 +434,7 @@ class UI extends React.PureComponent {
if (account) {
this.props.dispatch(expandHomeTimeline());
this.props.dispatch(expandNotifications());
this.props.dispatch(fetchChats());
// this.props.dispatch(fetchGroups('member'));
if (isStaff(account))
this.props.dispatch(fetchReports({ state: 'open' }));

@ -1,10 +1,17 @@
import { ADMIN_CONFIG_UPDATE_SUCCESS } from '../actions/admin';
import { SOAPBOX_CONFIG_REQUEST_SUCCESS } from '../actions/soapbox';
import {
SOAPBOX_CONFIG_REQUEST_SUCCESS,
SOAPBOX_CONFIG_REQUEST_FAIL,
} from '../actions/soapbox';
import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
import { ConfigDB } from 'soapbox/utils/config_db';
const initialState = ImmutableMap();
const fallbackState = ImmutableMap({
brandColor: '#0482d8', // Azure
});
const updateFromAdmin = (state, config) => {
const configs = config.get('configs', ImmutableList());
@ -22,6 +29,8 @@ export default function soapbox(state = initialState, action) {
switch(action.type) {
case SOAPBOX_CONFIG_REQUEST_SUCCESS:
return fromJS(action.soapboxConfig);
case SOAPBOX_CONFIG_REQUEST_FAIL:
return fallbackState.mergeDeep(state);
case ADMIN_CONFIG_UPDATE_SUCCESS:
return updateFromAdmin(state, fromJS(action.config));
default:

@ -1,21 +1,26 @@
import { Map as ImmutableMap } from 'immutable';
import { List as ImmutableList } from 'immutable';
export const getDomain = account => {
const guessDomain = account => {
try {
let re = /https?:\/\/(.*?)\//i;
return re.exec(account.get('url'))[1];
} catch(e) {
return null;
}
};
export const getDomain = account => {
let domain = account.get('acct').split('@')[1];
if (!domain) domain = guessDomain(account);
return domain;
};
// user@domain even for local users
export const acctFull = account => {
let [user, domain] = account.get('acct').split('@');
try {
if (!domain) domain = getDomain(account);
} catch(e) {
console.warning('Could not get domain for acctFull. Falling back to acct.');
const [user, domain] = account.get('acct').split('@');
if (!domain) return [user, guessDomain(account)].join('@');
return account.get('acct');
}
return [user, domain].join('@');
};
export const isStaff = (account = ImmutableMap()) => (

@ -113,6 +113,8 @@
background-color: var(--background-color);
overflow: hidden;
text-overflow: ellipsis;
overflow-wrap: break-word;
white-space: break-spaces;
a {
color: var(--brand-color--hicontrast);
@ -252,7 +254,7 @@
background: transparent;
border: 0;
padding: 0;
color: #fff;
color: var(--primary-text-color);
font-weight: bold;
text-align: left;
font-size: 14px;

@ -9,10 +9,6 @@
.react-toggle {
vertical-align: middle;
&-track {
background-color: var(--foreground-color);
}
&-track-check,
&-track-x {
display: flex;

Loading…
Cancel
Save