Skip to content
This repository was archived by the owner on Oct 11, 2022. It is now read-only.

Commit 69052f0

Browse files
authored
Merge pull request #3394 from withspectrum/2.4.13
2.4.13
2 parents 4134b69 + 6b888cd commit 69052f0

File tree

21 files changed

+579
-473
lines changed

21 files changed

+579
-473
lines changed

api/queries/directMessageThread/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
// @flow
22
import directMessageThread from './rootDirectMessageThread';
3+
import directMessageThreadByUserId from './rootDirectMessageThreadByUserId';
34
import messageConnection from './messageConnection';
45
import participants from './participants';
56
import snippet from './snippet';
67

78
module.exports = {
89
Query: {
910
directMessageThread,
11+
directMessageThreadByUserId,
1012
},
1113
DirectMessageThread: {
1214
messageConnection,
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// @flow
2+
import type { GraphQLContext } from '../../';
3+
import { checkForExistingDMThread } from '../../models/directMessageThread';
4+
import { isAuthedResolver as requireAuth } from '../../utils/permissions';
5+
6+
type Args = {
7+
userId: string,
8+
};
9+
10+
export default requireAuth(async (_: any, args: Args, ctx: GraphQLContext) => {
11+
// signed out users will never be able to view a dm thread
12+
const { user: currentUser, loaders } = ctx;
13+
const { userId } = args;
14+
15+
const allMemberIds = [userId, currentUser.id];
16+
const existingThread = await checkForExistingDMThread(allMemberIds);
17+
18+
if (!existingThread) return null;
19+
20+
return loaders.directMessageThread.load(existingThread);
21+
});

api/types/DirectMessageThread.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ const DirectMessageThread = /* GraphQL */ `
3131
3232
extend type Query {
3333
directMessageThread(id: ID!): DirectMessageThread
34+
directMessageThreadByUserId(userId: ID!): DirectMessageThread
3435
}
3536
3637
enum MessageType {

config-overrides.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,10 +104,13 @@ module.exports = function override(config, env) {
104104
});
105105
config.plugins.push(
106106
new OfflinePlugin({
107-
appShell: '/index.html',
108107
caches: process.env.NODE_ENV === 'development' ? {} : 'all',
109108
externals,
110109
autoUpdate: true,
110+
// NOTE(@mxstbr): Normally this is handled by setting
111+
// appShell: './index.html'
112+
// but we don't want to serve the app shell for the `/api` and `/auth` routes
113+
// which means we have to manually do this and filter any of those routes out
111114
cacheMaps: [
112115
{
113116
match: function(url) {
@@ -118,8 +121,12 @@ module.exports = function override(config, env) {
118121
})
119122
)
120123
return false;
121-
return url;
124+
// This function will be stringified and injected into the ServiceWorker on the client, where
125+
// location will be a thing
126+
// eslint-disable-next-line no-restricted-globals
127+
return new URL('./index.html', location);
122128
},
129+
requestTypes: ['navigate'],
123130
},
124131
],
125132
rewrites: arg => arg,

mobile/App.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import Toasts from './components/Toasts';
1515
import theme from '../shared/theme';
1616
import { createClient } from '../shared/graphql';
1717
import Login from './views/Login';
18-
import TabBar from './views/TabBar';
18+
import TabBar from './views/TabBar/App';
1919
import { SetUsername, ExploreCommunities } from './views/UserOnboarding';
2020
import { authenticate } from './actions/authentication';
2121

mobile/components/Lists/ThreadListItem/index.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,11 @@ class ThreadListItemHandlers extends Component<Props> {
161161
const canModerateCommunity = isCommunityModerator || isCommunityOwner;
162162

163163
let options, cancelButtonIndex, destructiveButtonIndex;
164-
options = [SHARE, MESSAGE_AUTHOR];
164+
options = [SHARE];
165+
166+
if (thread.author.user.id !== currentUser.id) {
167+
options = options.concat(MESSAGE_AUTHOR);
168+
}
165169

166170
options = options.concat(
167171
thread.receiveNotifications ? MUTE_CONVERSATION : SUBSCRIBE_CONVERSATION
@@ -211,7 +215,7 @@ class ThreadListItemHandlers extends Component<Props> {
211215
routeName: 'DirectMessageComposer',
212216
key: thread.author.user.id,
213217
params: {
214-
presetUserIds: [thread.author.user.id],
218+
presetUserId: thread.author.user.id,
215219
},
216220
});
217221
}

mobile/components/Toasts/style.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export const Container = styled.View`
1212
left: 0;
1313
right: 0;
1414
width: 100%;
15-
height: 40px;
15+
height: 48px;
1616
z-index: 1;
1717
overflow: hidden;
1818
`;
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
// @flow
2+
import React from 'react';
3+
import compose from 'recompose/compose';
4+
import type { NavigationProps } from 'react-navigation';
5+
import { withCurrentUser } from '../../components/WithCurrentUser';
6+
import { throttle, debounce } from 'throttle-debounce';
7+
import withSafeView from '../../components/SafeAreaView';
8+
import ChatInput from '../../components/ChatInput';
9+
import PeopleSearchView from '../Search/PeopleSearchView';
10+
import getCurrentUserDMThreadConnection from '../../../shared/graphql/queries/directMessageThread/getCurrentUserDMThreadConnection';
11+
import createDirectMessageThread, {
12+
type CreateDirectMessageThreadType,
13+
type CreateDirectMessageThreadProps,
14+
} from '../../../shared/graphql/mutations/directMessageThread/createDirectMessageThread';
15+
import type { GetUserType } from '../../../shared/graphql/queries/user/getUser';
16+
import {
17+
ComposerWrapper,
18+
SearchInputArea,
19+
SelectedUsers,
20+
UserSearchInput,
21+
} from './style';
22+
import { events, track } from '../../utils/analytics';
23+
import { FullscreenNullState } from '../../components/NullStates';
24+
import SelectedUser from './SelectedUser';
25+
26+
type Props = {
27+
...$Exact<NavigationProps>,
28+
...$Exact<CreateDirectMessageThreadProps>,
29+
currentUser: GetUserType,
30+
presetUserId?: string,
31+
};
32+
33+
type State = {
34+
wipSearchString: ?string,
35+
searchString: ?string,
36+
selectedUsers: string[],
37+
};
38+
39+
class DirectMessageComposer extends React.Component<Props, State> {
40+
state = {
41+
wipSearchString: '',
42+
searchString: '',
43+
selectedUsers: [],
44+
};
45+
46+
searchInput: ?{
47+
focus: () => void,
48+
};
49+
50+
constructor() {
51+
super();
52+
this.searchDebounced = debounce(300, this.searchDebounced);
53+
this.searchThrottled = throttle(300, this.searchThrottled);
54+
}
55+
56+
componentDidMount() {
57+
track(events.DIRECT_MESSAGE_THREAD_COMPOSER_VIEWED);
58+
59+
const { presetUserId } = this.props;
60+
61+
if (presetUserId) {
62+
this.setState({
63+
selectedUsers: [presetUserId],
64+
});
65+
}
66+
}
67+
68+
onChangeText = (text: string) => {
69+
this.setState({
70+
wipSearchString: text,
71+
});
72+
};
73+
74+
onChangeText = (text: string) => {
75+
this.setState({ wipSearchString: text }, () => {
76+
const { wipSearchString } = this.state;
77+
if (wipSearchString && wipSearchString.length < 5) {
78+
this.searchThrottled(wipSearchString);
79+
} else {
80+
this.searchDebounced(wipSearchString);
81+
}
82+
});
83+
};
84+
85+
onFinishTyping = (e: { nativeEvent: { text: string } }) => {
86+
this.search(e.nativeEvent.text);
87+
};
88+
89+
searchDebounced = (searchString: ?string) => {
90+
this.setState({
91+
searchString,
92+
});
93+
};
94+
95+
searchThrottled = (searchString: ?string) => {
96+
this.setState({
97+
searchString,
98+
});
99+
};
100+
101+
search = (searchString: ?string) => {
102+
this.setState({
103+
searchString,
104+
});
105+
};
106+
107+
selectUser = (userId: string) => {
108+
this.setState(prev => ({
109+
selectedUsers: prev.selectedUsers.concat([userId]),
110+
wipSearchString: '',
111+
searchString: '',
112+
}));
113+
this.searchInput && this.searchInput.focus();
114+
};
115+
116+
removeSelectedUser = (userId: string) => () => {
117+
this.setState(prev => ({
118+
selectedUsers: prev.selectedUsers.filter(
119+
selectedId => selectedId !== userId
120+
),
121+
}));
122+
};
123+
124+
onSubmit = (text: string) => {
125+
if (this.state.selectedUsers.length === 0) return;
126+
this.props
127+
.createDirectMessageThread({
128+
participants: this.state.selectedUsers,
129+
message: {
130+
messageType: 'text',
131+
threadType: 'directMessageThread',
132+
content: {
133+
body: text,
134+
},
135+
},
136+
})
137+
.then((result: CreateDirectMessageThreadType) => {
138+
const { navigation } = this.props;
139+
140+
return navigation.navigate('DirectMessageThread', {
141+
id: result.data.createDirectMessageThread.id,
142+
});
143+
})
144+
.catch(err => {
145+
console.error(err);
146+
});
147+
};
148+
149+
filterResults = (results: Array<Object>) => {
150+
const { currentUser } = this.props;
151+
const { selectedUsers } = this.state;
152+
153+
return results
154+
.filter(row => row.id !== currentUser.id)
155+
.filter(row => selectedUsers.indexOf(row.id) < 0);
156+
};
157+
158+
render() {
159+
return (
160+
<ComposerWrapper>
161+
<SearchInputArea>
162+
<SelectedUsers
163+
horizontal
164+
empty={this.state.selectedUsers.length === 0}
165+
>
166+
{this.state.selectedUsers.map(userId => (
167+
<SelectedUser
168+
key={userId}
169+
id={userId}
170+
keyboardShouldPersistTaps={'always'}
171+
onPressHandler={this.removeSelectedUser(userId)}
172+
/>
173+
))}
174+
</SelectedUsers>
175+
<UserSearchInput
176+
onChangeText={this.onChangeText}
177+
onEndEditing={this.onFinishTyping}
178+
onSubmitEditing={this.onFinishTyping}
179+
value={this.state.wipSearchString}
180+
returnKeyType="search"
181+
autoFocus
182+
autoCorrect={false}
183+
autoCapitalize={'none'}
184+
innerRef={elem => (this.searchInput = elem)}
185+
placeholder={'Search for people...'}
186+
/>
187+
</SearchInputArea>
188+
189+
{!this.state.searchString && (
190+
<FullscreenNullState title={''} subtitle={''} />
191+
)}
192+
193+
{this.state.searchString && (
194+
<PeopleSearchView
195+
onPress={this.selectUser}
196+
queryString={this.state.searchString}
197+
keyboardShouldPersistTaps={'always'}
198+
filter={results => this.filterResults(results)}
199+
/>
200+
)}
201+
202+
<ChatInput
203+
onSubmit={this.onSubmit}
204+
disableSubmit={this.state.selectedUsers.length === 0}
205+
/>
206+
</ComposerWrapper>
207+
);
208+
}
209+
}
210+
211+
export default compose(
212+
withCurrentUser,
213+
getCurrentUserDMThreadConnection,
214+
createDirectMessageThread,
215+
withSafeView
216+
)(DirectMessageComposer);

0 commit comments

Comments
 (0)