Skip to content

Commit d55fed3

Browse files
authored
Merge pull request #22 from opencom-org/openspec/introduce-mobile-local-convex-wrapper-hooks
introduce-mobile-local-convex-wrapper-hooks
1 parent 344c0d2 commit d55fed3

19 files changed

Lines changed: 996 additions & 454 deletions

File tree

apps/mobile/app/(app)/conversation/[id].tsx

Lines changed: 18 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -11,68 +11,9 @@ import {
1111
ActivityIndicator,
1212
} from "react-native";
1313
import { useLocalSearchParams } from "expo-router";
14-
import { useQuery, useMutation } from "convex/react";
15-
import { makeFunctionReference } from "convex/server";
1614
import { useAuth } from "../../../src/contexts/AuthContext";
17-
import type { Id } from "@opencom/convex/dataModel";
18-
19-
interface Message {
20-
_id: string;
21-
content: string;
22-
senderType: "user" | "visitor" | "agent" | "bot";
23-
createdAt: number;
24-
}
25-
26-
type ConversationRecord = {
27-
_id: Id<"conversations">;
28-
visitorId?: Id<"visitors">;
29-
status: "open" | "closed" | "snoozed";
30-
};
31-
32-
type VisitorRecord = {
33-
_id: Id<"visitors">;
34-
name?: string;
35-
email?: string;
36-
readableId?: string;
37-
location?: { city?: string; country?: string };
38-
device?: { browser?: string; os?: string };
39-
};
40-
41-
const conversationGetQueryRef = makeFunctionReference<
42-
"query",
43-
{ id: Id<"conversations"> },
44-
ConversationRecord | null
45-
>("conversations:get");
46-
47-
const visitorGetQueryRef = makeFunctionReference<
48-
"query",
49-
{ id: Id<"visitors"> },
50-
VisitorRecord | null
51-
>("visitors:get");
52-
53-
const messagesListQueryRef = makeFunctionReference<
54-
"query",
55-
{ conversationId: Id<"conversations"> },
56-
Message[]
57-
>("messages:list");
58-
59-
const sendMessageMutationRef = makeFunctionReference<
60-
"mutation",
61-
{ conversationId: Id<"conversations">; senderId: string; senderType: "agent"; content: string },
62-
Id<"messages">
63-
>("messages:send");
64-
65-
const updateConversationStatusMutationRef = makeFunctionReference<
66-
"mutation",
67-
{ id: Id<"conversations">; status: "open" | "closed" | "snoozed" },
68-
null
69-
>("conversations:updateStatus");
70-
71-
const markConversationReadMutationRef = makeFunctionReference<
72-
"mutation",
73-
{ id: Id<"conversations">; readerType: "agent" | "visitor" },
74-
null
75-
>("conversations:markAsRead");
15+
import { useConversationConvex } from "../../../src/hooks/convex/useConversationConvex";
16+
import type { MobileConversationMessage as Message } from "../../../src/hooks/convex/types";
7617

7718
function formatTime(timestamp: number): string {
7819
return new Date(timestamp).toLocaleTimeString([], {
@@ -86,32 +27,22 @@ export default function ConversationScreen() {
8627
const { user } = useAuth();
8728
const [inputText, setInputText] = useState("");
8829
const flatListRef = useRef<FlatList>(null);
89-
90-
const conversation = useQuery(
91-
conversationGetQueryRef,
92-
id ? { id: id as Id<"conversations"> } : "skip"
93-
) as ConversationRecord | null | undefined;
94-
95-
const visitor = useQuery(
96-
visitorGetQueryRef,
97-
conversation?.visitorId ? { id: conversation.visitorId } : "skip"
98-
) as VisitorRecord | null | undefined;
99-
100-
const messages = useQuery(
101-
messagesListQueryRef,
102-
id ? { conversationId: id as Id<"conversations"> } : "skip"
103-
) as Message[] | undefined;
104-
105-
const sendMessage = useMutation(sendMessageMutationRef);
106-
const updateStatus = useMutation(updateConversationStatusMutationRef);
107-
const markAsRead = useMutation(markConversationReadMutationRef);
30+
const {
31+
resolvedConversationId,
32+
conversation,
33+
visitor,
34+
messages,
35+
sendMessage,
36+
updateConversationStatus: updateStatus,
37+
markConversationRead: markAsRead,
38+
} = useConversationConvex(id);
10839

10940
// Mark conversation as read when viewing
11041
useEffect(() => {
111-
if (id && conversation) {
112-
markAsRead({ id: id as Id<"conversations">, readerType: "agent" }).catch(console.error);
42+
if (resolvedConversationId && conversation) {
43+
markAsRead({ id: resolvedConversationId, readerType: "agent" }).catch(console.error);
11344
}
114-
}, [id, conversation, markAsRead]);
45+
}, [conversation, markAsRead, resolvedConversationId]);
11546

11647
useEffect(() => {
11748
if (messages && messages.length > 0) {
@@ -122,14 +53,14 @@ export default function ConversationScreen() {
12253
}, [messages]);
12354

12455
const handleSend = async () => {
125-
if (!inputText.trim() || !id || !user) return;
56+
if (!inputText.trim() || !resolvedConversationId || !user) return;
12657

12758
const content = inputText.trim();
12859
setInputText("");
12960

13061
try {
13162
await sendMessage({
132-
conversationId: id as Id<"conversations">,
63+
conversationId: resolvedConversationId,
13364
senderId: user._id,
13465
senderType: "agent",
13566
content,
@@ -141,10 +72,10 @@ export default function ConversationScreen() {
14172
};
14273

14374
const handleStatusChange = async (status: "open" | "closed" | "snoozed") => {
144-
if (!id) return;
75+
if (!resolvedConversationId) return;
14576
try {
14677
await updateStatus({
147-
id: id as Id<"conversations">,
78+
id: resolvedConversationId,
14879
status,
14980
});
15081
} catch (error) {

apps/mobile/app/(app)/index.tsx

Lines changed: 9 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,15 @@
11
import { View, Text, StyleSheet, FlatList, TouchableOpacity, RefreshControl } from "react-native";
2-
import { useQuery } from "convex/react";
3-
import { makeFunctionReference } from "convex/server";
42
import { useAuth } from "../../src/contexts/AuthContext";
53
import { router } from "expo-router";
64
import { useState, useCallback } from "react";
7-
import type { Id } from "@opencom/convex/dataModel";
8-
9-
interface ConversationItem {
10-
_id: string;
11-
visitorId?: string;
12-
status: "open" | "closed" | "snoozed";
13-
lastMessageAt?: number;
14-
createdAt: number;
15-
unreadByAgent?: number;
16-
visitor: {
17-
name?: string;
18-
email?: string;
19-
readableId?: string;
20-
} | null;
21-
lastMessage: {
22-
content: string;
23-
senderType: string;
24-
createdAt: number;
25-
} | null;
26-
}
27-
28-
type InboxPageResult =
29-
| ConversationItem[]
30-
| {
31-
conversations: ConversationItem[];
32-
};
33-
34-
const visitorIsOnlineQueryRef = makeFunctionReference<
35-
"query",
36-
{ visitorId: Id<"visitors"> },
37-
boolean
38-
>("visitors:isOnline");
39-
40-
const inboxListQueryRef = makeFunctionReference<
41-
"query",
42-
{ workspaceId: Id<"workspaces">; status?: "open" | "closed" | "snoozed" },
43-
InboxPageResult
44-
>("conversations:listForInbox");
5+
import { useInboxConvex, useVisitorPresenceConvex } from "../../src/hooks/convex/useInboxConvex";
6+
import type {
7+
MobileConversationItem as ConversationItem,
8+
MobileConversationStatus,
9+
} from "../../src/hooks/convex/types";
4510

4611
function PresenceIndicator({ visitorId }: { visitorId: string }) {
47-
const isOnline = useQuery(visitorIsOnlineQueryRef, { visitorId: visitorId as Id<"visitors"> });
12+
const { isOnline } = useVisitorPresenceConvex(visitorId);
4813
return (
4914
<View
5015
style={[styles.presenceIndicator, isOnline ? styles.presenceOnline : styles.presenceOffline]}
@@ -110,17 +75,9 @@ function ConversationListItem({ item, onPress }: { item: ConversationItem; onPre
11075
export default function InboxScreen() {
11176
const { activeWorkspaceId } = useAuth();
11277
const [refreshing, setRefreshing] = useState(false);
113-
const [statusFilter, setStatusFilter] = useState<"open" | "closed" | "snoozed" | undefined>(
114-
undefined
115-
);
116-
117-
const inboxPage = useQuery(
118-
inboxListQueryRef,
119-
activeWorkspaceId ? { workspaceId: activeWorkspaceId, status: statusFilter } : "skip"
120-
) as InboxPageResult | undefined;
121-
const conversations = (Array.isArray(inboxPage) ? inboxPage : inboxPage?.conversations) as
122-
| ConversationItem[]
123-
| undefined;
78+
const [statusFilter, setStatusFilter] = useState<MobileConversationStatus | undefined>(undefined);
79+
const { inboxPage } = useInboxConvex({ workspaceId: activeWorkspaceId, status: statusFilter });
80+
const conversations = inboxPage?.conversations as ConversationItem[] | undefined;
12481

12582
const onRefresh = useCallback(() => {
12683
setRefreshing(true);

apps/mobile/app/(app)/onboarding.tsx

Lines changed: 12 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -10,69 +10,12 @@ import {
1010
} from "react-native";
1111
import * as Clipboard from "expo-clipboard";
1212
import { router } from "expo-router";
13-
import { useMutation, useQuery } from "convex/react";
14-
import { makeFunctionReference } from "convex/server";
1513
import { useAuth } from "../../src/contexts/AuthContext";
1614
import { useBackend } from "../../src/contexts/BackendContext";
17-
import type { Id } from "@opencom/convex/dataModel";
15+
import { useOnboardingConvex } from "../../src/hooks/convex/useOnboardingConvex";
1816

1917
type VerificationStatus = "idle" | "checking" | "success" | "error";
2018

21-
type HostedOnboardingState = {
22-
status: "not_started" | "started" | "completed";
23-
isWidgetVerified: boolean;
24-
verificationToken?: string | null;
25-
} | null;
26-
27-
type HostedOnboardingIntegrationSignals = {
28-
integrations: Array<{
29-
id: string;
30-
integrationKey: string;
31-
clientType: string;
32-
clientVersion?: string | null;
33-
status: "recognized" | "active" | "inactive";
34-
isActiveNow: boolean;
35-
matchesCurrentVerificationWindow: boolean;
36-
origin?: string | null;
37-
currentUrl?: string | null;
38-
clientIdentifier?: string | null;
39-
lastSeenAt?: number | null;
40-
activeSessionCount: number;
41-
detectedAt?: number | null;
42-
metadata?: Record<string, unknown> | null;
43-
}>;
44-
} | null;
45-
46-
const hostedOnboardingStateQueryRef = makeFunctionReference<
47-
"query",
48-
{ workspaceId: Id<"workspaces"> },
49-
HostedOnboardingState
50-
>("workspaces:getHostedOnboardingState");
51-
52-
const hostedOnboardingSignalsQueryRef = makeFunctionReference<
53-
"query",
54-
{ workspaceId: Id<"workspaces"> },
55-
HostedOnboardingIntegrationSignals
56-
>("workspaces:getHostedOnboardingIntegrationSignals");
57-
58-
const startHostedOnboardingMutationRef = makeFunctionReference<
59-
"mutation",
60-
{ workspaceId: Id<"workspaces"> },
61-
null
62-
>("workspaces:startHostedOnboarding");
63-
64-
const issueVerificationTokenMutationRef = makeFunctionReference<
65-
"mutation",
66-
{ workspaceId: Id<"workspaces"> },
67-
{ token: string }
68-
>("workspaces:issueHostedOnboardingVerificationToken");
69-
70-
const completeWidgetStepMutationRef = makeFunctionReference<
71-
"mutation",
72-
{ workspaceId: Id<"workspaces">; token?: string },
73-
{ success: boolean }
74-
>("workspaces:completeHostedOnboardingWidgetStep");
75-
7619
const VERIFY_TIMEOUT_MS = 15000;
7720

7821
function formatTimestamp(value: number | null | undefined): string {
@@ -108,19 +51,13 @@ export default function OnboardingScreen() {
10851
const startRequestedRef = useRef(false);
10952
const tokenRequestedRef = useRef(false);
11053
const verifyTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
111-
112-
const onboardingState = useQuery(
113-
hostedOnboardingStateQueryRef,
114-
workspaceId ? { workspaceId } : "skip"
115-
) as HostedOnboardingState | undefined;
116-
const integrationSignals = useQuery(
117-
hostedOnboardingSignalsQueryRef,
118-
workspaceId ? { workspaceId } : "skip"
119-
) as HostedOnboardingIntegrationSignals | undefined;
120-
121-
const startHostedOnboarding = useMutation(startHostedOnboardingMutationRef);
122-
const issueVerificationToken = useMutation(issueVerificationTokenMutationRef);
123-
const completeWidgetStep = useMutation(completeWidgetStepMutationRef);
54+
const {
55+
onboardingState,
56+
integrationSignals,
57+
startHostedOnboarding,
58+
issueVerificationToken,
59+
completeWidgetStep,
60+
} = useOnboardingConvex(workspaceId);
12461

12562
useEffect(() => {
12663
if (!onboardingState?.verificationToken) {
@@ -387,7 +324,10 @@ await OpencomSDK.initialize({
387324
</View>
388325
</View>
389326
<Text style={styles.integrationMeta} numberOfLines={2}>
390-
{signal.origin ?? signal.currentUrl ?? signal.clientIdentifier ?? "Unknown source"}
327+
{signal.origin ??
328+
signal.currentUrl ??
329+
signal.clientIdentifier ??
330+
"Unknown source"}
391331
{" · Last seen "}
392332
{formatTimestamp(signal.lastSeenAt)}
393333
{" · Active sessions "}

0 commit comments

Comments
 (0)