Skip to content

Commit 8beaa2e

Browse files
authored
Merge branch 'main' into cancel-reservation-request
2 parents 394b511 + 28502a1 commit 8beaa2e

62 files changed

Lines changed: 4160 additions & 1689 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/ui-sharethrift/src/components/layouts/home/messages/components/conversation-box.container.graphql

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,22 @@ query ConversationBoxContainerConversation($conversationId: ObjectID!) {
44
}
55
}
66

7+
mutation ConversationBoxContainerSendMessage($input: SendMessageInput!) {
8+
sendMessage(input: $input) {
9+
status {
10+
success
11+
errorMessage
12+
}
13+
message {
14+
id
15+
messagingMessageId
16+
authorId
17+
content
18+
createdAt
19+
}
20+
}
21+
}
22+
723
fragment ConversationBoxContainerConversationFields on Conversation {
824
id
925
schemaVersion
Lines changed: 130 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,136 @@
1-
import { ComponentQueryLoader } from "@sthrift/ui-components";
2-
import { ConversationBox } from "./conversation-box.tsx";
1+
import { useMutation, useQuery } from '@apollo/client/react';
2+
import { ComponentQueryLoader } from '@sthrift/ui-components';
3+
import { message as antdMessage } from 'antd';
4+
import { useCallback } from 'react';
35
import {
4-
ConversationBoxContainerConversationDocument,
5-
type Conversation,
6-
} from "../../../../../generated.tsx";
7-
import { useQuery } from "@apollo/client/react";
6+
type Conversation,
7+
ConversationBoxContainerConversationDocument,
8+
ConversationBoxContainerSendMessageDocument,
9+
} from '../../../../../generated.tsx';
10+
import { useUserId } from '../../../../shared/user-context.tsx';
11+
import { ConversationBox } from './conversation-box.tsx';
812

913
interface ConversationBoxContainerProps {
10-
selectedConversationId: string;
14+
selectedConversationId: string;
1115
}
1216

13-
export const ConversationBoxContainer: React.FC<ConversationBoxContainerProps> = (props) => {
14-
const {
15-
data: currentUserConversationsData,
16-
loading: loadingConversations,
17-
error: conversationsError,
18-
} = useQuery(ConversationBoxContainerConversationDocument, {
19-
variables: {
20-
conversationId: props.selectedConversationId,
21-
},
22-
});
23-
24-
return (
25-
<ComponentQueryLoader
26-
loading={loadingConversations}
27-
hasData={currentUserConversationsData}
28-
error={conversationsError}
29-
hasDataComponent={
30-
<ConversationBox
31-
data={currentUserConversationsData?.conversation as Conversation}
32-
/>
33-
}
34-
/>
35-
);
17+
export const ConversationBoxContainer: React.FC<
18+
ConversationBoxContainerProps
19+
> = (props) => {
20+
const currentUserId = useUserId();
21+
22+
const {
23+
data: currentUserConversationsData,
24+
loading: loadingConversations,
25+
error: conversationsError,
26+
} = useQuery(ConversationBoxContainerConversationDocument, {
27+
variables: {
28+
conversationId: props.selectedConversationId,
29+
},
30+
});
31+
32+
const [sendMessageMutation, { loading: sendingMessage }] = useMutation(
33+
ConversationBoxContainerSendMessageDocument,
34+
{
35+
update: (cache, { data }, { variables }) => {
36+
try {
37+
// Guard against missing or invalid sendMessage payload
38+
if (!data?.sendMessage?.status?.success || !data.sendMessage.message) {
39+
return;
40+
}
41+
42+
// Use variables from mutation call instead of props closure to avoid stale reference
43+
const conversationId = variables?.input?.conversationId;
44+
if (!conversationId) return;
45+
46+
// Update Apollo cache instead of refetch to avoid unnecessary network round-trip
47+
const existingConversation = cache.readQuery({
48+
query: ConversationBoxContainerConversationDocument,
49+
variables: { conversationId },
50+
});
51+
52+
if (existingConversation?.conversation) {
53+
// Spread the entire existing result to preserve query shape and avoid field drift
54+
cache.writeQuery({
55+
query: ConversationBoxContainerConversationDocument,
56+
variables: { conversationId },
57+
data: {
58+
...existingConversation,
59+
conversation: {
60+
...existingConversation.conversation,
61+
messages: [
62+
...(existingConversation.conversation.messages || []),
63+
data.sendMessage.message,
64+
],
65+
},
66+
},
67+
});
68+
}
69+
} catch (error) {
70+
// Cache update errors should not block the mutation from succeeding
71+
// Log for debugging but don't show error to user - message was sent successfully
72+
console.warn(
73+
'[ConversationBoxContainer] Apollo cache update failed, message sent but UI may need refresh',
74+
error,
75+
);
76+
}
77+
},
78+
onCompleted: (data) => {
79+
// Guard against missing status before accessing nested properties
80+
if (!data?.sendMessage?.status) {
81+
return;
82+
}
83+
84+
if (!data.sendMessage.status.success) {
85+
antdMessage.error(
86+
data.sendMessage.status.errorMessage || 'Failed to send message',
87+
);
88+
}
89+
},
90+
onError: (error) => {
91+
antdMessage.error(error.message || 'Failed to send message');
92+
},
93+
},
94+
);
95+
96+
const handleSendMessage = useCallback(
97+
async (content: string): Promise<boolean> => {
98+
if (!content.trim()) return false;
99+
100+
try {
101+
const result = await sendMessageMutation({
102+
variables: {
103+
input: {
104+
conversationId: props.selectedConversationId,
105+
content: content.trim(),
106+
},
107+
},
108+
});
109+
110+
// Return whether the message was successfully sent
111+
return result.data?.sendMessage?.status?.success ?? false;
112+
} catch {
113+
// Network errors are already handled by onError callback
114+
// Return false to indicate failure so input is not cleared
115+
return false;
116+
}
117+
},
118+
[props.selectedConversationId, sendMessageMutation],
119+
);
120+
121+
return (
122+
<ComponentQueryLoader
123+
loading={loadingConversations}
124+
hasData={currentUserConversationsData}
125+
error={conversationsError}
126+
hasDataComponent={
127+
<ConversationBox
128+
data={currentUserConversationsData?.conversation as Conversation}
129+
currentUserId={currentUserId}
130+
onSendMessage={handleSendMessage}
131+
sendingMessage={sendingMessage}
132+
/>
133+
}
134+
/>
135+
);
36136
};
Lines changed: 48 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,58 @@
1-
import type { Conversation } from "../../../../../generated.tsx";
2-
import { ListingBanner } from "./listing-banner.tsx";
3-
import { MessageThread } from "./index.ts";
4-
import { useState } from "react";
1+
import { useState } from 'react';
2+
import type { Conversation } from '../../../../../generated.tsx';
3+
import { MessageThread } from './index.ts';
4+
import { ListingBanner } from './listing-banner.tsx';
55

66
interface ConversationBoxProps {
7-
data: Conversation;
7+
data: Conversation;
8+
currentUserId?: string;
9+
onSendMessage: (content: string) => Promise<boolean>;
10+
sendingMessage: boolean;
811
}
912

1013
export const ConversationBox: React.FC<ConversationBoxProps> = (props) => {
11-
const [messageText, setMessageText] = useState("");
14+
const [messageText, setMessageText] = useState('');
1215

13-
const currentUserId = props?.data?.sharer?.id;
16+
const currentUserId = props.currentUserId ?? props?.data?.sharer?.id;
1417

15-
const handleSendMessage = (e: React.FormEvent) => {
16-
e.preventDefault();
17-
console.log("Send message logic to be implemented", messageText);
18-
};
18+
const handleSendMessage = async (e: React.FormEvent) => {
19+
e.preventDefault();
20+
if (props.sendingMessage) return; // Prevent duplicate submits while send is in flight
21+
if (!messageText.trim()) return;
1922

20-
return (
21-
<>
22-
<div style={{ marginBottom: 24 }}>
23-
<ListingBanner owner={props.data?.sharer} />
24-
</div>
23+
// Only clear input on successful send so users don't lose unsent content on error
24+
const success = await props.onSendMessage(messageText);
25+
if (success) {
26+
setMessageText('');
27+
}
28+
};
2529

26-
<div
27-
style={{
28-
flex: 1,
29-
minHeight: 0,
30-
display: "flex",
31-
flexDirection: "column",
32-
}}
33-
>
34-
<MessageThread
35-
conversationId={props.data.id}
36-
messages={props.data.messages || []}
37-
loading={false}
38-
error={null}
39-
sendingMessage={false}
40-
messageText={messageText}
41-
setMessageText={setMessageText}
42-
handleSendMessage={handleSendMessage}
43-
currentUserId={currentUserId}
44-
/>
45-
</div>
46-
</>
47-
);
30+
return (
31+
<>
32+
<div style={{ marginBottom: 24 }}>
33+
<ListingBanner owner={props.data?.sharer} />
34+
</div>
35+
36+
<div
37+
style={{
38+
flex: 1,
39+
minHeight: 0,
40+
display: 'flex',
41+
flexDirection: 'column',
42+
}}
43+
>
44+
<MessageThread
45+
conversationId={props.data.id}
46+
messages={props.data.messages || []}
47+
loading={false}
48+
error={null}
49+
sendingMessage={props.sendingMessage}
50+
messageText={messageText}
51+
setMessageText={setMessageText}
52+
handleSendMessage={handleSendMessage}
53+
currentUserId={currentUserId}
54+
/>
55+
</div>
56+
</>
57+
);
4858
};

apps/ui-sharethrift/src/components/layouts/home/messages/components/message-thread.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ export const MessageThread: React.FC<MessageThreadProps> = (props) => {
148148
htmlType="submit"
149149
icon={<SendOutlined />}
150150
loading={props.sendingMessage}
151-
disabled={!props.messageText.trim()}
151+
disabled={!props.messageText.trim() || props.sendingMessage}
152152
style={{ fontFamily: "var(--Urbanist, Arial, sans-serif)" }}
153153
>
154154
Send

0 commit comments

Comments
 (0)