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

feature: sync preview label #661

Closed
wants to merge 35 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
9b90d9e
Starts to add sync settings screen.
cimigree Aug 14, 2024
08f15c3
Messages.
cimigree Aug 14, 2024
0ba4f20
Merges with develop
cimigree Sep 3, 2024
7cfd8e8
Styles sync settings screen.
cimigree Sep 3, 2024
086e631
Merges with develop
cimigree Sep 3, 2024
bcfee3b
Adds bold
cimigree Sep 3, 2024
8a05e88
Messages
cimigree Sep 3, 2024
8931da1
Merges with develop
cimigree Sep 4, 2024
e8bb678
Adds the bottom sheet modals.
cimigree Sep 4, 2024
2c2c4f5
Adds confirmation to persisted settings. Messages
cimigree Sep 4, 2024
9015640
Consolidates the bottom sheet modal.
cimigree Sep 4, 2024
042be70
Adds an env variable to use as a feature flag.
cimigree Sep 4, 2024
7df5f5b
Merge branch 'develop' into chore/sync-data-selector
cimigree Sep 4, 2024
58e5dfe
Changes default
cimigree Sep 4, 2024
d052445
Adds media label to observation.
cimigree Sep 4, 2024
0991449
Changes as suggested by PR review.
cimigree Sep 4, 2024
301a1a7
Merge branch 'chore/sync-data-selector' into feature/sync-preview-label
cimigree Sep 4, 2024
09c62c2
Messages.
cimigree Sep 4, 2024
2cf1e26
Merge branch 'develop' into feature/sync-preview-label
cimigree Sep 5, 2024
234eb59
Adds default as a choice for sync settings. Adds the media label comp…
cimigree Sep 5, 2024
76ee3ce
Messages
cimigree Sep 5, 2024
ebe671e
Removes extra messages.
cimigree Sep 5, 2024
5ccdfa0
Updates messages
cimigree Sep 5, 2024
745bedc
Adds feature flag
cimigree Sep 5, 2024
9309487
Merge branch 'develop' into chore/sync-data-selector
cimigree Sep 5, 2024
af6f2f0
Adjusts text to use separate text pieces instead of newline. Changes …
cimigree Sep 5, 2024
5877df0
Forgot to commit the new app screens.
cimigree Sep 5, 2024
bfeb2ec
Merges with other branch.
cimigree Sep 5, 2024
75e69f0
Adjusts name from sync setting to media sync setting.
cimigree Sep 5, 2024
a582d8e
Merges with develop.
cimigree Sep 5, 2024
558afe1
Merges with develop.
cimigree Sep 30, 2024
29c5770
Trying to make a method to get media type availability.
cimigree Oct 1, 2024
839e3af
Fixes some bugs with label.
cimigree Oct 1, 2024
2216395
removes console log.
cimigree Oct 1, 2024
87ae468
Adds a method to save only preview if that is the media sync setting.
cimigree Oct 1, 2024
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
9 changes: 9 additions & 0 deletions messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -691,6 +691,9 @@
"screens.ManualGpsScreen.zoneNumber": {
"message": "Zone Number"
},
"screens.MediaLabel.labelFullSizePreviews": {
"message": "Full size and previews available"
},
"screens.MediaSyncActionSheetContent.cancel": {
"message": "Cancel"
},
Expand Down Expand Up @@ -780,6 +783,9 @@
"description": "Fallback name used when category name cannot be determined for observation",
"message": "Observation"
},
"screens.Observation.labelPreviewsOnlyObservation": {
"message": "Only previews are available"
},
"screens.Observation.shareMediaTitle": {
"description": "Title of dialog to share an observation with media",
"message": "Sharing image"
Expand Down Expand Up @@ -943,6 +949,9 @@
"screens.PhotoPreviewModal.DeletePhoto.headerButtonText": {
"message": "Delete Photo"
},
"screens.PhotoPreviewModal.labelPreviewsOnlyOpenMedia": {
"message": "Only preview is available"
},
"screens.PrivacyPolicy.aboutAwana": {
"message": "About Awana Digital"
},
Expand Down
38 changes: 26 additions & 12 deletions src/frontend/contexts/PhotoPromiseContext/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ import {
DraftPhoto,
} from './types';
import ImageResizer from '@bam.tech/react-native-image-resizer';
import {MediaSyncSetting} from '../../sharedTypes';

type AddPhotoPromiseProps = {
photo: Promise<CapturedPictureMM>;
mediaMetadata: MediaMetadata;
draftPhotoId: string;
syncSetting?: MediaSyncSetting;
};

type PhotoPromiseContextState = {
Expand Down Expand Up @@ -46,7 +48,12 @@ export const PhotoPromiseProvider = ({
>([]);

const addPhotoPromise = React.useCallback(
({photo, draftPhotoId, mediaMetadata}: AddPhotoPromiseProps) => {
({
photo,
draftPhotoId,
mediaMetadata,
syncSetting,
}: AddPhotoPromiseProps) => {
// Use signal to cancel processing by setting signal.didCancel = true
// Important because image resize/rotate is expensive
const signal: Signal = {};
Expand All @@ -56,6 +63,7 @@ export const PhotoPromiseProvider = ({
photo,
draftPhotoId,
mediaMetadata,
syncSetting: syncSetting ?? 'everything',
});

photoPromise.signal = signal;
Expand Down Expand Up @@ -117,12 +125,16 @@ async function processPhoto({
photo,
draftPhotoId,
mediaMetadata,
syncSetting = 'everything',
signal,
}: AddPhotoPromiseProps & {signal: Signal}): Promise<ProcessedDraftPhoto> {
}: AddPhotoPromiseProps & {
signal: Signal;
}): Promise<ProcessedDraftPhoto> {
const {uri: originalUri, rotate} = await photo;
const {didCancel} = signal;

if (didCancel) throw new Error('Cancelled');
let previewUri;

// rotate will be defined if the original photo failed to rotate (this
// happens on low-memory devices) so we rotate the preview and
Expand All @@ -138,21 +150,23 @@ async function processPhoto({

if (didCancel) throw new Error('Cancelled');

const {uri: previewUri} = await ImageResizer.createResizedImage(
originalUri,
PREVIEW_SIZE,
PREVIEW_SIZE,
'JPEG',
PREVIEW_QUALITY,
rotate,
);
if (syncSetting === 'previews' || syncSetting === 'everything') {
({uri: previewUri} = await ImageResizer.createResizedImage(
originalUri,
PREVIEW_SIZE,
PREVIEW_SIZE,
'JPEG',
PREVIEW_QUALITY,
rotate,
));
}

if (didCancel) throw new Error('Cancelled');

return {
draftPhotoId,
originalUri,
previewUri,
originalUri: syncSetting === 'everything' ? originalUri : '',
previewUri: previewUri || '',
thumbnailUri,
mediaMetadata,
type: 'processed',
Expand Down
2 changes: 1 addition & 1 deletion src/frontend/hooks/persistedState/usePersistedSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ type SettingsSlice = {
const settingsSlice: StateCreator<SettingsSlice> = (set, get) => ({
coordinateFormat: 'utm',
manualCoordinateEntryFormat: 'utm',
mediaSyncSetting: 'everything',
mediaSyncSetting: null,
actions: {
setCoordinateFormat: coordinateFormat => set({coordinateFormat}),
setManualCoordinateEntryFormat: coordinateFormat =>
Expand Down
5 changes: 5 additions & 0 deletions src/frontend/hooks/useDraftObservation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
PhotoPromiseWithMetadata,
UnprocessedDraftPhoto,
} from '../contexts/PhotoPromiseContext/types';
import {usePersistedSettings} from './persistedState/usePersistedSettings';
// react native does not have a random bytes generator, `non-secure` does not require a random bytes generator.
import {nanoid} from 'nanoid/non-secure';
import * as Sentry from '@sentry/react-native';
Expand All @@ -31,6 +32,9 @@ export const useDraftObservation = () => {
existingObservationToDraft,
} = _usePersistedDraftObservationActions();

const mediaSyncSetting =
usePersistedSettings(store => store.mediaSyncSetting) ?? 'everything';

const addPhoto = useCallback(
async ({capturePromise, mediaMetadata}: PhotoPromiseWithMetadata) => {
// creates an id, that is stored as a placeholder in persisted photots. This is associated with the processed photo, so when the photo is done processsing, we can replace the placeholder with the actual photo
Expand All @@ -41,6 +45,7 @@ export const useDraftObservation = () => {
draftPhotoId,
mediaMetadata,
photo: capturePromise,
syncSetting: mediaSyncSetting,
});
try {
// the promise is run
Expand Down
37 changes: 37 additions & 0 deletions src/frontend/hooks/useMediaAvailability.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import {useEffect, useState} from 'react';
import {Attachment} from '../sharedTypes';
import {useAttachmentUrlQueries} from './server/media';

export function useMediaAvailability(attachments: Attachment[]) {
const [availability, setAvailability] = useState<
'full' | 'preview' | 'both' | null
>(null);

const originalQueries = useAttachmentUrlQueries(attachments, 'original');
const previewQueries = useAttachmentUrlQueries(attachments, 'preview');

useEffect(() => {
if (
originalQueries.some(q => q.isPending) ||
previewQueries.some(q => q.isPending)
) {
setAvailability(null);
return;
}

const hasFullSize = originalQueries.some(q => q.data?.url);
const hasPreviews = previewQueries.some(q => q.data?.url);

if (hasFullSize && hasPreviews) {
setAvailability('both');
} else if (hasFullSize) {
setAvailability('full');
} else if (hasPreviews) {
setAvailability('preview');
} else {
setAvailability(null);
}
}, [originalQueries, previewQueries]);

return availability;
}
25 changes: 24 additions & 1 deletion src/frontend/screens/Observation/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@ import * as React from 'react';

import {Text, View, ScrollView, StyleSheet} from 'react-native';
import {defineMessages} from 'react-intl';
import {BLACK, WHITE, DARK_GREY, LIGHT_GREY} from '../../lib/styles';
import {
BLACK,
WHITE,
DARK_GREY,
LIGHT_GREY,
NEW_DARK_GREY,
} from '../../lib/styles';
import {UIActivityIndicator} from 'react-native-indicators';

import {FormattedObservationDate} from '../../sharedComponents/FormattedData';
Expand All @@ -20,6 +26,8 @@ import {useDeviceInfo} from '../../hooks/server/deviceInfo';
import {useOriginalVersionIdToDeviceId} from '../../hooks/server/projects.ts';
import {SavedPhoto} from '../../contexts/PhotoPromiseContext/types.ts';
import {ButtonFields} from './Buttons.tsx';
import {MediaLabel} from '../../sharedComponents/MediaLabel.tsx';
import {useMediaAvailability} from '../../hooks/useMediaAvailability.ts';

const m = defineMessages({
deleteTitle: {
Expand Down Expand Up @@ -79,6 +87,8 @@ export const ObservationScreen: NativeNavigationComponent<'Observation'> = ({
(attachment): attachment is SavedPhoto => attachment.type === 'photo',
);

const mediaAvailability = useMediaAvailability(photoAttachments);

return (
<ScrollView
style={styles.root}
Expand All @@ -102,6 +112,15 @@ export const ObservationScreen: NativeNavigationComponent<'Observation'> = ({
<Text style={styles.textNotes}>{observation.tags.notes}</Text>
</View>
) : null}
{process.env.EXPO_PUBLIC_FEATURE_MEDIA_MANAGER &&
photoAttachments.length > 0 && (
<MediaLabel
textColor={NEW_DARK_GREY}
style={styles.mediaLabel}
context="observation"
mediaAvailability={mediaAvailability}
/>
)}
{photoAttachments.length > 0 && (
<MediaScrollView
photos={photoAttachments}
Expand Down Expand Up @@ -158,4 +177,8 @@ const styles = StyleSheet.create({
paddingVertical: 10,
textAlign: 'center',
},
mediaLabel: {
marginVertical: 20,
marginLeft: 10,
},
});
27 changes: 26 additions & 1 deletion src/frontend/screens/PhotoPreviewModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ import {defineMessages, useIntl} from 'react-intl';
import {useDraftObservation} from '../hooks/useDraftObservation.ts';
import {PhotoPreparedView} from '../sharedComponents/PhotoPreparedView.tsx';
import ErrorIcon from '../images/Error.svg';
import {MediaLabel} from '../sharedComponents/MediaLabel.tsx';
import {useMediaAvailability} from '../hooks/useMediaAvailability.ts';
import {
SavedPhoto,
ProcessedDraftPhoto,
} from '../contexts/PhotoPromiseContext/types.ts';

const m = defineMessages({
headerDeleteButtonText: {
Expand All @@ -38,6 +44,13 @@ export const PhotoPreviewModal: FC<
NativeRootNavigationProps<'PhotoPreviewModal'>
> = ({route}) => {
const {photo} = route.params;
const isSavedPhoto = (
photo: SavedPhoto | ProcessedDraftPhoto,
): photo is SavedPhoto =>
'driveDiscoveryId' in photo && 'name' in photo && 'hash' in photo;
const mediaAvailability = isSavedPhoto(photo)
? useMediaAvailability([photo])
: null;
const navigation = useNavigationFromRoot();
const [showHeader, setShowHeader] = useState(true);
const draftObservation = useDraftObservation();
Expand Down Expand Up @@ -88,7 +101,14 @@ export const PhotoPreviewModal: FC<
photo={photo}
/>
)}

{process.env.EXPO_PUBLIC_FEATURE_MEDIA_MANAGER && (
<MediaLabel
textColor={WHITE}
style={styles.mediaLabel}
context="openMedia"
mediaAvailability={mediaAvailability}
/>
)}
<BottomSheetModal ref={sheetRef} isOpen={isOpen}>
<BottomSheetModalContent
title={t(m.deleteModalTitle)}
Expand Down Expand Up @@ -130,4 +150,9 @@ const styles = StyleSheet.create({
color: WHITE,
fontSize: 13,
},
mediaLabel: {
position: 'absolute',
bottom: 12,
left: 20,
},
});
67 changes: 67 additions & 0 deletions src/frontend/sharedComponents/MediaLabel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import React from 'react';
import {Text, StyleSheet, TextStyle} from 'react-native';
import {defineMessages, useIntl} from 'react-intl';
import {WHITE} from '../lib/styles';
import {usePersistedSettings} from '../hooks/persistedState/usePersistedSettings';

const m = defineMessages({
labelFullSizePreviews: {
id: 'screens.MediaLabel.labelFullSizePreviews',
defaultMessage: 'Full size and previews available',
},
labelPreviewsOnlyObservation: {
id: 'screens.Observation.labelPreviewsOnlyObservation',
defaultMessage: 'Only previews are available',
},
labelPreviewsOnlyOpenMedia: {
id: 'screens.PhotoPreviewModal.labelPreviewsOnlyOpenMedia',
defaultMessage: 'Only preview is available',
},
});

type MediaLabelProps = {
textColor?: string;
style?: TextStyle;
context: 'observation' | 'openMedia';
mediaAvailability: 'full' | 'preview' | 'both' | null;
};

export const MediaLabel: React.FC<MediaLabelProps> = ({
textColor = WHITE,
style,
context,
mediaAvailability,
}) => {
const {formatMessage: t} = useIntl();
const mediaSyncSetting = usePersistedSettings(
store => store.mediaSyncSetting,
);

if (!mediaSyncSetting) {
return null;
}

console.log('mediaAvailability', mediaAvailability);
let labelText = '';
if (mediaAvailability === 'both') {
labelText = t(m.labelFullSizePreviews);
} else if (mediaAvailability === 'preview') {
labelText =
context === 'observation'
? t(m.labelPreviewsOnlyObservation)
: t(m.labelPreviewsOnlyOpenMedia);
}
if (!labelText) return null;

return (
<Text style={[styles.mediaLabel, {color: textColor}, style]}>
{labelText}
</Text>
);
};

const styles = StyleSheet.create({
mediaLabel: {
fontSize: 14,
},
});
2 changes: 1 addition & 1 deletion src/frontend/sharedTypes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export type Attachment = Observation['attachments'][0];
export type PhotoVariant = 'original' | 'thumbnail' | 'preview';

export type CoordinateFormat = 'utm' | 'dd' | 'dms';
export type MediaSyncSetting = 'previews' | 'everything';
export type MediaSyncSetting = 'previews' | 'everything' | null;

// Copied from @comapeo/core/src/roles.js. Created an issue to eventually expose this: https://github.com/digidem/mapeo-core-next/issues/532
export const CREATOR_ROLE_ID = 'a12a6702b93bd7ff';
Expand Down
Loading