aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/components/comments/AddComment.tsx4
-rw-r--r--src/components/comments/CommentsContainer.tsx11
-rw-r--r--src/components/comments/MentionInputControlled.tsx195
-rw-r--r--src/components/common/TaggTypeahead.tsx19
-rw-r--r--src/constants/api.ts1
-rw-r--r--src/screens/onboarding/BasicInfoOnboarding.tsx153
-rw-r--r--src/screens/profile/MomentCommentsScreen.tsx5
-rw-r--r--src/services/UserProfileService.ts27
8 files changed, 350 insertions, 65 deletions
diff --git a/src/components/comments/AddComment.tsx b/src/components/comments/AddComment.tsx
index 9cf10b5e..befaa8fe 100644
--- a/src/components/comments/AddComment.tsx
+++ b/src/components/comments/AddComment.tsx
@@ -7,7 +7,6 @@ import {
TextInput,
View,
} from 'react-native';
-import {MentionInput} from 'react-native-controlled-mentions';
import {TouchableOpacity} from 'react-native-gesture-handler';
import {useDispatch, useSelector} from 'react-redux';
import UpArrowIcon from '../../assets/icons/up_arrow.svg';
@@ -20,6 +19,7 @@ import {CommentThreadType, CommentType} from '../../types';
import {SCREEN_HEIGHT, SCREEN_WIDTH} from '../../utils';
import {mentionPartTypes} from '../../utils/comments';
import {Avatar} from '../common';
+import {MentionInputControlled} from './MentionInputControlled';
export interface AddCommentProps {
momentId: string;
@@ -112,7 +112,7 @@ const AddComment: React.FC<AddCommentProps> = ({momentId, placeholderText}) => {
]}>
<View style={styles.textContainer}>
<Avatar style={styles.avatar} uri={avatar} />
- <MentionInput
+ <MentionInputControlled
containerStyle={styles.text}
placeholder={placeholderText}
value={inReplyToMention + comment}
diff --git a/src/components/comments/CommentsContainer.tsx b/src/components/comments/CommentsContainer.tsx
index bd8d5c49..595ec743 100644
--- a/src/components/comments/CommentsContainer.tsx
+++ b/src/components/comments/CommentsContainer.tsx
@@ -18,6 +18,7 @@ export type CommentsContainerProps = {
shouldUpdate: boolean;
setShouldUpdate: (update: boolean) => void;
isThread: boolean;
+ setCommentsLengthParent: (length: number) => void;
};
/**
@@ -31,6 +32,7 @@ const CommentsContainer: React.FC<CommentsContainerProps> = ({
shouldUpdate,
setShouldUpdate,
commentId,
+ setCommentsLengthParent,
}) => {
const {setCommentsLength, commentTapped} = useContext(CommentContext);
const {username: loggedInUsername} = useSelector(
@@ -41,6 +43,14 @@ const CommentsContainer: React.FC<CommentsContainerProps> = ({
const ref = useRef<FlatList<CommentType>>(null);
const ITEM_HEIGHT = SCREEN_HEIGHT / 7.0;
+ const countComments = (comments: CommentType[]) => {
+ let count = 0;
+ for (let i = 0; i < comments.length; i++) {
+ count += 1 + comments[i].replies_count;
+ }
+ return count;
+ }
+
useEffect(() => {
const loadComments = async () => {
await getComments(objectId, isThread).then((comments) => {
@@ -51,6 +61,7 @@ const CommentsContainer: React.FC<CommentsContainerProps> = ({
}
setShouldUpdate(false);
}
+ setCommentsLengthParent(countComments(comments));
});
};
let subscribedToLoadComments = true;
diff --git a/src/components/comments/MentionInputControlled.tsx b/src/components/comments/MentionInputControlled.tsx
new file mode 100644
index 00000000..6abcb566
--- /dev/null
+++ b/src/components/comments/MentionInputControlled.tsx
@@ -0,0 +1,195 @@
+import React, {FC, MutableRefObject, useMemo, useRef, useState} from 'react';
+import {
+ NativeSyntheticEvent,
+ Text,
+ TextInput,
+ TextInputSelectionChangeEventData,
+ View,
+} from 'react-native';
+
+import {
+ MentionInputProps,
+ MentionPartType,
+ Suggestion,
+} from 'react-native-controlled-mentions/dist/types';
+import {
+ defaultMentionTextStyle,
+ generateValueFromPartsAndChangedText,
+ generateValueWithAddedSuggestion,
+ getMentionPartSuggestionKeywords,
+ isMentionPartType,
+ parseValue,
+} from 'react-native-controlled-mentions/dist/utils';
+
+const MentionInputControlled: FC<MentionInputProps> = ({
+ value,
+ onChange,
+
+ partTypes = [],
+
+ inputRef: propInputRef,
+
+ containerStyle,
+
+ onSelectionChange,
+
+ ...textInputProps
+}) => {
+ const textInput = useRef<TextInput | null>(null);
+
+ const [selection, setSelection] = useState({start: 0, end: 0});
+
+ const [keyboardText, setKeyboardText] = useState<string>('');
+
+ const validRegex = () => {
+ if (partTypes.length === 0) {
+ return /.*\@[^ ]*$/;
+ } else {
+ return new RegExp(`.*\@${keywordByTrigger[partTypes[0].trigger]}.*$`);
+ }
+ };
+
+ const {plainText, parts} = useMemo(() => parseValue(value, partTypes), [
+ value,
+ partTypes,
+ ]);
+
+ const handleSelectionChange = (
+ event: NativeSyntheticEvent<TextInputSelectionChangeEventData>,
+ ) => {
+ setSelection(event.nativeEvent.selection);
+
+ onSelectionChange && onSelectionChange(event);
+ };
+
+ /**
+ * Callback that trigger on TextInput text change
+ *
+ * @param changedText
+ */
+ const onChangeInput = (changedText: string) => {
+ setKeyboardText(changedText);
+ onChange(
+ generateValueFromPartsAndChangedText(parts, plainText, changedText),
+ );
+ };
+
+ /**
+ * We memoize the keyword to know should we show mention suggestions or not
+ */
+ const keywordByTrigger = useMemo(() => {
+ return getMentionPartSuggestionKeywords(
+ parts,
+ plainText,
+ selection,
+ partTypes,
+ );
+ }, [parts, plainText, selection, partTypes]);
+
+ /**
+ * Callback on mention suggestion press. We should:
+ * - Get updated value
+ * - Trigger onChange callback with new value
+ */
+ const onSuggestionPress = (mentionType: MentionPartType) => (
+ suggestion: Suggestion,
+ ) => {
+ const newValue = generateValueWithAddedSuggestion(
+ parts,
+ mentionType,
+ plainText,
+ selection,
+ suggestion,
+ );
+
+ if (!newValue) {
+ return;
+ }
+
+ onChange(newValue);
+
+ /**
+ * Move cursor to the end of just added mention starting from trigger string and including:
+ * - Length of trigger string
+ * - Length of mention name
+ * - Length of space after mention (1)
+ *
+ * Not working now due to the RN bug
+ */
+ // const newCursorPosition = currentPart.position.start + triggerPartIndex + trigger.length +
+ // suggestion.name.length + 1;
+
+ // textInput.current?.setNativeProps({selection: {start: newCursorPosition, end: newCursorPosition}});
+ };
+
+ const handleTextInputRef = (ref: TextInput) => {
+ textInput.current = ref as TextInput;
+
+ if (propInputRef) {
+ if (typeof propInputRef === 'function') {
+ propInputRef(ref);
+ } else {
+ (propInputRef as MutableRefObject<TextInput>).current = ref as TextInput;
+ }
+ }
+ };
+
+ const renderMentionSuggestions = (mentionType: MentionPartType) => (
+ <React.Fragment key={mentionType.trigger}>
+ {mentionType.renderSuggestions &&
+ mentionType.renderSuggestions({
+ keyword: keywordByTrigger[mentionType.trigger],
+ onSuggestionPress: onSuggestionPress(mentionType),
+ })}
+ </React.Fragment>
+ );
+
+ const validateInput = (testString: string) => {
+ return validRegex().test(testString);
+ };
+
+ return (
+ <View style={containerStyle}>
+ {validateInput(keyboardText)
+ ? (partTypes.filter(
+ (one) =>
+ isMentionPartType(one) &&
+ one.renderSuggestions != null &&
+ !one.isBottomMentionSuggestionsRender,
+ ) as MentionPartType[]).map(renderMentionSuggestions)
+ : null}
+
+ <TextInput
+ multiline
+ {...textInputProps}
+ ref={handleTextInputRef}
+ onChangeText={onChangeInput}
+ onSelectionChange={handleSelectionChange}>
+ <Text>
+ {parts.map(({text, partType, data}, index) =>
+ partType ? (
+ <Text
+ key={`${index}-${data?.trigger ?? 'pattern'}`}
+ style={partType.textStyle ?? defaultMentionTextStyle}>
+ {text}
+ </Text>
+ ) : (
+ <Text key={index}>{text}</Text>
+ ),
+ )}
+ </Text>
+ </TextInput>
+
+ {validateInput(keyboardText)
+ ? (partTypes.filter(
+ (one) =>
+ isMentionPartType(one) &&
+ one.renderSuggestions != null &&
+ one.isBottomMentionSuggestionsRender,
+ ) as MentionPartType[]).map(renderMentionSuggestions)
+ : null}
+ </View>
+ );
+};
+
+export {MentionInputControlled};
diff --git a/src/components/common/TaggTypeahead.tsx b/src/components/common/TaggTypeahead.tsx
index 7cd99278..747e0bb5 100644
--- a/src/components/common/TaggTypeahead.tsx
+++ b/src/components/common/TaggTypeahead.tsx
@@ -1,26 +1,32 @@
import React, {Fragment, useEffect, useState} from 'react';
import {ScrollView, StyleSheet} from 'react-native';
import {MentionSuggestionsProps} from 'react-native-controlled-mentions';
+import {useSelector} from 'react-redux';
import {SEARCH_ENDPOINT_MESSAGES} from '../../constants';
import {loadSearchResults} from '../../services';
+import {RootState} from '../../store/rootReducer';
import {ProfilePreviewType} from '../../types';
-import {SCREEN_WIDTH} from '../../utils';
+import {SCREEN_WIDTH, shuffle} from '../../utils';
import TaggUserRowCell from './TaggUserRowCell';
const TaggTypeahead: React.FC<MentionSuggestionsProps> = ({
keyword,
onSuggestionPress,
}) => {
+ const {friends} = useSelector((state: RootState) => state.friends);
const [results, setResults] = useState<ProfilePreviewType[]>([]);
const [height, setHeight] = useState(0);
useEffect(() => {
- getQuerySuggested();
+ if (keyword === '') {
+ setResults(shuffle(friends));
+ } else {
+ getQuerySuggested();
+ }
}, [keyword]);
const getQuerySuggested = async () => {
- if (!keyword || keyword.length < 3) {
- setResults([]);
+ if (keyword === undefined || keyword === '@') {
return;
}
const searchResults = await loadSearchResults(
@@ -41,15 +47,16 @@ const TaggTypeahead: React.FC<MentionSuggestionsProps> = ({
showsVerticalScrollIndicator={false}
onLayout={(event) => {
setHeight(event.nativeEvent.layout.height);
- }}>
+ }}
+ keyboardShouldPersistTaps={'always'}>
{results.map((user) => (
<TaggUserRowCell
onPress={() => {
+ setResults([]);
onSuggestionPress({
id: user.id,
name: user.username,
});
- setResults([]);
}}
user={user}
/>
diff --git a/src/constants/api.ts b/src/constants/api.ts
index 6a924f1d..9d3f70c9 100644
--- a/src/constants/api.ts
+++ b/src/constants/api.ts
@@ -12,6 +12,7 @@ const API_URL: string = BASE_URL + 'api/';
export const LOGIN_ENDPOINT: string = API_URL + 'login/';
export const VERSION_ENDPOINT: string = API_URL + 'version/v2/';
export const REGISTER_ENDPOINT: string = API_URL + 'register/';
+export const REGISTER_VALIDATE_ENDPOINT: string = API_URL + 'register/validate/';
export const EDIT_PROFILE_ENDPOINT: string = API_URL + 'edit-profile/';
export const SEND_OTP_ENDPOINT: string = API_URL + 'send-otp/';
export const VERIFY_OTP_ENDPOINT: string = API_URL + 'verify-otp/';
diff --git a/src/screens/onboarding/BasicInfoOnboarding.tsx b/src/screens/onboarding/BasicInfoOnboarding.tsx
index 3fa33f63..3058a04e 100644
--- a/src/screens/onboarding/BasicInfoOnboarding.tsx
+++ b/src/screens/onboarding/BasicInfoOnboarding.tsx
@@ -38,7 +38,11 @@ import {
ERROR_T_AND_C_NOT_ACCEPTED,
} from '../../constants/strings';
import {OnboardingStackParams} from '../../routes';
-import {sendOtpStatusCode, sendRegister} from '../../services';
+import {
+ sendOtpStatusCode,
+ sendRegister,
+ verifyExistingInformation,
+} from '../../services';
import {BackgroundGradientType} from '../../types';
import {HeaderHeight, SCREEN_HEIGHT, SCREEN_WIDTH} from '../../utils';
@@ -63,12 +67,20 @@ const BasicInfoOnboarding: React.FC<BasicInfoOnboardingProps> = ({route}) => {
const [currentStep, setCurrentStep] = useState(0);
const [tcAccepted, setTCAccepted] = useState(false);
const [passVisibility, setPassVisibility] = useState(false);
+ const [invalidWithError, setInvalidWithError] = useState(
+ 'Please enter a valid ',
+ );
const [autoCapitalize, setAutoCap] = useState<
'none' | 'sentences' | 'words' | 'characters' | undefined
>('none');
const [fadeValue, setFadeValue] = useState<Animated.Value<number>>(
new Animated.Value(0),
);
+
+ useEffect(() => {
+ setValid(false);
+ }, [invalidWithError]);
+
const fadeButtonValue = useValue<number>(0);
const [form, setForm] = useState({
fname: '',
@@ -209,30 +221,37 @@ const BasicInfoOnboarding: React.FC<BasicInfoOnboardingProps> = ({route}) => {
const formSteps: {
placeholder: string;
onChangeText: (text: string) => void;
+ value: string;
}[] = [
{
placeholder: 'First Name',
onChangeText: (text) => handleNameUpdate(text, 0),
+ value: form.fname,
},
{
placeholder: 'Last Name',
onChangeText: (text) => handleNameUpdate(text, 1),
+ value: form.lname,
},
{
placeholder: 'Phone',
onChangeText: handlePhoneUpdate,
+ value: form.phone,
},
{
placeholder: 'School Email',
onChangeText: handleEmailUpdate,
+ value: form.email,
},
{
placeholder: 'Username',
onChangeText: (text) => handleNameUpdate(text, 2),
+ value: form.username,
},
{
placeholder: 'Password',
onChangeText: handlePasswordUpdate,
+ value: form.password,
},
];
const resetForm = (formStep: String) => {
@@ -277,9 +296,33 @@ const BasicInfoOnboarding: React.FC<BasicInfoOnboardingProps> = ({route}) => {
}
};
const step = formSteps[currentStep];
- const advance = () => {
+ useEffect(() => {
+ setInvalidWithError(
+ 'Please enter a valid ' + step.placeholder.toLowerCase(),
+ );
+ }, [currentStep]);
+ const advance = async () => {
setAttemptedSubmit(true);
if (valid) {
+ if (step.placeholder === 'School Email') {
+ const verifiedInfo = await verifyExistingInformation(
+ form.email,
+ undefined,
+ );
+ if (!verifiedInfo) {
+ setInvalidWithError('Email is taken');
+ return;
+ }
+ } else if (step.placeholder === 'Username') {
+ const verifiedInfo = await verifyExistingInformation(
+ undefined,
+ form.username,
+ );
+ if (!verifiedInfo) {
+ setInvalidWithError('Username is taken');
+ return;
+ }
+ }
setCurrentStep(currentStep + 1);
setAttemptedSubmit(false);
setValid(false);
@@ -421,6 +464,7 @@ const BasicInfoOnboarding: React.FC<BasicInfoOnboardingProps> = ({route}) => {
returnKeyType="done"
selectionColor="white"
onChangeText={step.onChangeText}
+ value={step.value}
onSubmitEditing={advance}
autoFocus={true}
blurOnSubmit={false}
@@ -428,7 +472,7 @@ const BasicInfoOnboarding: React.FC<BasicInfoOnboardingProps> = ({route}) => {
warning: styles.passWarning,
}}
valid={valid}
- invalidWarning={`Please enter a valid ${step.placeholder.toLowerCase()}`}
+ invalidWarning={invalidWithError}
attemptedSubmit={attemptedSubmit}
/>
<Animated.View style={{opacity: fadeButtonValue}}>
@@ -443,58 +487,61 @@ const BasicInfoOnboarding: React.FC<BasicInfoOnboardingProps> = ({route}) => {
</Animated.View>
</>
) : (
- <Animated.View
- style={[styles.formContainer, {opacity: fadeValue}]}>
- <TaggInput
- accessibilityHint="Enter a password."
- accessibilityLabel="Password input field."
- placeholder="Password"
- autoCompleteType="password"
- textContentType="oneTimeCode"
- returnKeyType="done"
- selectionColor="white"
- onChangeText={handlePasswordUpdate}
- onSubmitEditing={advanceRegistration}
- blurOnSubmit={false}
- autoFocus={true}
- secureTextEntry={!passVisibility}
- valid={valid}
- externalStyles={{
- warning: styles.passWarning,
- }}
- invalidWarning={
- 'Password must be at least 8 characters & contain at least one of a-z, A-Z, 0-9, and a special character.'
- }
- attemptedSubmit={attemptedSubmit}
- style={
- attemptedSubmit && !valid
- ? [styles.input, styles.invalidColor]
- : styles.input
- }
- />
- <TouchableOpacity
- accessibilityLabel="Show password button"
- accessibilityHint="Select this if you want to display your tagg password"
- style={styles.showPassContainer}
- onPress={() => setPassVisibility(!passVisibility)}>
- <Text style={styles.showPass}>Show Password</Text>
- </TouchableOpacity>
- <LoadingIndicator />
- <TermsConditions
- style={styles.tc}
- accepted={tcAccepted}
- onChange={setTCAccepted}
- />
- <Animated.View style={{opacity: fadeButtonValue}}>
- <TaggSquareButton
- onPress={advanceRegistration}
- title={'Next'}
- buttonStyle={'normal'}
- buttonColor={'white'}
- labelColor={'blue'}
+ <>
+ <Text style={styles.formHeader}>SIGN UP</Text>
+ <Animated.View
+ style={[styles.formContainer, {opacity: fadeValue}]}>
+ <TaggInput
+ accessibilityHint="Enter a password."
+ accessibilityLabel="Password input field."
+ placeholder="Password"
+ autoCompleteType="password"
+ textContentType="oneTimeCode"
+ returnKeyType="done"
+ selectionColor="white"
+ onChangeText={handlePasswordUpdate}
+ onSubmitEditing={advanceRegistration}
+ blurOnSubmit={false}
+ autoFocus={true}
+ secureTextEntry={!passVisibility}
+ valid={valid}
+ externalStyles={{
+ warning: styles.passWarning,
+ }}
+ invalidWarning={
+ 'Password must be at least 8 characters & contain at least one of a-z, A-Z, 0-9, and a special character.'
+ }
+ attemptedSubmit={attemptedSubmit}
+ style={
+ attemptedSubmit && !valid
+ ? [styles.input, styles.invalidColor]
+ : styles.input
+ }
/>
+ <TouchableOpacity
+ accessibilityLabel="Show password button"
+ accessibilityHint="Select this if you want to display your tagg password"
+ style={styles.showPassContainer}
+ onPress={() => setPassVisibility(!passVisibility)}>
+ <Text style={styles.showPass}>Show Password</Text>
+ </TouchableOpacity>
+ <LoadingIndicator />
+ <TermsConditions
+ style={styles.tc}
+ accepted={tcAccepted}
+ onChange={setTCAccepted}
+ />
+ <Animated.View style={{opacity: fadeButtonValue}}>
+ <TaggSquareButton
+ onPress={advanceRegistration}
+ title={'Next'}
+ buttonStyle={'normal'}
+ buttonColor={'white'}
+ labelColor={'blue'}
+ />
+ </Animated.View>
</Animated.View>
- </Animated.View>
+ </>
)}
</>
)}
diff --git a/src/screens/profile/MomentCommentsScreen.tsx b/src/screens/profile/MomentCommentsScreen.tsx
index ffe21f4c..4b332b56 100644
--- a/src/screens/profile/MomentCommentsScreen.tsx
+++ b/src/screens/profile/MomentCommentsScreen.tsx
@@ -32,8 +32,6 @@ type MomentCommentContextType = {
) => void;
shouldUpdateAllComments: boolean;
setShouldUpdateAllComments: (available: boolean) => void;
- commentsLength: number;
- setCommentsLength: (length: number) => void;
};
export const CommentContext = React.createContext(
@@ -68,8 +66,6 @@ const MomentCommentsScreen: React.FC<MomentCommentsScreenProps> = ({route}) => {
setCommentTapped,
shouldUpdateAllComments,
setShouldUpdateAllComments,
- commentsLength,
- setCommentsLength,
}}>
<View style={styles.background}>
<SafeAreaView>
@@ -81,6 +77,7 @@ const MomentCommentsScreen: React.FC<MomentCommentsScreenProps> = ({route}) => {
shouldUpdate={shouldUpdateAllComments}
setShouldUpdate={setShouldUpdateAllComments}
isThread={false}
+ setCommentsLengthParent={setCommentsLength}
/>
<AddComment
placeholderText={
diff --git a/src/services/UserProfileService.ts b/src/services/UserProfileService.ts
index c11d874f..8b7b78e1 100644
--- a/src/services/UserProfileService.ts
+++ b/src/services/UserProfileService.ts
@@ -11,6 +11,7 @@ import {
PROFILE_INFO_ENDPOINT,
PROFILE_PHOTO_ENDPOINT,
REGISTER_ENDPOINT,
+ REGISTER_VALIDATE_ENDPOINT,
SEND_OTP_ENDPOINT,
TAGG_CUSTOMER_SUPPORT,
USER_PROFILE_ENDPOINT,
@@ -432,3 +433,29 @@ export const visitedUserProfile = async (userId: string) => {
return undefined;
}
};
+
+export const verifyExistingInformation = async (
+ email: string | undefined,
+ username: string | undefined,
+) => {
+ try {
+ const form = new FormData();
+ if (email) {
+ form.append('email', email);
+ }
+ if (username) {
+ form.append('username', username);
+ }
+ const response = await fetch(REGISTER_VALIDATE_ENDPOINT, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'multipart/form-data',
+ },
+ body: form,
+ });
+ return response.status===200;
+ } catch (error) {
+ console.log(error);
+ return false;
+ }
+};