From 97850190540671f76103fbbd5d075024b2e273b5 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 24 Aug 2020 19:51:24 -0500 Subject: [PATCH 01/80] Add rudimentary support for pleroma:chat_mention notification type --- app/soapbox/actions/chats.js | 0 .../notifications/components/notification.js | 22 +++++++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 app/soapbox/actions/chats.js diff --git a/app/soapbox/actions/chats.js b/app/soapbox/actions/chats.js new file mode 100644 index 000000000..e69de29bb diff --git a/app/soapbox/features/notifications/components/notification.js b/app/soapbox/features/notifications/components/notification.js index d6ba86ced..3703f13f0 100644 --- a/app/soapbox/features/notifications/components/notification.js +++ b/app/soapbox/features/notifications/components/notification.js @@ -142,6 +142,26 @@ class Notification extends ImmutablePureComponent { ); } + renderChatMention(notification, link) { + const { intl } = this.props; + + return ( + +
+
+
+ +
+ + + + +
+
+
+ ); + } + renderEmojiReact(notification, link) { const { intl } = this.props; @@ -289,6 +309,8 @@ class Notification extends ImmutablePureComponent { return this.renderPoll(notification); case 'pleroma:emoji_reaction': return this.renderEmojiReact(notification, link); + case 'pleroma:chat_mention': + return this.renderChatMention(notification); } return null; From f1cff927c06c8d5295562dd75cb2132521d8e899 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 24 Aug 2020 21:26:42 -0500 Subject: [PATCH 02/80] Display Chat list --- app/soapbox/actions/chats.js | 16 +++++++ .../features/chats/components/chat_list.js | 44 +++++++++++++++++++ app/soapbox/features/ui/index.js | 2 + app/soapbox/reducers/chats.js | 15 +++++++ app/soapbox/reducers/index.js | 2 + app/styles/application.scss | 1 + app/styles/chats.scss | 24 ++++++++++ 7 files changed, 104 insertions(+) create mode 100644 app/soapbox/features/chats/components/chat_list.js create mode 100644 app/soapbox/reducers/chats.js create mode 100644 app/styles/chats.scss diff --git a/app/soapbox/actions/chats.js b/app/soapbox/actions/chats.js index e69de29bb..1f864ef80 100644 --- a/app/soapbox/actions/chats.js +++ b/app/soapbox/actions/chats.js @@ -0,0 +1,16 @@ +import api from '../api'; + +export const CHATS_FETCH_REQUEST = 'CHATS_FETCH_REQUEST'; +export const CHATS_FETCH_SUCCESS = 'CHATS_FETCH_SUCCESS'; +export const CHATS_FETCH_FAIL = 'CHATS_FETCH_FAIL'; + +export function fetchChats() { + return (dispatch, getState) => { + dispatch({ type: CHATS_FETCH_REQUEST }); + return api(getState).get('/api/v1/pleroma/chats').then(({ data }) => { + dispatch({ type: CHATS_FETCH_SUCCESS, data }); + }).catch(error => { + dispatch({ type: CHATS_FETCH_FAIL, error }); + }); + }; +} diff --git a/app/soapbox/features/chats/components/chat_list.js b/app/soapbox/features/chats/components/chat_list.js new file mode 100644 index 000000000..e2446e07f --- /dev/null +++ b/app/soapbox/features/chats/components/chat_list.js @@ -0,0 +1,44 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +import { injectIntl, FormattedMessage } from 'react-intl'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import { fetchChats } from 'soapbox/actions/chats'; + +const mapStateToProps = state => ({ + chats: state.get('chats'), +}); + +export default @connect(mapStateToProps) +@injectIntl +class ChatList extends ImmutablePureComponent { + + static propTypes = { + dispatch: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, + }; + + componentDidMount() { + this.props.dispatch(fetchChats()); + } + + render() { + const { chats } = this.props; + + return ( +
+
+ +
+
+ {chats.toList().map(chat => ( +
+ {chat.getIn(['account', 'acct'])} +
+ ))} +
+
+ ); + } + +} diff --git a/app/soapbox/features/ui/index.js b/app/soapbox/features/ui/index.js index cdac3a3d8..15b9ced5f 100644 --- a/app/soapbox/features/ui/index.js +++ b/app/soapbox/features/ui/index.js @@ -36,6 +36,7 @@ import { connectUserStream } from '../../actions/streaming'; import { Redirect } from 'react-router-dom'; import Icon from 'soapbox/components/icon'; import { isStaff } from 'soapbox/utils/accounts'; +import ChatList from 'soapbox/features/chats/components/chat_list'; import { Status, @@ -604,6 +605,7 @@ class UI extends React.PureComponent { {me && } + {me && } ); diff --git a/app/soapbox/reducers/chats.js b/app/soapbox/reducers/chats.js new file mode 100644 index 000000000..feb44c97a --- /dev/null +++ b/app/soapbox/reducers/chats.js @@ -0,0 +1,15 @@ +import { CHATS_FETCH_SUCCESS } from '../actions/chats'; +import { Map as ImmutableMap, fromJS } from 'immutable'; + +const initialState = ImmutableMap(); + +export default function admin(state = initialState, action) { + switch(action.type) { + case CHATS_FETCH_SUCCESS: + return state.merge(fromJS(action.data).reduce((acc, curr) => ( + acc.set(curr.get('id'), curr) + ), ImmutableMap())); + default: + return state; + } +}; diff --git a/app/soapbox/reducers/index.js b/app/soapbox/reducers/index.js index 7a8219522..90cd6af62 100644 --- a/app/soapbox/reducers/index.js +++ b/app/soapbox/reducers/index.js @@ -43,6 +43,7 @@ import instance from './instance'; import me from './me'; import auth from './auth'; import admin from './admin'; +import chats from './chats'; const reducers = { dropdown_menu, @@ -89,6 +90,7 @@ const reducers = { me, auth, admin, + chats, }; export default combineReducers(reducers); diff --git a/app/styles/application.scss b/app/styles/application.scss index 2b4c52a83..96eb4dcf4 100644 --- a/app/styles/application.scss +++ b/app/styles/application.scss @@ -28,6 +28,7 @@ @import 'demetricator'; @import 'pro'; @import 'overflow_hacks'; +@import 'chats'; // COMPONENTS @import 'components/buttons'; diff --git a/app/styles/chats.scss b/app/styles/chats.scss new file mode 100644 index 000000000..60d1355e2 --- /dev/null +++ b/app/styles/chats.scss @@ -0,0 +1,24 @@ +.chat-list { + position: fixed; + bottom: 0; + right: 20px; + width: 265px; + + &__header { + background: var(--brand-color); + color: #fff; + padding: 6px 10px; + font-size: 18px; + font-weight: bold; + border-radius: 6px 6px 0 0; + } + + &__content { + background: var(--foreground-color); + padding: 10px; + } + + &__actions { + background: var(--foreground-color); + } +} From e35e8f613f6fd9721cc59683214039dab65e1ec2 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 24 Aug 2020 21:32:39 -0500 Subject: [PATCH 03/80] Rudimentary ChatList display --- app/soapbox/features/chats/components/chat_list.js | 3 ++- app/styles/chats.scss | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/soapbox/features/chats/components/chat_list.js b/app/soapbox/features/chats/components/chat_list.js index e2446e07f..daac2a07c 100644 --- a/app/soapbox/features/chats/components/chat_list.js +++ b/app/soapbox/features/chats/components/chat_list.js @@ -4,6 +4,7 @@ import PropTypes from 'prop-types'; import { injectIntl, FormattedMessage } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { fetchChats } from 'soapbox/actions/chats'; +import Account from 'soapbox/components/account'; const mapStateToProps = state => ({ chats: state.get('chats'), @@ -33,7 +34,7 @@ class ChatList extends ImmutablePureComponent {
{chats.toList().map(chat => (
- {chat.getIn(['account', 'acct'])} +
))}
diff --git a/app/styles/chats.scss b/app/styles/chats.scss index 60d1355e2..7cd165b17 100644 --- a/app/styles/chats.scss +++ b/app/styles/chats.scss @@ -3,6 +3,7 @@ bottom: 0; right: 20px; width: 265px; + z-index: 99999; &__header { background: var(--brand-color); From 7693fb87cc0d316753109a2ad2d39f5e672c8a7e Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 25 Aug 2020 11:33:51 -0500 Subject: [PATCH 04/80] Scaffold chat click --- .../features/chats/components/chat_list.js | 11 +++++- .../chats/components/chat_list_account.js | 38 +++++++++++++++++++ app/styles/accounts.scss | 3 +- app/styles/basics.scss | 4 ++ app/styles/chats.scss | 1 - 5 files changed, 53 insertions(+), 4 deletions(-) create mode 100644 app/soapbox/features/chats/components/chat_list_account.js diff --git a/app/soapbox/features/chats/components/chat_list.js b/app/soapbox/features/chats/components/chat_list.js index daac2a07c..34808fa80 100644 --- a/app/soapbox/features/chats/components/chat_list.js +++ b/app/soapbox/features/chats/components/chat_list.js @@ -4,7 +4,7 @@ import PropTypes from 'prop-types'; import { injectIntl, FormattedMessage } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { fetchChats } from 'soapbox/actions/chats'; -import Account from 'soapbox/components/account'; +import ChatListAccount from './chat_list_account'; const mapStateToProps = state => ({ chats: state.get('chats'), @@ -23,6 +23,10 @@ class ChatList extends ImmutablePureComponent { this.props.dispatch(fetchChats()); } + handleClickChat = () => { + // TODO: Open or focus chat panel + } + render() { const { chats } = this.props; @@ -34,7 +38,10 @@ class ChatList extends ImmutablePureComponent {
{chats.toList().map(chat => (
- +
))}
diff --git a/app/soapbox/features/chats/components/chat_list_account.js b/app/soapbox/features/chats/components/chat_list_account.js new file mode 100644 index 000000000..9864e854e --- /dev/null +++ b/app/soapbox/features/chats/components/chat_list_account.js @@ -0,0 +1,38 @@ +import React from 'react'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import PropTypes from 'prop-types'; +import Avatar from '../../../components/avatar'; +import DisplayName from '../../../components/display_name'; +import ImmutablePureComponent from 'react-immutable-pure-component'; + +export default class ChatListAccount extends ImmutablePureComponent { + + static propTypes = { + account: ImmutablePropTypes.map.isRequired, + onClick: PropTypes.func, + }; + + handleClick = () => { + this.props.onClick(this.props.account); + } + + render() { + const { account } = this.props; + if (!account) return null; + + return ( +
+
+ ); + } + +} diff --git a/app/styles/accounts.scss b/app/styles/accounts.scss index 4b0980850..d9bbe2283 100644 --- a/app/styles/accounts.scss +++ b/app/styles/accounts.scss @@ -327,9 +327,10 @@ .account { padding: 10px; + position: relative; &:not(:last-of-type) { - border-bottom: 1px solid var(--brand-color--med); + border-bottom: 1px solid var(--brand-color--med); } &.compact { diff --git a/app/styles/basics.scss b/app/styles/basics.scss index 53119ffc4..88dad067e 100644 --- a/app/styles/basics.scss +++ b/app/styles/basics.scss @@ -228,4 +228,8 @@ noscript { left: 0; position: absolute; z-index: 9999; + background: transparent; + border: 0; + margin: 0; + padding: 0; } diff --git a/app/styles/chats.scss b/app/styles/chats.scss index 7cd165b17..ae95038a4 100644 --- a/app/styles/chats.scss +++ b/app/styles/chats.scss @@ -16,7 +16,6 @@ &__content { background: var(--foreground-color); - padding: 10px; } &__actions { From b98f06e3d354843e7c236be91a78eecf72b41479 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 25 Aug 2020 12:38:21 -0500 Subject: [PATCH 05/80] Chats: Import and normalize chats --- app/soapbox/actions/chats.js | 2 + app/soapbox/actions/importer/index.js | 40 ++++++++++++++++++- app/soapbox/actions/importer/normalizer.js | 8 ++++ .../features/chats/components/chat_list.js | 12 ++++-- app/soapbox/reducers/chats.js | 15 ++++--- app/soapbox/selectors/index.js | 17 ++++++++ 6 files changed, 84 insertions(+), 10 deletions(-) diff --git a/app/soapbox/actions/chats.js b/app/soapbox/actions/chats.js index 1f864ef80..0717a9fe6 100644 --- a/app/soapbox/actions/chats.js +++ b/app/soapbox/actions/chats.js @@ -1,4 +1,5 @@ import api from '../api'; +import { importFetchedChats } from 'soapbox/actions/importer'; export const CHATS_FETCH_REQUEST = 'CHATS_FETCH_REQUEST'; export const CHATS_FETCH_SUCCESS = 'CHATS_FETCH_SUCCESS'; @@ -8,6 +9,7 @@ export function fetchChats() { return (dispatch, getState) => { dispatch({ type: CHATS_FETCH_REQUEST }); return api(getState).get('/api/v1/pleroma/chats').then(({ data }) => { + dispatch(importFetchedChats(data)); dispatch({ type: CHATS_FETCH_SUCCESS, data }); }).catch(error => { dispatch({ type: CHATS_FETCH_FAIL, error }); diff --git a/app/soapbox/actions/importer/index.js b/app/soapbox/actions/importer/index.js index aaf603608..a9c7cd9f6 100644 --- a/app/soapbox/actions/importer/index.js +++ b/app/soapbox/actions/importer/index.js @@ -1,5 +1,10 @@ import { getSettings } from '../settings'; -import { normalizeAccount, normalizeStatus, normalizePoll } from './normalizer'; +import { + normalizeAccount, + normalizeStatus, + normalizePoll, + normalizeChat, +} from './normalizer'; export const ACCOUNT_IMPORT = 'ACCOUNT_IMPORT'; export const ACCOUNTS_IMPORT = 'ACCOUNTS_IMPORT'; @@ -7,6 +12,8 @@ export const STATUS_IMPORT = 'STATUS_IMPORT'; export const STATUSES_IMPORT = 'STATUSES_IMPORT'; export const POLLS_IMPORT = 'POLLS_IMPORT'; export const ACCOUNT_FETCH_FAIL_FOR_USERNAME_LOOKUP = 'ACCOUNT_FETCH_FAIL_FOR_USERNAME_LOOKUP'; +export const CHAT_IMPORT = 'CHAT_IMPORT'; +export const CHATS_IMPORT = 'CHATS_IMPORT'; function pushUnique(array, object) { if (array.every(element => element.id !== object.id)) { @@ -34,6 +41,14 @@ export function importPolls(polls) { return { type: POLLS_IMPORT, polls }; } +export function importChat(chat) { + return { type: CHAT_IMPORT, chat }; +} + +export function importChats(chats) { + return { type: CHATS_IMPORT, chats }; +} + export function importFetchedAccount(account) { return importFetchedAccounts([account]); } @@ -97,3 +112,26 @@ export function importFetchedPoll(poll) { export function importErrorWhileFetchingAccountByUsername(username) { return { type: ACCOUNT_FETCH_FAIL_FOR_USERNAME_LOOKUP, username }; }; + +export function importFetchedChat(chat) { + return importFetchedChats([chat]); +} + +export function importFetchedChats(chats) { + return (dispatch, getState) => { + const accounts = []; + const normalChats = []; + + function processChat(chat) { + const normalOldChat = getState().getIn(['chats', chat.id]); + + pushUnique(normalChats, normalizeChat(chat, normalOldChat)); + pushUnique(accounts, chat.account); + } + + chats.forEach(processChat); + + dispatch(importFetchedAccounts(accounts)); + dispatch(importChats(normalChats)); + }; +} diff --git a/app/soapbox/actions/importer/normalizer.js b/app/soapbox/actions/importer/normalizer.js index 0edac3e5c..697e72107 100644 --- a/app/soapbox/actions/importer/normalizer.js +++ b/app/soapbox/actions/importer/normalizer.js @@ -80,3 +80,11 @@ export function normalizePoll(poll) { return normalPoll; } + +export function normalizeChat(chat, normalOldChat) { + const normalChat = { ...chat }; + + normalChat.account = chat.account.id; + + return normalChat; +} diff --git a/app/soapbox/features/chats/components/chat_list.js b/app/soapbox/features/chats/components/chat_list.js index 34808fa80..fa741f96e 100644 --- a/app/soapbox/features/chats/components/chat_list.js +++ b/app/soapbox/features/chats/components/chat_list.js @@ -5,10 +5,14 @@ import { injectIntl, FormattedMessage } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { fetchChats } from 'soapbox/actions/chats'; import ChatListAccount from './chat_list_account'; +import { makeGetChat } from 'soapbox/selectors'; -const mapStateToProps = state => ({ - chats: state.get('chats'), -}); +const mapStateToProps = state => { + const getChat = makeGetChat(); + return { + chats: state.get('chats').map(chat => getChat(state, chat.toJS())), + }; +}; export default @connect(mapStateToProps) @injectIntl @@ -37,7 +41,7 @@ class ChatList extends ImmutablePureComponent {
{chats.toList().map(chat => ( -
+
state.set(chat.id, fromJS(chat)); + +const importChats = (state, chats) => + state.withMutations(mutable => chats.forEach(chat => importChat(mutable, chat))); + const initialState = ImmutableMap(); export default function admin(state = initialState, action) { switch(action.type) { - case CHATS_FETCH_SUCCESS: - return state.merge(fromJS(action.data).reduce((acc, curr) => ( - acc.set(curr.get('id'), curr) - ), ImmutableMap())); + case CHAT_IMPORT: + return importChat(state, action.chat); + case CHATS_IMPORT: + return importChats(state, action.chats); default: return state; } diff --git a/app/soapbox/selectors/index.js b/app/soapbox/selectors/index.js index d1f84330a..55090df10 100644 --- a/app/soapbox/selectors/index.js +++ b/app/soapbox/selectors/index.js @@ -157,3 +157,20 @@ export const getAccountGallery = createSelector([ .map(media => media.merge({ status, account }))); }, ImmutableList()); }); + +export const makeGetChat = () => { + return createSelector( + [ + (state, { id }) => state.getIn(['chats', id]), + (state, { id }) => state.getIn(['accounts', state.getIn(['chats', id, 'account'])]), + ], + + (chat, account) => { + if (!chat) return null; + + return chat.withMutations(map => { + map.set('account', account); + }); + } + ); +}; From d6b3268da4ed9db0091a2d6c32853d5d088404cf Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 25 Aug 2020 14:58:35 -0500 Subject: [PATCH 06/80] Render chats in panes layout --- app/soapbox/actions/settings.js | 7 +- .../features/chats/components/chat_list.js | 3 - .../features/chats/components/chat_panes.js | 66 +++++++++++++++++++ app/soapbox/features/ui/index.js | 4 +- app/styles/chats.scss | 2 +- 5 files changed, 75 insertions(+), 7 deletions(-) create mode 100644 app/soapbox/features/chats/components/chat_panes.js diff --git a/app/soapbox/actions/settings.js b/app/soapbox/actions/settings.js index 677556dee..4432fa6e0 100644 --- a/app/soapbox/actions/settings.js +++ b/app/soapbox/actions/settings.js @@ -1,7 +1,7 @@ import { debounce } from 'lodash'; import { showAlertForError } from './alerts'; import { patchMe } from 'soapbox/actions/me'; -import { Map as ImmutableMap } from 'immutable'; +import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; export const SETTING_CHANGE = 'SETTING_CHANGE'; export const SETTING_SAVE = 'SETTING_SAVE'; @@ -29,6 +29,11 @@ const defaultSettings = ImmutableMap({ dyslexicFont: false, demetricator: false, + chats: ImmutableMap({ + panes: ImmutableList(), + mainWindow: 'minimized', + }), + home: ImmutableMap({ shows: ImmutableMap({ reblog: true, diff --git a/app/soapbox/features/chats/components/chat_list.js b/app/soapbox/features/chats/components/chat_list.js index fa741f96e..a11617d19 100644 --- a/app/soapbox/features/chats/components/chat_list.js +++ b/app/soapbox/features/chats/components/chat_list.js @@ -36,9 +36,6 @@ class ChatList extends ImmutablePureComponent { return (
-
- -
{chats.toList().map(chat => (
diff --git a/app/soapbox/features/chats/components/chat_panes.js b/app/soapbox/features/chats/components/chat_panes.js new file mode 100644 index 000000000..cf1515152 --- /dev/null +++ b/app/soapbox/features/chats/components/chat_panes.js @@ -0,0 +1,66 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { injectIntl } from 'react-intl'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import { getSettings } from 'soapbox/actions/settings'; +import ChatList from './chat_list'; +import { FormattedMessage } from 'react-intl'; + +const mapStateToProps = state => ({ + panesData: getSettings(state).get('chats'), +}); + +export default @connect(mapStateToProps) +@injectIntl +class ChatPanes extends ImmutablePureComponent { + + static propTypes = { + dispatch: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, + panesData: ImmutablePropTypes.map, + } + + renderChatPane = (pane, i) => { + // const chat = getChat(pane.get('chat_id')) + return ( +
+
+ // {chat.getIn(['account', 'acct'])} +
+
+ // TODO: Show the chat messages +
+ +
+
+
+ ); + } + + renderChatPanes = (panes) => ( + panes.map((pane, i) => + this.renderChatPane(pane, i) + ) + ) + + render() { + const panes = this.props.panesData.get('panes'); + + return ( +
+
+
+ +
+
+ +
+
+ {this.renderChatPanes(panes)} +
+ ); + } + +} diff --git a/app/soapbox/features/ui/index.js b/app/soapbox/features/ui/index.js index 15b9ced5f..266d6f4e2 100644 --- a/app/soapbox/features/ui/index.js +++ b/app/soapbox/features/ui/index.js @@ -36,7 +36,7 @@ import { connectUserStream } from '../../actions/streaming'; import { Redirect } from 'react-router-dom'; import Icon from 'soapbox/components/icon'; import { isStaff } from 'soapbox/utils/accounts'; -import ChatList from 'soapbox/features/chats/components/chat_list'; +import ChatPanes from 'soapbox/features/chats/components/chat_panes'; import { Status, @@ -605,7 +605,7 @@ class UI extends React.PureComponent { {me && } - {me && } + {me && }
); diff --git a/app/styles/chats.scss b/app/styles/chats.scss index ae95038a4..8994bfe27 100644 --- a/app/styles/chats.scss +++ b/app/styles/chats.scss @@ -1,4 +1,4 @@ -.chat-list { +.pane { position: fixed; bottom: 0; right: 20px; From 6e0bac3d43f5727e98372cc57c5a2a102c0093da Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 25 Aug 2020 16:00:27 -0500 Subject: [PATCH 07/80] Chats: Get chats from paneData --- .../features/chats/components/chat_list.js | 2 +- .../features/chats/components/chat_panes.js | 35 ++++++++++++++++--- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/app/soapbox/features/chats/components/chat_list.js b/app/soapbox/features/chats/components/chat_list.js index a11617d19..5ba86ad18 100644 --- a/app/soapbox/features/chats/components/chat_list.js +++ b/app/soapbox/features/chats/components/chat_list.js @@ -1,7 +1,7 @@ import React from 'react'; import { connect } from 'react-redux'; import PropTypes from 'prop-types'; -import { injectIntl, FormattedMessage } from 'react-intl'; +import { injectIntl } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { fetchChats } from 'soapbox/actions/chats'; import ChatListAccount from './chat_list_account'; diff --git a/app/soapbox/features/chats/components/chat_panes.js b/app/soapbox/features/chats/components/chat_panes.js index cf1515152..a6b8bd36b 100644 --- a/app/soapbox/features/chats/components/chat_panes.js +++ b/app/soapbox/features/chats/components/chat_panes.js @@ -7,10 +7,33 @@ import ImmutablePureComponent from 'react-immutable-pure-component'; import { getSettings } from 'soapbox/actions/settings'; import ChatList from './chat_list'; import { FormattedMessage } from 'react-intl'; +import { makeGetChat } from 'soapbox/selectors'; +// import { fromJS } from 'immutable'; -const mapStateToProps = state => ({ - panesData: getSettings(state).get('chats'), -}); +const addChatsToPanes = (state, panesData) => { + const getChat = makeGetChat(); + + const newPanes = panesData.get('panes').map(pane => { + const chat = getChat(state, { id: pane.get('chat_id') }); + return pane.set('chat', chat); + }); + + return panesData.set('panes', newPanes); +}; + +const mapStateToProps = state => { + const panesData = getSettings(state).get('chats'); + + // const panesData = fromJS({ + // panes: [ + // { chat_id: '9ySohyWw0Gecd3WHKK', state: 'open' }, + // ], + // }); + + return { + panesData: addChatsToPanes(state, panesData), + }; +}; export default @connect(mapStateToProps) @injectIntl @@ -23,11 +46,13 @@ class ChatPanes extends ImmutablePureComponent { } renderChatPane = (pane, i) => { - // const chat = getChat(pane.get('chat_id')) + const chat = pane.get('chat'); + if (!chat) return null; + return (
- // {chat.getIn(['account', 'acct'])} + {chat.getIn(['account', 'acct'])}
// TODO: Show the chat messages From 0d7a926fa57810e51d14e62ecb529ab3f3a4a8d6 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 25 Aug 2020 17:07:07 -0500 Subject: [PATCH 08/80] Chats: Style the chat boxes --- .../features/chats/components/chat_panes.js | 34 +++++++++------- app/styles/chats.scss | 39 ++++++++++++++++++- 2 files changed, 59 insertions(+), 14 deletions(-) diff --git a/app/soapbox/features/chats/components/chat_panes.js b/app/soapbox/features/chats/components/chat_panes.js index a6b8bd36b..d99a6b371 100644 --- a/app/soapbox/features/chats/components/chat_panes.js +++ b/app/soapbox/features/chats/components/chat_panes.js @@ -4,11 +4,13 @@ import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import { injectIntl } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import { getSettings } from 'soapbox/actions/settings'; +// import { getSettings } from 'soapbox/actions/settings'; import ChatList from './chat_list'; import { FormattedMessage } from 'react-intl'; import { makeGetChat } from 'soapbox/selectors'; -// import { fromJS } from 'immutable'; +import { fromJS } from 'immutable'; +import Avatar from 'soapbox/components/avatar'; +import { acctFull } from 'soapbox/utils/accounts'; const addChatsToPanes = (state, panesData) => { const getChat = makeGetChat(); @@ -22,13 +24,15 @@ const addChatsToPanes = (state, panesData) => { }; const mapStateToProps = state => { - const panesData = getSettings(state).get('chats'); + // const panesData = getSettings(state).get('chats'); - // const panesData = fromJS({ - // panes: [ - // { chat_id: '9ySohyWw0Gecd3WHKK', state: 'open' }, - // ], - // }); + const panesData = fromJS({ + panes: [ + { chat_id: '9ySoi8J7eTY0x78OBc', state: 'open' }, + { chat_id: '9ySoi8J7eTY0x78OBc', state: 'open' }, + { chat_id: '9ySoi8J7eTY0x78OBc', state: 'open' }, + ], + }); return { panesData: addChatsToPanes(state, panesData), @@ -47,17 +51,21 @@ class ChatPanes extends ImmutablePureComponent { renderChatPane = (pane, i) => { const chat = pane.get('chat'); - if (!chat) return null; + const account = pane.getIn(['chat', 'account']); + if (!chat || !account) return null; + + const right = (285 * (i + 1)) + 20; return ( -
+
- {chat.getIn(['account', 'acct'])} + +
@{acctFull(account)}
- // TODO: Show the chat messages +
TODO: Show the chat messages
- +
diff --git a/app/styles/chats.scss b/app/styles/chats.scss index 8994bfe27..8cfe06532 100644 --- a/app/styles/chats.scss +++ b/app/styles/chats.scss @@ -1,24 +1,61 @@ .pane { + @include standard-panel-shadow; position: fixed; bottom: 0; right: 20px; width: 265px; + height: 265px; + max-height: calc(100vh - 70px); + display: flex; + flex-direction: column; z-index: 99999; + &--main { + height: calc(100vh - 70px); + } + &__header { background: var(--brand-color); color: #fff; padding: 6px 10px; - font-size: 18px; + font-size: 16px; font-weight: bold; border-radius: 6px 6px 0 0; + display: flex; + align-items: center; + + .account__avatar { + margin-right: 7px; + } + + .display-name__account { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } } &__content { + height: 100%; background: var(--foreground-color); + display: flex; + flex-direction: column; } &__actions { background: var(--foreground-color); + margin-top: auto; + + input { + width: 100%; + margin: 0; + box-sizing: border-box; + padding: 6px; + background: var(--background-color); + border: 6px solid var(--foreground-color); + border-radius: 10px; + color: var(--primary-text-color); + font-size: 16px; + } } } From c84ca30197cd02893dd98caa608e9fc74ae3bff2 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 25 Aug 2020 17:24:47 -0500 Subject: [PATCH 09/80] Chats: Click chat to open pane --- .../features/chats/components/chat_list.js | 9 +++----- .../chats/components/chat_list_account.js | 9 ++++---- .../features/chats/components/chat_panes.js | 21 +++++++++---------- 3 files changed, 18 insertions(+), 21 deletions(-) diff --git a/app/soapbox/features/chats/components/chat_list.js b/app/soapbox/features/chats/components/chat_list.js index 5ba86ad18..ca5349b20 100644 --- a/app/soapbox/features/chats/components/chat_list.js +++ b/app/soapbox/features/chats/components/chat_list.js @@ -21,16 +21,13 @@ class ChatList extends ImmutablePureComponent { static propTypes = { dispatch: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, + onClickChat: PropTypes.func, }; componentDidMount() { this.props.dispatch(fetchChats()); } - handleClickChat = () => { - // TODO: Open or focus chat panel - } - render() { const { chats } = this.props; @@ -40,8 +37,8 @@ class ChatList extends ImmutablePureComponent { {chats.toList().map(chat => (
))} diff --git a/app/soapbox/features/chats/components/chat_list_account.js b/app/soapbox/features/chats/components/chat_list_account.js index 9864e854e..48a813fa8 100644 --- a/app/soapbox/features/chats/components/chat_list_account.js +++ b/app/soapbox/features/chats/components/chat_list_account.js @@ -8,17 +8,18 @@ import ImmutablePureComponent from 'react-immutable-pure-component'; export default class ChatListAccount extends ImmutablePureComponent { static propTypes = { - account: ImmutablePropTypes.map.isRequired, + chat: ImmutablePropTypes.map.isRequired, onClick: PropTypes.func, }; handleClick = () => { - this.props.onClick(this.props.account); + this.props.onClick(this.props.chat); } render() { - const { account } = this.props; - if (!account) return null; + const { chat } = this.props; + if (!chat) return null; + const account = chat.get('account'); return (
diff --git a/app/soapbox/features/chats/components/chat_panes.js b/app/soapbox/features/chats/components/chat_panes.js index d99a6b371..24509002c 100644 --- a/app/soapbox/features/chats/components/chat_panes.js +++ b/app/soapbox/features/chats/components/chat_panes.js @@ -4,7 +4,7 @@ import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import { injectIntl } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; -// import { getSettings } from 'soapbox/actions/settings'; +import { getSettings, changeSetting } from 'soapbox/actions/settings'; import ChatList from './chat_list'; import { FormattedMessage } from 'react-intl'; import { makeGetChat } from 'soapbox/selectors'; @@ -24,15 +24,7 @@ const addChatsToPanes = (state, panesData) => { }; const mapStateToProps = state => { - // const panesData = getSettings(state).get('chats'); - - const panesData = fromJS({ - panes: [ - { chat_id: '9ySoi8J7eTY0x78OBc', state: 'open' }, - { chat_id: '9ySoi8J7eTY0x78OBc', state: 'open' }, - { chat_id: '9ySoi8J7eTY0x78OBc', state: 'open' }, - ], - }); + const panesData = getSettings(state).get('chats'); return { panesData: addChatsToPanes(state, panesData), @@ -49,6 +41,13 @@ class ChatPanes extends ImmutablePureComponent { panesData: ImmutablePropTypes.map, } + handleClickChat = (chat) => { + // TODO: Refactor + this.props.dispatch(changeSetting(['chats', 'panes'], fromJS([ + { chat_id: chat.get('id'), state: 'open' }, + ]))); + } + renderChatPane = (pane, i) => { const chat = pane.get('chat'); const account = pane.getIn(['chat', 'account']); @@ -88,7 +87,7 @@ class ChatPanes extends ImmutablePureComponent {
- +
{this.renderChatPanes(panes)} From f87f33fb94419712f31a2bd91ecc7d892548f2b5 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 25 Aug 2020 17:54:10 -0500 Subject: [PATCH 10/80] Chats: Click chat to open pane --- app/soapbox/actions/chats.js | 16 ++++++++++++++++ .../features/chats/components/chat_panes.js | 10 ++++------ 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/app/soapbox/actions/chats.js b/app/soapbox/actions/chats.js index 0717a9fe6..49ab6c782 100644 --- a/app/soapbox/actions/chats.js +++ b/app/soapbox/actions/chats.js @@ -1,5 +1,7 @@ import api from '../api'; import { importFetchedChats } from 'soapbox/actions/importer'; +import { getSettings, changeSetting } from 'soapbox/actions/settings'; +import { Map as ImmutableMap } from 'immutable'; export const CHATS_FETCH_REQUEST = 'CHATS_FETCH_REQUEST'; export const CHATS_FETCH_SUCCESS = 'CHATS_FETCH_SUCCESS'; @@ -16,3 +18,17 @@ export function fetchChats() { }); }; } + +export function openChat(chatId) { + return (dispatch, getState) => { + const panes = getSettings(getState()).getIn(['chats', 'panes']); + const idx = panes.findIndex(pane => pane.get('chat_id') === chatId); + + if (idx > -1) { + return dispatch(changeSetting(['chats', 'panes', idx, 'state'], 'open')); + } else { + const newPane = ImmutableMap({ chat_id: chatId, state: 'open' }); + return dispatch(changeSetting(['chats', 'panes'], panes.push(newPane))); + } + }; +} diff --git a/app/soapbox/features/chats/components/chat_panes.js b/app/soapbox/features/chats/components/chat_panes.js index 24509002c..43cb1b442 100644 --- a/app/soapbox/features/chats/components/chat_panes.js +++ b/app/soapbox/features/chats/components/chat_panes.js @@ -4,13 +4,13 @@ import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import { injectIntl } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import { getSettings, changeSetting } from 'soapbox/actions/settings'; +import { getSettings } from 'soapbox/actions/settings'; import ChatList from './chat_list'; import { FormattedMessage } from 'react-intl'; import { makeGetChat } from 'soapbox/selectors'; -import { fromJS } from 'immutable'; import Avatar from 'soapbox/components/avatar'; import { acctFull } from 'soapbox/utils/accounts'; +import { openChat } from 'soapbox/actions/chats'; const addChatsToPanes = (state, panesData) => { const getChat = makeGetChat(); @@ -42,10 +42,8 @@ class ChatPanes extends ImmutablePureComponent { } handleClickChat = (chat) => { - // TODO: Refactor - this.props.dispatch(changeSetting(['chats', 'panes'], fromJS([ - { chat_id: chat.get('id'), state: 'open' }, - ]))); + this.props.dispatch(openChat(chat.get('id'))); + // TODO: Focus chat input } renderChatPane = (pane, i) => { From 072aed02da128cdea8c704f8d63f263b1c7dd99a Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 25 Aug 2020 18:11:48 -0500 Subject: [PATCH 11/80] Chats: Allow closing a chat --- app/soapbox/actions/chats.js | 13 +++++++++++++ .../features/chats/components/chat_panes.js | 12 +++++++++++- app/styles/chats.scss | 14 ++++++++++++++ 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/app/soapbox/actions/chats.js b/app/soapbox/actions/chats.js index 49ab6c782..d6ecfa11d 100644 --- a/app/soapbox/actions/chats.js +++ b/app/soapbox/actions/chats.js @@ -32,3 +32,16 @@ export function openChat(chatId) { } }; } + +export function closeChat(chatId) { + return (dispatch, getState) => { + const panes = getSettings(getState()).getIn(['chats', 'panes']); + const idx = panes.findIndex(pane => pane.get('chat_id') === chatId); + + if (idx > -1) { + return dispatch(changeSetting(['chats', 'panes'], panes.delete(idx))); + } else { + return false; + } + }; +} diff --git a/app/soapbox/features/chats/components/chat_panes.js b/app/soapbox/features/chats/components/chat_panes.js index 43cb1b442..8c4dd2b90 100644 --- a/app/soapbox/features/chats/components/chat_panes.js +++ b/app/soapbox/features/chats/components/chat_panes.js @@ -10,7 +10,8 @@ import { FormattedMessage } from 'react-intl'; import { makeGetChat } from 'soapbox/selectors'; import Avatar from 'soapbox/components/avatar'; import { acctFull } from 'soapbox/utils/accounts'; -import { openChat } from 'soapbox/actions/chats'; +import { openChat, closeChat } from 'soapbox/actions/chats'; +import IconButton from 'soapbox/components/icon_button'; const addChatsToPanes = (state, panesData) => { const getChat = makeGetChat(); @@ -46,6 +47,12 @@ class ChatPanes extends ImmutablePureComponent { // TODO: Focus chat input } + handleChatClose = (chatId) => { + return (e) => { + this.props.dispatch(closeChat(chatId)); + }; + } + renderChatPane = (pane, i) => { const chat = pane.get('chat'); const account = pane.getIn(['chat', 'account']); @@ -58,6 +65,9 @@ class ChatPanes extends ImmutablePureComponent {
@{acctFull(account)}
+
+ +
TODO: Show the chat messages
diff --git a/app/styles/chats.scss b/app/styles/chats.scss index 8cfe06532..a45cb9e30 100644 --- a/app/styles/chats.scss +++ b/app/styles/chats.scss @@ -33,6 +33,20 @@ overflow: hidden; text-overflow: ellipsis; } + + .icon-button { + color: #fff; + + > div { + height: auto !important; + width: auto !important; + margin-right: -6px; + } + } + + .pane__close { + margin-left: auto; + } } &__content { From 1c6c9f0f5d2cdcd2f95b0ced2199d5feb7c7992e Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 25 Aug 2020 18:45:05 -0500 Subject: [PATCH 12/80] Chats: Toggle pane open and minimized --- app/soapbox/actions/chats.js | 14 ++++++++++++++ .../features/chats/components/chat_panes.js | 16 +++++++++++++--- app/styles/chats.scss | 6 ++++++ 3 files changed, 33 insertions(+), 3 deletions(-) diff --git a/app/soapbox/actions/chats.js b/app/soapbox/actions/chats.js index d6ecfa11d..732e8f34f 100644 --- a/app/soapbox/actions/chats.js +++ b/app/soapbox/actions/chats.js @@ -45,3 +45,17 @@ export function closeChat(chatId) { } }; } + +export function toggleChat(chatId) { + return (dispatch, getState) => { + const panes = getSettings(getState()).getIn(['chats', 'panes']); + const [idx, pane] = panes.findEntry(pane => pane.get('chat_id') === chatId); + + if (idx > -1) { + const state = pane.get('state') === 'minimized' ? 'open' : 'minimized'; + return dispatch(changeSetting(['chats', 'panes', idx, 'state'], state)); + } else { + return false; + } + }; +} diff --git a/app/soapbox/features/chats/components/chat_panes.js b/app/soapbox/features/chats/components/chat_panes.js index 8c4dd2b90..d93de925f 100644 --- a/app/soapbox/features/chats/components/chat_panes.js +++ b/app/soapbox/features/chats/components/chat_panes.js @@ -10,7 +10,7 @@ import { FormattedMessage } from 'react-intl'; import { makeGetChat } from 'soapbox/selectors'; import Avatar from 'soapbox/components/avatar'; import { acctFull } from 'soapbox/utils/accounts'; -import { openChat, closeChat } from 'soapbox/actions/chats'; +import { openChat, closeChat, toggleChat } from 'soapbox/actions/chats'; import IconButton from 'soapbox/components/icon_button'; const addChatsToPanes = (state, panesData) => { @@ -53,6 +53,12 @@ class ChatPanes extends ImmutablePureComponent { }; } + handleChatToggle = (chatId) => { + return (e) => { + this.props.dispatch(toggleChat(chatId)); + }; + } + renderChatPane = (pane, i) => { const chat = pane.get('chat'); const account = pane.getIn(['chat', 'account']); @@ -61,10 +67,14 @@ class ChatPanes extends ImmutablePureComponent { const right = (285 * (i + 1)) + 20; return ( -
+
-
@{acctFull(account)}
+
diff --git a/app/styles/chats.scss b/app/styles/chats.scss index a45cb9e30..cae548683 100644 --- a/app/styles/chats.scss +++ b/app/styles/chats.scss @@ -14,7 +14,12 @@ height: calc(100vh - 70px); } + &--minimized { + top: calc(100% - 31px); + } + &__header { + box-sizing: border-box; background: var(--brand-color); color: #fff; padding: 6px 10px; @@ -23,6 +28,7 @@ border-radius: 6px 6px 0 0; display: flex; align-items: center; + height: 31px; .account__avatar { margin-right: 7px; From 0736e6d46ce4769947af2523db79cd2c1bbc9416 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 25 Aug 2020 18:53:36 -0500 Subject: [PATCH 13/80] Chats: Let main window be minimized --- app/soapbox/actions/chats.js | 8 +++++++ .../features/chats/components/chat_panes.js | 21 +++++++++++++++---- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/app/soapbox/actions/chats.js b/app/soapbox/actions/chats.js index 732e8f34f..419608e2c 100644 --- a/app/soapbox/actions/chats.js +++ b/app/soapbox/actions/chats.js @@ -59,3 +59,11 @@ export function toggleChat(chatId) { } }; } + +export function toggleMainWindow() { + return (dispatch, getState) => { + const main = getSettings(getState()).getIn(['chats', 'mainWindow']); + const state = main === 'minimized' ? 'open' : 'minimized'; + return dispatch(changeSetting(['chats', 'mainWindow'], state)); + }; +} diff --git a/app/soapbox/features/chats/components/chat_panes.js b/app/soapbox/features/chats/components/chat_panes.js index d93de925f..5786f768f 100644 --- a/app/soapbox/features/chats/components/chat_panes.js +++ b/app/soapbox/features/chats/components/chat_panes.js @@ -10,7 +10,12 @@ import { FormattedMessage } from 'react-intl'; import { makeGetChat } from 'soapbox/selectors'; import Avatar from 'soapbox/components/avatar'; import { acctFull } from 'soapbox/utils/accounts'; -import { openChat, closeChat, toggleChat } from 'soapbox/actions/chats'; +import { + openChat, + closeChat, + toggleChat, + toggleMainWindow, +} from 'soapbox/actions/chats'; import IconButton from 'soapbox/components/icon_button'; const addChatsToPanes = (state, panesData) => { @@ -59,6 +64,10 @@ class ChatPanes extends ImmutablePureComponent { }; } + handleMainWindowToggle = () => { + this.props.dispatch(toggleMainWindow()); + } + renderChatPane = (pane, i) => { const chat = pane.get('chat'); const account = pane.getIn(['chat', 'account']); @@ -96,13 +105,17 @@ class ChatPanes extends ImmutablePureComponent { ) render() { - const panes = this.props.panesData.get('panes'); + const { panesData } = this.props; + const panes = panesData.get('panes'); + const mainWindow = panesData.get('mainWindow'); return (
-
+
- + + +
From c2c3fefbaa35a170af7a34616899e6a8991490db Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 25 Aug 2020 18:58:40 -0500 Subject: [PATCH 14/80] Chats: Improve minimize animation --- app/styles/chats.scss | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/styles/chats.scss b/app/styles/chats.scss index cae548683..4949e3af4 100644 --- a/app/styles/chats.scss +++ b/app/styles/chats.scss @@ -9,13 +9,14 @@ display: flex; flex-direction: column; z-index: 99999; + transition: 0.2s; &--main { height: calc(100vh - 70px); } &--minimized { - top: calc(100% - 31px); + height: 31px; } &__header { From a2cd0b7630fecddcc9b2d4318c29614d3119638b Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 25 Aug 2020 19:13:54 -0500 Subject: [PATCH 15/80] Chats: improve toggle surface area --- .../features/chats/components/chat_panes.js | 12 +++++------- app/styles/chats.scss | 18 +++++++++++++++--- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/app/soapbox/features/chats/components/chat_panes.js b/app/soapbox/features/chats/components/chat_panes.js index 5786f768f..58ab6d404 100644 --- a/app/soapbox/features/chats/components/chat_panes.js +++ b/app/soapbox/features/chats/components/chat_panes.js @@ -79,11 +79,9 @@ class ChatPanes extends ImmutablePureComponent {
- +
@@ -113,9 +111,9 @@ class ChatPanes extends ImmutablePureComponent {
diff --git a/app/styles/chats.scss b/app/styles/chats.scss index 4949e3af4..46cdc8df6 100644 --- a/app/styles/chats.scss +++ b/app/styles/chats.scss @@ -13,6 +13,10 @@ &--main { height: calc(100vh - 70px); + + .pane__header { + font-size: 16px; + } } &--minimized { @@ -23,8 +27,7 @@ box-sizing: border-box; background: var(--brand-color); color: #fff; - padding: 6px 10px; - font-size: 16px; + padding: 0 10px; font-weight: bold; border-radius: 6px 6px 0 0; display: flex; @@ -35,10 +38,19 @@ margin-right: 7px; } - .display-name__account { + .pane__title { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + width: 100%; + height: 100%; + background: transparent; + border: 0; + padding: 0; + color: #fff; + font-weight: bold; + text-align: left; + font-size: 14px; } .icon-button { From 55189595317b3e10b04051679f85e19bdc7effdc Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 25 Aug 2020 19:19:55 -0500 Subject: [PATCH 16/80] Chats: improve input box CSS --- app/styles/chats.scss | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/styles/chats.scss b/app/styles/chats.scss index 46cdc8df6..ec62ba6b5 100644 --- a/app/styles/chats.scss +++ b/app/styles/chats.scss @@ -78,6 +78,7 @@ &__actions { background: var(--foreground-color); margin-top: auto; + padding: 6px; input { width: 100%; @@ -85,8 +86,8 @@ box-sizing: border-box; padding: 6px; background: var(--background-color); - border: 6px solid var(--foreground-color); - border-radius: 10px; + border: 0; + border-radius: 6px; color: var(--primary-text-color); font-size: 16px; } From cab490e1d336012337c7a454fe2a2f7fe7ac2069 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 25 Aug 2020 20:33:49 -0500 Subject: [PATCH 17/80] Chats: display chat message content --- app/soapbox/actions/chats.js | 15 ++++ .../features/chats/components/chat_panes.js | 61 ++------------ .../features/chats/components/chat_window.js | 84 +++++++++++++++++++ app/soapbox/reducers/chat_messages.js | 13 +++ app/soapbox/reducers/chats.js | 2 +- app/soapbox/reducers/index.js | 2 + 6 files changed, 120 insertions(+), 57 deletions(-) create mode 100644 app/soapbox/features/chats/components/chat_window.js create mode 100644 app/soapbox/reducers/chat_messages.js diff --git a/app/soapbox/actions/chats.js b/app/soapbox/actions/chats.js index 419608e2c..35543ca8c 100644 --- a/app/soapbox/actions/chats.js +++ b/app/soapbox/actions/chats.js @@ -7,6 +7,10 @@ export const CHATS_FETCH_REQUEST = 'CHATS_FETCH_REQUEST'; export const CHATS_FETCH_SUCCESS = 'CHATS_FETCH_SUCCESS'; export const CHATS_FETCH_FAIL = 'CHATS_FETCH_FAIL'; +export const CHAT_MESSAGES_FETCH_REQUEST = 'CHAT_MESSAGES_FETCH_REQUEST'; +export const CHAT_MESSAGES_FETCH_SUCCESS = 'CHAT_MESSAGES_FETCH_SUCCESS'; +export const CHAT_MESSAGES_FETCH_FAIL = 'CHAT_MESSAGES_FETCH_FAIL'; + export function fetchChats() { return (dispatch, getState) => { dispatch({ type: CHATS_FETCH_REQUEST }); @@ -19,6 +23,17 @@ export function fetchChats() { }; } +export function fetchChatMessages(chatId) { + return (dispatch, getState) => { + dispatch({ type: CHAT_MESSAGES_FETCH_REQUEST, chatId }); + return api(getState).get(`/api/v1/pleroma/chats/${chatId}/messages`).then(({ data }) => { + dispatch({ type: CHAT_MESSAGES_FETCH_SUCCESS, chatId, data }); + }).catch(error => { + dispatch({ type: CHAT_MESSAGES_FETCH_FAIL, chatId, error }); + }); + }; +} + export function openChat(chatId) { return (dispatch, getState) => { const panes = getSettings(getState()).getIn(['chats', 'panes']); diff --git a/app/soapbox/features/chats/components/chat_panes.js b/app/soapbox/features/chats/components/chat_panes.js index 58ab6d404..8a9e28a04 100644 --- a/app/soapbox/features/chats/components/chat_panes.js +++ b/app/soapbox/features/chats/components/chat_panes.js @@ -8,15 +8,8 @@ import { getSettings } from 'soapbox/actions/settings'; import ChatList from './chat_list'; import { FormattedMessage } from 'react-intl'; import { makeGetChat } from 'soapbox/selectors'; -import Avatar from 'soapbox/components/avatar'; -import { acctFull } from 'soapbox/utils/accounts'; -import { - openChat, - closeChat, - toggleChat, - toggleMainWindow, -} from 'soapbox/actions/chats'; -import IconButton from 'soapbox/components/icon_button'; +import { openChat, toggleMainWindow } from 'soapbox/actions/chats'; +import ChatWindow from './chat_window'; const addChatsToPanes = (state, panesData) => { const getChat = makeGetChat(); @@ -52,56 +45,10 @@ class ChatPanes extends ImmutablePureComponent { // TODO: Focus chat input } - handleChatClose = (chatId) => { - return (e) => { - this.props.dispatch(closeChat(chatId)); - }; - } - - handleChatToggle = (chatId) => { - return (e) => { - this.props.dispatch(toggleChat(chatId)); - }; - } - handleMainWindowToggle = () => { this.props.dispatch(toggleMainWindow()); } - renderChatPane = (pane, i) => { - const chat = pane.get('chat'); - const account = pane.getIn(['chat', 'account']); - if (!chat || !account) return null; - - const right = (285 * (i + 1)) + 20; - - return ( -
-
- - -
- -
-
-
-
TODO: Show the chat messages
-
- -
-
-
- ); - } - - renderChatPanes = (panes) => ( - panes.map((pane, i) => - this.renderChatPane(pane, i) - ) - ) - render() { const { panesData } = this.props; const panes = panesData.get('panes'); @@ -119,7 +66,9 @@ class ChatPanes extends ImmutablePureComponent {
- {this.renderChatPanes(panes)} + {panes.map((pane, i) => + + )}
); } diff --git a/app/soapbox/features/chats/components/chat_window.js b/app/soapbox/features/chats/components/chat_window.js new file mode 100644 index 000000000..7eb6be45a --- /dev/null +++ b/app/soapbox/features/chats/components/chat_window.js @@ -0,0 +1,84 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { injectIntl } from 'react-intl'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import Avatar from 'soapbox/components/avatar'; +import { acctFull } from 'soapbox/utils/accounts'; +import IconButton from 'soapbox/components/icon_button'; +import { closeChat, toggleChat, fetchChatMessages } from 'soapbox/actions/chats'; +import { List as ImmutableList } from 'immutable'; + +const mapStateToProps = (state, { pane }) => ({ + chatMessages: state.getIn(['chat_messages', pane.get('chat_id')], ImmutableList()), +}); + +export default @connect(mapStateToProps) +@injectIntl +class ChatWindow extends ImmutablePureComponent { + + static propTypes = { + dispatch: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, + pane: ImmutablePropTypes.map.isRequired, + idx: PropTypes.number, + chatMessages: ImmutablePropTypes.list, + } + + static defaultProps = { + chatMessages: ImmutableList(), + } + + handleChatClose = (chatId) => { + return (e) => { + this.props.dispatch(closeChat(chatId)); + }; + } + + handleChatToggle = (chatId) => { + return (e) => { + this.props.dispatch(toggleChat(chatId)); + }; + } + + componentDidMount() { + const { dispatch, pane, chatMessages } = this.props; + if (chatMessages && chatMessages.count() < 1) + dispatch(fetchChatMessages(pane.get('chat_id'))); + } + + render() { + const { pane, idx, chatMessages } = this.props; + const chat = pane.get('chat'); + const account = pane.getIn(['chat', 'account']); + if (!chat || !account) return null; + + const right = (285 * (idx + 1)) + 20; + + return ( +
+
+ + +
+ +
+
+
+
+ {chatMessages.map(chatMessage => +
{chatMessage.get('content')}
+ )} +
+
+ +
+
+
+ ); + } + +} diff --git a/app/soapbox/reducers/chat_messages.js b/app/soapbox/reducers/chat_messages.js new file mode 100644 index 000000000..fe3ccbfe9 --- /dev/null +++ b/app/soapbox/reducers/chat_messages.js @@ -0,0 +1,13 @@ +import { CHAT_MESSAGES_FETCH_SUCCESS } from 'soapbox/actions/chats'; +import { Map as ImmutableMap, fromJS } from 'immutable'; + +const initialState = ImmutableMap(); + +export default function chatMessages(state = initialState, action) { + switch(action.type) { + case CHAT_MESSAGES_FETCH_SUCCESS: + return state.set(action.chatId, fromJS(action.data)); + default: + return state; + } +}; diff --git a/app/soapbox/reducers/chats.js b/app/soapbox/reducers/chats.js index aeca5958c..244c76b85 100644 --- a/app/soapbox/reducers/chats.js +++ b/app/soapbox/reducers/chats.js @@ -8,7 +8,7 @@ const importChats = (state, chats) => const initialState = ImmutableMap(); -export default function admin(state = initialState, action) { +export default function chats(state = initialState, action) { switch(action.type) { case CHAT_IMPORT: return importChat(state, action.chat); diff --git a/app/soapbox/reducers/index.js b/app/soapbox/reducers/index.js index 90cd6af62..320b78036 100644 --- a/app/soapbox/reducers/index.js +++ b/app/soapbox/reducers/index.js @@ -44,6 +44,7 @@ import me from './me'; import auth from './auth'; import admin from './admin'; import chats from './chats'; +import chat_messages from './chat_messages'; const reducers = { dropdown_menu, @@ -91,6 +92,7 @@ const reducers = { auth, admin, chats, + chat_messages, }; export default combineReducers(reducers); From 5373c5b1c4977586f0c3703c668331dfb9eece2a Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 25 Aug 2020 21:03:10 -0500 Subject: [PATCH 18/80] Chats: start styling ChatWindow --- .../features/chats/components/chat_window.js | 10 +++++++--- app/styles/chats.scss | 18 +++++++++++++++++- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/app/soapbox/features/chats/components/chat_window.js b/app/soapbox/features/chats/components/chat_window.js index 7eb6be45a..7997bca2e 100644 --- a/app/soapbox/features/chats/components/chat_window.js +++ b/app/soapbox/features/chats/components/chat_window.js @@ -69,9 +69,13 @@ class ChatWindow extends ImmutablePureComponent {
- {chatMessages.map(chatMessage => -
{chatMessage.get('content')}
- )} + {chatMessages.map(chatMessage => ( +
+ + {chatMessage.get('content')} + +
+ ))}
diff --git a/app/styles/chats.scss b/app/styles/chats.scss index ec62ba6b5..6ce047d3a 100644 --- a/app/styles/chats.scss +++ b/app/styles/chats.scss @@ -69,10 +69,11 @@ } &__content { - height: 100%; background: var(--foreground-color); display: flex; + flex: 1; flex-direction: column; + overflow: hidden; } &__actions { @@ -93,3 +94,18 @@ } } } + +.chat-messages { + overflow-y: scroll; +} + +.chat-message { + margin: 14px 10px; + + &__bubble { + padding: 4px 10px; + max-width: 70%; + border-radius: 10px; + background-color: var(--background-color); + } +} From 5a7cc148120f6b7d849fc07927546a12f7d2cccb Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 25 Aug 2020 21:31:34 -0500 Subject: [PATCH 19/80] Chats: rudimentary sending a message --- app/soapbox/actions/chats.js | 15 ++++++++++ .../features/chats/components/chat_window.js | 29 +++++++++++++++++-- app/soapbox/reducers/chat_messages.js | 7 ++++- 3 files changed, 47 insertions(+), 4 deletions(-) diff --git a/app/soapbox/actions/chats.js b/app/soapbox/actions/chats.js index 35543ca8c..0391a6e08 100644 --- a/app/soapbox/actions/chats.js +++ b/app/soapbox/actions/chats.js @@ -11,6 +11,10 @@ export const CHAT_MESSAGES_FETCH_REQUEST = 'CHAT_MESSAGES_FETCH_REQUEST'; export const CHAT_MESSAGES_FETCH_SUCCESS = 'CHAT_MESSAGES_FETCH_SUCCESS'; export const CHAT_MESSAGES_FETCH_FAIL = 'CHAT_MESSAGES_FETCH_FAIL'; +export const CHAT_MESSAGE_SEND_REQUEST = 'CHAT_MESSAGE_SEND_REQUEST'; +export const CHAT_MESSAGE_SEND_SUCCESS = 'CHAT_MESSAGE_SEND_SUCCESS'; +export const CHAT_MESSAGE_SEND_FAIL = 'CHAT_MESSAGE_SEND_FAIL'; + export function fetchChats() { return (dispatch, getState) => { dispatch({ type: CHATS_FETCH_REQUEST }); @@ -34,6 +38,17 @@ export function fetchChatMessages(chatId) { }; } +export function sendChatMessage(chatId, params) { + return (dispatch, getState) => { + dispatch({ type: CHAT_MESSAGE_SEND_REQUEST, chatId, params }); + return api(getState).post(`/api/v1/pleroma/chats/${chatId}/messages`, params).then(({ data }) => { + dispatch({ type: CHAT_MESSAGE_SEND_SUCCESS, chatId, data }); + }).catch(error => { + dispatch({ type: CHAT_MESSAGE_SEND_FAIL, chatId, error }); + }); + }; +} + export function openChat(chatId) { return (dispatch, getState) => { const panes = getSettings(getState()).getIn(['chats', 'panes']); diff --git a/app/soapbox/features/chats/components/chat_window.js b/app/soapbox/features/chats/components/chat_window.js index 7997bca2e..072aa0f5c 100644 --- a/app/soapbox/features/chats/components/chat_window.js +++ b/app/soapbox/features/chats/components/chat_window.js @@ -7,11 +7,11 @@ import ImmutablePureComponent from 'react-immutable-pure-component'; import Avatar from 'soapbox/components/avatar'; import { acctFull } from 'soapbox/utils/accounts'; import IconButton from 'soapbox/components/icon_button'; -import { closeChat, toggleChat, fetchChatMessages } from 'soapbox/actions/chats'; +import { closeChat, toggleChat, fetchChatMessages, sendChatMessage } from 'soapbox/actions/chats'; import { List as ImmutableList } from 'immutable'; const mapStateToProps = (state, { pane }) => ({ - chatMessages: state.getIn(['chat_messages', pane.get('chat_id')], ImmutableList()), + chatMessages: state.getIn(['chat_messages', pane.get('chat_id')], ImmutableList()).reverse(), }); export default @connect(mapStateToProps) @@ -30,6 +30,10 @@ class ChatWindow extends ImmutablePureComponent { chatMessages: ImmutableList(), } + state = { + content: '', + } + handleChatClose = (chatId) => { return (e) => { this.props.dispatch(closeChat(chatId)); @@ -42,6 +46,19 @@ class ChatWindow extends ImmutablePureComponent { }; } + handleKeyDown = (chatId) => { + return (e) => { + if (e.key === 'Enter') { + this.props.dispatch(sendChatMessage(chatId, this.state)); + this.setState({ content: '' }); + } + }; + } + + handleContentChange = (e) => { + this.setState({ content: e.target.value }); + } + componentDidMount() { const { dispatch, pane, chatMessages } = this.props; if (chatMessages && chatMessages.count() < 1) @@ -78,7 +95,13 @@ class ChatWindow extends ImmutablePureComponent { ))}
- +
diff --git a/app/soapbox/reducers/chat_messages.js b/app/soapbox/reducers/chat_messages.js index fe3ccbfe9..7eefb645a 100644 --- a/app/soapbox/reducers/chat_messages.js +++ b/app/soapbox/reducers/chat_messages.js @@ -1,4 +1,7 @@ -import { CHAT_MESSAGES_FETCH_SUCCESS } from 'soapbox/actions/chats'; +import { + CHAT_MESSAGES_FETCH_SUCCESS, + CHAT_MESSAGE_SEND_SUCCESS, +} from 'soapbox/actions/chats'; import { Map as ImmutableMap, fromJS } from 'immutable'; const initialState = ImmutableMap(); @@ -7,6 +10,8 @@ export default function chatMessages(state = initialState, action) { switch(action.type) { case CHAT_MESSAGES_FETCH_SUCCESS: return state.set(action.chatId, fromJS(action.data)); + case CHAT_MESSAGE_SEND_SUCCESS: + return state.set(action.chatId, state.get(action.chatId).insert(0, fromJS(action.data))); default: return state; } From dcaadb2153e4d6b734b2b352c0a3b14ca5807965 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 25 Aug 2020 21:45:05 -0500 Subject: [PATCH 20/80] Chats: autoscroll https://stackoverflow.com/a/41700815 --- .../features/chats/components/chat_window.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/app/soapbox/features/chats/components/chat_window.js b/app/soapbox/features/chats/components/chat_window.js index 072aa0f5c..6c7cdf09a 100644 --- a/app/soapbox/features/chats/components/chat_window.js +++ b/app/soapbox/features/chats/components/chat_window.js @@ -59,12 +59,24 @@ class ChatWindow extends ImmutablePureComponent { this.setState({ content: e.target.value }); } + scrollToBottom = () => { + if (!this.messagesEnd) return; + this.messagesEnd.scrollIntoView({ behavior: 'smooth' }); + } + + setRef = (el) => this.messagesEnd = el; + componentDidMount() { const { dispatch, pane, chatMessages } = this.props; + this.scrollToBottom(); if (chatMessages && chatMessages.count() < 1) dispatch(fetchChatMessages(pane.get('chat_id'))); } + componentDidUpdate() { + this.scrollToBottom(); + } + render() { const { pane, idx, chatMessages } = this.props; const chat = pane.get('chat'); @@ -93,6 +105,7 @@ class ChatWindow extends ImmutablePureComponent {
))} +
Date: Tue, 25 Aug 2020 22:03:53 -0500 Subject: [PATCH 21/80] Chats: focus input under some circumstances --- .../features/chats/components/chat_window.js | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/app/soapbox/features/chats/components/chat_window.js b/app/soapbox/features/chats/components/chat_window.js index 6c7cdf09a..3ec20837a 100644 --- a/app/soapbox/features/chats/components/chat_window.js +++ b/app/soapbox/features/chats/components/chat_window.js @@ -64,17 +64,31 @@ class ChatWindow extends ImmutablePureComponent { this.messagesEnd.scrollIntoView({ behavior: 'smooth' }); } - setRef = (el) => this.messagesEnd = el; + focusInput = () => { + if (!this.inputElem) return; + this.inputElem.focus(); + } + + setMessageEndRef = (el) => this.messagesEnd = el; + setInputRef = (el) => this.inputElem = el; componentDidMount() { const { dispatch, pane, chatMessages } = this.props; this.scrollToBottom(); if (chatMessages && chatMessages.count() < 1) dispatch(fetchChatMessages(pane.get('chat_id'))); + if (pane.get('state') === 'open') + this.focusInput(); } - componentDidUpdate() { + componentDidUpdate(prevProps) { this.scrollToBottom(); + + const oldState = prevProps.pane.get('state'); + const newState = this.props.pane.get('state'); + + if (oldState !== newState && newState === 'open') + this.focusInput(); } render() { @@ -105,7 +119,7 @@ class ChatWindow extends ImmutablePureComponent {
))} -
+
From c94258dfb91dfa0bb3386cfee3565cba0b379681 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 25 Aug 2020 22:12:08 -0500 Subject: [PATCH 22/80] Chats: improve scroll behavior --- .../features/chats/components/chat_window.js | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/app/soapbox/features/chats/components/chat_window.js b/app/soapbox/features/chats/components/chat_window.js index 3ec20837a..016729255 100644 --- a/app/soapbox/features/chats/components/chat_window.js +++ b/app/soapbox/features/chats/components/chat_window.js @@ -61,7 +61,7 @@ class ChatWindow extends ImmutablePureComponent { scrollToBottom = () => { if (!this.messagesEnd) return; - this.messagesEnd.scrollIntoView({ behavior: 'smooth' }); + this.messagesEnd.scrollIntoView(); } focusInput = () => { @@ -69,20 +69,26 @@ class ChatWindow extends ImmutablePureComponent { this.inputElem.focus(); } - setMessageEndRef = (el) => this.messagesEnd = el; - setInputRef = (el) => this.inputElem = el; + setMessageEndRef = (el) => { + this.messagesEnd = el; + this.scrollToBottom(); + }; + + setInputRef = (el) => { + const { pane } = this.props; + this.inputElem = el; + if (pane.get('state') === 'open') this.focusInput(); + }; componentDidMount() { const { dispatch, pane, chatMessages } = this.props; - this.scrollToBottom(); if (chatMessages && chatMessages.count() < 1) dispatch(fetchChatMessages(pane.get('chat_id'))); - if (pane.get('state') === 'open') - this.focusInput(); } componentDidUpdate(prevProps) { - this.scrollToBottom(); + if (prevProps.chatMessages !== this.props.chatMessages) + this.scrollToBottom(); const oldState = prevProps.pane.get('state'); const newState = this.props.pane.get('state'); From a296e6fcd1243ac16257479e77997d53be955b58 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 26 Aug 2020 00:21:54 -0500 Subject: [PATCH 23/80] Differentiate chat bubbles --- app/soapbox/features/chats/components/chat_window.js | 6 ++++-- app/styles/chats.scss | 8 +++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/app/soapbox/features/chats/components/chat_window.js b/app/soapbox/features/chats/components/chat_window.js index 016729255..b3a9dd45b 100644 --- a/app/soapbox/features/chats/components/chat_window.js +++ b/app/soapbox/features/chats/components/chat_window.js @@ -11,6 +11,7 @@ import { closeChat, toggleChat, fetchChatMessages, sendChatMessage } from 'soapb import { List as ImmutableList } from 'immutable'; const mapStateToProps = (state, { pane }) => ({ + me: state.get('me'), chatMessages: state.getIn(['chat_messages', pane.get('chat_id')], ImmutableList()).reverse(), }); @@ -24,6 +25,7 @@ class ChatWindow extends ImmutablePureComponent { pane: ImmutablePropTypes.map.isRequired, idx: PropTypes.number, chatMessages: ImmutablePropTypes.list, + me: PropTypes.node, } static defaultProps = { @@ -98,7 +100,7 @@ class ChatWindow extends ImmutablePureComponent { } render() { - const { pane, idx, chatMessages } = this.props; + const { pane, idx, chatMessages, me } = this.props; const chat = pane.get('chat'); const account = pane.getIn(['chat', 'account']); if (!chat || !account) return null; @@ -119,7 +121,7 @@ class ChatWindow extends ImmutablePureComponent {
{chatMessages.map(chatMessage => ( -
+
{chatMessage.get('content')} diff --git a/app/styles/chats.scss b/app/styles/chats.scss index 6ce047d3a..0ca597a1f 100644 --- a/app/styles/chats.scss +++ b/app/styles/chats.scss @@ -1,5 +1,5 @@ .pane { - @include standard-panel-shadow; + box-shadow: 0 0 6px 0 rgba(0, 0, 0, 0.3); position: fixed; bottom: 0; right: 20px; @@ -101,6 +101,7 @@ .chat-message { margin: 14px 10px; + display: flex; &__bubble { padding: 4px 10px; @@ -108,4 +109,9 @@ border-radius: 10px; background-color: var(--background-color); } + + &--me .chat-message__bubble { + margin-left: auto; + background-color: hsla(var(--brand-color_hsl), 0.2); + } } From 5fc8b3ff3da94b378e1673d9760ac14fc51ee527 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 26 Aug 2020 00:33:26 -0500 Subject: [PATCH 24/80] Chats: improve display of links and emoji --- app/soapbox/features/chats/components/chat_window.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/soapbox/features/chats/components/chat_window.js b/app/soapbox/features/chats/components/chat_window.js index b3a9dd45b..9071fb945 100644 --- a/app/soapbox/features/chats/components/chat_window.js +++ b/app/soapbox/features/chats/components/chat_window.js @@ -9,6 +9,7 @@ import { acctFull } from 'soapbox/utils/accounts'; import IconButton from 'soapbox/components/icon_button'; import { closeChat, toggleChat, fetchChatMessages, sendChatMessage } from 'soapbox/actions/chats'; import { List as ImmutableList } from 'immutable'; +import emojify from 'soapbox/features/emoji/emoji'; const mapStateToProps = (state, { pane }) => ({ me: state.get('me'), @@ -122,9 +123,10 @@ class ChatWindow extends ImmutablePureComponent {
{chatMessages.map(chatMessage => (
- - {chatMessage.get('content')} - +
))}
From f52186bc0139d75ee9b734eff49a5bd68c03977f Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 26 Aug 2020 00:37:55 -0500 Subject: [PATCH 25/80] Don't write settings back to Redux after PATCH, prevents race conditions --- app/soapbox/reducers/settings.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/app/soapbox/reducers/settings.js b/app/soapbox/reducers/settings.js index 2134c4cdc..1066716d7 100644 --- a/app/soapbox/reducers/settings.js +++ b/app/soapbox/reducers/settings.js @@ -3,7 +3,7 @@ import { NOTIFICATIONS_FILTER_SET } from '../actions/notifications'; import { STORE_HYDRATE } from '../actions/store'; import { EMOJI_USE } from '../actions/emojis'; import { LIST_DELETE_SUCCESS, LIST_FETCH_FAIL } from '../actions/lists'; -import { ME_FETCH_SUCCESS, ME_PATCH_SUCCESS } from 'soapbox/actions/me'; +import { ME_FETCH_SUCCESS } from 'soapbox/actions/me'; import { Map as ImmutableMap, fromJS } from 'immutable'; import uuid from '../uuid'; @@ -32,13 +32,8 @@ export default function settings(state = initialState, action) { case STORE_HYDRATE: return hydrate(state, action.state.get('settings')); case ME_FETCH_SUCCESS: - case ME_PATCH_SUCCESS: const me = fromJS(action.me); let fePrefs = me.getIn(['pleroma', 'settings_store', FE_NAME], ImmutableMap()); - // Spinster migration hotfix - if (fePrefs.get('locale') === '') { - fePrefs = fePrefs.delete('locale'); - } return state.merge(fePrefs); case NOTIFICATIONS_FILTER_SET: case SETTING_CHANGE: From 80a78ac0e15c172884c9df5e636d117523a0e4fb Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 26 Aug 2020 11:54:22 -0500 Subject: [PATCH 26/80] Chats: fix border-radius and chat content height --- app/styles/chats.scss | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/styles/chats.scss b/app/styles/chats.scss index 0ca597a1f..3fb237369 100644 --- a/app/styles/chats.scss +++ b/app/styles/chats.scss @@ -1,5 +1,6 @@ .pane { box-shadow: 0 0 6px 0 rgba(0, 0, 0, 0.3); + border-radius: 6px 6px 0 0; position: fixed; bottom: 0; right: 20px; @@ -97,6 +98,7 @@ .chat-messages { overflow-y: scroll; + flex: 1; } .chat-message { From c0dc4a69cdee43bf62869959ad3642d6226205dc Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 26 Aug 2020 12:29:42 -0500 Subject: [PATCH 27/80] Allow local dev against gleasonator.com --- webpack/development.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/webpack/development.js b/webpack/development.js index 7cfab6de5..52a47eaa1 100644 --- a/webpack/development.js +++ b/webpack/development.js @@ -29,11 +29,13 @@ const makeProxyConfig = () => { proxyConfig['/api/patron'] = { target: patronUrl, secure: secureProxy, + changeOrigin: true, }; backendEndpoints.map(endpoint => { proxyConfig[endpoint] = { target: backendUrl, secure: secureProxy, + changeOrigin: true, }; }); return proxyConfig; From dbafbbc06557f8ac8e2e75791e340169d115e91e Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 26 Aug 2020 13:39:38 -0500 Subject: [PATCH 28/80] Chats: make streaming mostly work --- app/soapbox/actions/streaming.js | 4 ++++ app/soapbox/reducers/chat_messages.js | 26 +++++++++++++++++++++++--- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/app/soapbox/actions/streaming.js b/app/soapbox/actions/streaming.js index 099cbab69..2b7836666 100644 --- a/app/soapbox/actions/streaming.js +++ b/app/soapbox/actions/streaming.js @@ -9,6 +9,7 @@ import { import { updateNotificationsQueue, expandNotifications } from './notifications'; import { updateConversations } from './conversations'; import { fetchFilters } from './filters'; +import { importFetchedChat } from './importer'; import { getSettings } from 'soapbox/actions/settings'; import messages from 'soapbox/locales/messages'; @@ -52,6 +53,9 @@ export function connectTimelineStream(timelineId, path, pollingRefresh = null, a case 'filters_changed': dispatch(fetchFilters()); break; + case 'pleroma:chat_update': + dispatch(importFetchedChat(JSON.parse(data.payload))); + break; } }, }; diff --git a/app/soapbox/reducers/chat_messages.js b/app/soapbox/reducers/chat_messages.js index 7eefb645a..b79ab2820 100644 --- a/app/soapbox/reducers/chat_messages.js +++ b/app/soapbox/reducers/chat_messages.js @@ -2,16 +2,36 @@ import { CHAT_MESSAGES_FETCH_SUCCESS, CHAT_MESSAGE_SEND_SUCCESS, } from 'soapbox/actions/chats'; -import { Map as ImmutableMap, fromJS } from 'immutable'; +import { CHAT_IMPORT, CHATS_IMPORT } from 'soapbox/actions/importer'; +import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable'; const initialState = ImmutableMap(); +const insertMessage = (state, chatId, message) => { + const newMessages = state.get(chatId, ImmutableList()).insert(0, message); + return state.set(chatId, newMessages); +}; + +const importMessage = (state, message) => { + const chatId = message.get('chat_id'); + return insertMessage(state, chatId, message); +}; + +const importLastMessages = (state, chats) => + state.withMutations(mutable => + chats.forEach(chat => importMessage(mutable, chat.get('last_message')))); + export default function chatMessages(state = initialState, action) { switch(action.type) { + case CHAT_IMPORT: + return importMessage(state, fromJS(action.chat.last_message)); + case CHATS_IMPORT: + return importLastMessages(state, fromJS(action.chats)); case CHAT_MESSAGES_FETCH_SUCCESS: return state.set(action.chatId, fromJS(action.data)); - case CHAT_MESSAGE_SEND_SUCCESS: - return state.set(action.chatId, state.get(action.chatId).insert(0, fromJS(action.data))); + // TODO: Prevent conflicts + // case CHAT_MESSAGE_SEND_SUCCESS: + // return insertMessage(state, action.chatId, fromJS(action.data)); default: return state; } From b66e28d8bbb5fdfc6a9b191b8039180a1191a319 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 26 Aug 2020 15:54:14 -0500 Subject: [PATCH 29/80] Chats: store `chat_message_lists` in Redux --- app/soapbox/actions/importer/index.js | 13 +++++--- app/soapbox/reducers/chat_message_lists.js | 36 ++++++++++++++++++++++ app/soapbox/reducers/chat_messages.js | 25 ++++++--------- app/soapbox/reducers/chats.js | 4 +-- app/soapbox/reducers/index.js | 2 ++ 5 files changed, 57 insertions(+), 23 deletions(-) create mode 100644 app/soapbox/reducers/chat_message_lists.js diff --git a/app/soapbox/actions/importer/index.js b/app/soapbox/actions/importer/index.js index a9c7cd9f6..7989b3ebe 100644 --- a/app/soapbox/actions/importer/index.js +++ b/app/soapbox/actions/importer/index.js @@ -12,8 +12,8 @@ export const STATUS_IMPORT = 'STATUS_IMPORT'; export const STATUSES_IMPORT = 'STATUSES_IMPORT'; export const POLLS_IMPORT = 'POLLS_IMPORT'; export const ACCOUNT_FETCH_FAIL_FOR_USERNAME_LOOKUP = 'ACCOUNT_FETCH_FAIL_FOR_USERNAME_LOOKUP'; -export const CHAT_IMPORT = 'CHAT_IMPORT'; export const CHATS_IMPORT = 'CHATS_IMPORT'; +export const CHAT_MESSAGES_IMPORT = 'CHAT_MESSAGES_IMPORT'; function pushUnique(array, object) { if (array.every(element => element.id !== object.id)) { @@ -41,14 +41,14 @@ export function importPolls(polls) { return { type: POLLS_IMPORT, polls }; } -export function importChat(chat) { - return { type: CHAT_IMPORT, chat }; -} - export function importChats(chats) { return { type: CHATS_IMPORT, chats }; } +export function importChatMessages(chatMessages) { + return { type: CHAT_MESSAGES_IMPORT, chatMessages }; +} + export function importFetchedAccount(account) { return importFetchedAccounts([account]); } @@ -120,6 +120,7 @@ export function importFetchedChat(chat) { export function importFetchedChats(chats) { return (dispatch, getState) => { const accounts = []; + const chatMessages = []; const normalChats = []; function processChat(chat) { @@ -127,11 +128,13 @@ export function importFetchedChats(chats) { pushUnique(normalChats, normalizeChat(chat, normalOldChat)); pushUnique(accounts, chat.account); + pushUnique(chatMessages, chat.last_message); } chats.forEach(processChat); dispatch(importFetchedAccounts(accounts)); + dispatch(importChatMessages(chatMessages)); dispatch(importChats(normalChats)); }; } diff --git a/app/soapbox/reducers/chat_message_lists.js b/app/soapbox/reducers/chat_message_lists.js new file mode 100644 index 000000000..c05b4e85e --- /dev/null +++ b/app/soapbox/reducers/chat_message_lists.js @@ -0,0 +1,36 @@ +import { + CHAT_MESSAGES_FETCH_SUCCESS, + CHAT_MESSAGE_SEND_SUCCESS, +} from 'soapbox/actions/chats'; +import { CHAT_MESSAGES_IMPORT } from 'soapbox/actions/importer'; +import { Map as ImmutableMap, OrderedSet as ImmutableOrderedSet } from 'immutable'; + +const initialState = ImmutableMap(); + +const updateList = (state, chatId, messageIds) => { + const newIds = state.get(chatId, ImmutableOrderedSet()).union(messageIds); + return state.set(chatId, newIds); +}; + +const importMessage = (state, chatMessage) => { + return updateList(state, chatMessage.chat_id, [chatMessage.id]); +}; + +const importMessages = (state, chatMessages) => ( + state.withMutations(map => + chatMessages.forEach(chatMessage => + importMessage(map, chatMessage))) +); + +export default function chatMessageLists(state = initialState, action) { + switch(action.type) { + case CHAT_MESSAGES_IMPORT: + return importMessages(state, action.chatMessages); + case CHAT_MESSAGES_FETCH_SUCCESS: + return updateList(state, action.chatId, action.data.map(chat => chat.id)); + case CHAT_MESSAGE_SEND_SUCCESS: + return updateList(state, action.chatId, action.data.id); + default: + return state; + } +}; diff --git a/app/soapbox/reducers/chat_messages.js b/app/soapbox/reducers/chat_messages.js index b79ab2820..d7f9b3c29 100644 --- a/app/soapbox/reducers/chat_messages.js +++ b/app/soapbox/reducers/chat_messages.js @@ -2,20 +2,18 @@ import { CHAT_MESSAGES_FETCH_SUCCESS, CHAT_MESSAGE_SEND_SUCCESS, } from 'soapbox/actions/chats'; -import { CHAT_IMPORT, CHATS_IMPORT } from 'soapbox/actions/importer'; -import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable'; +import { CHATS_IMPORT } from 'soapbox/actions/importer'; +import { Map as ImmutableMap, fromJS } from 'immutable'; const initialState = ImmutableMap(); -const insertMessage = (state, chatId, message) => { - const newMessages = state.get(chatId, ImmutableList()).insert(0, message); - return state.set(chatId, newMessages); +const importMessage = (state, message) => { + return state.set(message.get('id'), message); }; -const importMessage = (state, message) => { - const chatId = message.get('chat_id'); - return insertMessage(state, chatId, message); -}; +const importMessages = (state, messages) => + state.withMutations(mutable => + messages.forEach(message => importMessage(mutable, message))); const importLastMessages = (state, chats) => state.withMutations(mutable => @@ -23,15 +21,12 @@ const importLastMessages = (state, chats) => export default function chatMessages(state = initialState, action) { switch(action.type) { - case CHAT_IMPORT: - return importMessage(state, fromJS(action.chat.last_message)); case CHATS_IMPORT: return importLastMessages(state, fromJS(action.chats)); case CHAT_MESSAGES_FETCH_SUCCESS: - return state.set(action.chatId, fromJS(action.data)); - // TODO: Prevent conflicts - // case CHAT_MESSAGE_SEND_SUCCESS: - // return insertMessage(state, action.chatId, fromJS(action.data)); + return importMessages(state, fromJS(action.data)); + case CHAT_MESSAGE_SEND_SUCCESS: + return importMessage(state, fromJS(action.data)); default: return state; } diff --git a/app/soapbox/reducers/chats.js b/app/soapbox/reducers/chats.js index 244c76b85..2930af578 100644 --- a/app/soapbox/reducers/chats.js +++ b/app/soapbox/reducers/chats.js @@ -1,4 +1,4 @@ -import { CHAT_IMPORT, CHATS_IMPORT } from 'soapbox/actions/importer'; +import { CHATS_IMPORT } from 'soapbox/actions/importer'; import { Map as ImmutableMap, fromJS } from 'immutable'; const importChat = (state, chat) => state.set(chat.id, fromJS(chat)); @@ -10,8 +10,6 @@ const initialState = ImmutableMap(); export default function chats(state = initialState, action) { switch(action.type) { - case CHAT_IMPORT: - return importChat(state, action.chat); case CHATS_IMPORT: return importChats(state, action.chats); default: diff --git a/app/soapbox/reducers/index.js b/app/soapbox/reducers/index.js index 320b78036..2caec469a 100644 --- a/app/soapbox/reducers/index.js +++ b/app/soapbox/reducers/index.js @@ -45,6 +45,7 @@ import auth from './auth'; import admin from './admin'; import chats from './chats'; import chat_messages from './chat_messages'; +import chat_message_lists from './chat_message_lists'; const reducers = { dropdown_menu, @@ -93,6 +94,7 @@ const reducers = { admin, chats, chat_messages, + chat_message_lists, }; export default combineReducers(reducers); From ecefab9956ae2df11f9ea0758b7dda6dfec146c3 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 26 Aug 2020 16:12:42 -0500 Subject: [PATCH 30/80] Chats: get messages showing up again --- .../chats/components/chat_message_list.js | 64 +++++++++++++++++++ .../features/chats/components/chat_window.js | 35 ++-------- 2 files changed, 70 insertions(+), 29 deletions(-) create mode 100644 app/soapbox/features/chats/components/chat_message_list.js diff --git a/app/soapbox/features/chats/components/chat_message_list.js b/app/soapbox/features/chats/components/chat_message_list.js new file mode 100644 index 000000000..2960995ec --- /dev/null +++ b/app/soapbox/features/chats/components/chat_message_list.js @@ -0,0 +1,64 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { injectIntl } from 'react-intl'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import { List as ImmutableList } from 'immutable'; +import emojify from 'soapbox/features/emoji/emoji'; + +const mapStateToProps = (state, { chatMessageIds }) => ({ + me: state.get('me'), + chatMessages: chatMessageIds.map(id => state.getIn(['chat_messages', id])).toList(), +}); + +export default @connect(mapStateToProps) +@injectIntl +class ChatMessageList extends ImmutablePureComponent { + + static propTypes = { + dispatch: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, + chatMessages: ImmutablePropTypes.list, + chatMessageIds: ImmutablePropTypes.orderedSet, + me: PropTypes.node, + } + + static defaultProps = { + chatMessages: ImmutableList(), + } + + scrollToBottom = () => { + if (!this.messagesEnd) return; + this.messagesEnd.scrollIntoView(); + } + + setMessageEndRef = (el) => { + this.messagesEnd = el; + this.scrollToBottom(); + }; + + componentDidUpdate(prevProps) { + if (prevProps.chatMessages !== this.props.chatMessages) + this.scrollToBottom(); + } + + render() { + const { chatMessages, me } = this.props; + + return ( +
+ {chatMessages.map(chatMessage => ( +
+ +
+ ))} +
+
+ ); + } + +} diff --git a/app/soapbox/features/chats/components/chat_window.js b/app/soapbox/features/chats/components/chat_window.js index 9071fb945..4847be101 100644 --- a/app/soapbox/features/chats/components/chat_window.js +++ b/app/soapbox/features/chats/components/chat_window.js @@ -8,12 +8,12 @@ import Avatar from 'soapbox/components/avatar'; import { acctFull } from 'soapbox/utils/accounts'; import IconButton from 'soapbox/components/icon_button'; import { closeChat, toggleChat, fetchChatMessages, sendChatMessage } from 'soapbox/actions/chats'; -import { List as ImmutableList } from 'immutable'; -import emojify from 'soapbox/features/emoji/emoji'; +import { List as ImmutableList, OrderedSet as ImmutableOrderedSet } from 'immutable'; +import ChatMessageList from './chat_message_list'; const mapStateToProps = (state, { pane }) => ({ me: state.get('me'), - chatMessages: state.getIn(['chat_messages', pane.get('chat_id')], ImmutableList()).reverse(), + chatMessageIds: state.getIn(['chat_message_lists', pane.get('chat_id')], ImmutableOrderedSet()), }); export default @connect(mapStateToProps) @@ -25,7 +25,7 @@ class ChatWindow extends ImmutablePureComponent { intl: PropTypes.object.isRequired, pane: ImmutablePropTypes.map.isRequired, idx: PropTypes.number, - chatMessages: ImmutablePropTypes.list, + chatMessageIds: ImmutablePropTypes.orderedSet, me: PropTypes.node, } @@ -62,21 +62,11 @@ class ChatWindow extends ImmutablePureComponent { this.setState({ content: e.target.value }); } - scrollToBottom = () => { - if (!this.messagesEnd) return; - this.messagesEnd.scrollIntoView(); - } - focusInput = () => { if (!this.inputElem) return; this.inputElem.focus(); } - setMessageEndRef = (el) => { - this.messagesEnd = el; - this.scrollToBottom(); - }; - setInputRef = (el) => { const { pane } = this.props; this.inputElem = el; @@ -90,9 +80,6 @@ class ChatWindow extends ImmutablePureComponent { } componentDidUpdate(prevProps) { - if (prevProps.chatMessages !== this.props.chatMessages) - this.scrollToBottom(); - const oldState = prevProps.pane.get('state'); const newState = this.props.pane.get('state'); @@ -101,7 +88,7 @@ class ChatWindow extends ImmutablePureComponent { } render() { - const { pane, idx, chatMessages, me } = this.props; + const { pane, idx, chatMessageIds } = this.props; const chat = pane.get('chat'); const account = pane.getIn(['chat', 'account']); if (!chat || !account) return null; @@ -120,17 +107,7 @@ class ChatWindow extends ImmutablePureComponent {
-
- {chatMessages.map(chatMessage => ( -
- -
- ))} -
-
+
Date: Wed, 26 Aug 2020 16:54:44 -0500 Subject: [PATCH 31/80] Fix chat submission --- app/soapbox/features/chats/components/chat_message_list.js | 5 ++++- app/soapbox/reducers/chat_message_lists.js | 5 +++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/app/soapbox/features/chats/components/chat_message_list.js b/app/soapbox/features/chats/components/chat_message_list.js index 2960995ec..e7c37d8cc 100644 --- a/app/soapbox/features/chats/components/chat_message_list.js +++ b/app/soapbox/features/chats/components/chat_message_list.js @@ -9,7 +9,10 @@ import emojify from 'soapbox/features/emoji/emoji'; const mapStateToProps = (state, { chatMessageIds }) => ({ me: state.get('me'), - chatMessages: chatMessageIds.map(id => state.getIn(['chat_messages', id])).toList(), + chatMessages: chatMessageIds.reduce((acc, curr) => { + const chatMessage = state.getIn(['chat_messages', curr]); + return chatMessage ? acc.push(chatMessage) : acc; + }, ImmutableList()), }); export default @connect(mapStateToProps) diff --git a/app/soapbox/reducers/chat_message_lists.js b/app/soapbox/reducers/chat_message_lists.js index c05b4e85e..c039a4a0b 100644 --- a/app/soapbox/reducers/chat_message_lists.js +++ b/app/soapbox/reducers/chat_message_lists.js @@ -8,7 +8,8 @@ import { Map as ImmutableMap, OrderedSet as ImmutableOrderedSet } from 'immutabl const initialState = ImmutableMap(); const updateList = (state, chatId, messageIds) => { - const newIds = state.get(chatId, ImmutableOrderedSet()).union(messageIds); + const ids = state.get(chatId, ImmutableOrderedSet()); + const newIds = ids.union(messageIds); return state.set(chatId, newIds); }; @@ -29,7 +30,7 @@ export default function chatMessageLists(state = initialState, action) { case CHAT_MESSAGES_FETCH_SUCCESS: return updateList(state, action.chatId, action.data.map(chat => chat.id)); case CHAT_MESSAGE_SEND_SUCCESS: - return updateList(state, action.chatId, action.data.id); + return updateList(state, action.chatId, [action.data.id]); default: return state; } From 4b173f05807552092fda5216ecff0838bff3e2b0 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 26 Aug 2020 17:02:25 -0500 Subject: [PATCH 32/80] Chats: sort messages properly --- app/soapbox/features/chats/components/chat_message_list.js | 2 +- app/soapbox/reducers/chat_message_lists.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/soapbox/features/chats/components/chat_message_list.js b/app/soapbox/features/chats/components/chat_message_list.js index e7c37d8cc..d0b2a005f 100644 --- a/app/soapbox/features/chats/components/chat_message_list.js +++ b/app/soapbox/features/chats/components/chat_message_list.js @@ -12,7 +12,7 @@ const mapStateToProps = (state, { chatMessageIds }) => ({ chatMessages: chatMessageIds.reduce((acc, curr) => { const chatMessage = state.getIn(['chat_messages', curr]); return chatMessage ? acc.push(chatMessage) : acc; - }, ImmutableList()), + }, ImmutableList()).sort(), }); export default @connect(mapStateToProps) diff --git a/app/soapbox/reducers/chat_message_lists.js b/app/soapbox/reducers/chat_message_lists.js index c039a4a0b..d13f02ef3 100644 --- a/app/soapbox/reducers/chat_message_lists.js +++ b/app/soapbox/reducers/chat_message_lists.js @@ -28,7 +28,7 @@ export default function chatMessageLists(state = initialState, action) { case CHAT_MESSAGES_IMPORT: return importMessages(state, action.chatMessages); case CHAT_MESSAGES_FETCH_SUCCESS: - return updateList(state, action.chatId, action.data.map(chat => chat.id)); + return updateList(state, action.chatId, action.data.map(chat => chat.id).reverse()); case CHAT_MESSAGE_SEND_SUCCESS: return updateList(state, action.chatId, [action.data.id]); default: From b9d7f927a6771a63ebad92713941e487274f06ec Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 26 Aug 2020 17:29:22 -0500 Subject: [PATCH 33/80] Chats: get streaming working again --- app/soapbox/actions/importer/normalizer.js | 1 + app/soapbox/actions/streaming.js | 5 +++-- app/soapbox/reducers/chat_message_lists.js | 6 +++--- app/soapbox/reducers/chat_messages.js | 9 ++++++--- 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/app/soapbox/actions/importer/normalizer.js b/app/soapbox/actions/importer/normalizer.js index 697e72107..c867e52e1 100644 --- a/app/soapbox/actions/importer/normalizer.js +++ b/app/soapbox/actions/importer/normalizer.js @@ -85,6 +85,7 @@ export function normalizeChat(chat, normalOldChat) { const normalChat = { ...chat }; normalChat.account = chat.account.id; + normalChat.last_message = chat.last_message.id; return normalChat; } diff --git a/app/soapbox/actions/streaming.js b/app/soapbox/actions/streaming.js index 2b7836666..8c6a1651d 100644 --- a/app/soapbox/actions/streaming.js +++ b/app/soapbox/actions/streaming.js @@ -9,10 +9,11 @@ import { import { updateNotificationsQueue, expandNotifications } from './notifications'; import { updateConversations } from './conversations'; import { fetchFilters } from './filters'; -import { importFetchedChat } from './importer'; import { getSettings } from 'soapbox/actions/settings'; import messages from 'soapbox/locales/messages'; +export const STREAMING_CHAT_UPDATE = 'STREAMING_CHAT_UPDATE'; + const validLocale = locale => Object.keys(messages).includes(locale); const getLocale = state => { @@ -54,7 +55,7 @@ export function connectTimelineStream(timelineId, path, pollingRefresh = null, a dispatch(fetchFilters()); break; case 'pleroma:chat_update': - dispatch(importFetchedChat(JSON.parse(data.payload))); + dispatch({ type: STREAMING_CHAT_UPDATE, payload: JSON.parse(data.payload) }); break; } }, diff --git a/app/soapbox/reducers/chat_message_lists.js b/app/soapbox/reducers/chat_message_lists.js index d13f02ef3..e19ee2bdd 100644 --- a/app/soapbox/reducers/chat_message_lists.js +++ b/app/soapbox/reducers/chat_message_lists.js @@ -2,7 +2,7 @@ import { CHAT_MESSAGES_FETCH_SUCCESS, CHAT_MESSAGE_SEND_SUCCESS, } from 'soapbox/actions/chats'; -import { CHAT_MESSAGES_IMPORT } from 'soapbox/actions/importer'; +import { STREAMING_CHAT_UPDATE } from 'soapbox/actions/streaming'; import { Map as ImmutableMap, OrderedSet as ImmutableOrderedSet } from 'immutable'; const initialState = ImmutableMap(); @@ -25,8 +25,8 @@ const importMessages = (state, chatMessages) => ( export default function chatMessageLists(state = initialState, action) { switch(action.type) { - case CHAT_MESSAGES_IMPORT: - return importMessages(state, action.chatMessages); + case STREAMING_CHAT_UPDATE: + return importMessages(state, [action.payload.last_message]); case CHAT_MESSAGES_FETCH_SUCCESS: return updateList(state, action.chatId, action.data.map(chat => chat.id).reverse()); case CHAT_MESSAGE_SEND_SUCCESS: diff --git a/app/soapbox/reducers/chat_messages.js b/app/soapbox/reducers/chat_messages.js index d7f9b3c29..96ff24c8d 100644 --- a/app/soapbox/reducers/chat_messages.js +++ b/app/soapbox/reducers/chat_messages.js @@ -1,8 +1,9 @@ import { + CHATS_FETCH_SUCCESS, CHAT_MESSAGES_FETCH_SUCCESS, CHAT_MESSAGE_SEND_SUCCESS, } from 'soapbox/actions/chats'; -import { CHATS_IMPORT } from 'soapbox/actions/importer'; +import { STREAMING_CHAT_UPDATE } from 'soapbox/actions/streaming'; import { Map as ImmutableMap, fromJS } from 'immutable'; const initialState = ImmutableMap(); @@ -21,12 +22,14 @@ const importLastMessages = (state, chats) => export default function chatMessages(state = initialState, action) { switch(action.type) { - case CHATS_IMPORT: - return importLastMessages(state, fromJS(action.chats)); + case CHATS_FETCH_SUCCESS: + return importLastMessages(state, fromJS(action.data)); case CHAT_MESSAGES_FETCH_SUCCESS: return importMessages(state, fromJS(action.data)); case CHAT_MESSAGE_SEND_SUCCESS: return importMessage(state, fromJS(action.data)); + case STREAMING_CHAT_UPDATE: + return importLastMessages(state, fromJS([action.payload])); default: return state; } From da6239c4fcacc01e00862ccd086c52f023e368c6 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 26 Aug 2020 18:17:47 -0500 Subject: [PATCH 34/80] Chats: move out of importer pipeline, entirely through reducers --- app/soapbox/actions/chats.js | 2 -- app/soapbox/actions/importer/index.js | 37 ---------------------- app/soapbox/reducers/accounts.js | 15 +++++++++ app/soapbox/reducers/chat_message_lists.js | 7 ++++ app/soapbox/reducers/chats.js | 12 ++++--- 5 files changed, 30 insertions(+), 43 deletions(-) diff --git a/app/soapbox/actions/chats.js b/app/soapbox/actions/chats.js index 0391a6e08..e0eb762f4 100644 --- a/app/soapbox/actions/chats.js +++ b/app/soapbox/actions/chats.js @@ -1,5 +1,4 @@ import api from '../api'; -import { importFetchedChats } from 'soapbox/actions/importer'; import { getSettings, changeSetting } from 'soapbox/actions/settings'; import { Map as ImmutableMap } from 'immutable'; @@ -19,7 +18,6 @@ export function fetchChats() { return (dispatch, getState) => { dispatch({ type: CHATS_FETCH_REQUEST }); return api(getState).get('/api/v1/pleroma/chats').then(({ data }) => { - dispatch(importFetchedChats(data)); dispatch({ type: CHATS_FETCH_SUCCESS, data }); }).catch(error => { dispatch({ type: CHATS_FETCH_FAIL, error }); diff --git a/app/soapbox/actions/importer/index.js b/app/soapbox/actions/importer/index.js index 7989b3ebe..0736dd7ce 100644 --- a/app/soapbox/actions/importer/index.js +++ b/app/soapbox/actions/importer/index.js @@ -3,7 +3,6 @@ import { normalizeAccount, normalizeStatus, normalizePoll, - normalizeChat, } from './normalizer'; export const ACCOUNT_IMPORT = 'ACCOUNT_IMPORT'; @@ -12,8 +11,6 @@ export const STATUS_IMPORT = 'STATUS_IMPORT'; export const STATUSES_IMPORT = 'STATUSES_IMPORT'; export const POLLS_IMPORT = 'POLLS_IMPORT'; export const ACCOUNT_FETCH_FAIL_FOR_USERNAME_LOOKUP = 'ACCOUNT_FETCH_FAIL_FOR_USERNAME_LOOKUP'; -export const CHATS_IMPORT = 'CHATS_IMPORT'; -export const CHAT_MESSAGES_IMPORT = 'CHAT_MESSAGES_IMPORT'; function pushUnique(array, object) { if (array.every(element => element.id !== object.id)) { @@ -41,14 +38,6 @@ export function importPolls(polls) { return { type: POLLS_IMPORT, polls }; } -export function importChats(chats) { - return { type: CHATS_IMPORT, chats }; -} - -export function importChatMessages(chatMessages) { - return { type: CHAT_MESSAGES_IMPORT, chatMessages }; -} - export function importFetchedAccount(account) { return importFetchedAccounts([account]); } @@ -112,29 +101,3 @@ export function importFetchedPoll(poll) { export function importErrorWhileFetchingAccountByUsername(username) { return { type: ACCOUNT_FETCH_FAIL_FOR_USERNAME_LOOKUP, username }; }; - -export function importFetchedChat(chat) { - return importFetchedChats([chat]); -} - -export function importFetchedChats(chats) { - return (dispatch, getState) => { - const accounts = []; - const chatMessages = []; - const normalChats = []; - - function processChat(chat) { - const normalOldChat = getState().getIn(['chats', chat.id]); - - pushUnique(normalChats, normalizeChat(chat, normalOldChat)); - pushUnique(accounts, chat.account); - pushUnique(chatMessages, chat.last_message); - } - - chats.forEach(processChat); - - dispatch(importFetchedAccounts(accounts)); - dispatch(importChatMessages(chatMessages)); - dispatch(importChats(normalChats)); - }; -} diff --git a/app/soapbox/reducers/accounts.js b/app/soapbox/reducers/accounts.js index fd915d7ff..64cf96515 100644 --- a/app/soapbox/reducers/accounts.js +++ b/app/soapbox/reducers/accounts.js @@ -3,6 +3,9 @@ import { ACCOUNTS_IMPORT, ACCOUNT_FETCH_FAIL_FOR_USERNAME_LOOKUP, } from '../actions/importer'; +import { CHATS_FETCH_SUCCESS } from 'soapbox/actions/chats'; +import { STREAMING_CHAT_UPDATE } from 'soapbox/actions/streaming'; +import { normalizeAccount as normalizeAccount2 } from 'soapbox/actions/importer/normalizer'; import { Map as ImmutableMap, fromJS } from 'immutable'; const initialState = ImmutableMap(); @@ -25,6 +28,14 @@ const normalizeAccounts = (state, accounts) => { return state; }; +const importAccountFromChat = (state, chat) => + // TODO: Fix this monstrosity + normalizeAccount(state, normalizeAccount2(chat.account)); + +const importAccountsFromChats = (state, chats) => + state.withMutations(mutable => + chats.forEach(chat => importAccountFromChat(mutable, chat))); + export default function accounts(state = initialState, action) { switch(action.type) { case ACCOUNT_IMPORT: @@ -35,6 +46,10 @@ export default function accounts(state = initialState, action) { return state.set(-1, ImmutableMap({ username: action.username, })); + case CHATS_FETCH_SUCCESS: + return importAccountsFromChats(state, action.data); + case STREAMING_CHAT_UPDATE: + return importAccountsFromChats(state, [action.payload]); default: return state; } diff --git a/app/soapbox/reducers/chat_message_lists.js b/app/soapbox/reducers/chat_message_lists.js index e19ee2bdd..535499a16 100644 --- a/app/soapbox/reducers/chat_message_lists.js +++ b/app/soapbox/reducers/chat_message_lists.js @@ -1,4 +1,5 @@ import { + CHATS_FETCH_SUCCESS, CHAT_MESSAGES_FETCH_SUCCESS, CHAT_MESSAGE_SEND_SUCCESS, } from 'soapbox/actions/chats'; @@ -23,8 +24,14 @@ const importMessages = (state, chatMessages) => ( importMessage(map, chatMessage))) ); +const importLastMessages = (state, chats) => + state.withMutations(mutable => + chats.forEach(chat => importMessage(mutable, chat.last_message))); + export default function chatMessageLists(state = initialState, action) { switch(action.type) { + case CHATS_FETCH_SUCCESS: + return importLastMessages(state, action.data); case STREAMING_CHAT_UPDATE: return importMessages(state, [action.payload.last_message]); case CHAT_MESSAGES_FETCH_SUCCESS: diff --git a/app/soapbox/reducers/chats.js b/app/soapbox/reducers/chats.js index 2930af578..912a47c06 100644 --- a/app/soapbox/reducers/chats.js +++ b/app/soapbox/reducers/chats.js @@ -1,7 +1,9 @@ -import { CHATS_IMPORT } from 'soapbox/actions/importer'; +import { CHATS_FETCH_SUCCESS } from 'soapbox/actions/chats'; +import { STREAMING_CHAT_UPDATE } from 'soapbox/actions/streaming'; +import { normalizeChat } from 'soapbox/actions/importer/normalizer'; import { Map as ImmutableMap, fromJS } from 'immutable'; -const importChat = (state, chat) => state.set(chat.id, fromJS(chat)); +const importChat = (state, chat) => state.set(chat.id, fromJS(normalizeChat(chat))); const importChats = (state, chats) => state.withMutations(mutable => chats.forEach(chat => importChat(mutable, chat))); @@ -10,8 +12,10 @@ const initialState = ImmutableMap(); export default function chats(state = initialState, action) { switch(action.type) { - case CHATS_IMPORT: - return importChats(state, action.chats); + case CHATS_FETCH_SUCCESS: + return importChats(state, action.data); + case STREAMING_CHAT_UPDATE: + return importChats(state, [action.payload]); default: return state; } From efa6f94cdd9ae2876b18f85780b03f96002d56e6 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 26 Aug 2020 19:20:18 -0500 Subject: [PATCH 35/80] Fix onEndorseToggle props warning --- app/soapbox/features/account_timeline/components/header.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/soapbox/features/account_timeline/components/header.js b/app/soapbox/features/account_timeline/components/header.js index c7951ec66..7f6d69046 100644 --- a/app/soapbox/features/account_timeline/components/header.js +++ b/app/soapbox/features/account_timeline/components/header.js @@ -19,7 +19,7 @@ export default class Header extends ImmutablePureComponent { onMute: PropTypes.func.isRequired, onBlockDomain: PropTypes.func.isRequired, onUnblockDomain: PropTypes.func.isRequired, - onEndorseToggle: PropTypes.func.isRequired, + // onEndorseToggle: PropTypes.func.isRequired, onAddToList: PropTypes.func.isRequired, username: PropTypes.string, }; From 9a3aab27c90eeb0de9eebaa121480a2becac51c5 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 26 Aug 2020 19:46:23 -0500 Subject: [PATCH 36/80] Chats: clicking the profile message button calls onMessage --- app/soapbox/actions/chats.js | 15 +++++++++++++++ app/soapbox/features/account/components/header.js | 2 +- .../account_timeline/components/header.js | 5 +++++ .../containers/header_container.js | 4 ++++ 4 files changed, 25 insertions(+), 1 deletion(-) diff --git a/app/soapbox/actions/chats.js b/app/soapbox/actions/chats.js index e0eb762f4..bdd8d8a85 100644 --- a/app/soapbox/actions/chats.js +++ b/app/soapbox/actions/chats.js @@ -14,6 +14,10 @@ export const CHAT_MESSAGE_SEND_REQUEST = 'CHAT_MESSAGE_SEND_REQUEST'; export const CHAT_MESSAGE_SEND_SUCCESS = 'CHAT_MESSAGE_SEND_SUCCESS'; export const CHAT_MESSAGE_SEND_FAIL = 'CHAT_MESSAGE_SEND_FAIL'; +export const CHAT_FETCH_REQUEST = 'CHAT_FETCH_REQUEST'; +export const CHAT_FETCH_SUCCESS = 'CHAT_FETCH_SUCCESS'; +export const CHAT_FETCH_FAIL = 'CHAT_FETCH_FAIL'; + export function fetchChats() { return (dispatch, getState) => { dispatch({ type: CHATS_FETCH_REQUEST }); @@ -95,3 +99,14 @@ export function toggleMainWindow() { return dispatch(changeSetting(['chats', 'mainWindow'], state)); }; } + +export function startChat(accountId) { + return (dispatch, getState) => { + dispatch({ type: CHAT_FETCH_REQUEST, accountId }); + return api(getState).post(`/api/v1/pleroma/chats/by-account-id/${accountId}`).then(({ data }) => { + dispatch({ type: CHAT_FETCH_SUCCESS, chat: data }); + }).catch(error => { + dispatch({ type: CHAT_FETCH_FAIL, accountId, error }); + }); + }; +} diff --git a/app/soapbox/features/account/components/header.js b/app/soapbox/features/account/components/header.js index 9d24bf0a1..4875dd8a0 100644 --- a/app/soapbox/features/account/components/header.js +++ b/app/soapbox/features/account/components/header.js @@ -294,7 +294,7 @@ class Header extends ImmutablePureComponent {
{account.get('id') !== me && - } diff --git a/app/soapbox/features/account_timeline/components/header.js b/app/soapbox/features/account_timeline/components/header.js index c0c30b759..534a12dbc 100644 --- a/app/soapbox/features/account_timeline/components/header.js +++ b/app/soapbox/features/account_timeline/components/header.js @@ -72,11 +72,10 @@ export default class Header extends ImmutablePureComponent { this.props.onUnblockDomain(domain); } - handleMessage = () => { - this.props.onMessage(this.props.account, this.context.router.history); + handleChat = () => { + this.props.onChat(this.props.account, this.context.router.history); } - // handleEndorseToggle = () => { // this.props.onEndorseToggle(this.props.account); // } @@ -100,7 +99,7 @@ export default class Header extends ImmutablePureComponent { onBlock={this.handleBlock} onMention={this.handleMention} onDirect={this.handleDirect} - onMessage={this.handleMessage} + onChat={this.handleChat} onReblogToggle={this.handleReblogToggle} onReport={this.handleReport} onMute={this.handleMute} diff --git a/app/soapbox/features/account_timeline/containers/header_container.js b/app/soapbox/features/account_timeline/containers/header_container.js index f254818d3..504675a42 100644 --- a/app/soapbox/features/account_timeline/containers/header_container.js +++ b/app/soapbox/features/account_timeline/containers/header_container.js @@ -134,7 +134,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ })); }, - onMessage(account, router) { + onChat(account, router) { dispatch(startChat(account.get('id'))); }, }); From d9df091f75b1badd3f55cde538ea8506ec2f08a8 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 27 Aug 2020 13:37:53 -0500 Subject: [PATCH 51/80] Chats: increase main pane header font size --- app/styles/chats.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/styles/chats.scss b/app/styles/chats.scss index 4600d1d9c..10a682400 100644 --- a/app/styles/chats.scss +++ b/app/styles/chats.scss @@ -15,7 +15,7 @@ &--main { height: calc(100vh - 70px); - .pane__header { + .pane__header .pane__title { font-size: 16px; } } From 44f7ad5e1cfc8c9677352265ac651fb89f05b471 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 27 Aug 2020 13:45:37 -0500 Subject: [PATCH 52/80] Chats: sort chats in reducer instead of component --- .../features/chats/components/chat_list.js | 13 +------------ app/soapbox/reducers/chats.js | 17 ++++++++++++++--- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/app/soapbox/features/chats/components/chat_list.js b/app/soapbox/features/chats/components/chat_list.js index 0928c3040..ca5349b20 100644 --- a/app/soapbox/features/chats/components/chat_list.js +++ b/app/soapbox/features/chats/components/chat_list.js @@ -10,18 +10,7 @@ import { makeGetChat } from 'soapbox/selectors'; const mapStateToProps = state => { const getChat = makeGetChat(); return { - chats: state.get('chats').map(chat => - getChat(state, chat.toJS()) - ).sort((valueA, valueB) => { - // Sort most recently updated chats at the top - const a = new Date(valueA.get('updated_at')); - const b = new Date(valueB.get('updated_at')); - - if (a === b) return 0; - if (a > b) return -1; - if (a < b) return 1; - return 0; - }), + chats: state.get('chats').map(chat => getChat(state, chat.toJS())), }; }; diff --git a/app/soapbox/reducers/chats.js b/app/soapbox/reducers/chats.js index 7659e38e7..6c85bfe7b 100644 --- a/app/soapbox/reducers/chats.js +++ b/app/soapbox/reducers/chats.js @@ -8,16 +8,27 @@ const importChat = (state, chat) => state.set(chat.id, fromJS(normalizeChat(chat const importChats = (state, chats) => state.withMutations(mutable => chats.forEach(chat => importChat(mutable, chat))); +const chatDateComparator = (chatA, chatB) => { + // Sort most recently updated chats at the top + const a = new Date(chatA.get('updated_at')); + const b = new Date(chatB.get('updated_at')); + + if (a === b) return 0; + if (a > b) return -1; + if (a < b) return 1; + return 0; +}; + const initialState = ImmutableMap(); export default function chats(state = initialState, action) { switch(action.type) { case CHATS_FETCH_SUCCESS: - return importChats(state, action.chats); + return importChats(state, action.chats).sort(chatDateComparator); case STREAMING_CHAT_UPDATE: - return importChats(state, [action.chat]); + return importChats(state, [action.chat]).sort(chatDateComparator); case CHAT_FETCH_SUCCESS: - return importChats(state, [action.chat]); + return importChats(state, [action.chat]).sort(chatDateComparator); default: return state; } From 14eec701cb5a4590d198fd0ecfddb0c2c5a0303b Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 27 Aug 2020 14:02:52 -0500 Subject: [PATCH 53/80] Chats: add empty chats message --- .../features/chats/components/chat_list.js | 10 ++++--- .../features/chats/components/chat_panes.js | 27 ++++++++++++------- app/styles/chats.scss | 12 +++++++++ app/styles/components/columns.scss | 1 - 4 files changed, 36 insertions(+), 14 deletions(-) diff --git a/app/soapbox/features/chats/components/chat_list.js b/app/soapbox/features/chats/components/chat_list.js index ca5349b20..84fd34244 100644 --- a/app/soapbox/features/chats/components/chat_list.js +++ b/app/soapbox/features/chats/components/chat_list.js @@ -10,7 +10,7 @@ import { makeGetChat } from 'soapbox/selectors'; const mapStateToProps = state => { const getChat = makeGetChat(); return { - chats: state.get('chats').map(chat => getChat(state, chat.toJS())), + chats: state.get('chats').map(chat => getChat(state, chat.toJS())).toList(), }; }; @@ -22,6 +22,7 @@ class ChatList extends ImmutablePureComponent { dispatch: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, onClickChat: PropTypes.func, + emptyMessage: PropTypes.node, }; componentDidMount() { @@ -29,12 +30,15 @@ class ChatList extends ImmutablePureComponent { } render() { - const { chats } = this.props; + const { chats, emptyMessage } = this.props; return (
- {chats.toList().map(chat => ( + {chats.count() === 0 && +
{emptyMessage}
+ } + {chats.map(chat => (
+
+ +
+
+ } + /> +
+
+ ); + return (
-
-
- -
-
- -
-
+ {mainWindowPane} {panes.map((pane, i) => )} diff --git a/app/styles/chats.scss b/app/styles/chats.scss index 10a682400..c73542db3 100644 --- a/app/styles/chats.scss +++ b/app/styles/chats.scss @@ -130,4 +130,16 @@ .chat-list { overflow-y: auto; + flex: 1; + + &__content { + height: 100%; + } + + .empty-column-indicator { + height: 100%; + box-sizing: border-box; + background: transparent; + align-items: start; + } } diff --git a/app/styles/components/columns.scss b/app/styles/components/columns.scss index 2151b168b..d8034e497 100644 --- a/app/styles/components/columns.scss +++ b/app/styles/components/columns.scss @@ -678,7 +678,6 @@ align-items: center; justify-content: center; min-height: 160px; - border-radius: 0 0 10px 10px; @supports(display: grid) { // hack to fix Chrome <57 contain: strict; From 859f340716ec27a15c88d6e9d6708c3455dae024 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 27 Aug 2020 14:33:03 -0500 Subject: [PATCH 54/80] Chats: rudimentary display of chat message notifications --- .../features/notifications/components/notification.js | 11 +++++++++-- app/soapbox/reducers/notifications.js | 1 + 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/app/soapbox/features/notifications/components/notification.js b/app/soapbox/features/notifications/components/notification.js index 3703f13f0..73fa00629 100644 --- a/app/soapbox/features/notifications/components/notification.js +++ b/app/soapbox/features/notifications/components/notification.js @@ -150,7 +150,7 @@ class Notification extends ImmutablePureComponent {
- +
@@ -158,6 +158,13 @@ class Notification extends ImmutablePureComponent {
+ +
+ +
); } @@ -310,7 +317,7 @@ class Notification extends ImmutablePureComponent { case 'pleroma:emoji_reaction': return this.renderEmojiReact(notification, link); case 'pleroma:chat_mention': - return this.renderChatMention(notification); + return this.renderChatMention(notification, link); } return null; diff --git a/app/soapbox/reducers/notifications.js b/app/soapbox/reducers/notifications.js index cbf9ef5d5..e49d5dc11 100644 --- a/app/soapbox/reducers/notifications.js +++ b/app/soapbox/reducers/notifications.js @@ -43,6 +43,7 @@ const notificationToMap = notification => ImmutableMap({ created_at: notification.created_at, status: notification.status ? notification.status.id : null, emoji: notification.emoji, + chat_message: notification.chat_message, is_seen: get(notification, ['pleroma', 'is_seen'], true), }); From 497a603a888e482747bc29eb19925069d6440e3e Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 27 Aug 2020 14:35:28 -0500 Subject: [PATCH 55/80] Notifications: drop chat mentions Prevent streaming API from pushing in unwanted notifications https://git.pleroma.social/pleroma/pleroma/-/issues/2076 --- app/soapbox/actions/notifications.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/soapbox/actions/notifications.js b/app/soapbox/actions/notifications.js index 8f2b2f1c2..eb6b92aeb 100644 --- a/app/soapbox/actions/notifications.js +++ b/app/soapbox/actions/notifications.js @@ -71,6 +71,8 @@ export function updateNotifications(notification, intlMessages, intlLocale) { export function updateNotificationsQueue(notification, intlMessages, intlLocale, curPath) { return (dispatch, getState) => { + if (notification.type === 'pleroma:chat_mention') return; // Drop chat notifications, handle them per-chat + const showAlert = getSettings(getState()).getIn(['notifications', 'alerts', notification.type]); const filters = getFilters(getState(), { contextType: 'notifications' }); const playSound = getSettings(getState()).getIn(['notifications', 'sounds', notification.type]); From 020e21adcdcc7adbec4f027d96858f2615874a3c Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 27 Aug 2020 15:07:15 -0500 Subject: [PATCH 56/80] Oh right, maps can't be sorted... --- .../features/chats/components/chat_list.js | 13 ++++++++++++- app/soapbox/reducers/chats.js | 17 +++-------------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/app/soapbox/features/chats/components/chat_list.js b/app/soapbox/features/chats/components/chat_list.js index 84fd34244..7cd03401d 100644 --- a/app/soapbox/features/chats/components/chat_list.js +++ b/app/soapbox/features/chats/components/chat_list.js @@ -7,10 +7,21 @@ import { fetchChats } from 'soapbox/actions/chats'; import ChatListAccount from './chat_list_account'; import { makeGetChat } from 'soapbox/selectors'; +const chatDateComparator = (chatA, chatB) => { + // Sort most recently updated chats at the top + const a = new Date(chatA.get('updated_at')); + const b = new Date(chatB.get('updated_at')); + + if (a === b) return 0; + if (a > b) return -1; + if (a < b) return 1; + return 0; +}; + const mapStateToProps = state => { const getChat = makeGetChat(); return { - chats: state.get('chats').map(chat => getChat(state, chat.toJS())).toList(), + chats: state.get('chats').map(chat => getChat(state, chat.toJS())).toList().sort(chatDateComparator), }; }; diff --git a/app/soapbox/reducers/chats.js b/app/soapbox/reducers/chats.js index 6c85bfe7b..7659e38e7 100644 --- a/app/soapbox/reducers/chats.js +++ b/app/soapbox/reducers/chats.js @@ -8,27 +8,16 @@ const importChat = (state, chat) => state.set(chat.id, fromJS(normalizeChat(chat const importChats = (state, chats) => state.withMutations(mutable => chats.forEach(chat => importChat(mutable, chat))); -const chatDateComparator = (chatA, chatB) => { - // Sort most recently updated chats at the top - const a = new Date(chatA.get('updated_at')); - const b = new Date(chatB.get('updated_at')); - - if (a === b) return 0; - if (a > b) return -1; - if (a < b) return 1; - return 0; -}; - const initialState = ImmutableMap(); export default function chats(state = initialState, action) { switch(action.type) { case CHATS_FETCH_SUCCESS: - return importChats(state, action.chats).sort(chatDateComparator); + return importChats(state, action.chats); case STREAMING_CHAT_UPDATE: - return importChats(state, [action.chat]).sort(chatDateComparator); + return importChats(state, [action.chat]); case CHAT_FETCH_SUCCESS: - return importChats(state, [action.chat]).sort(chatDateComparator); + return importChats(state, [action.chat]); default: return state; } From ac028ed58469f8d849f18ecc064918612adc8745 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 27 Aug 2020 15:43:19 -0500 Subject: [PATCH 57/80] Chats: add unread counters to chats --- .../features/chats/components/chat_list.js | 8 +++++++- .../chats/components/chat_list_account.js | 3 +++ .../features/chats/components/chat_panes.js | 5 ++++- .../features/chats/components/chat_window.js | 8 ++++++-- app/styles/chats.scss | 16 ++++++++++++++++ 5 files changed, 36 insertions(+), 4 deletions(-) diff --git a/app/soapbox/features/chats/components/chat_list.js b/app/soapbox/features/chats/components/chat_list.js index 7cd03401d..a1fbbcc57 100644 --- a/app/soapbox/features/chats/components/chat_list.js +++ b/app/soapbox/features/chats/components/chat_list.js @@ -20,8 +20,14 @@ const chatDateComparator = (chatA, chatB) => { const mapStateToProps = state => { const getChat = makeGetChat(); + + const chats = state.get('chats') + .map(chat => getChat(state, chat.toJS())) + .toList() + .sort(chatDateComparator); + return { - chats: state.get('chats').map(chat => getChat(state, chat.toJS())).toList().sort(chatDateComparator), + chats, }; }; diff --git a/app/soapbox/features/chats/components/chat_list_account.js b/app/soapbox/features/chats/components/chat_list_account.js index 48a813fa8..531587784 100644 --- a/app/soapbox/features/chats/components/chat_list_account.js +++ b/app/soapbox/features/chats/components/chat_list_account.js @@ -4,6 +4,7 @@ import PropTypes from 'prop-types'; import Avatar from '../../../components/avatar'; import DisplayName from '../../../components/display_name'; import ImmutablePureComponent from 'react-immutable-pure-component'; +import { shortNumberFormat } from 'soapbox/utils/numbers'; export default class ChatListAccount extends ImmutablePureComponent { @@ -20,6 +21,7 @@ export default class ChatListAccount extends ImmutablePureComponent { const { chat } = this.props; if (!chat) return null; const account = chat.get('account'); + const unreadCount = chat.get('unread'); return (
@@ -30,6 +32,7 @@ export default class ChatListAccount extends ImmutablePureComponent {
+ {unreadCount > 0 && {shortNumberFormat(unreadCount)}}
diff --git a/app/soapbox/features/chats/components/chat_panes.js b/app/soapbox/features/chats/components/chat_panes.js index e3a7db113..321c0796d 100644 --- a/app/soapbox/features/chats/components/chat_panes.js +++ b/app/soapbox/features/chats/components/chat_panes.js @@ -10,6 +10,7 @@ import { FormattedMessage } from 'react-intl'; import { makeGetChat } from 'soapbox/selectors'; import { openChat, toggleMainWindow } from 'soapbox/actions/chats'; import ChatWindow from './chat_window'; +import { shortNumberFormat } from 'soapbox/utils/numbers'; const addChatsToPanes = (state, panesData) => { const getChat = makeGetChat(); @@ -27,6 +28,7 @@ const mapStateToProps = state => { return { panesData: addChatsToPanes(state, panesData), + unreadCount: state.get('chats').reduce((acc, curr) => acc + curr.get('unread'), 0), }; }; @@ -50,7 +52,7 @@ class ChatPanes extends ImmutablePureComponent { } render() { - const { panesData } = this.props; + const { panesData, unreadCount } = this.props; const panes = panesData.get('panes'); const mainWindow = panesData.get('mainWindow'); @@ -60,6 +62,7 @@ class ChatPanes extends ImmutablePureComponent { + {unreadCount > 0 && {shortNumberFormat(unreadCount)}}
({ me: state.get('me'), + chat: state.getIn(['chats', pane.get('chat_id')]), chatMessageIds: state.getIn(['chat_message_lists', pane.get('chat_id')], ImmutableOrderedSet()), }); @@ -26,6 +28,7 @@ class ChatWindow extends ImmutablePureComponent { pane: ImmutablePropTypes.map.isRequired, idx: PropTypes.number, chatMessageIds: ImmutablePropTypes.orderedSet, + chat: ImmutablePropTypes.map, me: PropTypes.node, } @@ -88,12 +91,12 @@ class ChatWindow extends ImmutablePureComponent { } render() { - const { pane, idx, chatMessageIds } = this.props; - const chat = pane.get('chat'); + const { pane, idx, chatMessageIds, chat } = this.props; const account = pane.getIn(['chat', 'account']); if (!chat || !account) return null; const right = (285 * (idx + 1)) + 20; + const unreadCount = chat.get('unread'); return (
@@ -102,6 +105,7 @@ class ChatWindow extends ImmutablePureComponent { + {unreadCount > 0 && {shortNumberFormat(unreadCount)}}
diff --git a/app/styles/chats.scss b/app/styles/chats.scss index c73542db3..410769b8d 100644 --- a/app/styles/chats.scss +++ b/app/styles/chats.scss @@ -67,6 +67,11 @@ .pane__close { margin-left: auto; } + + .icon-with-badge__badge { + position: static; + pointer-events: none; + } } &__content { @@ -142,4 +147,15 @@ background: transparent; align-items: start; } + + .account__display-name { + position: relative; + } + + .icon-with-badge__badge { + top: 0; + right: 0; + left: auto; + bottom: auto; + } } From f80f18d376f3b84779fec700f82ab41963325343 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 27 Aug 2020 16:09:03 -0500 Subject: [PATCH 58/80] Chats: mark chats as read --- app/soapbox/actions/chats.js | 27 ++++++++++++++++++- .../features/chats/components/chat_window.js | 15 +++++++++-- app/soapbox/reducers/chats.js | 11 +++++++- 3 files changed, 49 insertions(+), 4 deletions(-) diff --git a/app/soapbox/actions/chats.js b/app/soapbox/actions/chats.js index 1adc41e1e..93938e3c3 100644 --- a/app/soapbox/actions/chats.js +++ b/app/soapbox/actions/chats.js @@ -19,6 +19,10 @@ export const CHAT_FETCH_REQUEST = 'CHAT_FETCH_REQUEST'; export const CHAT_FETCH_SUCCESS = 'CHAT_FETCH_SUCCESS'; export const CHAT_FETCH_FAIL = 'CHAT_FETCH_FAIL'; +export const CHAT_READ_REQUEST = 'CHAT_READ_REQUEST'; +export const CHAT_READ_SUCCESS = 'CHAT_READ_SUCCESS'; +export const CHAT_READ_FAIL = 'CHAT_READ_FAIL'; + export function fetchChats() { return (dispatch, getState) => { dispatch({ type: CHATS_FETCH_REQUEST }); @@ -56,9 +60,12 @@ export function sendChatMessage(chatId, params) { export function openChat(chatId) { return (dispatch, getState) => { - const panes = getSettings(getState()).getIn(['chats', 'panes']); + const state = getState(); + const panes = getSettings(state).getIn(['chats', 'panes']); const idx = panes.findIndex(pane => pane.get('chat_id') === chatId); + dispatch(markChatRead(chatId)); + if (idx > -1) { return dispatch(changeSetting(['chats', 'panes', idx, 'state'], 'open')); } else { @@ -88,6 +95,7 @@ export function toggleChat(chatId) { if (idx > -1) { const state = pane.get('state') === 'minimized' ? 'open' : 'minimized'; + if (state === 'open') dispatch(markChatRead(chatId)); return dispatch(changeSetting(['chats', 'panes', idx, 'state'], state)); } else { return false; @@ -114,3 +122,20 @@ export function startChat(accountId) { }); }; } + +export function markChatRead(chatId, lastReadId) { + return (dispatch, getState) => { + const chat = getState().getIn(['chats', chatId]); + if (!lastReadId) lastReadId = chat.get('last_message'); + + if (chat.get('unread') < 1) return; + if (!lastReadId) return; + + dispatch({ type: CHAT_READ_REQUEST, chatId, lastReadId }); + api(getState).post(`/api/v1/pleroma/chats/${chatId}/read`, { last_read_id: lastReadId }).then(({ data }) => { + dispatch({ type: CHAT_READ_SUCCESS, chat: data, lastReadId }); + }).catch(error => { + dispatch({ type: CHAT_READ_FAIL, chatId, error, lastReadId }); + }); + }; +} diff --git a/app/soapbox/features/chats/components/chat_window.js b/app/soapbox/features/chats/components/chat_window.js index 7fc266309..6448e4a67 100644 --- a/app/soapbox/features/chats/components/chat_window.js +++ b/app/soapbox/features/chats/components/chat_window.js @@ -7,7 +7,13 @@ import ImmutablePureComponent from 'react-immutable-pure-component'; import Avatar from 'soapbox/components/avatar'; import { acctFull } from 'soapbox/utils/accounts'; import IconButton from 'soapbox/components/icon_button'; -import { closeChat, toggleChat, fetchChatMessages, sendChatMessage } from 'soapbox/actions/chats'; +import { + closeChat, + toggleChat, + fetchChatMessages, + sendChatMessage, + markChatRead, +} from 'soapbox/actions/chats'; import { List as ImmutableList, OrderedSet as ImmutableOrderedSet } from 'immutable'; import ChatMessageList from './chat_message_list'; import { shortNumberFormat } from 'soapbox/utils/numbers'; @@ -65,6 +71,11 @@ class ChatWindow extends ImmutablePureComponent { this.setState({ content: e.target.value }); } + handleReadChat = (e) => { + const { dispatch, chat } = this.props; + dispatch(markChatRead(chat.get('id'))); + } + focusInput = () => { if (!this.inputElem) return; this.inputElem.focus(); @@ -99,7 +110,7 @@ class ChatWindow extends ImmutablePureComponent { const unreadCount = chat.get('unread'); return ( -
+
- {unreadCount > 0 && {shortNumberFormat(unreadCount)}}
- + {unreadCount > 0 + ? {shortNumberFormat(unreadCount)} + : + } - {unreadCount > 0 && {shortNumberFormat(unreadCount)}}
diff --git a/app/styles/chats.scss b/app/styles/chats.scss index 410769b8d..cb498e628 100644 --- a/app/styles/chats.scss +++ b/app/styles/chats.scss @@ -71,6 +71,12 @@ .icon-with-badge__badge { position: static; pointer-events: none; + width: 18px; + height: 18px; + display: flex; + align-items: center; + justify-content: center; + margin-right: 7px; } } From abd6c419f14cf88a6cc2208a47bd9f91f6c8ec93 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 27 Aug 2020 16:54:07 -0500 Subject: [PATCH 61/80] Chats: hide for mobile (for now) --- app/soapbox/features/ui/index.js | 13 +++++++++++-- app/styles/ui.scss | 2 +- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/app/soapbox/features/ui/index.js b/app/soapbox/features/ui/index.js index 266d6f4e2..8c6cbcc22 100644 --- a/app/soapbox/features/ui/index.js +++ b/app/soapbox/features/ui/index.js @@ -292,6 +292,7 @@ class UI extends React.PureComponent { state = { draggingOver: false, + mobile: isMobile(window.innerWidth), }; handleBeforeUnload = (e) => { @@ -400,10 +401,17 @@ class UI extends React.PureComponent { } } + handleResize = debounce(() => { + this.setState({ mobile: isMobile(window.innerWidth) }); + }, 500, { + trailing: true, + }); + componentDidMount() { const { account } = this.props; if (!account) return; window.addEventListener('beforeunload', this.handleBeforeUnload, false); + window.addEventListener('resize', this.handleResize, { passive: true }); document.addEventListener('dragenter', this.handleDragEnter, false); document.addEventListener('dragover', this.handleDragOver, false); @@ -437,6 +445,7 @@ class UI extends React.PureComponent { componentWillUnmount() { window.removeEventListener('beforeunload', this.handleBeforeUnload); + window.removeEventListener('resize', this.handleResize); document.removeEventListener('dragenter', this.handleDragEnter); document.removeEventListener('dragover', this.handleDragOver); document.removeEventListener('drop', this.handleDrop); @@ -565,7 +574,7 @@ class UI extends React.PureComponent { render() { const { streamingUrl } = this.props; - const { draggingOver } = this.state; + const { draggingOver, mobile } = this.state; const { intl, children, isComposing, location, dropdownMenuIsOpen, me } = this.props; if (me === null || !streamingUrl) return null; @@ -605,7 +614,7 @@ class UI extends React.PureComponent { {me && } - {me && } + {me && !mobile && }
); diff --git a/app/styles/ui.scss b/app/styles/ui.scss index a0ad254bf..a8f09663a 100644 --- a/app/styles/ui.scss +++ b/app/styles/ui.scss @@ -369,7 +369,7 @@ justify-content: center; transition: 0.2s; - @media screen and (max-width: 895px) { + @media screen and (max-width: 630px) { display: flex; } From a349bce10fecc4996149c6743d40aa351878087f Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 27 Aug 2020 17:27:37 -0500 Subject: [PATCH 62/80] Chats: show message date on hover --- .../chats/components/chat_message_list.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/app/soapbox/features/chats/components/chat_message_list.js b/app/soapbox/features/chats/components/chat_message_list.js index 5f5f4db9e..269dfcdb5 100644 --- a/app/soapbox/features/chats/components/chat_message_list.js +++ b/app/soapbox/features/chats/components/chat_message_list.js @@ -42,6 +42,20 @@ class ChatMessageList extends ImmutablePureComponent { this.scrollToBottom(); }; + getFormattedTimestamp = (chatMessage) => { + const { intl } = this.props; + return intl.formatDate( + new Date(chatMessage.get('created_at')), { + hour12: false, + year: 'numeric', + month: 'short', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + } + ); + }; + componentDidUpdate(prevProps) { if (prevProps.chatMessages !== this.props.chatMessages) this.scrollToBottom(); @@ -61,6 +75,7 @@ class ChatMessageList extends ImmutablePureComponent { key={chatMessage.get('id')} > From 3fbdce6901a2797e8a6cedb2254bb4d9e0cdf11f Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 27 Aug 2020 18:06:25 -0500 Subject: [PATCH 63/80] Chats: mark as read when the input is focused --- app/soapbox/features/chats/components/chat_window.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/soapbox/features/chats/components/chat_window.js b/app/soapbox/features/chats/components/chat_window.js index fed5cb525..af9f9bcee 100644 --- a/app/soapbox/features/chats/components/chat_window.js +++ b/app/soapbox/features/chats/components/chat_window.js @@ -99,6 +99,15 @@ class ChatWindow extends ImmutablePureComponent { if (oldState !== newState && newState === 'open') this.focusInput(); + + const markReadConditions = [ + () => this.props.chat !== undefined, + () => document.activeElement === this.inputElem, + () => this.props.chat.get('unread') > 0, + ]; + + if (markReadConditions.every(c => c() === true)) + this.handleReadChat(); } render() { From e682c3db2875742d5971fb6fe07c493d9db1b795 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 27 Aug 2020 19:25:16 -0500 Subject: [PATCH 64/80] Chats: use textarea instead of input --- app/soapbox/features/chats/components/chat_window.js | 7 ++++--- app/styles/chats.scss | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/app/soapbox/features/chats/components/chat_window.js b/app/soapbox/features/chats/components/chat_window.js index af9f9bcee..e1c67a2b2 100644 --- a/app/soapbox/features/chats/components/chat_window.js +++ b/app/soapbox/features/chats/components/chat_window.js @@ -63,6 +63,7 @@ class ChatWindow extends ImmutablePureComponent { if (e.key === 'Enter') { this.props.dispatch(sendChatMessage(chatId, this.state)); this.setState({ content: '' }); + e.preventDefault(); } }; } @@ -134,9 +135,9 @@ class ChatWindow extends ImmutablePureComponent {
-
- +