Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
216 changes: 201 additions & 15 deletions mobile/app/citizen/main/form/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,17 @@ 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 Card from "../../../../components/Card";
import { AttachmentMimeType } from "../../../../services/api/get-attachments.api";
import { MediaDialog } from "../../../../components/MediaDialog";
// import { Buffer } from "buffer";
// import * as FileSystem from "expo-file-system";

const CitizenForm = () => {
const { t } = useTranslation(["citizen_form", "network_banner"]);
Expand All @@ -36,6 +47,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 @@ -66,6 +86,11 @@ const CitizenForm = () => {
const [questionId, setQuestionId] = useState<string>(initialQuestionId);
const [isReviewSheetOpen, setIsReviewSheetOpen] = useState(false);
const [showWarningDialog, setShowWarningDialog] = useState(false);
const [selectedAttachmentToDelete, setSelectedAttachmentToDelete] = useState<string | null>(null);
const [previewAttachment, setPreviewAttachment] = useState<{
fileMetadata: FileMetadata;
id: string;
} | null>(null);

const {
data: currentForm,
Expand Down Expand Up @@ -113,7 +138,7 @@ const CitizenForm = () => {
});

const handleGoBack = () => {
if (isDirty) {
if (isDirty || Object.keys(attachments).length) {
Keyboard.dismiss();
setShowWarningDialog(true);
} else {
Expand Down Expand Up @@ -227,6 +252,83 @@ 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 = () => {
Keyboard.dismiss();
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 +375,72 @@ const CitizenForm = () => {
required={true}
/>

{/* attachments */}
{/* {activeElectionRound?.id && selectedPollingStation?.pollingStationId && formId && (
<QuestionAttachments
electionRoundId={activeElectionRound.id}
pollingStationId={selectedPollingStation.pollingStationId}
formId={formId}
questionId={questionId}
{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"
onPress={() => setPreviewAttachment(attachment)}
>
<Typography preset="body1" fontWeight="700" maxWidth="85%" numberOfLines={1}>
{attachment.fileMetadata.name}
</Typography>
<YStack
padding="$md"
onPress={() => {
Keyboard.dismiss();
setSelectedAttachmentToDelete(attachment.id);
}}
pressStyle={{ opacity: 0.5 }}
>
<Icon icon="xCircle" size={24} color="$gray5" />
</YStack>
</Card>
);
})}
</YStack>
</YStack>
) : (
false
)}

{selectedAttachmentToDelete && (
<WarningDialog
title={t("attachments.delete.title")}
description={t("attachments.delete.description")}
actionBtnText={t("attachments.delete.actions.clear")}
cancelBtnText={t("attachments.delete.actions.cancel")}
action={() => {
removeAttachmentLocal(selectedAttachmentToDelete);
setSelectedAttachmentToDelete(null);
}}
onCancel={() => setSelectedAttachmentToDelete(null)}
/>
)}

{previewAttachment && previewAttachment.fileMetadata.type === AttachmentMimeType.IMG && (
<MediaDialog
media={{
type: previewAttachment.fileMetadata.type,
src: previewAttachment.fileMetadata.uri,
}}
onClose={() => setPreviewAttachment(null)}
/>
)} */}
)}

{/* <AddAttachment
<AddAttachment
label={t("attachments.add")}
marginTop="$sm"
onPress={() => {
Keyboard.dismiss();
return setIsOptionsSheetOpen(true);
}}
/> */}
onPress={handleOnShowAttachementSheet}
/>
</YStack>
</ScrollView>
<WizzardControls
Expand All @@ -312,11 +462,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
29 changes: 29 additions & 0 deletions mobile/assets/locales/en/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,35 @@
"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."
},
"delete": {
"title": "Delete media file",
"description": "This action cannot be undone. Once deleted, a media file cannot be retrieved.",
"actions": {
"cancel": "Cancel",
"clear": "Delete"
}
}
}
},
"login": {
Expand Down
Loading