Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 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
6 changes: 6 additions & 0 deletions apps/mobile/app/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useQuery } from '@tanstack/react-query';
import { Redirect } from 'expo-router';
import { createAuthClient } from '@/modules/auth/auth.client';
import { configLocalStorage } from '@/modules/config/config.local-storage';

export default function Index() {
Expand All @@ -17,6 +18,11 @@ export default function Index() {
return <Redirect href="/config/server-selection" />;
}

const authClient = createAuthClient({ baseUrl: query.data });
if (authClient.getCookie()) {
return <Redirect href="/(app)/(with-organizations)/(tabs)/list" />;
}

return <Redirect href="/auth/login" />;
};

Expand Down
1 change: 1 addition & 0 deletions apps/mobile/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"expo-linking": "~8.0.8",
"expo-router": "~6.0.14",
"expo-secure-store": "^15.0.7",
"expo-sharing": "^14.0.7",
"expo-splash-screen": "~31.0.10",
"expo-status-bar": "~3.0.8",
"expo-symbols": "~1.0.7",
Expand Down
2 changes: 1 addition & 1 deletion apps/mobile/src/modules/api/api.models.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
type DateKeys = 'createdAt' | 'updatedAt' | 'deletedAt' | 'expiresAt' | 'lastTriggeredAt' | 'lastUsedAt' | 'scheduledPurgeAt';

type CoerceDate<T> = T extends string | Date
export type CoerceDate<T> = T extends string | Date
? Date
: T extends string | Date | null | undefined
? Date | undefined
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,322 @@
import type { CoerceDate } from '@/modules/api/api.models';
import type { Document } from '@/modules/documents/documents.types';
import type { ThemeColors } from '@/modules/ui/theme.constants';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import * as Sharing from 'expo-sharing';
import {
Modal,
StyleSheet,
Text,
TouchableOpacity,
TouchableWithoutFeedback,
View,
} from 'react-native';
import { useAuthClient } from '@/modules/api/providers/api.provider';
import { configLocalStorage } from '@/modules/config/config.local-storage';
import { fetchDocumentFile } from '@/modules/documents/documents.services';
import { useAlert } from '@/modules/ui/providers/alert-provider';
import { useThemeColor } from '@/modules/ui/providers/use-theme-color';

type DocumentActionSheetProps = {
visible: boolean;
document: CoerceDate<Document> | undefined;
onClose: () => void;
onView: () => void;
onDownloadAndShare: () => void;
Copy link

Copilot AI Nov 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The onDownloadAndShare prop is passed but never used. The component defines its own handleDownloadAndShare function that is called instead. Consider removing this unused prop or implementing the callback if it's needed for parent component coordination.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in next PR

};

export function DocumentActionSheet({
visible,
document,
onClose,
onView,
onDownloadAndShare,
}: DocumentActionSheetProps) {
const themeColors = useThemeColor();
const styles = createStyles({ themeColors });
const { showAlert } = useAlert();
const authClient = useAuthClient();

if (document === undefined) {
return null;
}

// Check if document can be viewed in DocumentViewerScreen
// Supported types: images (image/*) and PDFs (application/pdf)
const isViewable
= document.mimeType.startsWith('image/')
|| document.mimeType.startsWith('application/pdf');

const formatFileSize = (bytes: number): string => {
if (bytes < 1024) {
return `${bytes} B`;
}
if (bytes < 1024 * 1024) {
return `${(bytes / 1024).toFixed(1)} KB`;
}
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
};

const formatDate = (dateString: string): string => {
const date = new Date(dateString);
return date.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
});
};

const handleDownloadAndShare = async () => {
const baseUrl = await configLocalStorage.getApiServerBaseUrl();

if (!baseUrl) {
showAlert({
title: 'Error',
message: 'Base URL not found',
});
return;
}

const canShare = await Sharing.isAvailableAsync();
if (!canShare) {
showAlert({
title: 'Sharing Failed',
message: 'Sharing is not available on this device. Please share the document manually.',
});
return;
}

try {
const fileUri = await fetchDocumentFile({
document,
organizationId: document.organizationId,
baseUrl,
authClient,
});

await Sharing.shareAsync(fileUri);
} catch (error) {
console.error('Error downloading document file:', error);
showAlert({
title: 'Error',
message: 'Failed to download document file',
});
}
};

return (
<Modal
visible={visible}
transparent
animationType="slide"
onRequestClose={onClose}
>
<TouchableWithoutFeedback onPress={onClose}>
<View style={styles.overlay}>
<TouchableWithoutFeedback>
<View style={styles.sheet}>
{/* Handle bar */}
<View style={styles.handleBar} />

{/* Document info */}
<View style={styles.documentInfo}>
<Text style={styles.documentName} numberOfLines={2}>
{document.name}
</Text>

{/* Document details */}
<View style={styles.detailsContainer}>
<View style={styles.detailRow}>
<MaterialCommunityIcons
name="file"
size={14}
color={themeColors.mutedForeground}
style={styles.detailIcon}
/>
<Text style={styles.detailText}>{formatFileSize(document.originalSize)}</Text>
</View>
<View style={styles.detailRow}>
<MaterialCommunityIcons
name="calendar"
size={14}
color={themeColors.mutedForeground}
style={styles.detailIcon}
/>
<Text style={styles.detailText}>{formatDate(document.createdAt)}</Text>
</View>
<View style={styles.detailRow}>
<MaterialCommunityIcons
name="file-document-outline"
size={14}
color={themeColors.mutedForeground}
style={styles.detailIcon}
/>
<Text style={styles.detailText} numberOfLines={1}>
{document.mimeType.split('/')[1] || document.mimeType}
</Text>
</View>
</View>
</View>

{/* Action buttons */}
<View style={styles.actions}>
{isViewable && (
<TouchableOpacity
style={styles.actionButton}
onPress={() => {
onClose();
onView();
}}
activeOpacity={0.7}
>
<View style={[styles.actionIcon, styles.viewIcon]}>
<MaterialCommunityIcons
name="eye"
size={20}
color={themeColors.primary}
/>
</View>
<Text style={styles.actionText}>View</Text>
</TouchableOpacity>
)}

<TouchableOpacity
style={styles.actionButton}
onPress={() => {
onClose();
handleDownloadAndShare();
}}
activeOpacity={0.7}
>
<View style={[styles.actionIcon, styles.downloadIcon]}>
<MaterialCommunityIcons
name="download"
size={20}
color={themeColors.primary}
/>
</View>
<Text style={styles.actionText}>Share</Text>
</TouchableOpacity>
</View>

{/* Cancel button */}
<TouchableOpacity
style={styles.cancelButton}
onPress={onClose}
activeOpacity={0.7}
>
<Text style={styles.cancelButtonText}>Cancel</Text>
</TouchableOpacity>
</View>
</TouchableWithoutFeedback>
</View>
</TouchableWithoutFeedback>
</Modal>
);
}

function createStyles({ themeColors }: { themeColors: ThemeColors }) {
return StyleSheet.create({
overlay: {
flex: 1,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
justifyContent: 'flex-end',
},
sheet: {
backgroundColor: themeColors.secondaryBackground,
borderTopLeftRadius: 20,
borderTopRightRadius: 20,
paddingBottom: 34, // Safe area for bottom
paddingTop: 16,
},
handleBar: {
width: 40,
height: 4,
backgroundColor: themeColors.border,
borderRadius: 2,
alignSelf: 'center',
marginBottom: 16,
},
documentInfo: {
paddingHorizontal: 24,
paddingVertical: 16,
borderBottomWidth: 1,
borderBottomColor: themeColors.border,
},
documentName: {
fontSize: 16,
fontWeight: '600',
color: themeColors.foreground,
textAlign: 'center',
marginBottom: 12,
},
detailsContainer: {
flexDirection: 'row',
justifyContent: 'center',
flexWrap: 'wrap',
gap: 16,
marginTop: 8,
},
detailRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 6,
},
detailIcon: {
marginRight: 2,
},
detailText: {
fontSize: 12,
color: themeColors.mutedForeground,
},
actions: {
flexDirection: 'row',
paddingHorizontal: 24,
paddingVertical: 16,
gap: 16,
},
actionButton: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 12,
backgroundColor: themeColors.secondaryBackground,
borderRadius: 12,
},
actionIcon: {
width: 40,
height: 40,
borderRadius: 20,
justifyContent: 'center',
alignItems: 'center',
marginBottom: 8,
},
viewIcon: {
backgroundColor: `${themeColors.primary}15`,
},
downloadIcon: {
backgroundColor: `${themeColors.primary}15`,
},
actionText: {
fontSize: 14,
fontWeight: '500',
color: themeColors.foreground,
},
cancelButton: {
marginHorizontal: 24,
marginTop: 12,
paddingVertical: 16,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'transparent',
borderWidth: 1,
borderColor: themeColors.border,
borderRadius: 12,
},
cancelButtonText: {
fontSize: 16,
fontWeight: '600',
color: themeColors.foreground,
},
});
}
Loading