Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multipart Upload for Citizen Report #773

Merged
merged 5 commits into from
Oct 17, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions mobile/app/(observer)/(app)/form-questionnaire/[questionId].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export type SearchParamType = {
};

const FormQuestionnaire = () => {
const queryClient = useQueryClient()
const queryClient = useQueryClient();
const { t } = useTranslation(["polling_station_form_wizard", "common"]);
const { questionId, formId, language } = useLocalSearchParams<SearchParamType>();

Expand Down Expand Up @@ -369,7 +369,6 @@ const FormQuestionnaire = () => {
activeQuestion.question.id
) {
try {

const totalParts = Math.ceil(cameraResult.size! / MULTIPART_FILE_UPLOAD_SIZE);
const attachmentId = Crypto.randomUUID();
const payload: AddAttachmentStartAPIPayload = {
Expand All @@ -394,7 +393,7 @@ const FormQuestionnaire = () => {
totalParts,
);
setUploadProgress(t("attachments.upload.completed"));
setIsOptionsSheetOpen(false)
setIsOptionsSheetOpen(false);
} catch (err) {
Sentry.captureException(err, { data: activeElectionRound });
}
Expand Down Expand Up @@ -434,7 +433,7 @@ const FormQuestionnaire = () => {
const data = await addAttachmentStart(payload);
await handleChunkUpload(file.uri, data.uploadUrls, data.uploadId, payload.id, totalParts);
setUploadProgress(t("attachments.upload.completed"));
setIsOptionsSheetOpen(false)
setIsOptionsSheetOpen(false);
} catch (err) {
Sentry.captureException(err, { data: activeElectionRound });
}
Expand Down Expand Up @@ -475,7 +474,13 @@ const FormQuestionnaire = () => {
electionRoundId: activeElectionRound?.id,
id: attachmentId,
});
queryClient.invalidateQueries({ queryKey: AttachmentsKeys.attachments(activeElectionRound?.id, selectedPollingStation?.pollingStationId, formId) })
queryClient.invalidateQueries({
queryKey: AttachmentsKeys.attachments(
activeElectionRound?.id,
selectedPollingStation?.pollingStationId,
formId,
),
});
}
} catch (err) {
Sentry.captureException(err, { data: activeElectionRound });
Expand Down
183 changes: 166 additions & 17 deletions mobile/app/citizen/main/form/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useEffect, useMemo, useRef, useState } from "react";
import { ScrollView, XStack, YStack } from "tamagui";
import { Card, ScrollView, XStack, YStack } from "tamagui";
import { Screen } from "../../../../components/Screen";
import Header from "../../../../components/Header";
import { useLocalSearchParams, useRouter } from "expo-router";
Expand All @@ -26,6 +26,14 @@ import Toast from "react-native-toast-message";
import ReviewCitizenFormSheet from "../../../../components/ReviewCitizenFormSheet";
import WarningDialog from "../../../../components/WarningDialog";
import i18n from "../../../../common/config/i18n";
import AddAttachment from "../../../../components/AddAttachment";
import OptionsSheet from "../../../../components/OptionsSheet";
import MediaLoading from "../../../../components/MediaLoading";
import { FileMetadata, useCamera } from "../../../../hooks/useCamera";
import * as DocumentPicker from "expo-document-picker";
import * as Crypto from "expo-crypto";
// import { Buffer } from "buffer";
// import * as FileSystem from "expo-file-system";

const CitizenForm = () => {
const { t } = useTranslation(["citizen_form", "network_banner"]);
Expand All @@ -36,6 +44,15 @@ const CitizenForm = () => {
const { selectedElectionRound } = useCitizenUserData();
const { isOnline } = useNetInfoContext();

const [isOptionsSheetOpen, setIsOptionsSheetOpen] = useState(false);
const [isPreparingFile, setIsPreparingFile] = useState(false);
const [uploadProgress, setUploadProgress] = useState("");
const [attachments, setAttachments] = useState<
Record<string, { fileMetadata: FileMetadata; id: string }[]>
>({});

const { uploadCameraOrMedia } = useCamera();

if (!selectedElectionRound) {
return (
<Typography>
Expand Down Expand Up @@ -227,6 +244,82 @@ const CitizenForm = () => {
}
};

const onCompressionProgress = (progress: number) => {
setUploadProgress(`${t("attachments.upload.compressing")} ${Math.ceil(progress * 100)}%`);
};

const removeAttachmentLocal = (id: string): void => {
setAttachments((prevAttachments) => {
return {
...prevAttachments,
[questionId]: prevAttachments[questionId].filter((attachment) => attachment.id !== id),
};
});
};

const handleCameraUpload = async (type: "library" | "cameraPhoto") => {
setIsPreparingFile(true);
setUploadProgress(t("attachments.upload.preparing"));
const cameraResult = await uploadCameraOrMedia(type, onCompressionProgress);

if (!cameraResult) {
setIsPreparingFile(false);
return;
}

setIsOptionsSheetOpen(false);
setAttachments((prevAttachments) => ({
...prevAttachments,
[questionId]: prevAttachments[questionId]
? [...prevAttachments[questionId], { fileMetadata: cameraResult, id: Crypto.randomUUID() }]
: [{ fileMetadata: cameraResult, id: Crypto.randomUUID() }],
}));
setIsPreparingFile(false);
};

const handleUploadAudio = async () => {
const doc = await DocumentPicker.getDocumentAsync({
type: "audio/*",
multiple: false,
});

if (doc?.assets?.[0]) {
const file = doc?.assets?.[0];

const fileMetadata: FileMetadata = {
name: file.name,
type: file.mimeType || "audio/mpeg",
uri: file.uri,
size: file.size || 0,
};

setIsOptionsSheetOpen(false);
setAttachments((prevAttachments) => ({
...prevAttachments,
[questionId]: prevAttachments[questionId]
? [...prevAttachments[questionId], { fileMetadata, id: Crypto.randomUUID() }]
: [{ fileMetadata, id: Crypto.randomUUID() }],
}));
setIsPreparingFile(false);
} else {
// Cancelled
}

setIsPreparingFile(false);
};
const handleOnShowAttachementSheet = () => {
if (isOnline) {
setIsOptionsSheetOpen(true);
} else {
Toast.show({
type: "error",
text2: t("attachments.upload.offline"),
visibilityTime: 5000,
text2Style: { textAlign: "center" },
});
}
};

if (isLoadingCurrentForm || !activeQuestion) {
return <Typography>Loading...</Typography>;
}
Expand Down Expand Up @@ -273,24 +366,44 @@ const CitizenForm = () => {
required={true}
/>

{/* attachments */}
{/* {activeElectionRound?.id && selectedPollingStation?.pollingStationId && formId && (
<QuestionAttachments
electionRoundId={activeElectionRound.id}
pollingStationId={selectedPollingStation.pollingStationId}
formId={formId}
questionId={questionId}
/>
)} */}

{/* <AddAttachment
{attachments[questionId]?.length ? (
<YStack gap="$xxs" paddingTop="$lg">
<Typography fontWeight="500">{t("attachments.heading")}</Typography>
<YStack gap="$xxs">
{attachments[questionId].map((attachment) => {
return (
<Card
padding="$0"
paddingLeft="$md"
key={attachment.id}
flexDirection="row"
justifyContent="space-between"
alignItems="center"
>
<Typography preset="body1" fontWeight="700" maxWidth="85%" numberOfLines={1}>
{attachment.fileMetadata.name}
</Typography>
<YStack
padding="$md"
onPress={removeAttachmentLocal.bind(null, attachment.id)}
pressStyle={{ opacity: 0.5 }}
>
<Icon icon="xCircle" size={24} color="$gray5" />
</YStack>
</Card>
);
})}
</YStack>
</YStack>
) : (
false
)}

<AddAttachment
label={t("attachments.add")}
marginTop="$sm"
onPress={() => {
Keyboard.dismiss();
return setIsOptionsSheetOpen(true);
}}
/> */}
onPress={handleOnShowAttachementSheet}
/>
</YStack>
</ScrollView>
<WizzardControls
Expand All @@ -312,11 +425,47 @@ const CitizenForm = () => {
currentForm={currentForm}
answers={answers}
questions={currentForm?.questions}
attachments={attachments}
setIsReviewSheetOpen={setIsReviewSheetOpen}
selectedLocationId={selectedLocationId}
/>
)}

{isOptionsSheetOpen && (
<OptionsSheet setOpen={setIsOptionsSheetOpen} open isLoading={isPreparingFile}>
{isPreparingFile ? (
<MediaLoading progress={uploadProgress} />
) : (
<YStack paddingHorizontal="$sm" gap="$xxs">
<Typography
onPress={handleCameraUpload.bind(null, "library")}
preset="body1"
paddingVertical="$md"
pressStyle={{ color: "$purple5" }}
>
{t("attachments.menu.load")}
</Typography>
<Typography
onPress={handleCameraUpload.bind(null, "cameraPhoto")}
preset="body1"
paddingVertical="$md"
pressStyle={{ color: "$purple5" }}
>
{t("attachments.menu.take_picture")}
</Typography>
<Typography
onPress={handleUploadAudio.bind(null)}
preset="body1"
paddingVertical="$md"
pressStyle={{ color: "$purple5" }}
>
{t("attachments.menu.upload_audio")}
</Typography>
</YStack>
)}
</OptionsSheet>
)}

{showWarningDialog && (
<WarningDialog
theme="info"
Expand Down
21 changes: 21 additions & 0 deletions mobile/assets/locales/en/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,27 @@
"discard": "Go back",
"save": "Stay here"
}
},
"attachments": {
"heading": "Uploaded media",
"loading": "Adding attachment... ",
"error": "Error while sending the attachment!",
"add": "Add media files",
"menu": {
"load": "Load from gallery",
"take_picture": "Take a photo",
"record_video": "Record a video",
"upload_audio": "Upload audio file"
},
"upload": {
"compressing": "Compressing attachment: ",
"preparing": "Preparing attachment...",
"starting": "Starting the upload...",
"progress": "Uploading attachments: ",
"completed": "Upload completed.",
"aborted": "Upload aborted.",
"offline": "You need an internet connection in order to perform this action."
}
}
},
"login": {
Expand Down
22 changes: 22 additions & 0 deletions mobile/assets/locales/ro/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,28 @@
"discard": "Întoarce-te",
"save": "Rămâi aici"
}
},
"attachments": {
"heading": "Fișiere media încărcate",
"loading": "Se adaugă atașamentul... ",
"error": "Eroare la încărcarea atașamentului.",
"add": "Adaugă notițe sau fișiere media",
"menu": {
"add_note": "Adaugă o notiță",
"load": "Încarcă din galerie",
"take_picture": "Fă o poză",
"record_video": "Înregistrează un video",
"upload_audio": "Încarcă un fișier audio"
},
"upload": {
"compressing": "Se comprimă atașamentul: ",
"preparing": "Se pregătește atașamentul...",
"starting": "Se începe încărcarea atașamentelor...",
"progress": "Progresul încărcării:",
"completed": "Încărcarea a fost finalizată.",
"aborted": "Încărcarea a fost anulată.",
"offline": "Ai nevoie de conexiune la internet pentru a face acestă acțiune."
}
}
},
"login": {
Expand Down
Loading
Loading