Skip to content

Commit 74a451b

Browse files
add threaded chat + new TextButton component, refactor Chat API (#1374)
Threaded chat implementation Co-authored-by: Glenn 'devalias' Grant <[email protected]>
1 parent 36913d6 commit 74a451b

File tree

27 files changed

+805
-214
lines changed

27 files changed

+805
-214
lines changed

src/api/chat.ts

Lines changed: 84 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,18 @@
1+
import Bugsnag from "@bugsnag/js";
12
import firebase from "firebase/app";
3+
import noop from "lodash/noop";
24

35
import { VenueChatMessage, PrivateChatMessage } from "types/chat";
46

7+
import { getVenueRef } from "./venue";
8+
9+
export const userChatsCollectionRef = (userId: string) =>
10+
firebase
11+
.firestore()
12+
.collection("privatechats")
13+
.doc(userId)
14+
.collection("chats");
15+
516
export interface SendVenueMessageProps {
617
venueId: string;
718
message: VenueChatMessage;
@@ -10,24 +21,42 @@ export interface SendVenueMessageProps {
1021
export const sendVenueMessage = async ({
1122
venueId,
1223
message,
13-
}: SendVenueMessageProps) =>
14-
await firebase
15-
.firestore()
16-
.collection("venues")
17-
.doc(venueId)
24+
}: SendVenueMessageProps): Promise<void> =>
25+
getVenueRef(venueId)
1826
.collection("chats")
19-
.add(message);
27+
.add(message)
28+
.then(noop)
29+
.catch((err) => {
30+
Bugsnag.notify(err, (event) => {
31+
event.addMetadata("context", {
32+
location: "api/chat::sendVenueMessage",
33+
venueId,
34+
message,
35+
});
36+
});
37+
// @debt rethrow error, when we can handle it to show UI error
38+
});
39+
40+
export const sendPrivateMessage = async (
41+
message: PrivateChatMessage
42+
): Promise<void> => {
43+
const batch = firebase.firestore().batch();
44+
45+
const authorRef = userChatsCollectionRef(message.from).doc();
46+
const recipientRef = userChatsCollectionRef(message.to).doc();
47+
48+
batch.set(authorRef, message);
49+
batch.set(recipientRef, message);
2050

21-
export const sendPrivateMessage = async (message: PrivateChatMessage) => {
22-
// @debt This is the legacy way of saving private messages. Would be nice to have it saved in one operation
23-
[message.from, message.to].forEach((messageUser) =>
24-
firebase
25-
.firestore()
26-
.collection("privatechats")
27-
.doc(messageUser)
28-
.collection("chats")
29-
.add(message)
30-
);
51+
return batch.commit().catch((err) => {
52+
Bugsnag.notify(err, (event) => {
53+
event.addMetadata("context", {
54+
location: "api/chat::sendPrivateMessage",
55+
message,
56+
});
57+
});
58+
// @debt rethrow error, when we can handle it to show UI error
59+
});
3160
};
3261

3362
export type SetChatMessageIsReadProps = {
@@ -38,14 +67,20 @@ export type SetChatMessageIsReadProps = {
3867
export const setChatMessageRead = async ({
3968
userId,
4069
messageId,
41-
}: SetChatMessageIsReadProps) =>
42-
firebase
43-
.firestore()
44-
.collection("privatechats")
45-
.doc(userId)
46-
.collection("chats")
70+
}: SetChatMessageIsReadProps): Promise<void> =>
71+
userChatsCollectionRef(userId)
4772
.doc(messageId)
48-
.update({ isRead: true });
73+
.update({ isRead: true })
74+
.catch((err) => {
75+
Bugsnag.notify(err, (event) => {
76+
event.addMetadata("context", {
77+
location: "api/chat::setChatMessageRead",
78+
userId,
79+
messageId,
80+
});
81+
});
82+
// @debt rethrow error, when we can handle it to show UI error
83+
});
4984

5085
export type DeleteVenueMessageProps = {
5186
venueId: string;
@@ -55,14 +90,21 @@ export type DeleteVenueMessageProps = {
5590
export const deleteVenueMessage = async ({
5691
venueId,
5792
messageId,
58-
}: DeleteVenueMessageProps) =>
59-
await firebase
60-
.firestore()
61-
.collection("venues")
62-
.doc(venueId)
93+
}: DeleteVenueMessageProps): Promise<void> =>
94+
getVenueRef(venueId)
6395
.collection("chats")
6496
.doc(messageId)
65-
.update({ deleted: true });
97+
.update({ deleted: true })
98+
.catch((err) => {
99+
Bugsnag.notify(err, (event) => {
100+
event.addMetadata("context", {
101+
location: "api/chat::deleteVenueMessage",
102+
venueId,
103+
messageId,
104+
});
105+
});
106+
// @debt rethrow error, when we can handle it to show UI error
107+
});
66108

67109
export type DeletePrivateMessageProps = {
68110
userId: string;
@@ -72,11 +114,17 @@ export type DeletePrivateMessageProps = {
72114
export const deletePrivateMessage = async ({
73115
userId,
74116
messageId,
75-
}: DeletePrivateMessageProps) =>
76-
await firebase
77-
.firestore()
78-
.collection("privatechats")
79-
.doc(userId)
80-
.collection("chats")
117+
}: DeletePrivateMessageProps): Promise<void> =>
118+
userChatsCollectionRef(userId)
81119
.doc(messageId)
82-
.update({ deleted: true });
120+
.update({ deleted: true })
121+
.catch((err) => {
122+
Bugsnag.notify(err, (event) => {
123+
event.addMetadata("context", {
124+
location: "api/chat::deletePrivateMessage",
125+
userId,
126+
messageId,
127+
});
128+
});
129+
// @debt rethrow error, when we can handle it to show UI error
130+
});

src/api/venue.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import Bugsnag from "@bugsnag/js";
22
import firebase from "firebase/app";
33

4+
export const getVenueCollectionRef = () =>
5+
firebase.firestore().collection("venues");
6+
7+
export const getVenueRef = (venueId: string) =>
8+
getVenueCollectionRef().doc(venueId);
9+
410
export interface SetVenueLiveStatusProps {
511
venueId: string;
612
isLive: boolean;
Lines changed: 90 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,82 +1,137 @@
11
@import "scss/constants";
22

3-
$counterparty-text-border-radius: 28px 28px 28px 4px;
4-
$my-text-border-radius: 28px 28px 4px 28px;
3+
$counterparty-bottom-border-radius: 0 0 $spacing--xxl $spacing--xs;
4+
$counterparty-text-border-radius: $spacing--xxl $spacing--xxl $spacing--xxl
5+
$spacing--xs;
6+
$my-text-bottom-border-radius: 0 0 $spacing--xs $spacing--xxl;
7+
$my-text-border-radius: $spacing--xxl $spacing--xxl $spacing--xs $spacing--xxl;
58

6-
.chat-message {
9+
$replies-max-height: 200px;
10+
11+
$reply-icon-radius: 20px;
12+
13+
.ChatMessage {
714
align-self: flex-start;
815
display: flex;
916
align-items: flex-start;
1017
flex-direction: column;
11-
margin-bottom: 16px;
18+
margin-bottom: $spacing--lg;
1219

13-
&__text {
14-
margin-bottom: 6px;
20+
&:hover {
21+
.ChatMessage__reply-icon {
22+
display: flex;
23+
}
24+
}
25+
26+
&__bulb {
27+
margin-bottom: $spacing--sm;
1528
width: auto;
1629
border-radius: $counterparty-text-border-radius;
1730
overflow-wrap: break-word;
1831
word-wrap: break-word;
1932
word-break: break-word;
2033
hyphens: auto;
21-
background-color: $intermediate-grey;
22-
padding: 12px 16px;
23-
font-size: 0.9rem;
34+
background-color: $secondary;
35+
36+
font-size: $font-size--md;
2437
}
2538

26-
&__info {
39+
&__text-content {
40+
position: relative;
2741
display: flex;
28-
align-items: center;
29-
font-size: 0.8rem;
42+
flex-flow: column;
43+
padding: $spacing--md $spacing--lg;
3044
}
3145

32-
&__author {
33-
opacity: 0.8;
34-
color: $white;
35-
margin: 0 8px 0 4px;
46+
&__text {
47+
font-size: $font-size--md;
48+
}
49+
50+
&__show-replies-button {
51+
margin-top: $spacing--sm;
52+
align-self: flex-start;
53+
}
54+
55+
&__reply-icon {
56+
display: none;
57+
justify-content: center;
58+
align-items: center;
59+
// Overflow the userAvatar in the replies list
60+
z-index: z(chatmessage-reply-button);
61+
62+
position: absolute;
63+
3664
cursor: pointer;
3765

66+
width: $reply-icon-radius;
67+
height: $reply-icon-radius;
68+
69+
right: $spacing--sm;
70+
71+
bottom: calc(-#{$reply-icon-radius / 2});
72+
73+
border-radius: $border-radius--max;
74+
background-color: $secondary--light;
75+
3876
&:hover {
39-
opacity: 1;
77+
background-color: $secondary--lightest;
4078
}
4179
}
4280

43-
&__time {
44-
opacity: 0.6;
45-
font-weight: 300;
81+
&__replies-content {
82+
border-radius: $counterparty-bottom-border-radius;
83+
overflow: hidden;
84+
max-height: $replies-max-height;
4685
}
4786

48-
&__delete-icon {
49-
margin-left: 5px;
50-
opacity: 0.6;
51-
cursor: pointer;
87+
&__replies {
88+
@include scrollbar;
89+
background-color: opaque-black(0.14);
90+
padding: 0 $spacing--lg $spacing--lg $spacing--lg;
91+
font-size: $font-size--md;
92+
display: flex;
93+
flex-flow: column-reverse;
5294

53-
&:hover {
54-
opacity: 0.8;
55-
}
95+
max-height: $replies-max-height;
96+
97+
overflow: auto;
98+
99+
align-items: flex-start;
100+
text-align: left;
101+
}
102+
103+
&__reply {
104+
// We add padding to single reply instead of margin so that the container has spacing at the top
105+
// https://stackoverflow.com/questions/13471910/no-padding-when-using-overflow-auto
106+
padding-top: $spacing--md;
56107
}
57108

58109
&--me {
59110
align-self: flex-end;
60111
align-items: flex-end;
61112

62-
.chat-message__text {
113+
.ChatMessage__bulb {
63114
text-align: right;
64115
background-color: $primary;
65116
border-radius: $my-text-border-radius;
66117
}
67118

68-
.chat-message__info {
69-
flex-direction: row-reverse;
119+
.ChatMessage__show-replies-button {
70120
align-self: flex-end;
71121
}
72122

73-
.chat-message__author {
74-
margin: 0 4px 0 8px;
123+
.ChatMessage__replies-content {
124+
border-radius: $my-text-bottom-border-radius;
75125
}
76126

77-
.chat-message__delete-icon {
78-
margin-left: 0;
79-
margin-right: 5px;
127+
.ChatMessage__reply-icon {
128+
background-color: $primary--light;
129+
right: unset;
130+
left: $spacing--sm;
131+
132+
&:hover {
133+
background-color: $primary--lightest;
134+
}
80135
}
81136
}
82137
}

0 commit comments

Comments
 (0)