-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathConversationWindow.tsx
159 lines (146 loc) · 6.03 KB
/
ConversationWindow.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
import { useState, useEffect } from "react"
import { useParams, Redirect } from "react-router-dom"
import { ConversationMessageBatch, ConversationChatbox } from "../../components"
import { createConversationSnapshotListener, createConversationMessagesSnapshotListener } from "../../firebase"
import { Conversation, Message, User } from "../../models"
import { useAuth } from "../../context"
import styles from "./ConversationWindow.module.css"
const ALLOWED_TYPES = ["image/png", "image/jpeg", "image/gif"]
/**
* Turns a 1-d array of messages into a 2d array of messages grouped by user
* e.g. given a conversation with user's a and b, and given the message list: [a, a, b, a, b, b]
* the resulting array should be sorted into: [[a, a], [b], [a], [b, b]]
*/
function deepen(messages: Message[]): Message[][] {
const ret: Message[][] = []
let curr: Message[] = []
let currId = ""
for (const message of messages) {
if (currId && currId !== message.sender.id) {
// switch
ret.push(curr)
curr = [message]
} else {
// add to curr
curr.unshift(message)
}
currId = message.sender.id
}
if (curr.length > 0) {
ret.push(curr)
}
return ret
}
// The main window where conversation messages are being streamed, rendered by AuthedScreens
export function ConversationWindow() {
// conversationId is retrieved from the URL
const { conversationId } = useParams<{ conversationId: string }>()
// Retrieved from the App component
const { currentUser } = useAuth()
const [recipient, setRecipient] = useState<User | null>(null)
const [conversation, setConversation] = useState<Conversation | null>(null)
const [messages, setMessages] = useState<Message[]>([])
const [loadingError, setLoadingError] = useState<string | null>(null)
const [error, setError] = useState<string | null>(null)
const [recipientIsTyping, setRecipientIsTyping] = useState<boolean>(false)
const [redirectWithoutError, setRedirectWithoutError] = useState<boolean>(false)
// On component mounting, we start listening to both changes to this conversation document (when a user is typing) and
// new documents from the nested collection of messages
useEffect(() => {
const unsubscribeToConversation = createConversationSnapshotListener(conversationId, (conv, error) => {
if (conv) {
if (conv.users.findIndex((user) => user.id === currentUser!.id) > -1) {
const recipient = conv.users.find((user) => user.id !== currentUser!.id)!
setRecipient(recipient)
setConversation(conv)
setRecipientIsTyping(conv.usersTyping[recipient.id] === true)
} else {
setLoadingError("Forbidden conversation.")
}
} else {
if (error) {
setLoadingError(error!.message)
} else {
// TODO: This is intended to be called when a conversation deletes itself, but it is never actually called.
// How should we go about fixing this?
setRedirectWithoutError(true)
}
}
})
const unsubscribeToMessages = createConversationMessagesSnapshotListener(conversationId, (messages, error) => {
if (messages) {
setMessages(messages)
} else {
setLoadingError(error!.message)
}
})
// Stop listening once this component stops rendering
return () => {
unsubscribeToConversation()
unsubscribeToMessages()
}
}, [currentUser, conversationId])
// Redirect if there's an error set during snapshot update
if (loadingError !== null) {
return (
<Redirect
to={{
pathname: "/error",
state: { error: loadingError },
}}
/>
)
}
// We redirect if the conversation we're listening to is deleted
if (redirectWithoutError) {
return <Redirect to="/" />
}
if (conversation === null) {
// set loading
return null
}
// passed into ConversationChatbox component, called when a user submits their message
const handleMessageSend = (message: string, file?: File) => {
if (message.length === 0 && !file) {
setError("Cannot send empty message.")
} else if (file && !ALLOWED_TYPES.includes(file.type)) {
setError("Invalid file type.")
} else {
conversation.addMessage(currentUser!, message, file)
}
}
// passed into ConvergentChatbox
const handleTypingStart = async () => {
await conversation.setUserTyping(currentUser!, true)
}
// passed into ConvergentChatbox
const handleTypingStop = async () => {
await conversation.setUserTyping(currentUser!, false)
}
return (
<div className={styles.conversationWindowContainer}>
<div className={styles.conversationWindow}>
{/* Render message stream here */}
{conversation ? (
deepen(messages).map((messageBatch, i) => {
return <ConversationMessageBatch key={i} messages={messageBatch} />
})
) : (
<p>No messages yet.</p>
)}
</div>
<div className={styles.notificationBar}>
{recipientIsTyping && (
<div className={styles.typingText}>{recipient!.name ?? recipient!.username} is typing</div>
)}
{error !== null && <div className={styles.notificationError}>{error}</div>}
</div>
{/* Render chatbox here */}
<ConversationChatbox
onMessageSend={handleMessageSend}
onTypingStart={handleTypingStart}
onTypingStop={handleTypingStop}
/>
</div>
)
}