Skip to content

Commit 1336424

Browse files
committed
feat: rework conversations ui (#443)
1 parent c6532fc commit 1336424

File tree

18 files changed

+94
-146
lines changed

18 files changed

+94
-146
lines changed

app/soapbox/__fixtures__/intlMessages.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@
6060
"bundle_modal_error.retry": "Try again",
6161
"column.blocks": "Blocked users",
6262
"column.community": "Local timeline",
63-
"column.direct": "Direct messages",
63+
"column.direct": "Conversations",
6464
"column.domain_blocks": "Hidden domains",
6565
"column.edit_profile": "Edit profile",
6666
"column.filters": "Muted words",
@@ -538,7 +538,7 @@
538538
"bundle_modal_error.retry": "Try again",
539539
"column.blocks": "Blocked users",
540540
"column.community": "Local timeline",
541-
"column.direct": "Direct messages",
541+
"column.direct": "Conversations",
542542
"column.domain_blocks": "Hidden domains",
543543
"column.edit_profile": "Edit profile",
544544
"column.filters": "Muted words",

app/soapbox/actions/compose.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -212,26 +212,25 @@ const mentionCompose = (account: Account) =>
212212
dispatch(openModal('COMPOSE'));
213213
};
214214

215-
const directCompose = (account: Account) =>
215+
const directCompose = (history: History, account: Account) =>
216216
(dispatch: AppDispatch) => {
217+
dispatch(resetCompose());
217218
dispatch({
218219
type: COMPOSE_DIRECT,
219220
account: account,
220221
});
221-
222-
dispatch(openModal('COMPOSE'));
222+
history.push('/statuses/compose');
223223
};
224224

225-
const directComposeById = (accountId: string) =>
225+
const directComposeById = (history: History, accountId: string) =>
226226
(dispatch: AppDispatch, getState: () => RootState) => {
227227
const account = getState().accounts.get(accountId);
228-
228+
dispatch(resetCompose());
229229
dispatch({
230230
type: COMPOSE_DIRECT,
231231
account: account,
232232
});
233-
234-
dispatch(openModal('COMPOSE'));
233+
history.push('/statuses/compose');
235234
};
236235

237236
const handleComposeSubmit = (dispatch: AppDispatch, getState: () => RootState, data: APIEntity, status: string, edit?: boolean) => {

app/soapbox/actions/timelines.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -204,9 +204,6 @@ const expandRemoteTimeline = (instance: string, { maxId, onlyMedia, excludeRepli
204204
const expandCommunityTimeline = ({ maxId, onlyMedia, excludeReplies }: Record<string, any> = {}, done = noOp) =>
205205
expandTimeline(`community${onlyMedia ? ':media' : ''}${excludeReplies ? ':exclude_replies' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia, exclude_replies: excludeReplies }, done);
206206

207-
const expandDirectTimeline = ({ maxId }: Record<string, any> = {}, done = noOp) =>
208-
expandTimeline('direct', '/api/v1/timelines/direct', { max_id: maxId }, done);
209-
210207
const expandAccountTimeline = (accountId: string, { maxId, withReplies }: Record<string, any> = {}) =>
211208
expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, max_id: maxId, with_muted: true });
212209

@@ -307,7 +304,6 @@ export {
307304
expandPublicTimeline,
308305
expandRemoteTimeline,
309306
expandCommunityTimeline,
310-
expandDirectTimeline,
311307
expandAccountTimeline,
312308
expandAccountFeaturedTimeline,
313309
expandAccountMediaTimeline,

app/soapbox/components/sidebar-navigation.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -114,11 +114,11 @@ const SidebarNavigation = () => {
114114
/>
115115

116116
{
117-
(features.directTimeline || features.conversations) && (
117+
(features.conversations) && (
118118
<SidebarNavigationLink
119-
to='/messages'
120-
icon={require('@tabler/icons/mail.svg')}
121-
text={<FormattedMessage id='column.direct' defaultMessage='Direct messages' />}
119+
to='/conversations'
120+
icon={require('@tabler/icons/messages.svg')}
121+
text={<FormattedMessage id='column.direct' defaultMessage='Conversations' />}
122122
/>
123123
)
124124
}

app/soapbox/components/sidebar_menu.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ const messages = defineMessages({
3434
invites: { id: 'navigation_bar.invites', defaultMessage: 'Invites' },
3535
developers: { id: 'navigation.developers', defaultMessage: 'Developers' },
3636
addAccount: { id: 'profile_dropdown.add_account', defaultMessage: 'Add an existing account' },
37-
direct: { id: 'column.direct', defaultMessage: 'Direct messages' },
37+
direct: { id: 'column.direct', defaultMessage: 'Conversations' },
3838
directory: { id: 'navigation_bar.profile_directory', defaultMessage: 'Profile directory' },
3939
dashboard: { id: 'tabs_bar.dashboard', defaultMessage: 'Dashboard' },
4040
tags: { id: 'navigation_bar.tags', defaultMessage: 'Hashtags' },
@@ -278,12 +278,12 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
278278
/>
279279

280280
{
281-
(features.directTimeline || features.conversations) && (
281+
(features.conversations) && (
282282
<SidebarLink
283283
onClick={onClose}
284-
to='/messages'
285-
icon={require('@tabler/icons/mail.svg')}
286-
text={<FormattedMessage id='column.direct' defaultMessage='Direct messages' />}
284+
to='/conversations'
285+
icon={require('@tabler/icons/messages.svg')}
286+
text={<FormattedMessage id='column.direct' defaultMessage='Conversations' />}
287287
/>
288288
)
289289
}

app/soapbox/components/status-action-bar.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,8 +144,8 @@ const StatusActionBarMenu: React.FC<IStatusActionBarMenu> = ({ status, withDismi
144144

145145
const handleDirectClick = React.useCallback<React.EventHandler<React.MouseEvent>>((e) => {
146146
e.stopPropagation();
147-
dispatch(directCompose(status.account as Account));
148-
}, [dispatch, status.account]);
147+
dispatch(directCompose(history, status.account as Account));
148+
}, [dispatch, history, status.account]);
149149

150150
const handleMuteClick = React.useCallback<React.EventHandler<React.MouseEvent>>((e) => {
151151
e.stopPropagation();

app/soapbox/features/account/components/header.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ const Header: React.FC<IHeader> = ({ account }) => {
134134
}, [account, dispatch]);
135135

136136
const onDirect = useCallback(() => {
137-
dispatch(directCompose(account));
137+
dispatch(directCompose(history, account));
138138
}, [account, dispatch]);
139139

140140
const onReblogToggle = useCallback(() => {

app/soapbox/features/conversations/components/conversation.tsx

Lines changed: 45 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,84 @@
1-
import React from 'react';
1+
import React, { useCallback, useMemo } from 'react';
2+
import { HotKeys } from 'react-hotkeys';
23
import { useHistory } from 'react-router-dom';
34

45
import { markConversationRead } from 'soapbox/actions/conversations';
5-
import StatusContainer from 'soapbox/containers/status_container';
6+
import Account from 'soapbox/components/account';
7+
import { Icon } from 'soapbox/components/ui';
68
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
9+
import { Account as AccountEntity, Status } from 'soapbox/types/entities';
710

811
interface IConversation {
12+
className?: string,
913
conversationId: string,
1014
onMoveUp: (id: string) => void,
1115
onMoveDown: (id: string) => void,
1216
}
1317

14-
const Conversation: React.FC<IConversation> = ({ conversationId, onMoveUp, onMoveDown }) => {
18+
const Conversation: React.FC<IConversation> = ({ conversationId, onMoveUp, onMoveDown, className }) => {
1519
const dispatch = useAppDispatch();
1620
const history = useHistory();
1721

18-
const { accounts, unread, lastStatusId } = useAppSelector((state) => {
22+
const { accounts, unread, lastStatusId, lastStatus } = useAppSelector((state) => {
1923
const conversation = state.conversations.items.find(x => x.id === conversationId)!;
2024

2125
return {
22-
accounts: conversation.accounts.map((accountId: string) => state.accounts.get(accountId, null)!),
26+
accounts: conversation.accounts.map((accountId: string) => state.accounts.get<AccountEntity>(accountId, null)!),
2327
unread: conversation.unread,
28+
lastStatus: state.statuses.get<Status>(conversation.last_status, null)!,
2429
lastStatusId: conversation.last_status || null,
2530
};
2631
});
2732

28-
const handleClick = () => {
33+
const handleClick = useCallback(() => {
2934
if (unread) {
3035
dispatch(markConversationRead(conversationId));
3136
}
32-
3337
history.push(`/statuses/${lastStatusId}`);
34-
};
38+
}, [conversationId, dispatch, history, lastStatusId, unread]);
3539

36-
const handleHotkeyMoveUp = () => {
40+
const handleHotkeyMoveUp = useCallback(() => {
3741
onMoveUp(conversationId);
38-
};
42+
}, [conversationId, onMoveUp]);
3943

40-
const handleHotkeyMoveDown = () => {
44+
const handleHotkeyMoveDown = useCallback(() => {
4145
onMoveDown(conversationId);
42-
};
46+
}, [conversationId, onMoveDown]);
47+
48+
const handlers = useMemo(() => {
49+
return {
50+
open: handleClick,
51+
moveUp: handleHotkeyMoveUp,
52+
moveDown: handleHotkeyMoveDown,
53+
};
54+
}, [handleClick, handleHotkeyMoveDown, handleHotkeyMoveUp]);
4355

4456
if (lastStatusId === null) {
4557
return null;
4658
}
4759

4860
return (
49-
// @ts-ignore
50-
<StatusContainer
51-
id={lastStatusId}
52-
unread={unread}
53-
otherAccounts={accounts}
54-
onMoveUp={handleHotkeyMoveUp}
55-
onMoveDown={handleHotkeyMoveDown}
56-
onClick={handleClick}
57-
/>
61+
<HotKeys handlers={handlers} data-testid='status'>
62+
<div onClick={handleClick} role='button' tabIndex={0} className={`bg-white dark:bg-slate-800 px-4 py-6 sm:shadow-sm dark:shadow-inset sm:p-5 sm:rounded-xl ${className}`}>
63+
<div className='flex justify-between gap-3'>
64+
<div className='flex gap-2 grow'>
65+
{accounts.map((a) => <Account withLinkToProfile={false} account={a} />)}
66+
</div>
67+
<Icon
68+
src={require('@tabler/icons/messages.svg')}
69+
count={unread ? 1 : 0}
70+
className='h-6 w-6 dark:group-hover:text-primary-500'
71+
/>
72+
</div>
73+
<div className='flex justify-between mt-3 items-end gap-2'>
74+
<div style={{ 'whiteSpace': 'nowrap' }} className='overflow-x-hidden text-ellipsis text-gray-700 dark:text-gray-300' dangerouslySetInnerHTML={{ __html: lastStatus.contentHtml.replace(/<br.*>/, '') }} />
75+
<div className='text-gray-300 dark:text-gray-300 text-xs'>
76+
{new Date(lastStatus.get('created_at')).toLocaleDateString(undefined, { day: '2-digit', month: '2-digit', year: 'numeric' })}
77+
</div>
78+
</div>
79+
80+
</div>
81+
</HotKeys>
5882
);
5983
};
6084

app/soapbox/features/conversations/components/conversations_list.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,12 @@ const ConversationsList: React.FC = () => {
5555
scrollKey='direct'
5656
ref={ref}
5757
isLoading={isLoading}
58-
showLoading={isLoading && conversations.size === 0}
58+
showLoading={isLoading}
5959
emptyMessage={<FormattedMessage id='empty_column.direct' defaultMessage="You don't have any direct messages yet. When you send or receive one, it will show up here." />}
6060
>
6161
{conversations.map((item: any) => (
6262
<Conversation
63+
className='my-3'
6364
key={item.id}
6465
conversationId={item.id}
6566
onMoveUp={handleMoveUp}

app/soapbox/features/conversations/index.tsx

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,26 @@
11
import React, { useEffect } from 'react';
22
import { defineMessages, useIntl } from 'react-intl';
3+
import { useHistory } from 'react-router-dom';
34

45
import { directComposeById } from 'soapbox/actions/compose';
56
import { mountConversations, unmountConversations, expandConversations } from 'soapbox/actions/conversations';
67
import { connectDirectStream } from 'soapbox/actions/streaming';
78
import AccountSearch from 'soapbox/components/account_search';
9+
import SubNavigation from 'soapbox/components/sub_navigation';
810
import { Column } from 'soapbox/components/ui';
911
import { useAppDispatch } from 'soapbox/hooks';
1012

1113
import ConversationsList from './components/conversations_list';
1214

1315
const messages = defineMessages({
14-
title: { id: 'column.direct', defaultMessage: 'Direct messages' },
16+
title: { id: 'column.direct', defaultMessage: 'Conversations' },
1517
searchPlaceholder: { id: 'direct.search_placeholder', defaultMessage: 'Send a message to…' },
1618
});
1719

1820
const ConversationsTimeline = () => {
1921
const intl = useIntl();
2022
const dispatch = useAppDispatch();
23+
const history = useHistory();
2124

2225
useEffect(() => {
2326
dispatch(mountConversations());
@@ -29,19 +32,24 @@ const ConversationsTimeline = () => {
2932
dispatch(unmountConversations());
3033
disconnect();
3134
};
32-
}, []);
35+
}, [dispatch]);
3336

3437
const handleSuggestion = (accountId: string) => {
35-
dispatch(directComposeById(accountId));
38+
dispatch(directComposeById(history, accountId));
3639
};
3740

3841
return (
39-
<Column label={intl.formatMessage(messages.title)}>
40-
<AccountSearch
41-
placeholder={intl.formatMessage(messages.searchPlaceholder)}
42-
onSelected={handleSuggestion}
43-
/>
44-
<ConversationsList />
42+
<Column label={intl.formatMessage(messages.title)} transparent withHeader={false}>
43+
<div className='px-4 pt-4 pb-8 sm:px-0 sm:pt-0'>
44+
<SubNavigation message={intl.formatMessage(messages.title)} />
45+
<div className='flex flex-col gap-3'>
46+
<AccountSearch
47+
placeholder={intl.formatMessage(messages.searchPlaceholder)}
48+
onSelected={handleSuggestion}
49+
/>
50+
<ConversationsList />
51+
</div>
52+
</div>
4553
</Column>
4654
);
4755
};

0 commit comments

Comments
 (0)