TP-80461 | merge master
This commit is contained in:
37
.github/workflows/codePush.yml
vendored
37
.github/workflows/codePush.yml
vendored
@@ -61,9 +61,44 @@ jobs:
|
||||
if: ((github.event.inputs.environment == 'Prod' || inputs.environment == 'Prod'))
|
||||
run: yarn move:prod && appcenter codepush release-react -a nfa-navi.com/nfa-app -d Production -t "${{github.event.inputs.target_versions}}" --description "${{github.event.inputs.description}}"
|
||||
|
||||
create_release_tag:
|
||||
generate_source_map:
|
||||
needs: generate
|
||||
runs-on: [default]
|
||||
if: success() && (github.event.inputs.environment == 'Prod') # Only create source map for Prod releases
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
token: ${{ secrets.MY_REPO_PAT }}
|
||||
submodules: recursive
|
||||
- name: Set Node.js 16.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16.x
|
||||
- name: Install yarn
|
||||
run: npm install --global yarn
|
||||
- name: Install dependency
|
||||
run: yarn
|
||||
- name: Generate Android Bundle and Source Map
|
||||
run: |
|
||||
npx react-native bundle \
|
||||
--dev false \
|
||||
--minify true \
|
||||
--platform android \
|
||||
--entry-file index.js \
|
||||
--reset-cache \
|
||||
--bundle-output index.android.bundle \
|
||||
--sourcemap-output index.android.bundle.map
|
||||
|
||||
- name: Upload Source Map
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: source-map
|
||||
path: index.android.bundle.map
|
||||
|
||||
create_release_tag:
|
||||
needs: generate_source_map
|
||||
runs-on: [default]
|
||||
if: success() && (github.event.inputs.environment == 'Prod') # Only create tag for Prod releases
|
||||
steps:
|
||||
- name: Checkout
|
||||
|
||||
14
.github/workflows/newBuild.yml
vendored
14
.github/workflows/newBuild.yml
vendored
@@ -130,10 +130,20 @@ jobs:
|
||||
|
||||
ls
|
||||
|
||||
chmod +r ./android/app/build/outputs/apk/${{ github.event.inputs.flavor || inputs.flavor }}${{github.event.inputs.environment || inputs.environment}}/${{github.event.inputs.type || inputs.type}}/app-${{github.event.inputs.flavor}}${{github.event.inputs.environment}}-release.apk
|
||||
apk_path="./android/app/build/outputs/apk/${{ github.event.inputs.flavor || inputs.flavor }}${{github.event.inputs.environment || inputs.environment}}/${{github.event.inputs.type || inputs.type}}/app-${{ github.event.inputs.flavor || inputs.flavor }}${{github.event.inputs.environment || inputs.environment}}-release.apk"
|
||||
|
||||
echo "$apk_path"
|
||||
|
||||
# Check if APK exists, exit if not
|
||||
if [ ! -f "$apk_path" ]; then
|
||||
echo "Error: APK file not found at $apk_path"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
chmod +r "$apk_path"
|
||||
|
||||
curl --location --request PUT "$upload_url" \
|
||||
--data-binary "@./android/app/build/outputs/apk/${{ github.event.inputs.flavor || inputs.flavor }}${{github.event.inputs.environment || inputs.environment}}/${{github.event.inputs.type || inputs.type}}/app-fieldAgentsQA-release.apk"
|
||||
--data-binary "@$apk_path"
|
||||
|
||||
|
||||
echo "upload compleate"
|
||||
|
||||
2
App.tsx
2
App.tsx
@@ -108,7 +108,7 @@ function App() {
|
||||
async function setForegroundTimeStampAndClickstream() {
|
||||
const now = dayJs().toString();
|
||||
await setItem(StorageKeys.APP_FOREGROUND_TIMESTAMP, now);
|
||||
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.AV_APP_FOREGROUND, { now });
|
||||
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.AV_APP_FOREGROUND, { now }, true);
|
||||
}
|
||||
|
||||
usePolling(askForPermissions, PERMISSION_CHECK_POLL_INTERVAL);
|
||||
|
||||
@@ -134,8 +134,8 @@ def reactNativeArchitectures() {
|
||||
return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"]
|
||||
}
|
||||
|
||||
def VERSION_CODE = 190
|
||||
def VERSION_NAME = "2.13.2"
|
||||
def VERSION_CODE = 195
|
||||
def VERSION_NAME = "2.13.7"
|
||||
|
||||
android {
|
||||
ndkVersion rootProject.ext.ndkVersion
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "AV_APP",
|
||||
"version": "2.13.2",
|
||||
"buildNumber": "190",
|
||||
"version": "2.13.7",
|
||||
"buildNumber": "195",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"android:dev": "yarn move:dev && react-native run-android",
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
import { CaseAllocationType, ICaseItem, IPinnedCasesPayload } from '../screens/allCases/interface';
|
||||
import { AppDispatch } from '../store/store';
|
||||
import { addClickstreamEvent } from '../services/clickstreamEventService';
|
||||
import { CLICKSTREAM_EVENT_NAMES } from '../common/Constants';
|
||||
import { CLICKSTREAM_EVENT_NAMES, LocalStorageKeys } from '../common/Constants';
|
||||
import { logError } from '../components/utlis/errorUtils';
|
||||
import { setFilters } from '../reducer/filtersSlice';
|
||||
import { toast } from '../../RN-UI-LIB/src/components/toast';
|
||||
@@ -25,6 +25,7 @@ import { ToastMessages } from '../screens/allCases/constants';
|
||||
import { GenericFunctionArgs } from '../common/GenericTypes';
|
||||
import { GLOBAL } from '../constants/Global';
|
||||
import { MY_CASE_ITEM } from '../reducer/userSlice';
|
||||
import { getAsyncStorageItem } from '@components/utlis/commonFunctions';
|
||||
|
||||
let _signedApiCallBucket: { req: any; added_At: number; callback: GenericFunctionArgs }[] = [];
|
||||
let _signedApiCallBucketTimer: number = 0;
|
||||
@@ -222,10 +223,23 @@ async function makeBulkSignedApiRequest(
|
||||
callback: GenericFunctionArgs | GenericFunctionArgs[],
|
||||
skipFirebaseUpdate = false,
|
||||
) {
|
||||
let url = getApiUrl(ApiKeys.GET_SIGNED_URL, {}, {skipFirebaseUpdate});
|
||||
const enableCaseCollectionManager =
|
||||
(await getAsyncStorageItem(LocalStorageKeys.COSMOS_CASE_COLLECTION_MANAGER_ENABLE, true)) ??
|
||||
false;
|
||||
let url = getApiUrl(
|
||||
enableCaseCollectionManager ? ApiKeys.GET_SIGNED_URL_V2 : ApiKeys.GET_SIGNED_URL,
|
||||
{},
|
||||
{ skipFirebaseUpdate }
|
||||
);
|
||||
const reporteeReferenceId = GLOBAL?.SELECTED_AGENT_ID;
|
||||
if (reporteeReferenceId && reporteeReferenceId !== MY_CASE_ITEM.referenceId) {
|
||||
url = getApiUrl(ApiKeys.GET_SIGNED_URL_FOR_REPORTEE, {}, { reporteeReferenceId });
|
||||
url = getApiUrl(
|
||||
enableCaseCollectionManager
|
||||
? ApiKeys.GET_SIGNED_URL_FOR_REPORTEE_V2
|
||||
: ApiKeys.GET_SIGNED_URL_FOR_REPORTEE,
|
||||
{},
|
||||
{ reporteeReferenceId }
|
||||
);
|
||||
}
|
||||
_signedApiCallBucket = [];
|
||||
await axiosInstance
|
||||
|
||||
@@ -1336,11 +1336,11 @@ export const LocalStorageKeys = {
|
||||
IMAGE_SYNC_START_TIME: 'imageSyncStartTime',
|
||||
IMAGE_SYNC_TIME: 'imageSyncTime',
|
||||
IMAGE_FILES: 'imageFiles',
|
||||
IS_DATA_SYNC_ALLOWED: 'isDataSyncAllowed',
|
||||
LAST_VIDEO_SYNC_TIME: 'lastVideoSyncTime',
|
||||
LAST_AUDIO_SYNC_TIME: 'lastAudioSyncTime',
|
||||
LAST_ACCOUNTS_SYNC_TIME: 'lastAccountsSyncTime',
|
||||
LAST_CALENDAR_SYNC_TIME: 'lastCalendarSyncTime',
|
||||
COSMOS_CASE_COLLECTION_MANAGER_ENABLE: 'cosmosCaseCollectionManager',
|
||||
};
|
||||
|
||||
export const SourceTextFocused = new Set(['Primary Contact', 'Secondary Contact']);
|
||||
|
||||
@@ -69,7 +69,6 @@ import { sendVideosToServer } from '@services/videoSyncService';
|
||||
import { getSyncUrl } from '@services/syncJsonDataToBe';
|
||||
import { handleCheckAndUpdatePullToRefreshStateForNearbyCases } from '@screens/allCases/utils';
|
||||
import { initialize } from 'react-native-clarity';
|
||||
import { updateImageUploadComponent } from '@components/form/services/formComponents';
|
||||
import { getWifiDetailsSyncUrl } from '@components/utlis/WifiDetails';
|
||||
import useFirestoreUpdates from '@hooks/useFirestoreUpdates';
|
||||
|
||||
@@ -89,7 +88,7 @@ export enum FOREGROUND_TASKS {
|
||||
AUDIO_UPLOAD_JOB = 'AUDIO_UPLOAD_JOB',
|
||||
DATA_SYNC_JOB = 'DATA_SYNC_JOB',
|
||||
NEARBY_CASES_GEOLOCATION_CHECK = 'NEARBY_CASES_GEOLOCATION_CHECK',
|
||||
WIFI_DETAILS_SYNC = 'WIFI_DETAILS_SYNC'
|
||||
WIFI_DETAILS_SYNC = 'WIFI_DETAILS_SYNC',
|
||||
}
|
||||
|
||||
interface ITrackingComponent {
|
||||
@@ -394,15 +393,6 @@ const TrackingComponent: React.FC<ITrackingComponent> = ({ children }) => {
|
||||
LitmusExperimentNameMap[LitmusExperimentName.MS_CLARITY],
|
||||
{ deviceId: GLOBAL.DEVICE_ID }
|
||||
);
|
||||
const dataSyncResponse = await getLitmusExperimentResult(
|
||||
LitmusExperimentNameMap[LitmusExperimentName.COSMOS_DATA_SYNC],
|
||||
{ 'x-customer-id': GLOBAL.AGENT_ID }
|
||||
);
|
||||
const enableFeedbackImageGeotagging = await getLitmusExperimentResult(
|
||||
LitmusExperimentNameMap[LitmusExperimentName.ENABLE_IMAGE_GEO_TAGGING],
|
||||
{ 'x-customer-id': GLOBAL.AGENT_ID }
|
||||
);
|
||||
updateImageUploadComponent(enableFeedbackImageGeotagging);
|
||||
if (
|
||||
MS_CLARITY_PROJECT_ID &&
|
||||
!GLOBAL.MS_CLARITY_INITIALIZED &&
|
||||
@@ -415,7 +405,6 @@ const TrackingComponent: React.FC<ITrackingComponent> = ({ children }) => {
|
||||
initialize(MS_CLARITY_PROJECT_ID);
|
||||
GLOBAL.MS_CLARITY_INITIALIZED = true;
|
||||
}
|
||||
setAsyncStorageItem(LocalStorageKeys.IS_DATA_SYNC_ALLOWED, dataSyncResponse);
|
||||
}
|
||||
if (nextAppState === AppStates.BACKGROUND) {
|
||||
await setItem(StorageKeys.APP_BACKGROUND_TIMESTAMP, now);
|
||||
|
||||
@@ -8,14 +8,12 @@ import { GenericObject, GenericType } from '../../common/GenericTypes';
|
||||
import StarRating from '../../../RN-UI-LIB/src/components/star_rating/StarRating';
|
||||
import { getPhoneNumberString, memoize } from '../utlis/commonFunctions';
|
||||
import {
|
||||
getImageFromDB,
|
||||
getImageHeightWrtAspectRatio,
|
||||
} from '../../services/casePayload.transformer';
|
||||
import { formatAmount } from '../../../RN-UI-LIB/src/utlis/amount';
|
||||
import GeolocationAddressAnswer from './components/GeolocationAddressAnswer';
|
||||
import dayjs from 'dayjs';
|
||||
import { BUSINESS_DATE_FORMAT, CUSTOM_ISO_DATE_FORMAT } from '@rn-ui-lib/utils/dates';
|
||||
import { IOfflineImage } from './components/imageUpload/interfaces';
|
||||
|
||||
const RATING_COMPONENT = 'Rating';
|
||||
const MAX_RATING = 5;
|
||||
@@ -33,6 +31,7 @@ interface IAnswerRender {
|
||||
caseId: string;
|
||||
journey: string;
|
||||
metaData?: GenericObject;
|
||||
questionId: string;
|
||||
}
|
||||
|
||||
interface IDarkBoldText {
|
||||
@@ -50,7 +49,7 @@ const DarkBoldText = ({ text }: IDarkBoldText) => {
|
||||
};
|
||||
|
||||
const AnswerRender: React.FC<IAnswerRender> = (props) => {
|
||||
const { answer, visited, section, caseId, journey, metaData } = props;
|
||||
const { answer, visited, section, caseId, journey, metaData, questionId } = props;
|
||||
const data = useAppSelector((state) => state.case.caseForm?.[caseId]?.[journey]);
|
||||
const caseType = useAppSelector((state) => state.allCases.caseDetails[caseId]?.caseType);
|
||||
const templateData = useAppSelector((state) => state.case.templateData[caseType]);
|
||||
@@ -59,23 +58,19 @@ const AnswerRender: React.FC<IAnswerRender> = (props) => {
|
||||
const getPhoneNumberStringFromNumber = memoize((phoneNumber: string) => {
|
||||
return getPhoneNumberString(mobileNumbers?.find((a) => a.referenceId === phoneNumber));
|
||||
});
|
||||
const [image, setImage] = React.useState<IOfflineImage | null>(null);
|
||||
|
||||
const questions = templateData.questions;
|
||||
const options = templateData.options;
|
||||
|
||||
React.useEffect(() => {
|
||||
if (answer?.type === AnswerType.image) {
|
||||
(async () => {
|
||||
const data = await getImageFromDB(answer.answer);
|
||||
setImage(data);
|
||||
})();
|
||||
}
|
||||
}, [answer]);
|
||||
const intermediateDocsToBeUploaded = useAppSelector(
|
||||
(state) => state.feedbackImages.intermediateDocsToBeUploaded
|
||||
);
|
||||
|
||||
const imageDoc = intermediateDocsToBeUploaded?.[caseId]?.documents?.[questionId];
|
||||
|
||||
const imageHeightWrtAspectRatio = getImageHeightWrtAspectRatio(
|
||||
image?.imageWidth || 0,
|
||||
image?.imageHeight || 0,
|
||||
imageDoc?.imageWidth || 0,
|
||||
imageDoc?.imageHeight || 0,
|
||||
SCREEN_WIDTH - ANSWER_HORIZONTAL_PADDING
|
||||
);
|
||||
|
||||
@@ -101,6 +96,7 @@ const AnswerRender: React.FC<IAnswerRender> = (props) => {
|
||||
journey={journey}
|
||||
answer={newAnswer as unknown as IAnswer}
|
||||
metaData={questions[question]?.metadata}
|
||||
questionId={question}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
@@ -126,13 +122,13 @@ const AnswerRender: React.FC<IAnswerRender> = (props) => {
|
||||
}
|
||||
return <DarkBoldText text={answer.answer} />;
|
||||
case AnswerType.image:
|
||||
if(!image?.imageData) return null;
|
||||
if(!imageDoc?.fileUri) return null;
|
||||
return (
|
||||
<ImageBackground
|
||||
style={[styles.image, { height: Number(imageHeightWrtAspectRatio) || 350 }]}
|
||||
imageStyle={[styles.br8, GenericStyles.mt8]}
|
||||
source={{
|
||||
uri: image?.imageData || '',
|
||||
uri: imageDoc?.fileUri || '',
|
||||
}}
|
||||
></ImageBackground>
|
||||
);
|
||||
|
||||
@@ -101,6 +101,7 @@ const Submit: React.FC<ISubmit> = (props) => {
|
||||
journey={journey}
|
||||
answer={answer as unknown as IAnswer}
|
||||
metaData={questions[question]?.metadata}
|
||||
questionId={question}
|
||||
/>
|
||||
<SeparatorBorderComponent />
|
||||
</View>
|
||||
|
||||
@@ -73,7 +73,7 @@ const TextInput: React.FC<ITextInput> = (props) => {
|
||||
maxLength={question.metadata.validators?.phoneNumber?.value as number}
|
||||
/>
|
||||
)}
|
||||
name={`widgetContext.${widgetId}.sectionContext.${props.sectionId}.questionContext.${questionId}`}
|
||||
name={`widgetContext.${widgetId}.sectionContext.${sectionId}.questionContext.${questionId}`}
|
||||
/>
|
||||
<ErrorMessage
|
||||
show={
|
||||
|
||||
@@ -10,7 +10,6 @@ import { useAppDispatch, useAppSelector } from '../../../../hooks';
|
||||
import { RootState } from '../../../../store/store';
|
||||
import { AnswerType } from '../../interface';
|
||||
import ErrorMessage from '../ErrorMessage';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
import OfflineImageDAO from '../../../../wmDB/dao/OfflineImageDAO';
|
||||
import { CLICKSTREAM_EVENT_NAMES, PrefixJpegBase64Image } from '../../../../common/Constants';
|
||||
import { addClickstreamEvent, ClickstreamDesc } from '../../../../services/clickstreamEventService';
|
||||
@@ -24,10 +23,16 @@ import { capturePhoto } from '@components/utlis/ImageUtlis';
|
||||
import CameraIcon from '@rn-ui-lib/icons/CameraIcon';
|
||||
import { getImageHeightWrtAspectRatio } from '@services/casePayload.transformer';
|
||||
import { IImageUpload, ImageValidationError } from './interfaces';
|
||||
import { getImageUri } from './utils';
|
||||
import dayjs from 'dayjs';
|
||||
import ImagePlaceholder from './ImagePlaceholder';
|
||||
import { IMAGE_FORMAT_JPEG, IMAGE_PADDING, IMAGE_QUALITY, MAX_HEIGHT, MAX_WIDTH, PhotoUploadErrorMessages } from './constants';
|
||||
import {
|
||||
IMAGE_FORMAT_JPEG,
|
||||
IMAGE_PADDING,
|
||||
IMAGE_QUALITY,
|
||||
MAX_HEIGHT,
|
||||
MAX_WIDTH,
|
||||
PhotoUploadErrorMessages,
|
||||
} from './constants';
|
||||
import { toast } from '@rn-ui-lib/components/toast';
|
||||
import { IImageDetails, LAUNCH_REQUEST } from '@rn-ui-lib/components/photoUpload/PhotoUpload';
|
||||
import UploadIcon from '@rn-ui-lib/icons/UploadIcon';
|
||||
@@ -46,6 +51,10 @@ const ImageUploadV2: React.FC<IImageUpload> = (props) => {
|
||||
const deviceGeolocationCoordinate = useAppSelector(
|
||||
(state) => state.foregroundService.deviceGeolocationCoordinate
|
||||
);
|
||||
const intermediateDocsToBeUploaded = useAppSelector(
|
||||
(state) => state.feedbackImages.intermediateDocsToBeUploaded
|
||||
);
|
||||
const imageDoc = intermediateDocsToBeUploaded?.[caseId]?.documents?.[questionId];
|
||||
const { latitude, longitude, timestamp } = deviceGeolocationCoordinate || {};
|
||||
const question = template.questions[questionId as keyof typeof template.questions];
|
||||
const dataFromRedux = useSelector(
|
||||
@@ -67,11 +76,17 @@ const ImageUploadV2: React.FC<IImageUpload> = (props) => {
|
||||
return null;
|
||||
}
|
||||
|
||||
const addOriginalFileUriToDocs = (caseId: string, fileUri: string, questionKey: string) => {
|
||||
const addOriginalFileUriToDocs = (
|
||||
caseId: string,
|
||||
fileUri: string,
|
||||
questionKey: string,
|
||||
imageWidth: number,
|
||||
imageHeight: number
|
||||
) => {
|
||||
if (!fileUri) {
|
||||
return;
|
||||
}
|
||||
dispatch(addIntermediateDocument({ caseId, fileUri, questionKey }));
|
||||
dispatch(addIntermediateDocument({ caseId, fileUri, questionKey, imageWidth, imageHeight }));
|
||||
};
|
||||
|
||||
const handleImageDelete = () => {
|
||||
@@ -171,7 +186,6 @@ const ImageUploadV2: React.FC<IImageUpload> = (props) => {
|
||||
});
|
||||
const { base64 = '', uri = '', imageWidth, imageHeight } = result;
|
||||
handlePictureClickClickstream();
|
||||
addOriginalFileUriToDocs(caseId, uri, questionId);
|
||||
if (!base64) {
|
||||
return;
|
||||
}
|
||||
@@ -194,6 +208,7 @@ const ImageUploadV2: React.FC<IImageUpload> = (props) => {
|
||||
await OfflineImageDAO.addImage(base64Image, uri, uniqueId, imageWidth, imageHeight);
|
||||
toast({ type: 'success', text1: 'Geolocation & Timestamp added successfully' });
|
||||
setImageLoading(false);
|
||||
addOriginalFileUriToDocs(caseId, uri, questionId, imageWidth, imageHeight);
|
||||
} catch (err: unknown) {
|
||||
const error = err as ImageValidationError;
|
||||
handlePictureClickClickstream(error);
|
||||
@@ -206,7 +221,11 @@ const ImageUploadV2: React.FC<IImageUpload> = (props) => {
|
||||
}
|
||||
};
|
||||
|
||||
const { uri, imageWidth = 0, imageHeight = 0 } = getImageUri(props.offlineImages, imageId) || {};
|
||||
const {
|
||||
fileUri,
|
||||
imageHeight = 350,
|
||||
imageWidth = 350
|
||||
} = imageDoc || {};
|
||||
|
||||
const imageHeightWrtAspectRatio = getImageHeightWrtAspectRatio(
|
||||
imageWidth,
|
||||
@@ -337,7 +356,7 @@ const ImageUploadV2: React.FC<IImageUpload> = (props) => {
|
||||
style={[styles.image, { height: Number(imageHeightWrtAspectRatio) || 350 }]}
|
||||
imageStyle={GenericStyles.br8}
|
||||
source={{
|
||||
uri,
|
||||
uri: fileUri,
|
||||
}}
|
||||
onError={(error) => handleError('Error in image rendering')}
|
||||
>
|
||||
@@ -399,8 +418,5 @@ const styles = StyleSheet.create({
|
||||
borderRadius: 8,
|
||||
},
|
||||
});
|
||||
const enhance = withObservables([], () => ({
|
||||
offlineImages: OfflineImageDAO.observeOfflineImage(),
|
||||
}));
|
||||
|
||||
export default enhance(ImageUploadV2);
|
||||
export default ImageUploadV2;
|
||||
|
||||
@@ -77,7 +77,9 @@ const Widget: React.FC<IWidget> = (props) => {
|
||||
const templateData = useAppSelector((state) => state.case?.templateData?.[caseType]);
|
||||
const caseData = useAppSelector((state) => state.allCases?.caseDetails?.[caseId]);
|
||||
const dataToBeValidated = useAppSelector((state) => state.case?.caseForm?.[caseId]?.[journey]);
|
||||
const intermediateDocsToBeUploaded = useAppSelector((state) => state.feedbackImages?.intermediateDocsToBeUploaded);
|
||||
const intermediateDocsToBeUploaded = useAppSelector(
|
||||
(state) => state.feedbackImages?.intermediateDocsToBeUploaded
|
||||
);
|
||||
|
||||
const name = getWidgetNameFromRoute(props.route.name, caseType);
|
||||
const { sections, conditionActions: widgetConditionActions, isLeaf } = templateData.widget[name];
|
||||
@@ -402,6 +404,7 @@ const Widget: React.FC<IWidget> = (props) => {
|
||||
onBack={handleCloseIconPress}
|
||||
/>
|
||||
<ScrollView
|
||||
keyboardShouldPersistTaps="handled"
|
||||
contentContainerStyle={[
|
||||
GenericStyles.p16,
|
||||
GenericStyles.silverBackground,
|
||||
|
||||
@@ -16,7 +16,7 @@ export const FormComponentList = {
|
||||
TextInput,
|
||||
TextArea,
|
||||
RadioButton,
|
||||
ImageUpload,
|
||||
ImageUpload: ImageUploadV2,
|
||||
Checkbox,
|
||||
Rating,
|
||||
Dropdown,
|
||||
@@ -28,10 +28,3 @@ export const FormComponentList = {
|
||||
Amount: TextInput,
|
||||
};
|
||||
|
||||
export const updateImageUploadComponent = (enableImageGeoTagging: boolean) => {
|
||||
if (enableImageGeoTagging) {
|
||||
FormComponentList.ImageUpload = ImageUploadV2;
|
||||
} else {
|
||||
FormComponentList.ImageUpload = ImageUpload;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -65,7 +65,9 @@ export enum ApiKeys {
|
||||
GET_TELEPHONE_NUMBERS = 'GET_TELEPHONE_NUMBERS',
|
||||
GET_TELEPHONE_NUMBERS_V2 = 'GET_TELEPHONE_NUMBERS_V2',
|
||||
FIRESTORE_INCONSISTENCY_INFO = 'FIRESTORE_INCONSISTENCY_INFO',
|
||||
FIRESTORE_INCONSISTENCY_INFO_V2 = 'FIRESTORE_INCONSISTENCY_INFO_V2',
|
||||
GET_CASE_DETAILS_FROM_API = 'GET_CASE_DETAILS_FROM_API',
|
||||
GET_CASE_DETAILS_FROM_API_V2 = 'GET_CASE_DETAILS_FROM_API_V2',
|
||||
DAILY_COMMITMENT = 'DAILY_COMMITMENT',
|
||||
GET_PTP_AMOUNT = 'GET_PTP_AMOUNT',
|
||||
GET_VISIBILITY_STATUS = 'GET_VISIBILITY_STATUS',
|
||||
@@ -94,7 +96,9 @@ export enum ApiKeys {
|
||||
SEND_COMMUNICATION_NAVI_ACCOUNT = 'SEND_COMMUNICATION_NAVI_ACCOUNT',
|
||||
SYNC_CALL_FEEDBACK_NUDGE_DETAILS = 'SYNC_CALL_FEEDBACK_NUDGE_DETAILS',
|
||||
GENERATE_DYNAMIC_DOCUMENT = 'GENERATE_DYNAMIC_DOCUMENT',
|
||||
DOWNLOAD_LATEST_APP = 'DOWNLOAD_LATEST_APP'
|
||||
DOWNLOAD_LATEST_APP = 'DOWNLOAD_LATEST_APP',
|
||||
GET_SIGNED_URL_V2 = 'GET_SIGNED_URL_V2',
|
||||
GET_SIGNED_URL_FOR_REPORTEE_V2 = 'GET_SIGNED_URL_FOR_REPORTEE_V2'
|
||||
}
|
||||
|
||||
export const API_URLS: Record<ApiKeys, string> = {} as Record<ApiKeys, string>;
|
||||
@@ -112,7 +116,9 @@ API_URLS[ApiKeys.GENERATE_PAYMENT_LINK_V2] = '/payments/v2/send-payment-link';
|
||||
API_URLS[ApiKeys.ADDRESSES_GEOLOCATION] = '/addresses-geolocations';
|
||||
API_URLS[ApiKeys.NEW_ADDRESS] = '/addresses';
|
||||
API_URLS[ApiKeys.GET_SIGNED_URL] = '/cases/get-signed-urls';
|
||||
API_URLS[ApiKeys.GET_SIGNED_URL_V2] = '/cases/v2/get-signed-urls';
|
||||
API_URLS[ApiKeys.GET_SIGNED_URL_FOR_REPORTEE] = '/cases/get-signed-urls-for-reportee';
|
||||
API_URLS[ApiKeys.GET_SIGNED_URL_FOR_REPORTEE_V2] = '/cases/v2/get-signed-urls-for-reportee';
|
||||
API_URLS[ApiKeys.CASE_UNIFIED_DETAILS] = '/v3/collection-cases/unified-details/{loanAccountNumber}';
|
||||
API_URLS[ApiKeys.CASE_UNIFIED_DETAILS_V4] =
|
||||
'/v5/collection-cases/unified-details/{loanAccountNumber}';
|
||||
@@ -144,10 +150,14 @@ API_URLS[ApiKeys.GET_PERFORMANCE_METRICS] = '/allocation-cycle/agent-performance
|
||||
API_URLS[ApiKeys.GET_CASH_COLLECTED] = '/allocation-cycle/cash-collected-split';
|
||||
API_URLS[ApiKeys.GET_TELEPHONE_NUMBERS] =
|
||||
'/v2/collection-cases/telephones-view/{loanAccountNumber}';
|
||||
API_URLS[ApiKeys.GET_TELEPHONE_NUMBERS_V2] = '/collections/{loanAccountNumber}/telephones-agent-call-activity-view';
|
||||
API_URLS[ApiKeys.GET_TELEPHONE_NUMBERS_V2] =
|
||||
'/collections/{loanAccountNumber}/telephones-agent-call-activity-view';
|
||||
API_URLS[ApiKeys.FIRESTORE_INCONSISTENCY_INFO] = '/cases/sync-status';
|
||||
API_URLS[ApiKeys.FIRESTORE_INCONSISTENCY_INFO_V2] = '/cases/v2/sync-status';
|
||||
API_URLS[ApiKeys.GET_CASE_DETAILS_FROM_API] =
|
||||
'/collection-cases/minimal-collection-case-view/{caseId}';
|
||||
API_URLS[ApiKeys.GET_CASE_DETAILS_FROM_API_V2] =
|
||||
'/v2/collection-cases/minimal-collection-case-view/{caseId}';
|
||||
API_URLS[ApiKeys.DAILY_COMMITMENT] = '/daily-commitment';
|
||||
API_URLS[ApiKeys.GET_PTP_AMOUNT] = '/ptps-due-view/agent-detail';
|
||||
API_URLS[ApiKeys.GET_VISIBILITY_STATUS] = '/daily-commitment/visibility';
|
||||
@@ -172,10 +182,12 @@ API_URLS[ApiKeys.DUE_AMOUNT_SUMMARY] = '/collection-cases/{loanAccountNumber}/am
|
||||
API_URLS[ApiKeys.FEE_WAIVER_HISTORY] = '/collection-cases/{loanAccountNumber}/waiver-history';
|
||||
API_URLS[ApiKeys.FEE_WAIVER_V2] = '/loan/request/{loanAccountNumber}/adjust-component/v2';
|
||||
API_URLS[ApiKeys.GET_PIN_CODES_DETAILS] = '/api/v1/pincodes/{pinCode}';
|
||||
API_URLS[ApiKeys.CALL_CUSTOMER] = '/call-recording/call-request/{loanAccountNumber}/{telephoneReferenceId}';
|
||||
API_URLS[ApiKeys.CALL_CUSTOMER] =
|
||||
'/call-recording/call-request/{loanAccountNumber}/{telephoneReferenceId}';
|
||||
API_URLS[ApiKeys.SYNC_ACTIVE_CALL_DETAILS] = '/call-recording/call-status';
|
||||
API_URLS[ApiKeys.GET_CALL_HISTORY] = '/call-recording/call-history/{loanAccountNumber}';
|
||||
API_URLS[ApiKeys.SYNC_CALL_FEEDBACK_NUDGE_DETAILS] = '/call-recording/acknowledge-feedback-nudge/{callId}';
|
||||
API_URLS[ApiKeys.SYNC_CALL_FEEDBACK_NUDGE_DETAILS] =
|
||||
'/call-recording/acknowledge-feedback-nudge/{callId}';
|
||||
API_URLS[ApiKeys.FETCH_CUSTOMER_DOCUMENTS] = '/documents/{loanAccountNumber}';
|
||||
API_URLS[ApiKeys.FETCH_AGENT_DOCUMENTS] = '/documents/agent';
|
||||
API_URLS[ApiKeys.FETCH_DOCUMENT_SPECIFIC_LANGUAGE] = '/documents/language/{loanAccountNumber}';
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import crashlytics from '@react-native-firebase/crashlytics';
|
||||
import { IUserSlice } from '../../reducer/userSlice';
|
||||
import { getAppVersion } from './commonFunctions';
|
||||
|
||||
export const initCrashlytics = async (userState: IUserSlice) => {
|
||||
if (!userState) return;
|
||||
@@ -10,7 +11,8 @@ export const initCrashlytics = async (userState: IUserSlice) => {
|
||||
deviceId: userState.deviceId,
|
||||
phoneNumber: userState.user?.phoneNumber as string,
|
||||
emailId: userState.user?.emailId as string,
|
||||
agentId: userState.user?.referenceId as string
|
||||
agentId: userState.user?.referenceId as string,
|
||||
appVersion: getAppVersion(),
|
||||
}),
|
||||
]);
|
||||
};
|
||||
|
||||
@@ -2,7 +2,12 @@ import firestore from '@react-native-firebase/firestore';
|
||||
import { useAppDispatch, useAppSelector } from '@hooks';
|
||||
import store, { type RootState } from '@store';
|
||||
import { updateCaseDetailsFirestore } from '@reducers/allCasesSlice';
|
||||
import { CLICKSTREAM_EVENT_NAMES, FirestoreUpdateTypes, SyncedSource } from '@common/Constants';
|
||||
import {
|
||||
CLICKSTREAM_EVENT_NAMES,
|
||||
FirestoreUpdateTypes,
|
||||
LocalStorageKeys,
|
||||
SyncedSource,
|
||||
} from '@common/Constants';
|
||||
import axiosInstance, { ApiKeys, getApiUrl } from '@utils/apiHelper';
|
||||
import { getSyncCaseIds } from '@utils/firebaseFallbackUtils';
|
||||
import { logError } from '@utils/errorUtils';
|
||||
@@ -15,6 +20,7 @@ import {
|
||||
getFirestoreResyncIntervalInMinutes,
|
||||
} from '@common/AgentActivityConfigurableConstants';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import { getAsyncStorageItem } from '@components/utlis/commonFunctions';
|
||||
|
||||
const selectedAgentReferenceIDForMyCases = 'MY_CASES';
|
||||
|
||||
@@ -33,10 +39,19 @@ const useResyncFirebase = () => {
|
||||
const selectedAgentRefId = store?.getState()?.user?.selectedAgent?.referenceId || '';
|
||||
const refIdForLoggedInAndSelectedUser =
|
||||
selectedAgentRefId === selectedAgentReferenceIDForMyCases ? refId : selectedAgentRefId;
|
||||
const _getCaseDetailsFromApi = (caseId: string) => {
|
||||
const getCaseDetailsFromApiUrl = getApiUrl(ApiKeys.GET_CASE_DETAILS_FROM_API, {
|
||||
caseId: caseId,
|
||||
});
|
||||
|
||||
const _getCaseDetailsFromApi = async (caseId: string) => {
|
||||
const enableCaseCollectionManager =
|
||||
(await getAsyncStorageItem(LocalStorageKeys.COSMOS_CASE_COLLECTION_MANAGER_ENABLE, true)) ??
|
||||
false;
|
||||
const getCaseDetailsFromApiUrl = getApiUrl(
|
||||
enableCaseCollectionManager
|
||||
? ApiKeys.GET_CASE_DETAILS_FROM_API_V2
|
||||
: ApiKeys.GET_CASE_DETAILS_FROM_API,
|
||||
{
|
||||
caseId: caseId,
|
||||
}
|
||||
);
|
||||
|
||||
return axiosInstance.get(getCaseDetailsFromApiUrl, {
|
||||
params: {
|
||||
@@ -78,10 +93,17 @@ const useResyncFirebase = () => {
|
||||
if (minutesSinceLastResync < getFirestoreResyncIntervalInMinutes()) {
|
||||
return;
|
||||
}
|
||||
console.log('firebase resync started');
|
||||
|
||||
void addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_FIREBASE_RESYNC_STARTED);
|
||||
const getFirestoreInconsistencyUrl = getApiUrl(ApiKeys.FIRESTORE_INCONSISTENCY_INFO);
|
||||
const enableCaseCollectionManager =
|
||||
(await getAsyncStorageItem(LocalStorageKeys.COSMOS_CASE_COLLECTION_MANAGER_ENABLE, true)) ??
|
||||
false;
|
||||
|
||||
const getFirestoreInconsistencyUrl = getApiUrl(
|
||||
enableCaseCollectionManager
|
||||
? ApiKeys.FIRESTORE_INCONSISTENCY_INFO_V2
|
||||
: ApiKeys.FIRESTORE_INCONSISTENCY_INFO
|
||||
);
|
||||
const casesList = store?.getState()?.allCases?.casesList || [];
|
||||
const casesPath = `allocations/${refIdForLoggedInAndSelectedUser}/cases`;
|
||||
const localCases = getSyncCaseIds(casesList);
|
||||
|
||||
@@ -79,7 +79,7 @@ const getCaseListComponents = (casesList: ICaseItem[], caseDetails: Record<strin
|
||||
const pinnedList: ICaseItem[] = [];
|
||||
casesList.forEach((item) => {
|
||||
const { caseReferenceId, pinRank } = item;
|
||||
const { caseStatus } = caseDetails[caseReferenceId];
|
||||
const { caseStatus } = caseDetails[caseReferenceId] || {};
|
||||
const isCaseCompleted = COMPLETED_STATUSES.includes(caseStatus);
|
||||
isCaseCompleted
|
||||
? completedList.push(item)
|
||||
@@ -161,7 +161,7 @@ const allCasesSlice = createSlice({
|
||||
switch (updateType) {
|
||||
case FirestoreUpdateTypes.MODIFIED: {
|
||||
const index = state.casesList?.findIndex(
|
||||
(caseItem) => caseItem.caseReferenceId === caseId
|
||||
(caseItem) => caseItem.caseReferenceId?.toString() === caseId?.toString()
|
||||
);
|
||||
if (index !== -1) {
|
||||
if (pinRank && !state.casesList[index].pinRank) {
|
||||
@@ -235,7 +235,7 @@ const allCasesSlice = createSlice({
|
||||
}
|
||||
case FirestoreUpdateTypes.REMOVED: {
|
||||
const index = state.casesList.findIndex(
|
||||
(caseItem) => caseItem.caseReferenceId === caseId
|
||||
(caseItem) => caseItem.caseReferenceId?.toString() === caseId?.toString()
|
||||
);
|
||||
const currentScreen = getCurrentScreen();
|
||||
// Redirect to home screen if the case deletes which the agent is seeing
|
||||
|
||||
@@ -3,6 +3,8 @@ import { isEmpty } from '../../RN-UI-LIB/src/utlis/common';
|
||||
|
||||
export interface IDocument {
|
||||
fileUri?: string;
|
||||
imageWidth?: number;
|
||||
imageHeight?: number;
|
||||
}
|
||||
|
||||
interface IDocumentDetail {
|
||||
@@ -25,12 +27,14 @@ const feedbackImagesSlice = createSlice({
|
||||
initialState,
|
||||
reducers: {
|
||||
addIntermediateDocument: (state, action) => {
|
||||
const { caseId, questionKey, fileUri } = action.payload;
|
||||
const { caseId, questionKey, fileUri, imageWidth, imageHeight } = action.payload;
|
||||
if(!caseId) return;
|
||||
const doc = {
|
||||
questionKey,
|
||||
fileUri,
|
||||
originalImageDocumentReferenceId: '',
|
||||
imageWidth,
|
||||
imageHeight
|
||||
};
|
||||
if (state.intermediateDocsToBeUploaded?.[caseId]?.documents) {
|
||||
state.intermediateDocsToBeUploaded[caseId].documents[questionKey] = doc;
|
||||
|
||||
@@ -70,7 +70,7 @@ const ListItem: React.FC<IListItem> = (props) => {
|
||||
isVisitPlan,
|
||||
} = props;
|
||||
const {
|
||||
id: caseId,
|
||||
caseReferenceId: caseId,
|
||||
isIntermediateOrSelectedTodoCaseItem,
|
||||
caseStatus,
|
||||
caseType,
|
||||
@@ -194,7 +194,7 @@ const ListItem: React.FC<IListItem> = (props) => {
|
||||
const distanceMapOfNearbyCases =
|
||||
useAppSelector((state) => state.nearbyCasesSlice.caseReferenceIdToDistanceMap) || {};
|
||||
const selectedTab = useAppSelector((state) => state?.nearbyCasesSlice?.sortTabSelected);
|
||||
const distanceOfCaseItem = distanceMapOfNearbyCases.get(caseListItemDetailObj?.id);
|
||||
const distanceOfCaseItem = distanceMapOfNearbyCases.get(caseListItemDetailObj?.caseReferenceId);
|
||||
const isNearestCaseView = selectedTab === TABS_KEYS.NEAREST_CASE;
|
||||
const showInVisitPlanTag = isCaseItemPinnedMainView && !caseCompleted;
|
||||
const widthStyle = {
|
||||
|
||||
@@ -135,11 +135,6 @@ function AuthRouter() {
|
||||
CosmosForegroundService.stopAll();
|
||||
}
|
||||
});
|
||||
getLitmusExperimentResult(LitmusExperimentNameMap[LitmusExperimentName.COSMOS_DATA_SYNC], {
|
||||
'x-customer-id': GLOBAL.AGENT_ID,
|
||||
}).then((response) => {
|
||||
setAsyncStorageItem(LocalStorageKeys.IS_DATA_SYNC_ALLOWED, response);
|
||||
});
|
||||
}
|
||||
}, [isLoggedIn]);
|
||||
|
||||
|
||||
@@ -5,8 +5,11 @@ import {
|
||||
import React, { useEffect } from 'react';
|
||||
import { _map, MILLISECONDS_IN_A_MINUTE } from '../../../RN-UI-LIB/src/utlis/common';
|
||||
import { getNotifications, notificationAction } from '../../action/notificationActions';
|
||||
import { SCREEN_ANIMATION_DURATION } from '../../common/Constants';
|
||||
import { getScreenFocusListenerObj } from '../../components/utlis/commonFunctions';
|
||||
import { LocalStorageKeys, SCREEN_ANIMATION_DURATION } from '../../common/Constants';
|
||||
import {
|
||||
getScreenFocusListenerObj,
|
||||
setAsyncStorageItem,
|
||||
} from '../../components/utlis/commonFunctions';
|
||||
import { useAppDispatch, useAppSelector } from '../../hooks';
|
||||
import useIsOnline from '../../hooks/useIsOnline';
|
||||
import AllCasesMain from '../allCases';
|
||||
@@ -28,7 +31,6 @@ import getLitmusExperimentResult, {
|
||||
LitmusExperimentNameMap,
|
||||
} from '@services/litmusExperiments.service';
|
||||
import { GLOBAL } from '@constants/Global';
|
||||
import { updateImageUploadComponent } from '@components/form/services/formComponents';
|
||||
|
||||
const Stack = createNativeStackNavigator();
|
||||
|
||||
@@ -62,24 +64,19 @@ const ProtectedRouter = () => {
|
||||
useEffect(() => {
|
||||
if (isOnline) {
|
||||
dispatch(getNotifications());
|
||||
dispatch(getAgentDetail());
|
||||
dispatch(getSelfieDocument());
|
||||
getLitmusExperimentResult(
|
||||
LitmusExperimentNameMap[LitmusExperimentName.ENABLE_IMAGE_GEO_TAGGING],
|
||||
LitmusExperimentNameMap[LitmusExperimentName.COSMOS_CASE_COLLECTION_MANAGER],
|
||||
{
|
||||
'x-customer-id': GLOBAL.AGENT_ID,
|
||||
}
|
||||
).then((response) => {
|
||||
updateImageUploadComponent(response);
|
||||
setAsyncStorageItem(LocalStorageKeys.COSMOS_CASE_COLLECTION_MANAGER_ENABLE, response);
|
||||
});
|
||||
}
|
||||
}, [isOnline]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOnline) {
|
||||
dispatch(getAgentDetail());
|
||||
dispatch(getSelfieDocument());
|
||||
}
|
||||
}, [isOnline]);
|
||||
|
||||
// Check the queue of read action notifications in case of offline
|
||||
useEffect(() => {
|
||||
if (isOnline && notificationsWithActions.length) {
|
||||
|
||||
@@ -72,11 +72,6 @@ export const prepareAudioForUpload = async () => {
|
||||
|
||||
export const sendAudiosToServer = async () => {
|
||||
// check if there are any files to upload
|
||||
|
||||
const isDataSyncEnabled = await getAsyncStorageItem(LocalStorageKeys.IS_DATA_SYNC_ALLOWED, true) ?? false;
|
||||
|
||||
if (!isDataSyncEnabled) return;
|
||||
|
||||
const zipFiles = FileDB.getFiles((files) => files.mimeType === MimeTypes.ZIP && files.type === 'AUDIOS');
|
||||
|
||||
if (zipFiles.length === 0) {
|
||||
|
||||
@@ -14,11 +14,6 @@ export const minutesAgo = (minutes: number) => {
|
||||
}
|
||||
|
||||
export const imageSyncService = async () => {
|
||||
|
||||
const isDataSyncAllowed = await getAsyncStorageItem(LocalStorageKeys.IS_DATA_SYNC_ALLOWED, true) ?? false;
|
||||
if (!isDataSyncAllowed) return;
|
||||
|
||||
|
||||
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_DEVICE_DATA_SYNC_START);
|
||||
|
||||
const endTime = Date.now();
|
||||
@@ -112,11 +107,6 @@ export const prepareImagesForUpload = async () => {
|
||||
|
||||
export const sendImagesToServer = async () => {
|
||||
// check if there are any files to upload
|
||||
|
||||
const isDataSyncAllowed = await getAsyncStorageItem(LocalStorageKeys.IS_DATA_SYNC_ALLOWED, true) ?? false;
|
||||
|
||||
if (!isDataSyncAllowed) return;
|
||||
|
||||
const zipFiles = FileDB.getFiles((files) => files.mimeType === MimeTypes.ZIP && files.type === 'IMAGES');
|
||||
|
||||
if (zipFiles.length === 0) {
|
||||
|
||||
@@ -10,6 +10,7 @@ export enum LitmusExperimentName {
|
||||
COSMOS_DATA_SYNC = 'data_sync',
|
||||
MS_CLARITY = 'ms_clarity',
|
||||
ENABLE_IMAGE_GEO_TAGGING = 'enable_image_geotagging',
|
||||
COSMOS_CASE_COLLECTION_MANAGER = 'cosmos_case_collection_manager',
|
||||
}
|
||||
|
||||
export const LitmusExperimentNameMap = {
|
||||
@@ -18,6 +19,7 @@ export const LitmusExperimentNameMap = {
|
||||
[LitmusExperimentName.COSMOS_DATA_SYNC]: 'data_sync',
|
||||
[LitmusExperimentName.MS_CLARITY]: 'cosmos_ms_clarity',
|
||||
[LitmusExperimentName.ENABLE_IMAGE_GEO_TAGGING]: 'enable_image_geotagging',
|
||||
[LitmusExperimentName.COSMOS_CASE_COLLECTION_MANAGER]: 'cosmos_case_collection_manager',
|
||||
};
|
||||
|
||||
const getLitmusExperimentResult = async (
|
||||
|
||||
@@ -71,18 +71,8 @@ export const prepareVideosForUpload = async () => {
|
||||
|
||||
export const sendVideosToServer = async () => {
|
||||
// check if there are any files to upload
|
||||
|
||||
|
||||
const isDataSyncEnabled = await getAsyncStorageItem(LocalStorageKeys.IS_DATA_SYNC_ALLOWED, true) ?? false;
|
||||
|
||||
|
||||
if (!isDataSyncEnabled) return;
|
||||
|
||||
const zipFiles = FileDB.getFiles((files) => files.mimeType === MimeTypes.ZIP && files.type === 'VIDEOS');
|
||||
|
||||
|
||||
|
||||
|
||||
if (zipFiles.length === 0) {
|
||||
prepareVideosForUpload();
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user