Skip to content

Commit

Permalink
Merge pull request #18 from vintasoftware/feat/dark-mode
Browse files Browse the repository at this point in the history
Support dark mode
  • Loading branch information
fjsj authored Jan 28, 2025
2 parents a36de0b + 8aa7336 commit c12a4d7
Show file tree
Hide file tree
Showing 13 changed files with 183 additions and 121 deletions.
2 changes: 1 addition & 1 deletion app.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"orientation": "portrait",
"icon": "./assets/images/icon.png",
"scheme": "myapp",
"userInterfaceStyle": "light",
"userInterfaceStyle": "automatic",
"newArchEnabled": true,
"jsEngine": "hermes",
"ios": {
Expand Down
2 changes: 1 addition & 1 deletion app/(app)/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export default function Index() {

if (isLoading || isAvatarsLoading) {
return (
<View className="flex-1 items-center justify-center">
<View className="flex-1 items-center justify-center bg-background-50">
<Spinner size="large" />
</View>
);
Expand Down
2 changes: 1 addition & 1 deletion app/(app)/thread/[id].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ export default function ThreadPage() {

if (!thread || isAvatarsLoading) {
return (
<View className="flex-1 items-center justify-center">
<View className="flex-1 items-center justify-center bg-background-50">
<Spinner size="large" />
</View>
);
Expand Down
45 changes: 28 additions & 17 deletions app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,17 @@ import { MedplumProvider } from "@medplum/react-hooks";
import { makeRedirectUri } from "expo-auth-session";
import { router, Stack } from "expo-router";
import * as SplashScreen from "expo-splash-screen";
import { StatusBar } from "expo-status-bar";
import { useColorScheme } from "nativewind";
import { useEffect } from "react";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import {
initialWindowMetrics,
SafeAreaProvider,
SafeAreaView,
} from "react-native-safe-area-context";

import { GluestackUIProvider } from "@/components/gluestack-ui-provider";
import { SafeAreaView } from "@/components/ui/safe-area-view";
import { oauth2ClientId } from "@/utils/medplum-oauth2";

export const unstable_settings = {
Expand All @@ -24,7 +30,7 @@ export const unstable_settings = {

SplashScreen.preventAutoHideAsync();

polyfillMedplumWebAPIs({ location: false });
polyfillMedplumWebAPIs();
const medplum = new MedplumClient({
clientId: oauth2ClientId,
storage: new ExpoClientStorage(),
Expand All @@ -47,21 +53,26 @@ export default function RootLayout() {
}
}, []);

const { colorScheme } = useColorScheme();
return (
<SafeAreaView className="h-full md:w-full">
<GluestackUIProvider mode="light">
<MedplumProvider medplum={medplum}>
<GestureHandlerRootView className="flex-1">
<Stack
screenOptions={{
headerShown: false,
// Prevents flickering:
animation: "none",
}}
/>
</GestureHandlerRootView>
</MedplumProvider>
</GluestackUIProvider>
</SafeAreaView>
<SafeAreaProvider initialMetrics={initialWindowMetrics}>
<SafeAreaView className="h-full bg-background-50 md:w-full">
<StatusBar style={colorScheme === "dark" ? "dark" : "light"} />

<GluestackUIProvider mode={colorScheme}>
<MedplumProvider medplum={medplum}>
<GestureHandlerRootView className="flex-1">
<Stack
screenOptions={{
headerShown: false,
// Prevents flickering:
animation: "none",
}}
/>
</GestureHandlerRootView>
</MedplumProvider>
</GluestackUIProvider>
</SafeAreaView>
</SafeAreaProvider>
);
}
2 changes: 1 addition & 1 deletion app/sign-in.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ export default function SignIn() {
}, [medplumLogin]);

return (
<View className="flex-1 items-center justify-center">
<View className="flex-1 items-center justify-center bg-background-50">
{isLoading && <Spinner size="large" />}
{!isLoading && <Button title="Connect to Medplum" onPress={handleLogin} />}
</View>
Expand Down
2 changes: 1 addition & 1 deletion components/ChatHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ export function ChatHeader({
</Pressable>
<View className="flex-1 flex-row items-center gap-3">
<Avatar size="md" className="border-2 border-primary-200">
<Icon as={UserRound} size="lg" className="stroke-white" />
<Icon as={UserRound} size="lg" className="stroke-typography-0" />
{avatarURL && <AvatarImage source={{ uri: avatarURL }} />}
</Avatar>
<View className="flex-col">
Expand Down
18 changes: 11 additions & 7 deletions components/ChatMessageBubble.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useMedplumProfile } from "@medplum/react-hooks";
import { useVideoPlayer } from "expo-video";
import { VideoView } from "expo-video";
import { CirclePlay, FileDown, UserRound } from "lucide-react-native";
import { useColorScheme } from "nativewind";
import { memo, useCallback, useRef, useState } from "react";
import { Pressable, StyleSheet, View } from "react-native";
import { Alert } from "react-native";
Expand Down Expand Up @@ -67,7 +68,7 @@ const VideoAttachment = memo(
/>
<Pressable
onPress={handlePlayPress}
className="absolute inset-0 items-center justify-center bg-black/50"
className="absolute inset-0 items-center justify-center bg-background-dark/50 dark:bg-background-dark/90"
>
<Icon as={CirclePlay} size="xl" className="text-typography-0" />
</Pressable>
Expand All @@ -81,6 +82,7 @@ VideoAttachment.displayName = "VideoAttachment";

function FileAttachment({ attachment }: { attachment: AttachmentWithUrl }) {
const [isDownloading, setIsDownloading] = useState(false);
const { colorScheme } = useColorScheme();

const handleShare = useCallback(async () => {
setIsDownloading(true);
Expand All @@ -101,11 +103,13 @@ function FileAttachment({ attachment }: { attachment: AttachmentWithUrl }) {
disabled={isDownloading}
>
{isDownloading ? (
<ButtonSpinner className="text-white" />
<ButtonSpinner color={colorScheme === "dark" ? "black" : "white"} />
) : (
<ButtonIcon as={FileDown} className="text-typography-600 text-white" />
<ButtonIcon as={FileDown} className="text-typography-100" />
)}
<ButtonText className="text-sm text-white">{attachment.title || "Attachment"}</ButtonText>
<ButtonText className="text-sm text-typography-100">
{attachment.title || "Attachment"}
</ButtonText>
</Button>
);
}
Expand All @@ -118,14 +122,14 @@ export function ChatMessageBubble({ message, avatarURL }: ChatMessageBubbleProps
const hasVideo = message.attachment?.contentType?.startsWith("video/");

const wrapperAlignment = isCurrentUser ? "self-end" : "self-start";
const bubbleColor = isPatientMessage ? "bg-secondary-100" : "bg-tertiary-200";
const borderColor = isPatientMessage ? "border-secondary-200" : "border-tertiary-300";
const bubbleColor = isPatientMessage ? "bg-secondary-200" : "bg-tertiary-200";
const borderColor = isPatientMessage ? "border-secondary-300" : "border-tertiary-300";
const flexDirection = isCurrentUser ? "flex-row-reverse" : "flex-row";
return (
<View className={`mx-2 max-w-[80%] p-2 ${wrapperAlignment}`}>
<View className={`${flexDirection} items-end gap-2`}>
<Avatar size="sm" className="border border-primary-200">
<Icon as={UserRound} size="sm" className="stroke-white" />
<Icon as={UserRound} size="sm" className="stroke-typography-0" />
{avatarURL && <AvatarImage source={{ uri: avatarURL }} />}
</Avatar>
<View className={`rounded-xl border p-3 ${bubbleColor} ${borderColor}`}>
Expand Down
24 changes: 17 additions & 7 deletions components/CreateThreadModal.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { useRouter } from "expo-router";
import { X } from "lucide-react-native";
import { useColorScheme } from "nativewind";
import { useCallback, useState } from "react";
import { Modal as RNModal, Pressable } from "react-native";
import Animated, { FadeIn, FadeOut } from "react-native-reanimated";

import { Button, ButtonText } from "@/components/ui/button";
import { Button, ButtonSpinner, ButtonText } from "@/components/ui/button";
import { Icon } from "@/components/ui/icon";
import { Input, InputField } from "@/components/ui/input";
import { Spinner } from "@/components/ui/spinner";
import { Text } from "@/components/ui/text";
import { View } from "@/components/ui/view";

Expand All @@ -23,12 +23,12 @@ interface ModalHeaderProps {

function ModalHeader({ onClose }: ModalHeaderProps) {
return (
<View className="flex-row items-center justify-between border-b border-gray-200 p-4">
<View className="flex-row items-center justify-between border-b border-outline-100 p-4">
<Text size="lg" bold>
New Thread
</Text>
<Pressable
className="mr-2 rounded-full p-2 active:bg-secondary-100"
className="mr-2 rounded-full p-2 active:bg-background-100"
onPress={onClose}
hitSlop={8}
>
Expand Down Expand Up @@ -69,13 +69,19 @@ interface ModalFooterProps {
}

function ModalFooter({ onClose, onCreate, isCreating, isValid }: ModalFooterProps) {
const { colorScheme } = useColorScheme();

return (
<View className="flex-row justify-end gap-2 p-4">
<Button variant="outline" onPress={onClose} className="mr-2">
<ButtonText>Cancel</ButtonText>
</Button>
<Button className="min-w-[100px]" disabled={!isValid || isCreating} onPress={onCreate}>
{isCreating ? <Spinner size="small" color="white" /> : <ButtonText>Create</ButtonText>}
{isCreating ? (
<ButtonSpinner color={colorScheme === "dark" ? "black" : "white"} />
) : (
<ButtonText>Create</ButtonText>
)}
</Button>
</View>
);
Expand Down Expand Up @@ -110,15 +116,19 @@ export function CreateThreadModal({ isOpen, onClose, onCreateThread }: CreateThr

return (
<RNModal visible={isOpen} transparent animationType="fade" onRequestClose={handleClose}>
<Animated.View entering={FadeIn} exiting={FadeOut} className="flex-1 bg-black/50">
<Animated.View
entering={FadeIn}
exiting={FadeOut}
className="flex-1 bg-background-dark/50 dark:bg-background-dark/90"
>
<View
className="flex-1 justify-center px-4"
onStartShouldSetResponder={() => true}
onTouchEnd={(e) => e.stopPropagation()}
>
<Animated.View
entering={FadeIn.delay(100)}
className="overflow-hidden rounded-lg bg-white"
className="overflow-hidden rounded-lg bg-background-0 dark:border dark:border-outline-100"
>
<ModalHeader onClose={handleClose} />
<ModalBody topic={topic} isCreating={isCreating} onTopicChange={setTopic} />
Expand Down
7 changes: 2 additions & 5 deletions components/ThreadList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,11 @@ interface ThreadListProps {

function ThreadItem({
thread,
index,
onPress,
avatarURL,
isPractitioner,
}: {
thread: Thread;
index: number;
onPress: () => void;
avatarURL: string | undefined;
isPractitioner: boolean;
Expand All @@ -48,7 +46,7 @@ function ThreadItem({
>
<View className="flex-row items-center gap-3 p-4">
<Avatar size="md" className="border-2 border-primary-200">
<Icon as={UserRound} size="lg" className="stroke-white" />
<Icon as={UserRound} size="lg" className="stroke-typography-0" />
<AvatarImage source={{ uri: avatarURL }} />
</Avatar>

Expand Down Expand Up @@ -113,10 +111,9 @@ export function ThreadList({ threads, getAvatarURL, onCreateThread }: ThreadList
const isPractitioner = profile?.resourceType === "Practitioner";

const renderItem: ListRenderItem<Thread> = useCallback(
({ item: thread, index }) => (
({ item: thread }) => (
<ThreadItem
thread={thread}
index={index}
onPress={() => router.push(`/thread/${thread.id}`)}
avatarURL={getAvatarURL(thread.getAvatarRef({ profile })) ?? undefined}
isPractitioner={isPractitioner}
Expand Down
Loading

0 comments on commit c12a4d7

Please sign in to comment.