diff --git a/app/soapbox/actions/soapbox.js b/app/soapbox/actions/soapbox.js index c072a0bbc..58a2417c7 100644 --- a/app/soapbox/actions/soapbox.js +++ b/app/soapbox/actions/soapbox.js @@ -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, diff --git a/app/soapbox/components/status.js b/app/soapbox/components/status.js index ffca8efeb..f8d46b043 100644 --- a/app/soapbox/components/status.js +++ b/app/soapbox/components/status.js @@ -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 ( @@ -468,9 +471,9 @@ class Status extends ImmutablePureComponent { - {status.hasIn(['account', 'pleroma', 'favicon']) && + {favicon &&
- +
}
diff --git a/app/soapbox/features/chats/chat_room.js b/app/soapbox/features/chats/chat_room.js index e6df1953d..4a8c480ea 100644 --- a/app/soapbox/features/chats/chat_room.js +++ b/app/soapbox/features/chats/chat_room.js @@ -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() { diff --git a/app/soapbox/features/chats/components/chat_box.js b/app/soapbox/features/chats/components/chat_box.js index 0f1de7dbb..40123ca25 100644 --- a/app/soapbox/features/chats/components/chat_box.js +++ b/app/soapbox/features/chats/components/chat_box.js @@ -40,11 +40,24 @@ class ChatBox extends ImmutablePureComponent { content: '', } - handleKeyDown = (e) => { + sendMessage = () => { const { chatId } = this.props; - if (e.key === 'Enter') { - this.props.dispatch(sendChatMessage(chatId, this.state)); - this.setState({ content: '' }); + 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(); } } diff --git a/app/soapbox/features/chats/components/chat_list.js b/app/soapbox/features/chats/components/chat_list.js index 41aae6397..e24d31ab9 100644 --- a/app/soapbox/features/chats/components/chat_list.js +++ b/app/soapbox/features/chats/components/chat_list.js @@ -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; diff --git a/app/soapbox/features/chats/components/chat_message_list.js b/app/soapbox/features/chats/components/chat_message_list.js index 8e52e3270..e2fe305bd 100644 --- a/app/soapbox/features/chats/components/chat_message_list.js +++ b/app/soapbox/features/chats/components/chat_message_list.js @@ -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, '
'); + } + + 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 {
diff --git a/app/soapbox/features/status/components/action_bar.js b/app/soapbox/features/status/components/action_bar.js index c5db0cab5..886e1a2b4 100644 --- a/app/soapbox/features/status/components/action_bar.js +++ b/app/soapbox/features/status/components/action_bar.js @@ -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 }); diff --git a/app/soapbox/features/status/components/detailed_status.js b/app/soapbox/features/status/components/detailed_status.js index 454c22fba..d58bd7e72 100644 --- a/app/soapbox/features/status/components/detailed_status.js +++ b/app/soapbox/features/status/components/detailed_status.js @@ -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 {
- {status.hasIn(['account', 'pleroma', 'favicon']) && + {favicon &&
- +
} {statusTypeIcon} diff --git a/app/soapbox/features/status/containers/detailed_status_container.js b/app/soapbox/features/status/containers/detailed_status_container.js index 7c78a2e4e..509587636 100644 --- a/app/soapbox/features/status/containers/detailed_status_container.js +++ b/app/soapbox/features/status/containers/detailed_status_container.js @@ -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)); diff --git a/app/soapbox/features/status/index.js b/app/soapbox/features/status/index.js index 8eb3530f6..d82f8c7a1 100644 --- a/app/soapbox/features/status/index.js +++ b/app/soapbox/features/status/index.js @@ -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} />
diff --git a/app/soapbox/features/ui/index.js b/app/soapbox/features/ui/index.js index 59269d577..ff80086ee 100644 --- a/app/soapbox/features/ui/index.js +++ b/app/soapbox/features/ui/index.js @@ -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' })); diff --git a/app/soapbox/reducers/soapbox.js b/app/soapbox/reducers/soapbox.js index dd7b0bea3..ecf1656f5 100644 --- a/app/soapbox/reducers/soapbox.js +++ b/app/soapbox/reducers/soapbox.js @@ -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: diff --git a/app/soapbox/utils/accounts.js b/app/soapbox/utils/accounts.js index 3d04a3360..929429b24 100644 --- a/app/soapbox/utils/accounts.js +++ b/app/soapbox/utils/accounts.js @@ -1,21 +1,26 @@ import { Map as ImmutableMap } from 'immutable'; import { List as ImmutableList } from 'immutable'; +const guessDomain = account => { + try { + let re = /https?:\/\/(.*?)\//i; + return re.exec(account.get('url'))[1]; + } catch(e) { + return null; + } +}; + export const getDomain = account => { - let re = /https?:\/\/(.*?)\//i; - return re.exec(account.get('url'))[1]; + 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.'); - return account.get('acct'); - } - return [user, domain].join('@'); + const [user, domain] = account.get('acct').split('@'); + if (!domain) return [user, guessDomain(account)].join('@'); + return account.get('acct'); }; export const isStaff = (account = ImmutableMap()) => ( diff --git a/app/styles/chats.scss b/app/styles/chats.scss index d6cb2bed8..011543ca3 100644 --- a/app/styles/chats.scss +++ b/app/styles/chats.scss @@ -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; diff --git a/app/styles/components/theme-toggle.scss b/app/styles/components/theme-toggle.scss index 1f2697b89..c0cadc68f 100644 --- a/app/styles/components/theme-toggle.scss +++ b/app/styles/components/theme-toggle.scss @@ -9,10 +9,6 @@ .react-toggle { vertical-align: middle; - &-track { - background-color: var(--foreground-color); - } - &-track-check, &-track-x { display: flex;