Compare commits

...

39 Commits

Author SHA1 Message Date
Jason Frerich
ade9cb49d9 remove setXX as dependency 2022-11-15 06:53:37 -06:00
Jason Frerich
3643adf391 reduce PR diffs 2022-11-14 08:28:53 -06:00
Jason Frerich
778b512951 reduce PR diffs 2022-11-14 08:24:20 -06:00
Jason Frerich
4d827d1e44 reduce PR diffs 2022-11-14 08:20:21 -06:00
Jason Frerich
92d37995c5 reduce PR diffs 2022-11-14 08:16:17 -06:00
Jason Frerich
b86ec123c0 reduce PR diffs 2022-11-14 08:14:47 -06:00
Jason Frerich
6e5c443c26 reduce PR diffs 2022-11-14 08:12:16 -06:00
Jason Frerich
b22c80f244 reduce PR diff 2022-11-14 07:55:32 -06:00
Jason Frerich
ed15730d53 reduce PR diffs 2022-11-14 07:53:13 -06:00
Jason Frerich
fc55dcd808 reduce PR diff 2022-11-14 07:49:55 -06:00
Jason Frerich
46bdc8fb26 reduce diffs 2022-11-14 07:19:09 -06:00
Jason Frerich
5b71c88e89 reduce PR diffs 2022-11-14 07:17:01 -06:00
Jason Frerich
0adc4a1eed reduce PR diffs 2022-11-14 07:12:52 -06:00
Jason Frerich
dfa015aca8 fix page ref 2022-11-14 06:57:15 -06:00
Jason Frerich
eaf783ad72 move handling of removing and selecting profiles to CreateDirectMessage component 2022-11-14 06:28:34 -06:00
Jason Frerich
eb76543229 move search and search functionality into the screen and pass the result
to the users modal for rendering
2022-11-14 05:38:54 -06:00
Jason Frerich
5d0fb1444c add missing t func 2022-11-11 12:44:15 -06:00
Jason Frerich
fd21cbc2f1 make use of the isSearch boolean 2022-11-11 12:39:58 -06:00
Jason Frerich
95985b1940 rename MembersModal to UsersModal
move from screens to components folder
2022-11-11 12:35:08 -06:00
Jason Frerich
ff7d3c9580 move subcomponents of members modal from screens to components 2022-11-11 12:14:27 -06:00
Jason Frerich
f1e5e6f58b pass props to child component since they already exist in the parent 2022-11-11 11:51:04 -06:00
Jason Frerich
c4d557f0f0 Merge branch 'gekidou' into modularize-members-new 2022-11-11 11:32:49 -06:00
Jason Frerich
1d5fae16c5 remove unused styles 2022-11-11 09:17:51 -06:00
Jason Frerich
afa446da59 organize constants
nit-refactors
2022-11-11 09:11:45 -06:00
Jason Frerich
0f0880bffc simplify 2022-11-11 09:06:55 -06:00
Jason Frerich
fdeac9001d nit refactor to remove else blocks 2022-11-11 09:03:42 -06:00
Jason Frerich
ef2537be07 pass max allowed selectable users to members modal 2022-11-11 08:54:00 -06:00
Jason Frerich
1ecfac0823 sort dependencies
reorganize functions order
2022-11-11 08:48:57 -06:00
Jason Frerich
5fdb1f05da pass button text to the members modal 2022-11-11 08:44:09 -06:00
Jason Frerich
2acbc13a6d rename startConversation to onButtonTap to be more intention revealing 2022-11-11 08:35:57 -06:00
Jason Frerich
68da5abfc1 rename function and props 2022-11-11 08:28:49 -06:00
Jason Frerich
09c40aaa74 refactor - move all but the functions from CreateDirectMessage to
MembersModal
2022-11-11 08:10:23 -06:00
Jason Frerich
1328902814 refactor - move code from CreateDirectMessage to MembersModal 2022-11-10 19:43:07 -06:00
Jason Frerich
391bd85d9d get data inside members_modal 2022-11-10 14:59:24 -06:00
Jason Frerich
c212f014ec move loading component to members_modal 2022-11-10 13:21:36 -06:00
Jason Frerich
1fbd1f43fe nit: use better prop name 2022-11-10 12:56:20 -06:00
Jason Frerich
9ce8144a50 remove empty lines 2022-11-10 12:38:58 -06:00
Jason Frerich
fae02306c8 add members_modal and reference it in createDirectChannel component 2022-11-10 12:26:27 -06:00
Jason Frerich
461f49cf6c move members components to the members_modal folder and reference from
createDirectChannel component
2022-11-10 09:28:49 -06:00
7 changed files with 303 additions and 223 deletions

View File

@@ -59,7 +59,7 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
};
});
export default function SelectedUsers({
export default function SelectedUsersPanel({
selectedIds,
teammateNameDisplay,
warnCount,

View File

@@ -0,0 +1,14 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import withObservables from '@nozbe/with-observables';
import {observeProfileLongPresTutorial} from '@queries/app/global';
import UsersModal from './users_modal';
const enhanced = withObservables([], () => ({
tutorialWatched: observeProfileLongPresTutorial(),
}));
export default enhanced(UsersModal);

View File

@@ -0,0 +1,234 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {MutableRefObject, useCallback, useEffect, useMemo, useReducer, useRef, useState} from 'react';
import {MessageDescriptor, useIntl} from 'react-intl';
import {Keyboard, Platform, StyleSheet, View} from 'react-native';
import CompassIcon from '@components/compass_icon';
import Loading from '@components/loading';
import SelectedUsersPanel from '@components/selected_users_panel';
import UserList from '@components/user_list';
import {useTheme} from '@context/theme';
import {debounce} from '@helpers/api/general';
import useNavButtonPressed from '@hooks/navigation_button_pressed';
import {dismissModal, setButtons} from '@screens/navigation';
import {filterProfilesMatchingTerm} from '@utils/user';
const style = StyleSheet.create({
container: {
flex: 1,
},
searchBar: {
marginLeft: 12,
marginRight: Platform.select({ios: 4, default: 12}),
marginVertical: 12,
},
});
const ACTION_BUTTON = 'action-button';
const CLOSE_BUTTON = 'close-dms';
const close = () => {
Keyboard.dismiss();
dismissModal();
};
type getProfilesError = {
users?: undefined;
error: unknown;
}
type getProfilesSuccess = {
users: UserProfile[];
error?: undefined;
}
type Props = {
buttonText: MessageDescriptor;
componentId: string;
currentUserId: string;
getProfiles: () => Promise<getProfilesSuccess | getProfilesError>;
handleRemoveProfile: (id: string) => void;
handleSelectProfile: (user: UserProfile) => void;
loading: boolean;
maxSelectedUsers: number;
onButtonTap: (selectedId?: {[id: string]: boolean}) => Promise<boolean>;
page: MutableRefObject<number>;
searchResults: UserProfile[];
selectedIds: {[id: string]: UserProfile};
setLoading: (loading: boolean) => void;
teammateNameDisplay: string;
term: string;
tutorialWatched: boolean;
}
function reduceProfiles(state: UserProfile[], action: {type: 'add'; values?: UserProfile[]}) {
if (action.type === 'add' && action.values?.length) {
return [...state, ...action.values];
}
return state;
}
const UsersModal = ({
buttonText,
componentId,
currentUserId,
getProfiles,
handleRemoveProfile,
handleSelectProfile,
loading,
maxSelectedUsers,
onButtonTap,
page,
searchResults,
selectedIds,
setLoading,
teammateNameDisplay,
term,
tutorialWatched,
}: Props) => {
const theme = useTheme();
const {formatMessage, locale} = useIntl();
const mounted = useRef(false);
const next = useRef(true);
const selectedCount = Object.keys(selectedIds).length;
const [startingButtonAction, setStartingButtonAction] = useState(false);
const [profiles, dispatchProfiles] = useReducer(reduceProfiles, []);
const isSearch = Boolean(term);
const loadedProfiles = useCallback(({users}: {users?: UserProfile[]}) => {
if (mounted.current) {
if (users && !users.length) {
next.current = false;
}
page.current += 1;
setLoading(false);
dispatchProfiles({type: 'add', values: users});
}
}, []);
const handleButtonTap = useCallback(async (selectedId?: {[id: string]: boolean}) => {
if (startingButtonAction) {
return;
}
setStartingButtonAction(true);
const idsToUse = selectedId ? Object.keys(selectedId) : Object.keys(selectedIds);
const success = idsToUse.length === 0 ? false : await onButtonTap();
if (success) {
close();
return;
}
setStartingButtonAction(false);
}, [onButtonTap, selectedIds, startingButtonAction]);
const handleGetProfiles = useCallback(debounce(() => {
if (next.current && !loading && !term && mounted.current) {
setLoading(true);
getProfiles().then(loadedProfiles);
}
}, 100), [getProfiles, loading, term]);
const data = useMemo(() => {
if (isSearch) {
const exactMatches: UserProfile[] = [];
const filterByTerm = (p: UserProfile) => {
if (selectedCount > 0 && p.id === currentUserId) {
return false;
}
if (p.username === term || p.username.startsWith(term)) {
exactMatches.push(p);
return false;
}
return true;
};
const results = filterProfilesMatchingTerm(searchResults, term).filter(filterByTerm);
return [...exactMatches, ...results];
}
return profiles;
}, [isSearch && selectedCount, isSearch && searchResults, profiles, term]);
const updateNavigationButtons = useCallback(async (startEnabled: boolean) => {
const closeIcon = await CompassIcon.getImageSource('close', 24, theme.sidebarHeaderTextColor);
setButtons(componentId, {
leftButtons: [{
id: CLOSE_BUTTON,
icon: closeIcon,
testID: 'close.button',
}],
rightButtons: [{
color: theme.sidebarHeaderTextColor,
id: ACTION_BUTTON,
text: formatMessage(buttonText),
showAsAction: 'always',
enabled: startEnabled,
testID: 'action.button',
}],
});
}, [buttonText, locale, theme]);
useEffect(() => {
mounted.current = true;
updateNavigationButtons(false);
handleGetProfiles();
return () => {
mounted.current = false;
};
}, []);
useEffect(() => {
const canStart = selectedCount > 0 && !startingButtonAction;
updateNavigationButtons(canStart);
}, [selectedCount > 0, startingButtonAction, updateNavigationButtons]);
useNavButtonPressed(ACTION_BUTTON, componentId, handleButtonTap, [handleButtonTap]);
useNavButtonPressed(CLOSE_BUTTON, componentId, close, [close]);
if (startingButtonAction) {
return (
<View style={style.container}>
<Loading color={theme.centerChannelColor}/>
</View>
);
}
return (
<>
{selectedCount > 0 &&
<SelectedUsersPanel
selectedIds={selectedIds}
warnCount={maxSelectedUsers - 2}
maxCount={maxSelectedUsers}
onRemove={handleRemoveProfile}
teammateNameDisplay={teammateNameDisplay}
/>
}
<UserList
currentUserId={currentUserId}
fetchMore={handleGetProfiles}
handleSelectProfile={handleSelectProfile}
loading={loading}
profiles={data}
selectedIds={selectedIds}
showNoResults={!loading && page?.current !== -1}
teammateNameDisplay={teammateNameDisplay}
term={term}
testID='members_modal.user_list'
tutorialWatched={tutorialWatched}
/>
</>
);
};
export default UsersModal;

View File

@@ -1,32 +1,47 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback, useEffect, useMemo, useReducer, useRef, useState} from 'react';
import {useIntl} from 'react-intl';
import {Keyboard, Platform, View} from 'react-native';
import {SafeAreaView} from 'react-native-safe-area-context';
import React, {useCallback, useRef, useState} from 'react';
import {defineMessages, useIntl} from 'react-intl';
import {Platform, SafeAreaView, StyleSheet, View} from 'react-native';
import {makeDirectChannel, makeGroupChannel} from '@actions/remote/channel';
import {fetchProfiles, fetchProfilesInTeam, searchProfiles} from '@actions/remote/user';
import CompassIcon from '@components/compass_icon';
import Loading from '@components/loading';
import Search from '@components/search';
import UsersModal from '@components/users_modal';
import {General} from '@constants';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import {debounce} from '@helpers/api/general';
import useNavButtonPressed from '@hooks/navigation_button_pressed';
import {t} from '@i18n';
import {dismissModal, setButtons} from '@screens/navigation';
import {alertErrorWithFallback} from '@utils/draft';
import {changeOpacity, getKeyboardAppearanceFromTheme, makeStyleSheetFromTheme} from '@utils/theme';
import {displayUsername, filterProfilesMatchingTerm} from '@utils/user';
import {changeOpacity, getKeyboardAppearanceFromTheme} from '@utils/theme';
import {displayUsername} from '@utils/user';
import SelectedUsers from './selected_users';
import UserList from './user_list';
const style = StyleSheet.create({
container: {
flex: 1,
},
searchBar: {
marginLeft: 12,
marginRight: Platform.select({ios: 4, default: 12}),
marginVertical: 12,
},
});
const START_BUTTON = 'start-conversation';
const CLOSE_BUTTON = 'close-dms';
const messages = defineMessages({
dm: {
id: t('mobile.open_dm.error'),
defaultMessage: "We couldn't open a direct message with {displayName}. Please check your connection and try again.",
},
gm: {
id: t('mobile.open_gm.error'),
defaultMessage: "We couldn't open a group message with those users. Please check your connection and try again.",
},
button: {
id: t('mobile.create_direct_message.start'),
defaultMessage: 'Start',
},
});
type Props = {
componentId: string;
@@ -34,51 +49,6 @@ type Props = {
currentUserId: string;
restrictDirectMessage: boolean;
teammateNameDisplay: string;
tutorialWatched: boolean;
}
const close = () => {
Keyboard.dismiss();
dismissModal();
};
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return {
container: {
flex: 1,
},
searchBar: {
marginLeft: 12,
marginRight: Platform.select({ios: 4, default: 12}),
marginVertical: 12,
},
loadingContainer: {
alignItems: 'center',
backgroundColor: theme.centerChannelBg,
height: 70,
justifyContent: 'center',
},
loadingText: {
color: changeOpacity(theme.centerChannelColor, 0.6),
},
noResultContainer: {
flexGrow: 1,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
},
noResultText: {
fontSize: 26,
color: changeOpacity(theme.centerChannelColor, 0.5),
},
};
});
function reduceProfiles(state: UserProfile[], action: {type: 'add'; values?: UserProfile[]}) {
if (action.type === 'add' && action.values?.length) {
return [...state, ...action.values];
}
return state;
}
export default function CreateDirectMessage({
@@ -87,56 +57,31 @@ export default function CreateDirectMessage({
currentUserId,
restrictDirectMessage,
teammateNameDisplay,
tutorialWatched,
}: Props) {
const serverUrl = useServerUrl();
const theme = useTheme();
const style = getStyleFromTheme(theme);
const intl = useIntl();
const {formatMessage} = intl;
const searchTimeoutId = useRef<NodeJS.Timeout | null>(null);
const next = useRef(true);
const page = useRef(-1);
const mounted = useRef(false);
const [profiles, dispatchProfiles] = useReducer(reduceProfiles, []);
const [searchResults, setSearchResults] = useState<UserProfile[]>([]);
const [loading, setLoading] = useState(false);
const [term, setTerm] = useState('');
const [startingConversation, setStartingConversation] = useState(false);
const [selectedIds, setSelectedIds] = useState<{[id: string]: UserProfile}>({});
const selectedCount = Object.keys(selectedIds).length;
const isSearch = Boolean(term);
const loadedProfiles = ({users}: {users?: UserProfile[]}) => {
if (mounted.current) {
if (users && !users.length) {
next.current = false;
}
page.current += 1;
setLoading(false);
dispatchProfiles({type: 'add', values: users});
}
};
const clearSearch = useCallback(() => {
setTerm('');
setSearchResults([]);
}, []);
const getProfiles = useCallback(debounce(() => {
if (next.current && !loading && !term && mounted.current) {
setLoading(true);
if (restrictDirectMessage) {
fetchProfilesInTeam(serverUrl, currentTeamId, page.current + 1, General.PROFILE_CHUNK_SIZE).then(loadedProfiles);
} else {
fetchProfiles(serverUrl, page.current + 1, General.PROFILE_CHUNK_SIZE).then(loadedProfiles);
}
const getProfiles = useCallback(async () => {
if (restrictDirectMessage) {
return fetchProfilesInTeam(serverUrl, currentTeamId, page.current + 1, General.PROFILE_CHUNK_SIZE);
}
}, 100), [loading, isSearch, restrictDirectMessage, serverUrl, currentTeamId]);
return fetchProfiles(serverUrl, page.current + 1, General.PROFILE_CHUNK_SIZE);
}, [restrictDirectMessage, serverUrl, currentTeamId]);
const handleRemoveProfile = useCallback((id: string) => {
const newSelectedIds = Object.assign({}, selectedIds);
@@ -148,68 +93,30 @@ export default function CreateDirectMessage({
const createDirectChannel = useCallback(async (id: string): Promise<boolean> => {
const user = selectedIds[id];
const displayName = displayUsername(user, intl.locale, teammateNameDisplay);
const result = await makeDirectChannel(serverUrl, id, displayName);
if (result.error) {
alertErrorWithFallback(
intl,
result.error,
{
id: 'mobile.open_dm.error',
defaultMessage: "We couldn't open a direct message with {displayName}. Please check your connection and try again.",
},
{
displayName,
},
);
alertErrorWithFallback(intl, result.error, messages.dm, {displayName});
}
return !result.error;
}, [selectedIds, intl.locale, teammateNameDisplay, serverUrl]);
const createGroupChannel = useCallback(async (ids: string[]): Promise<boolean> => {
const result = await makeGroupChannel(serverUrl, ids);
if (result.error) {
alertErrorWithFallback(
intl,
result.error,
{
id: t('mobile.open_gm.error'),
defaultMessage: "We couldn't open a group message with those users. Please check your connection and try again.",
},
);
alertErrorWithFallback(intl, result.error, messages.gm);
}
return !result.error;
}, [serverUrl]);
const startConversation = useCallback(async (selectedId?: {[id: string]: boolean}) => {
if (startingConversation) {
return;
}
setStartingConversation(true);
const onButtonTap = useCallback(async (selectedId?: {[id: string]: boolean}) => {
const idsToUse = selectedId ? Object.keys(selectedId) : Object.keys(selectedIds);
let success;
if (idsToUse.length === 0) {
success = false;
} else if (idsToUse.length > 1) {
success = await createGroupChannel(idsToUse);
} else {
success = await createDirectChannel(idsToUse[0]);
if (idsToUse.length > 1) {
return createGroupChannel(idsToUse);
}
if (success) {
close();
} else {
setStartingConversation(false);
}
}, [startingConversation, selectedIds, createGroupChannel, createDirectChannel]);
return createDirectChannel(idsToUse[0]);
}, [selectedIds, createGroupChannel, createDirectChannel]);
const handleSelectProfile = useCallback((user: UserProfile) => {
if (selectedIds[user.id]) {
@@ -221,8 +128,7 @@ export default function CreateDirectMessage({
const selectedId = {
[currentUserId]: true,
};
startConversation(selectedId);
onButtonTap(selectedId);
} else {
const wasSelected = selectedIds[user.id];
@@ -239,7 +145,7 @@ export default function CreateDirectMessage({
clearSearch();
}
}, [selectedIds, currentUserId, handleRemoveProfile, startConversation, clearSearch]);
}, [clearSearch, currentUserId, handleRemoveProfile, onButtonTap, selectedIds]);
const searchUsers = useCallback(async (searchTerm: string) => {
const lowerCasedTerm = searchTerm.toLowerCase();
@@ -280,72 +186,6 @@ export default function CreateDirectMessage({
}
}, [searchUsers, clearSearch]);
const updateNavigationButtons = useCallback(async (startEnabled: boolean) => {
const closeIcon = await CompassIcon.getImageSource('close', 24, theme.sidebarHeaderTextColor);
setButtons(componentId, {
leftButtons: [{
id: CLOSE_BUTTON,
icon: closeIcon,
testID: 'close.create_direct_message.button',
}],
rightButtons: [{
color: theme.sidebarHeaderTextColor,
id: START_BUTTON,
text: formatMessage({id: 'mobile.create_direct_message.start', defaultMessage: 'Start'}),
showAsAction: 'always',
enabled: startEnabled,
testID: 'create_direct_message.start.button',
}],
});
}, [intl.locale, theme]);
useNavButtonPressed(START_BUTTON, componentId, startConversation, [startConversation]);
useNavButtonPressed(CLOSE_BUTTON, componentId, close, [close]);
useEffect(() => {
mounted.current = true;
updateNavigationButtons(false);
getProfiles();
return () => {
mounted.current = false;
};
}, []);
useEffect(() => {
const canStart = selectedCount > 0 && !startingConversation;
updateNavigationButtons(canStart);
}, [selectedCount > 0, startingConversation, updateNavigationButtons]);
const data = useMemo(() => {
if (term) {
const exactMatches: UserProfile[] = [];
const filterByTerm = (p: UserProfile) => {
if (selectedCount > 0 && p.id === currentUserId) {
return false;
}
if (p.username === term || p.username.startsWith(term)) {
exactMatches.push(p);
return false;
}
return true;
};
const results = filterProfilesMatchingTerm(searchResults, term).filter(filterByTerm);
return [...exactMatches, ...results];
}
return profiles;
}, [term, isSearch && selectedCount, isSearch && searchResults, profiles]);
if (startingConversation) {
return (
<View style={style.container}>
<Loading color={theme.centerChannelColor}/>
</View>
);
}
return (
<SafeAreaView
style={style.container}
@@ -365,29 +205,23 @@ export default function CreateDirectMessage({
value={term}
/>
</View>
{selectedCount > 0 &&
<SelectedUsers
selectedIds={selectedIds}
warnCount={General.MAX_USERS_IN_GM - 2}
maxCount={General.MAX_USERS_IN_GM}
onRemove={handleRemoveProfile}
teammateNameDisplay={teammateNameDisplay}
/>
}
<UserList
<UsersModal
page={page}
buttonText={messages.button}
componentId={componentId}
currentUserId={currentUserId}
getProfiles={getProfiles}
handleRemoveProfile={handleRemoveProfile}
handleSelectProfile={handleSelectProfile}
loading={loading}
profiles={data}
maxSelectedUsers={General.MAX_USERS_IN_GM}
onButtonTap={onButtonTap}
searchResults={searchResults}
selectedIds={selectedIds}
showNoResults={!loading && page.current !== -1}
setLoading={setLoading}
teammateNameDisplay={teammateNameDisplay}
fetchMore={getProfiles}
term={term}
testID='create_direct_message.user_list'
tutorialWatched={tutorialWatched}
/>
</SafeAreaView>
);
}

View File

@@ -7,7 +7,6 @@ import {of as of$} from 'rxjs';
import {switchMap} from 'rxjs/operators';
import {General} from '@constants';
import {observeProfileLongPresTutorial} from '@queries/app/global';
import {observeConfigValue, observeCurrentTeamId, observeCurrentUserId} from '@queries/servers/system';
import {observeTeammateNameDisplay} from '@queries/servers/user';
@@ -24,7 +23,6 @@ const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
teammateNameDisplay: observeTeammateNameDisplay(database),
currentUserId: observeCurrentUserId(database),
currentTeamId: observeCurrentTeamId(database),
tutorialWatched: observeProfileLongPresTutorial(),
restrictDirectMessage,
};
});