Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
105 changes: 18 additions & 87 deletions apps/mobile/app/(app)/conversation/[id].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,68 +11,9 @@ import {
ActivityIndicator,
} from "react-native";
import { useLocalSearchParams } from "expo-router";
import { useQuery, useMutation } from "convex/react";
import { makeFunctionReference } from "convex/server";
import { useAuth } from "../../../src/contexts/AuthContext";
import type { Id } from "@opencom/convex/dataModel";

interface Message {
_id: string;
content: string;
senderType: "user" | "visitor" | "agent" | "bot";
createdAt: number;
}

type ConversationRecord = {
_id: Id<"conversations">;
visitorId?: Id<"visitors">;
status: "open" | "closed" | "snoozed";
};

type VisitorRecord = {
_id: Id<"visitors">;
name?: string;
email?: string;
readableId?: string;
location?: { city?: string; country?: string };
device?: { browser?: string; os?: string };
};

const conversationGetQueryRef = makeFunctionReference<
"query",
{ id: Id<"conversations"> },
ConversationRecord | null
>("conversations:get");

const visitorGetQueryRef = makeFunctionReference<
"query",
{ id: Id<"visitors"> },
VisitorRecord | null
>("visitors:get");

const messagesListQueryRef = makeFunctionReference<
"query",
{ conversationId: Id<"conversations"> },
Message[]
>("messages:list");

const sendMessageMutationRef = makeFunctionReference<
"mutation",
{ conversationId: Id<"conversations">; senderId: string; senderType: "agent"; content: string },
Id<"messages">
>("messages:send");

const updateConversationStatusMutationRef = makeFunctionReference<
"mutation",
{ id: Id<"conversations">; status: "open" | "closed" | "snoozed" },
null
>("conversations:updateStatus");

const markConversationReadMutationRef = makeFunctionReference<
"mutation",
{ id: Id<"conversations">; readerType: "agent" | "visitor" },
null
>("conversations:markAsRead");
import { useConversationConvex } from "../../../src/hooks/convex/useConversationConvex";
import type { MobileConversationMessage as Message } from "../../../src/hooks/convex/types";

function formatTime(timestamp: number): string {
return new Date(timestamp).toLocaleTimeString([], {
Expand All @@ -86,32 +27,22 @@ export default function ConversationScreen() {
const { user } = useAuth();
const [inputText, setInputText] = useState("");
const flatListRef = useRef<FlatList>(null);

const conversation = useQuery(
conversationGetQueryRef,
id ? { id: id as Id<"conversations"> } : "skip"
) as ConversationRecord | null | undefined;

const visitor = useQuery(
visitorGetQueryRef,
conversation?.visitorId ? { id: conversation.visitorId } : "skip"
) as VisitorRecord | null | undefined;

const messages = useQuery(
messagesListQueryRef,
id ? { conversationId: id as Id<"conversations"> } : "skip"
) as Message[] | undefined;

const sendMessage = useMutation(sendMessageMutationRef);
const updateStatus = useMutation(updateConversationStatusMutationRef);
const markAsRead = useMutation(markConversationReadMutationRef);
const {
resolvedConversationId,
conversation,
visitor,
messages,
sendMessage,
updateConversationStatus: updateStatus,
markConversationRead: markAsRead,
} = useConversationConvex(id);

// Mark conversation as read when viewing
useEffect(() => {
if (id && conversation) {
markAsRead({ id: id as Id<"conversations">, readerType: "agent" }).catch(console.error);
if (resolvedConversationId && conversation) {
markAsRead({ id: resolvedConversationId, readerType: "agent" }).catch(console.error);
}
}, [id, conversation, markAsRead]);
}, [conversation, markAsRead, resolvedConversationId]);

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

const handleSend = async () => {
if (!inputText.trim() || !id || !user) return;
if (!inputText.trim() || !resolvedConversationId || !user) return;

const content = inputText.trim();
setInputText("");

try {
await sendMessage({
conversationId: id as Id<"conversations">,
conversationId: resolvedConversationId,
senderId: user._id,
senderType: "agent",
content,
Expand All @@ -141,10 +72,10 @@ export default function ConversationScreen() {
};

const handleStatusChange = async (status: "open" | "closed" | "snoozed") => {
if (!id) return;
if (!resolvedConversationId) return;
try {
await updateStatus({
id: id as Id<"conversations">,
id: resolvedConversationId,
status,
});
} catch (error) {
Expand Down
61 changes: 9 additions & 52 deletions apps/mobile/app/(app)/index.tsx
Original file line number Diff line number Diff line change
@@ -1,50 +1,15 @@
import { View, Text, StyleSheet, FlatList, TouchableOpacity, RefreshControl } from "react-native";
import { useQuery } from "convex/react";
import { makeFunctionReference } from "convex/server";
import { useAuth } from "../../src/contexts/AuthContext";
import { router } from "expo-router";
import { useState, useCallback } from "react";
import type { Id } from "@opencom/convex/dataModel";

interface ConversationItem {
_id: string;
visitorId?: string;
status: "open" | "closed" | "snoozed";
lastMessageAt?: number;
createdAt: number;
unreadByAgent?: number;
visitor: {
name?: string;
email?: string;
readableId?: string;
} | null;
lastMessage: {
content: string;
senderType: string;
createdAt: number;
} | null;
}

type InboxPageResult =
| ConversationItem[]
| {
conversations: ConversationItem[];
};

const visitorIsOnlineQueryRef = makeFunctionReference<
"query",
{ visitorId: Id<"visitors"> },
boolean
>("visitors:isOnline");

const inboxListQueryRef = makeFunctionReference<
"query",
{ workspaceId: Id<"workspaces">; status?: "open" | "closed" | "snoozed" },
InboxPageResult
>("conversations:listForInbox");
import { useInboxConvex, useVisitorPresenceConvex } from "../../src/hooks/convex/useInboxConvex";
import type {
MobileConversationItem as ConversationItem,
MobileConversationStatus,
} from "../../src/hooks/convex/types";

function PresenceIndicator({ visitorId }: { visitorId: string }) {
const isOnline = useQuery(visitorIsOnlineQueryRef, { visitorId: visitorId as Id<"visitors"> });
const { isOnline } = useVisitorPresenceConvex(visitorId);
return (
<View
style={[styles.presenceIndicator, isOnline ? styles.presenceOnline : styles.presenceOffline]}
Expand Down Expand Up @@ -110,17 +75,9 @@ function ConversationListItem({ item, onPress }: { item: ConversationItem; onPre
export default function InboxScreen() {
const { activeWorkspaceId } = useAuth();
const [refreshing, setRefreshing] = useState(false);
const [statusFilter, setStatusFilter] = useState<"open" | "closed" | "snoozed" | undefined>(
undefined
);

const inboxPage = useQuery(
inboxListQueryRef,
activeWorkspaceId ? { workspaceId: activeWorkspaceId, status: statusFilter } : "skip"
) as InboxPageResult | undefined;
const conversations = (Array.isArray(inboxPage) ? inboxPage : inboxPage?.conversations) as
| ConversationItem[]
| undefined;
const [statusFilter, setStatusFilter] = useState<MobileConversationStatus | undefined>(undefined);
const { inboxPage } = useInboxConvex({ workspaceId: activeWorkspaceId, status: statusFilter });
const conversations = inboxPage?.conversations as ConversationItem[] | undefined;

const onRefresh = useCallback(() => {
setRefreshing(true);
Expand Down
84 changes: 12 additions & 72 deletions apps/mobile/app/(app)/onboarding.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,69 +10,12 @@ import {
} from "react-native";
import * as Clipboard from "expo-clipboard";
import { router } from "expo-router";
import { useMutation, useQuery } from "convex/react";
import { makeFunctionReference } from "convex/server";
import { useAuth } from "../../src/contexts/AuthContext";
import { useBackend } from "../../src/contexts/BackendContext";
import type { Id } from "@opencom/convex/dataModel";
import { useOnboardingConvex } from "../../src/hooks/convex/useOnboardingConvex";

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

type HostedOnboardingState = {
status: "not_started" | "started" | "completed";
isWidgetVerified: boolean;
verificationToken?: string | null;
} | null;

type HostedOnboardingIntegrationSignals = {
integrations: Array<{
id: string;
integrationKey: string;
clientType: string;
clientVersion?: string | null;
status: "recognized" | "active" | "inactive";
isActiveNow: boolean;
matchesCurrentVerificationWindow: boolean;
origin?: string | null;
currentUrl?: string | null;
clientIdentifier?: string | null;
lastSeenAt?: number | null;
activeSessionCount: number;
detectedAt?: number | null;
metadata?: Record<string, unknown> | null;
}>;
} | null;

const hostedOnboardingStateQueryRef = makeFunctionReference<
"query",
{ workspaceId: Id<"workspaces"> },
HostedOnboardingState
>("workspaces:getHostedOnboardingState");

const hostedOnboardingSignalsQueryRef = makeFunctionReference<
"query",
{ workspaceId: Id<"workspaces"> },
HostedOnboardingIntegrationSignals
>("workspaces:getHostedOnboardingIntegrationSignals");

const startHostedOnboardingMutationRef = makeFunctionReference<
"mutation",
{ workspaceId: Id<"workspaces"> },
null
>("workspaces:startHostedOnboarding");

const issueVerificationTokenMutationRef = makeFunctionReference<
"mutation",
{ workspaceId: Id<"workspaces"> },
{ token: string }
>("workspaces:issueHostedOnboardingVerificationToken");

const completeWidgetStepMutationRef = makeFunctionReference<
"mutation",
{ workspaceId: Id<"workspaces">; token?: string },
{ success: boolean }
>("workspaces:completeHostedOnboardingWidgetStep");

const VERIFY_TIMEOUT_MS = 15000;

function formatTimestamp(value: number | null | undefined): string {
Expand Down Expand Up @@ -108,19 +51,13 @@ export default function OnboardingScreen() {
const startRequestedRef = useRef(false);
const tokenRequestedRef = useRef(false);
const verifyTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);

const onboardingState = useQuery(
hostedOnboardingStateQueryRef,
workspaceId ? { workspaceId } : "skip"
) as HostedOnboardingState | undefined;
const integrationSignals = useQuery(
hostedOnboardingSignalsQueryRef,
workspaceId ? { workspaceId } : "skip"
) as HostedOnboardingIntegrationSignals | undefined;

const startHostedOnboarding = useMutation(startHostedOnboardingMutationRef);
const issueVerificationToken = useMutation(issueVerificationTokenMutationRef);
const completeWidgetStep = useMutation(completeWidgetStepMutationRef);
const {
onboardingState,
integrationSignals,
startHostedOnboarding,
issueVerificationToken,
completeWidgetStep,
} = useOnboardingConvex(workspaceId);

useEffect(() => {
if (!onboardingState?.verificationToken) {
Expand Down Expand Up @@ -387,7 +324,10 @@ await OpencomSDK.initialize({
</View>
</View>
<Text style={styles.integrationMeta} numberOfLines={2}>
{signal.origin ?? signal.currentUrl ?? signal.clientIdentifier ?? "Unknown source"}
{signal.origin ??
signal.currentUrl ??
signal.clientIdentifier ??
"Unknown source"}
{" · Last seen "}
{formatTimestamp(signal.lastSeenAt)}
{" · Active sessions "}
Expand Down
Loading
Loading