Skip to content

Commit

Permalink
Proper avatar loading
Browse files Browse the repository at this point in the history
  • Loading branch information
fjsj committed Jan 9, 2025
1 parent 9e72981 commit c0e78eb
Show file tree
Hide file tree
Showing 4 changed files with 82 additions and 33 deletions.
2 changes: 1 addition & 1 deletion components/ChatHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export function ChatHeader({ currentThread }: { currentThread: Thread }) {
<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" />
<AvatarImage source={{ uri: currentThread.imageURL }} />
<AvatarImage source={{ uri: currentThread.avatarURL }} />
</Avatar>
<View className="flex-col">
<Text size="md" bold className="text-typography-900">
Expand Down
7 changes: 5 additions & 2 deletions components/ThreadList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ function ThreadItem({
onPress: () => void;
}) {
const { profile } = useMedplumContext();
const isPractitioner = profile?.resourceType === "Practitioner";

return (
<Animated.View entering={FadeInDown.delay(index * 50).springify()}>
Expand All @@ -40,12 +41,14 @@ 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" />
<AvatarImage source={{ uri: thread.imageURL }} />
<AvatarImage source={{ uri: thread.avatarURL }} />
</Avatar>

<View className="flex-1">
<View className="flex-row items-center gap-2">
<Text className="text-base font-semibold text-typography-900">{thread.topic}</Text>
<Text className="text-sm font-semibold text-typography-900" isTruncated={true}>
{isPractitioner ? `${thread.patientName}: ${thread.topic}` : thread.topic}
</Text>
{profile && thread.getUnreadCount({ profile }) > 0 && (
<View className="rounded-full bg-primary-500 px-2 py-0.5">
<Text className="text-xs font-medium text-typography-0">
Expand Down
66 changes: 59 additions & 7 deletions contexts/ChatContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
ProfileResource,
QueryTypes,
} from "@medplum/core";
import { Bundle, Communication, Patient, Reference } from "@medplum/fhirtypes";
import { Bundle, Communication, Patient, Practitioner, Reference } from "@medplum/fhirtypes";
import { useMedplum, useSubscription } from "@medplum/react-hooks";
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react";

Expand Down Expand Up @@ -90,6 +90,59 @@ async function fetchThreadCommunications({
);
}

const imageURLCache = new Map<string, string>();
async function fetchThreadsImages({
medplum,
threads,
threadCommMap,
}: {
medplum: MedplumClient;
threads: Communication[];
threadCommMap: Map<string, Communication[]>;
}): Promise<Map<string, string>> {
const profile = medplum.getProfile();
if (!profile) return new Map();

const imageMap = new Map<string, string>();
await Promise.all(
threads.map(async (comm) => {
// If the profile is a patient, we need to get the practitioner's avatar, else get the patient's avatar:
const thread = Thread.fromCommunication({
// convert the communication to a thread
comm,
threadMessageComms: threadCommMap.get(comm.id!) || [],
avatarURL: undefined,
});
const avatarRefId =
profile.resourceType === "Patient" ? thread.practitionerId : thread.patientId;
if (!avatarRefId) return;

// If already cached, set the image URL
const avatarURL = imageURLCache.get(avatarRefId);
if (avatarURL) {
imageMap.set(thread.id!, avatarURL);
return;
}

// Otherwise, fetch the avatar URL
try {
const avatarProfile = (await medplum.readReference({
type: profile.resourceType === "Patient" ? "Practitioner" : "Patient",
reference: avatarRefId,
})) satisfies Practitioner | Patient;
if (avatarProfile.photo?.[0]?.url) {
const avatarURL = avatarProfile.photo[0].url;
imageURLCache.set(avatarRefId, avatarURL);
imageMap.set(thread.id!, avatarURL);
}
} catch {
// Ignore readReference errors
}
}),
);
return imageMap;
}

async function createThreadComm({
medplum,
profile,
Expand Down Expand Up @@ -197,6 +250,7 @@ export function ChatProvider({
const [profile, setProfile] = useState(medplum.getProfile());
const [threads, setThreads] = useState<Communication[]>([]);
const [threadCommMap, setThreadCommMap] = useState<Map<string, Communication[]>>(new Map());
const [threadImageMap, setThreadImageMap] = useState<Map<string, string>>(new Map());
const [currentThreadId, setCurrentThreadId] = useState<string | null>(null);
const [reconnecting, setReconnecting] = useState(false);
const [connectedOnce, setConnectedOnce] = useState(false);
Expand All @@ -211,15 +265,11 @@ export function ChatProvider({
Thread.fromCommunication({
comm: thread,
threadMessageComms: threadCommMap.get(thread.id!) || [],
avatarURL: threadImageMap.get(thread.id!),
}),
)
.sort((a, b) => b.threadOrder - a.threadOrder);
}, [threads, profile, threadCommMap]);

// Whenever threadsOut changes, load the image URL for each thread
useEffect(() => {
threadsOut.forEach((thread) => thread.loadImageURL({ medplum }));
}, [threadsOut, medplum]);
}, [profile, threads, threadCommMap, threadImageMap]);

// Current thread memoized
const currentThread = useMemo(() => {
Expand Down Expand Up @@ -260,6 +310,8 @@ export function ChatProvider({
const { threads, threadCommMap } = await fetchThreads({ medplum, threadsQuery });
setThreads(threads);
setThreadCommMap(threadCommMap);
const imageMap = await fetchThreadsImages({ medplum, threads, threadCommMap });
setThreadImageMap(imageMap);
} catch (err) {
onError?.(err as Error);
} finally {
Expand Down
40 changes: 17 additions & 23 deletions models/chat.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { MedplumClient, ProfileResource } from "@medplum/core";
import { Communication, Practitioner, Reference } from "@medplum/fhirtypes";
import { ProfileResource } from "@medplum/core";
import { Communication } from "@medplum/fhirtypes";

export class ChatMessage {
readonly originalCommunication: Communication;
Expand Down Expand Up @@ -52,30 +52,35 @@ export class ChatMessage {
export class Thread {
readonly messages: ChatMessage[];
readonly originalCommunication: Communication;
static imageURLMap = new Map<string, string>();
static loadImageURLPromiseMap = new Map<string, Promise<void>>();
readonly avatarURL: string | undefined;

constructor({
messages,
originalCommunication,
avatarURL,
}: {
messages: ChatMessage[];
originalCommunication: Communication;
avatarURL: string | undefined;
}) {
this.messages = [...messages].sort((a, b) => a.messageOrder - b.messageOrder);
this.originalCommunication = originalCommunication;
this.avatarURL = avatarURL;
}

static fromCommunication({
comm,
threadMessageComms,
avatarURL,
}: {
comm: Communication;
threadMessageComms: Communication[];
avatarURL: string | undefined;
}): Thread {
return new Thread({
messages: threadMessageComms.map((comm) => ChatMessage.fromCommunication({ comm })),
originalCommunication: comm,
avatarURL,
});
}

Expand Down Expand Up @@ -116,6 +121,10 @@ export class Thread {
?.originalCommunication;
}

get lastPatientCommunication(): Communication | undefined {
return this.messages.findLast((msg) => msg.senderType === "Patient")?.originalCommunication;
}

get practitionerName(): string | undefined {
return this.lastProviderCommunication?.sender?.display;
}
Expand All @@ -124,26 +133,11 @@ export class Thread {
return this.lastProviderCommunication?.sender?.reference;
}

get imageURL(): string | undefined {
return this.practitionerId ? Thread.imageURLMap.get(this.practitionerId) : undefined;
get patientName(): string | undefined {
return this.originalCommunication.subject?.display;
}

async loadImageURL({ medplum }: { medplum: MedplumClient }) {
// Load the image URL for the thread if it hasn't been loaded yet and it isn't already loading
if (!this.practitionerId) return;
if (Thread.imageURLMap.has(this.practitionerId)) return;
if (Thread.loadImageURLPromiseMap.has(this.practitionerId)) return;

try {
const practitioner = await medplum.readReference(
this.lastProviderCommunication?.sender as Reference<Practitioner>,
);
if (practitioner.photo?.[0]?.url) {
const imageUrl = practitioner.photo[0].url;
Thread.imageURLMap.set(this.practitionerId, imageUrl);
}
} catch {
// Ignore errors from fetching practitioner image
}
get patientId(): string | undefined {
return this.originalCommunication.subject?.reference;
}
}

0 comments on commit c0e78eb

Please sign in to comment.