+
{children}
@@ -604,6 +647,7 @@ class UI extends React.PureComponent {
{me && }
+ {me && !mobile && }
);
diff --git a/app/soapbox/features/ui/util/async-components.js b/app/soapbox/features/ui/util/async-components.js
index c93ad2f8e..73a8a5540 100644
--- a/app/soapbox/features/ui/util/async-components.js
+++ b/app/soapbox/features/ui/util/async-components.js
@@ -197,3 +197,11 @@ export function SecurityForm() {
export function MfaForm() {
return import(/* webpackChunkName: "features/security/mfa_form" */'../../security/mfa_form');
}
+
+export function ChatIndex() {
+ return import(/* webpackChunkName: "features/chats" */'../../chats');
+}
+
+export function ChatRoom() {
+ return import(/* webpackChunkName: "features/chats/chat_room" */'../../chats/chat_room');
+}
diff --git a/app/soapbox/reducers/__tests__/notifications-test.js b/app/soapbox/reducers/__tests__/notifications-test.js
index ef6d99d88..97141c756 100644
--- a/app/soapbox/reducers/__tests__/notifications-test.js
+++ b/app/soapbox/reducers/__tests__/notifications-test.js
@@ -1,10 +1,8 @@
import * as actions from 'soapbox/actions/notifications';
import reducer from '../notifications';
import notifications from 'soapbox/__fixtures__/notifications.json';
-import markers from 'soapbox/__fixtures__/markers.json';
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
import { take } from 'lodash';
-import { SAVE_MARKERS_SUCCESS } from 'soapbox/actions/markers';
import { ACCOUNT_BLOCK_SUCCESS, ACCOUNT_MUTE_SUCCESS } from 'soapbox/actions/accounts';
import notification from 'soapbox/__fixtures__/notification.json';
import intlMessages from 'soapbox/__fixtures__/intlMessages.json';
@@ -42,6 +40,7 @@ describe('notifications reducer', () => {
created_at: '2020-06-10T02:54:39.000Z',
status: '9vvNxoo5EFbbnfdXQu',
emoji: 'π’',
+ chat_message: undefined,
is_seen: false,
}),
ImmutableMap({
@@ -51,6 +50,7 @@ describe('notifications reducer', () => {
created_at: '2020-06-10T02:51:05.000Z',
status: '9vvNxoo5EFbbnfdXQu',
emoji: undefined,
+ chat_message: undefined,
is_seen: true,
}),
ImmutableMap({
@@ -60,6 +60,7 @@ describe('notifications reducer', () => {
created_at: '2020-06-10T02:05:06.000Z',
status: '9vvNxoo5EFbbnfdXQu',
emoji: undefined,
+ chat_message: undefined,
is_seen: true,
}),
]),
@@ -73,21 +74,6 @@ describe('notifications reducer', () => {
}));
});
- it('should handle SAVE_MARKERS_SUCCESS', () => {
- const state = ImmutableMap({
- unread: 1,
- lastRead: '35098811',
- });
- const action = {
- type: SAVE_MARKERS_SUCCESS,
- markers: markers,
- };
- expect(reducer(state, action)).toEqual(ImmutableMap({
- unread: 3,
- lastRead: '35098814',
- }));
- });
-
it('should handle NOTIFICATIONS_EXPAND_REQUEST', () => {
const state = ImmutableMap({
isLoading: false,
@@ -122,6 +108,7 @@ describe('notifications reducer', () => {
created_at: '2020-06-10T02:54:39.000Z',
status: '9vvNxoo5EFbbnfdXQu',
emoji: 'π’',
+ chat_message: undefined,
is_seen: false,
}),
ImmutableMap({
@@ -131,6 +118,7 @@ describe('notifications reducer', () => {
created_at: '2020-06-10T02:51:05.000Z',
status: '9vvNxoo5EFbbnfdXQu',
emoji: undefined,
+ chat_message: undefined,
is_seen: true,
}),
ImmutableMap({
@@ -140,6 +128,7 @@ describe('notifications reducer', () => {
created_at: '2020-06-10T02:05:06.000Z',
status: '9vvNxoo5EFbbnfdXQu',
emoji: undefined,
+ chat_message: undefined,
is_seen: true,
}),
]),
@@ -213,6 +202,7 @@ describe('notifications reducer', () => {
created_at: '2020-06-10T02:51:05.000Z',
status: '9vvNxoo5EFbbnfdXQu',
emoji: undefined,
+ chat_message: undefined,
is_seen: true,
}),
]),
@@ -270,6 +260,7 @@ describe('notifications reducer', () => {
created_at: '2020-06-10T02:54:39.000Z',
status: '9vvNxoo5EFbbnfdXQu',
emoji: 'π’',
+ chat_message: undefined,
is_seen: false,
}),
]),
@@ -291,6 +282,7 @@ describe('notifications reducer', () => {
created_at: '2020-06-10T02:54:39.000Z',
status: '9vvNxoo5EFbbnfdXQu',
emoji: 'π’',
+ chat_message: undefined,
is_seen: false,
}),
ImmutableMap({
@@ -300,6 +292,7 @@ describe('notifications reducer', () => {
created_at: '2020-06-10T02:51:05.000Z',
status: '9vvNxoo5EFbbnfdXQu',
emoji: undefined,
+ chat_message: undefined,
is_seen: true,
}),
ImmutableMap({
@@ -309,6 +302,7 @@ describe('notifications reducer', () => {
created_at: '2020-06-10T02:05:06.000Z',
status: '9vvNxoo5EFbbnfdXQu',
emoji: undefined,
+ chat_message: undefined,
is_seen: true,
}),
ImmutableMap({
@@ -318,6 +312,7 @@ describe('notifications reducer', () => {
created_at: '2020-06-10T02:54:39.000Z',
status: '9vvNxoo5EFbbnfdXQu',
emoji: 'π’',
+ chat_message: undefined,
is_seen: false,
}),
]),
@@ -348,6 +343,7 @@ describe('notifications reducer', () => {
created_at: '2020-06-10T02:54:39.000Z',
status: '9vvNxoo5EFbbnfdXQu',
emoji: 'π’',
+ chat_message: undefined,
is_seen: false,
}),
ImmutableMap({
@@ -357,6 +353,7 @@ describe('notifications reducer', () => {
created_at: '2020-06-10T02:51:05.000Z',
status: '9vvNxoo5EFbbnfdXQu',
emoji: undefined,
+ chat_message: undefined,
is_seen: true,
}),
ImmutableMap({
@@ -366,6 +363,7 @@ describe('notifications reducer', () => {
created_at: '2020-06-10T02:05:06.000Z',
status: '9vvNxoo5EFbbnfdXQu',
emoji: undefined,
+ chat_message: undefined,
is_seen: true,
}),
]),
@@ -385,6 +383,7 @@ describe('notifications reducer', () => {
created_at: '2020-06-10T02:54:39.000Z',
status: '9vvNxoo5EFbbnfdXQu',
emoji: 'π’',
+ chat_message: undefined,
is_seen: false,
}),
ImmutableMap({
@@ -394,6 +393,7 @@ describe('notifications reducer', () => {
created_at: '2020-06-10T02:51:05.000Z',
status: '9vvNxoo5EFbbnfdXQu',
emoji: undefined,
+ chat_message: undefined,
is_seen: true,
}),
ImmutableMap({
@@ -403,6 +403,7 @@ describe('notifications reducer', () => {
created_at: '2020-06-10T02:05:06.000Z',
status: '9vvNxoo5EFbbnfdXQu',
emoji: undefined,
+ chat_message: undefined,
is_seen: true,
}),
]),
@@ -420,6 +421,7 @@ describe('notifications reducer', () => {
created_at: '2020-06-10T02:51:05.000Z',
status: '9vvNxoo5EFbbnfdXQu',
emoji: undefined,
+ chat_message: undefined,
is_seen: true,
}),
ImmutableMap({
@@ -429,6 +431,7 @@ describe('notifications reducer', () => {
created_at: '2020-06-10T02:05:06.000Z',
status: '9vvNxoo5EFbbnfdXQu',
emoji: undefined,
+ chat_message: undefined,
is_seen: true,
}),
]),
@@ -445,6 +448,7 @@ describe('notifications reducer', () => {
created_at: '2020-06-10T02:54:39.000Z',
status: '9vvNxoo5EFbbnfdXQu',
emoji: 'π’',
+ chat_message: undefined,
is_seen: false,
}),
ImmutableMap({
@@ -454,6 +458,7 @@ describe('notifications reducer', () => {
created_at: '2020-06-10T02:51:05.000Z',
status: '9vvNxoo5EFbbnfdXQu',
emoji: undefined,
+ chat_message: undefined,
is_seen: true,
}),
ImmutableMap({
@@ -463,6 +468,7 @@ describe('notifications reducer', () => {
created_at: '2020-06-10T02:05:06.000Z',
status: '9vvNxoo5EFbbnfdXQu',
emoji: undefined,
+ chat_message: undefined,
is_seen: true,
}),
]),
@@ -480,6 +486,7 @@ describe('notifications reducer', () => {
created_at: '2020-06-10T02:51:05.000Z',
status: '9vvNxoo5EFbbnfdXQu',
emoji: undefined,
+ chat_message: undefined,
is_seen: true,
}),
ImmutableMap({
@@ -489,6 +496,7 @@ describe('notifications reducer', () => {
created_at: '2020-06-10T02:05:06.000Z',
status: '9vvNxoo5EFbbnfdXQu',
emoji: undefined,
+ chat_message: undefined,
is_seen: true,
}),
]),
@@ -533,6 +541,7 @@ describe('notifications reducer', () => {
created_at: '2020-06-10T02:54:39.000Z',
status: '9vvNxoo5EFbbnfdXQu',
emoji: 'π’',
+ chat_message: undefined,
is_seen: false,
}),
ImmutableMap({
@@ -542,6 +551,7 @@ describe('notifications reducer', () => {
created_at: '2020-06-10T02:51:05.000Z',
status: '9vvNxoo5EFbbnfdXQu',
emoji: undefined,
+ chat_message: undefined,
is_seen: true,
}),
ImmutableMap({
@@ -551,6 +561,7 @@ describe('notifications reducer', () => {
created_at: '2020-06-10T02:05:06.000Z',
status: '9vvNxoo5EFbbnfdXQu',
emoji: undefined,
+ chat_message: undefined,
is_seen: true,
}),
]),
@@ -574,6 +585,7 @@ describe('notifications reducer', () => {
created_at: '2020-06-10T02:54:39.000Z',
status: '9vvNxoo5EFbbnfdXQu',
emoji: 'π’',
+ chat_message: undefined,
is_seen: false,
}),
ImmutableMap({
@@ -583,6 +595,7 @@ describe('notifications reducer', () => {
created_at: '2020-06-10T02:51:05.000Z',
status: '9vvNxoo5EFbbnfdXQu',
emoji: undefined,
+ chat_message: undefined,
is_seen: true,
}),
ImmutableMap({
@@ -592,6 +605,7 @@ describe('notifications reducer', () => {
created_at: '2020-06-10T02:05:06.000Z',
status: '9vvNxoo5EFbbnfdXQu',
emoji: undefined,
+ chat_message: undefined,
is_seen: true,
}),
]),
@@ -610,6 +624,7 @@ describe('notifications reducer', () => {
created_at: '2020-06-10T02:54:39.000Z',
status: '9vvNxoo5EFbbnfdXQu',
emoji: 'π’',
+ chat_message: undefined,
is_seen: false,
}),
ImmutableMap({
@@ -619,6 +634,7 @@ describe('notifications reducer', () => {
created_at: '2020-06-10T02:51:05.000Z',
status: '9vvNxoo5EFbbnfdXQu',
emoji: undefined,
+ chat_message: undefined,
is_seen: true,
}),
ImmutableMap({
@@ -628,6 +644,7 @@ describe('notifications reducer', () => {
created_at: '2020-06-10T02:05:06.000Z',
status: '9vvNxoo5EFbbnfdXQu',
emoji: undefined,
+ chat_message: undefined,
is_seen: true,
}),
]),
diff --git a/app/soapbox/reducers/accounts.js b/app/soapbox/reducers/accounts.js
index fd915d7ff..b02ef763f 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, CHAT_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,11 @@ export default function accounts(state = initialState, action) {
return state.set(-1, ImmutableMap({
username: action.username,
}));
+ case CHATS_FETCH_SUCCESS:
+ return importAccountsFromChats(state, action.chats);
+ case CHAT_FETCH_SUCCESS:
+ case STREAMING_CHAT_UPDATE:
+ return importAccountsFromChats(state, [action.chat]);
default:
return state;
}
diff --git a/app/soapbox/reducers/chat_message_lists.js b/app/soapbox/reducers/chat_message_lists.js
new file mode 100644
index 000000000..0d635070f
--- /dev/null
+++ b/app/soapbox/reducers/chat_message_lists.js
@@ -0,0 +1,53 @@
+import {
+ CHATS_FETCH_SUCCESS,
+ CHAT_MESSAGES_FETCH_SUCCESS,
+ CHAT_MESSAGE_SEND_REQUEST,
+ CHAT_MESSAGE_SEND_SUCCESS,
+} from 'soapbox/actions/chats';
+import { STREAMING_CHAT_UPDATE } from 'soapbox/actions/streaming';
+import { Map as ImmutableMap, OrderedSet as ImmutableOrderedSet } from 'immutable';
+
+const initialState = ImmutableMap();
+
+const updateList = (state, chatId, messageIds) => {
+ const ids = state.get(chatId, ImmutableOrderedSet());
+ const newIds = ids.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)))
+);
+
+const importLastMessages = (state, chats) =>
+ state.withMutations(mutable =>
+ chats.forEach(chat => {
+ if (chat.last_message) importMessage(mutable, chat.last_message);
+ }));
+
+export default function chatMessageLists(state = initialState, action) {
+ switch(action.type) {
+ case CHAT_MESSAGE_SEND_REQUEST:
+ return updateList(state, action.chatId, [action.uuid]).sort();
+ case CHATS_FETCH_SUCCESS:
+ return importLastMessages(state, action.chats).sort();
+ case STREAMING_CHAT_UPDATE:
+ if (action.chat.last_message &&
+ action.chat.last_message.account_id !== action.me)
+ return importMessages(state, [action.chat.last_message]).sort();
+ else
+ return state;
+ case CHAT_MESSAGES_FETCH_SUCCESS:
+ return updateList(state, action.chatId, action.chatMessages.map(chat => chat.id).reverse()).sort();
+ case CHAT_MESSAGE_SEND_SUCCESS:
+ return updateList(state, action.chatId, [action.chatMessage.id]).sort();
+ default:
+ return state;
+ }
+};
diff --git a/app/soapbox/reducers/chat_messages.js b/app/soapbox/reducers/chat_messages.js
new file mode 100644
index 000000000..74d83ef79
--- /dev/null
+++ b/app/soapbox/reducers/chat_messages.js
@@ -0,0 +1,49 @@
+import {
+ CHATS_FETCH_SUCCESS,
+ CHAT_MESSAGES_FETCH_SUCCESS,
+ CHAT_MESSAGE_SEND_REQUEST,
+ CHAT_MESSAGE_SEND_SUCCESS,
+} from 'soapbox/actions/chats';
+import { STREAMING_CHAT_UPDATE } from 'soapbox/actions/streaming';
+import { Map as ImmutableMap, fromJS } from 'immutable';
+
+const initialState = ImmutableMap();
+
+const importMessage = (state, message) => {
+ return state.set(message.get('id'), message);
+};
+
+const importMessages = (state, messages) =>
+ state.withMutations(mutable =>
+ messages.forEach(message => importMessage(mutable, message)));
+
+const importLastMessages = (state, chats) =>
+ state.withMutations(mutable =>
+ chats.forEach(chat => {
+ if (chat.get('last_message'))
+ importMessage(mutable, chat.get('last_message'));
+ }));
+
+export default function chatMessages(state = initialState, action) {
+ switch(action.type) {
+ case CHAT_MESSAGE_SEND_REQUEST:
+ return importMessage(state, fromJS({
+ id: action.uuid, // Make fake message to get overriden later
+ chat_id: action.chatId,
+ account_id: action.me,
+ content: action.params.content,
+ created_at: (new Date()).toISOString(),
+ pending: true,
+ }));
+ case CHATS_FETCH_SUCCESS:
+ return importLastMessages(state, fromJS(action.chats));
+ case CHAT_MESSAGES_FETCH_SUCCESS:
+ return importMessages(state, fromJS(action.chatMessages));
+ case CHAT_MESSAGE_SEND_SUCCESS:
+ return importMessage(state, fromJS(action.chatMessage)).delete(action.uuid);
+ case STREAMING_CHAT_UPDATE:
+ return importLastMessages(state, fromJS([action.chat]));
+ default:
+ return state;
+ }
+};
diff --git a/app/soapbox/reducers/chats.js b/app/soapbox/reducers/chats.js
new file mode 100644
index 000000000..0efb5361f
--- /dev/null
+++ b/app/soapbox/reducers/chats.js
@@ -0,0 +1,33 @@
+import {
+ CHATS_FETCH_SUCCESS,
+ CHAT_FETCH_SUCCESS,
+ CHAT_READ_SUCCESS,
+ CHAT_READ_REQUEST,
+} 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(normalizeChat(chat)));
+
+const importChats = (state, chats) =>
+ state.withMutations(mutable => chats.forEach(chat => importChat(mutable, chat)));
+
+const initialState = ImmutableMap();
+
+export default function chats(state = initialState, action) {
+ switch(action.type) {
+ case CHATS_FETCH_SUCCESS:
+ return importChats(state, action.chats);
+ case STREAMING_CHAT_UPDATE:
+ return importChats(state, [action.chat]);
+ case CHAT_FETCH_SUCCESS:
+ return importChats(state, [action.chat]);
+ case CHAT_READ_REQUEST:
+ return state.setIn([action.chatId, 'unread'], 0);
+ case CHAT_READ_SUCCESS:
+ return importChats(state, [action.chat]);
+ default:
+ return state;
+ }
+};
diff --git a/app/soapbox/reducers/index.js b/app/soapbox/reducers/index.js
index 7a8219522..2caec469a 100644
--- a/app/soapbox/reducers/index.js
+++ b/app/soapbox/reducers/index.js
@@ -43,6 +43,9 @@ import instance from './instance';
import me from './me';
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,
@@ -89,6 +92,9 @@ const reducers = {
me,
auth,
admin,
+ chats,
+ chat_messages,
+ chat_message_lists,
};
export default combineReducers(reducers);
diff --git a/app/soapbox/reducers/notifications.js b/app/soapbox/reducers/notifications.js
index cbf9ef5d5..c4c97c73f 100644
--- a/app/soapbox/reducers/notifications.js
+++ b/app/soapbox/reducers/notifications.js
@@ -15,14 +15,9 @@ import {
ACCOUNT_BLOCK_SUCCESS,
ACCOUNT_MUTE_SUCCESS,
} from '../actions/accounts';
-import {
- FETCH_MARKERS_SUCCESS,
- SAVE_MARKERS_SUCCESS,
-} from '../actions/markers';
import { TIMELINE_DELETE, TIMELINE_DISCONNECT } from '../actions/timelines';
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
import compareId from '../compare_id';
-import { fromJS } from 'immutable';
import { get } from 'lodash';
const initialState = ImmutableMap({
@@ -43,6 +38,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),
});
@@ -136,13 +132,6 @@ const updateNotificationsQueue = (state, notification, intlMessages, intlLocale)
export default function notifications(state = initialState, action) {
switch(action.type) {
- case FETCH_MARKERS_SUCCESS:
- case SAVE_MARKERS_SUCCESS:
- const prevRead = state.get('lastRead');
- const marker = fromJS(action.markers);
- const unread = marker.getIn(['notifications', 'pleroma', 'unread_count'], state.get('unread', 0));
- const lastRead = marker.getIn(['notifications', 'last_read_id'], prevRead);
- return state.merge({ unread, lastRead });
case NOTIFICATIONS_EXPAND_REQUEST:
return state.set('isLoading', true);
case NOTIFICATIONS_EXPAND_FAIL:
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:
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);
+ });
+ }
+ );
+};
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/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/basics.scss b/app/styles/basics.scss
index 53119ffc4..092f06202 100644
--- a/app/styles/basics.scss
+++ b/app/styles/basics.scss
@@ -227,5 +227,9 @@ noscript {
bottom: 0;
left: 0;
position: absolute;
- z-index: 9999;
+ z-index: 201;
+ background: transparent;
+ border: 0;
+ margin: 0;
+ padding: 0;
}
diff --git a/app/styles/chats.scss b/app/styles/chats.scss
new file mode 100644
index 000000000..f1b443b5b
--- /dev/null
+++ b/app/styles/chats.scss
@@ -0,0 +1,255 @@
+.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;
+ width: 265px;
+ height: 265px;
+ max-height: calc(100vh - 70px);
+ display: flex;
+ flex-direction: column;
+ z-index: 999;
+ transition: 0.05s;
+
+ &--main {
+ height: calc(100vh - 70px);
+
+ .pane__header .pane__title {
+ font-size: 16px;
+ }
+ }
+
+ &--minimized {
+ height: 31px;
+ }
+
+ &__header {
+ box-sizing: border-box;
+ background: var(--brand-color);
+ color: #fff;
+ padding: 0 10px;
+ font-weight: bold;
+ border-radius: 6px 6px 0 0;
+ display: flex;
+ align-items: center;
+ height: 31px;
+
+ .account__avatar {
+ margin-right: 7px;
+ }
+
+ .pane__title {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ flex: 1;
+ height: 100%;
+ background: transparent;
+ border: 0;
+ padding: 0;
+ color: #fff;
+ font-weight: bold;
+ text-align: left;
+ font-size: 14px;
+ }
+
+ .icon-button {
+ color: #fff;
+
+ > div {
+ height: auto !important;
+ width: auto !important;
+ margin-right: -6px;
+ }
+ }
+
+ .pane__close {
+ margin-left: auto;
+ }
+
+ .icon-with-badge__badge {
+ position: static;
+ pointer-events: none;
+ width: 18px;
+ height: 18px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-right: 7px;
+ }
+ }
+
+ &__content {
+ background: var(--foreground-color);
+ display: flex;
+ flex: 1;
+ flex-direction: column;
+ overflow: hidden;
+
+ .chat-box {
+ display: flex;
+ flex: 1;
+ flex-direction: column;
+ overflow: hidden;
+ }
+ }
+}
+
+.chat-messages {
+ overflow-y: scroll;
+ flex: 1;
+}
+
+.chat-message {
+ margin: 14px 10px;
+ display: flex;
+
+ &__bubble {
+ font-size: 15px;
+ padding: 4px 10px;
+ max-width: 70%;
+ border-radius: 10px;
+ background-color: var(--background-color);
+ overflow: hidden;
+ text-overflow: ellipsis;
+
+ a {
+ color: var(--brand-color--hicontrast);
+ }
+ }
+
+ &--me .chat-message__bubble {
+ margin-left: auto;
+ background-color: hsla(var(--brand-color_hsl), 0.2);
+ }
+
+ &--pending .chat-message__bubble {
+ opacity: 0.5;
+ }
+}
+
+.chat-list {
+ overflow-y: auto;
+ flex: 1;
+
+ &__content {
+ height: 100%;
+ }
+
+ .empty-column-indicator {
+ height: 100%;
+ box-sizing: border-box;
+ background: transparent;
+ align-items: start;
+ }
+
+ .account__display-name {
+ position: relative;
+ }
+
+ .icon-with-badge__badge {
+ top: 0;
+ right: 0;
+ left: auto;
+ bottom: auto;
+ }
+}
+
+.chat-box {
+ &__actions {
+ background: var(--foreground-color);
+ margin-top: auto;
+ padding: 6px;
+
+ textarea {
+ width: 100%;
+ height: 100%;
+ margin: 0;
+ box-sizing: border-box;
+ padding: 6px;
+ background: var(--background-color);
+ border: 0;
+ border-radius: 6px;
+ color: var(--primary-text-color);
+ font-size: 15px;
+ }
+ }
+}
+
+.ui--chatroom {
+ padding-bottom: 0;
+
+ .columns-area__panels__main .columns-area {
+ height: calc(100vh - 100px);
+ box-sizing: border-box;
+ overflow: hidden;
+
+ @media(max-width: 630px) {
+ height: calc(100vh - 50px);
+ }
+ }
+
+ .page {
+ .chat-box {
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+ flex: 1;
+ border-radius: 0 0 10px 10px;
+
+ &__actions textarea {
+ padding: 10px;
+ }
+ }
+ }
+}
+
+@media(max-width: 630px) {
+ .chat-panes {
+ display: none;
+ }
+}
+
+@media(min-width: 630px) {
+ .tabs-bar .tabs-bar__link--chats {
+ display: none;
+ }
+}
+
+.chatroom__header {
+ display: flex;
+ margin-left: auto;
+ padding-right: 15px;
+ overflow: hidden;
+
+ .account__avatar {
+ margin-right: 7px;
+ }
+
+ .chatroom__title {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ flex: 1;
+ height: 100%;
+ background: transparent;
+ border: 0;
+ padding: 0;
+ color: #fff;
+ font-weight: bold;
+ text-align: left;
+ font-size: 14px;
+ }
+}
+
+.chatroom__back {
+ display: flex;
+ align-items: center;
+ background: var(--accent-color--faint);
+ border-radius: 10px 10px 0 0;
+
+ .column-back-button {
+ background: transparent;
+ }
+}
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;
diff --git a/app/styles/components/profile_hover_card.scss b/app/styles/components/profile_hover_card.scss
index 3293eeb8f..decb68711 100644
--- a/app/styles/components/profile_hover_card.scss
+++ b/app/styles/components/profile_hover_card.scss
@@ -14,7 +14,7 @@
transition-property: opacity;
transition-duration: 0.2s;
width: 320px;
- z-index: 998;
+ z-index: 200;
left: -10px;
padding: 20px;
margin-bottom: 10px;
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;
}
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;