Skip to content
This repository was archived by the owner on Sep 7, 2020. It is now read-only.

Commit 4f25d28

Browse files
committed
feat: add UI for subscribing to channels
Fixes #14 Fixes #9
1 parent 828d1df commit 4f25d28

11 files changed

+401
-6
lines changed

src/Chat/Twilio/ChannelView.tsx

+26
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,13 @@ import {
2323
Controls,
2424
} from '../components/ChannelView'
2525
import { UserDescriptor } from 'twilio-chat/lib/userdescriptor'
26+
import { SubscribeToChannel } from '../notifications/SubscribeToChannel'
2627

2728
import CloseIcon from 'feather-icons/dist/icons/x-square.svg'
2829
import MinimizeIcon from 'feather-icons/dist/icons/minimize.svg'
2930
import MaximizeIcon from 'feather-icons/dist/icons/maximize.svg'
3031
import SendIcon from 'feather-icons/dist/icons/send.svg'
32+
import AlertIcon from 'feather-icons/dist/icons/bell.svg'
3133

3234
type AuthorMap = { [key: string]: User }
3335
type AuthorNicks = { [key: string]: string | undefined }
@@ -86,6 +88,10 @@ export const ChannelView = ({
8688
}
8789
})
8890

91+
const [notificationSettingsVisible, showNotificationSettings] = useState<
92+
boolean
93+
>(false)
94+
8995
const onSlashCommand = SlashCommandHandler({
9096
apollo,
9197
updateMessages,
@@ -358,6 +364,7 @@ export const ChannelView = ({
358364
<Title>#{otherChannel}</Title>
359365
<Controls>
360366
<UIButton
367+
title={'Close channel'}
361368
onClick={e => {
362369
e.stopPropagation()
363370
onCloseChannel(otherChannel)
@@ -376,8 +383,18 @@ export const ChannelView = ({
376383
Chat: <strong>#{selectedChannel}</strong>
377384
</Title>
378385
<Controls>
386+
<UIButton
387+
title={'Enable offline notifications'}
388+
onClick={e => {
389+
e.stopPropagation()
390+
showNotificationSettings(visible => !visible)
391+
}}
392+
>
393+
<AlertIcon />
394+
</UIButton>
379395
{!isMinimized && (
380396
<UIButton
397+
title={'Minimize channel'}
381398
onClick={() => {
382399
memoMinimized(true)
383400
}}
@@ -387,6 +404,7 @@ export const ChannelView = ({
387404
)}
388405
{isMinimized && (
389406
<UIButton
407+
title={'Maximize channel'}
390408
onClick={() => {
391409
memoMinimized(false)
392410
}}
@@ -396,6 +414,7 @@ export const ChannelView = ({
396414
)}
397415
{joinedChannels.length > 1 && (
398416
<UIButton
417+
title={'Close channel'}
399418
onClick={e => {
400419
e.stopPropagation()
401420
onSwitchChannel(
@@ -409,6 +428,13 @@ export const ChannelView = ({
409428
)}
410429
</Controls>
411430
</Header>
431+
{channelConnection && notificationSettingsVisible && (
432+
<SubscribeToChannel
433+
channel={channelConnection.channel}
434+
apollo={apollo}
435+
token={token}
436+
/>
437+
)}
412438
{!isMinimized && (
413439
<>
414440
<MessageListContainer>

src/Chat/Twilio/TwilioChat.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { Channel } from 'twilio-chat/lib/channel'
77
import { Client } from 'twilio-chat'
88
import { ChatWidget } from '../components/ChatWidget'
99
import { Notice } from '../components/Notice'
10-
import { connectToChannel } from './api'
10+
import { connectToChannel, ErrorInfo } from './api'
1111
import { isLeft } from 'fp-ts/lib/Either'
1212

1313
export const TwilioChat = ({
@@ -22,7 +22,7 @@ export const TwilioChat = ({
2222
token: string
2323
}) => {
2424
const identity = JSON.parse(atob(token.split('.')[1])).sub
25-
const [error, setError] = useState<{ type: string; message: string }>()
25+
const [error, setError] = useState<ErrorInfo>()
2626
const [selectedChannel, setSelectedChannel] = useState<string>(context)
2727
const [channelConnection, setConnectedChannel] = useState<
2828
{ channel: Channel; client: Client; token: string } | undefined

src/Chat/Twilio/api.ts

+76-1
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,18 @@ import {
1818
VerifyTokenVariables,
1919
verifyTokenQuery,
2020
} from '../../graphql/verifyTokenQuery'
21+
import {
22+
EnableChannelNotificationsMutationResult,
23+
EnableChannelNotificationsMutationVariables,
24+
enableChannelNotificationsMutation,
25+
} from '../../graphql/enableChannelNotifications'
26+
import {
27+
VerifyEmailMutationResult,
28+
VerifyEmailMutationVariables,
29+
verifyEmailMutation,
30+
} from '../../graphql/verifyEmailMutation'
2131

22-
type ErrorInfo = {
32+
export type ErrorInfo = {
2333
type: string
2434
message: string
2535
}
@@ -169,3 +179,68 @@ export const connectToChannel = async ({
169179
),
170180
),
171181
)()
182+
183+
export const enableChannelNotifications = ({
184+
apollo,
185+
token,
186+
email,
187+
channel,
188+
}: {
189+
token: string
190+
channel: string
191+
email: string
192+
apollo: ApolloClient<NormalizedCacheObject>
193+
}) =>
194+
tryCatch<ErrorInfo, { emailVerified: boolean }>(
195+
async () =>
196+
apollo
197+
.mutate<
198+
EnableChannelNotificationsMutationResult,
199+
EnableChannelNotificationsMutationVariables
200+
>({
201+
mutation: enableChannelNotificationsMutation,
202+
variables: { token, channel, email },
203+
})
204+
.then(({ data }) => {
205+
if (!data) {
206+
throw new Error('No response received!')
207+
} else {
208+
return data.enableChannelNotifications
209+
}
210+
}),
211+
reason => ({
212+
type: 'IntegrationError',
213+
message: `Failed to enable channel notifications: ${
214+
(reason as Error).message
215+
}`,
216+
}),
217+
)
218+
219+
export const verifyEmail = ({
220+
apollo,
221+
email,
222+
code,
223+
}: {
224+
code: string
225+
email: string
226+
apollo: ApolloClient<NormalizedCacheObject>
227+
}) =>
228+
tryCatch<ErrorInfo, boolean>(
229+
async () =>
230+
apollo
231+
.mutate<VerifyEmailMutationResult, VerifyEmailMutationVariables>({
232+
mutation: verifyEmailMutation,
233+
variables: { code, email },
234+
})
235+
.then(({ data }) => {
236+
if (!data) {
237+
throw new Error('No response received!')
238+
} else {
239+
return data.verifyEmail
240+
}
241+
}),
242+
reason => ({
243+
type: 'IntegrationError',
244+
message: `Failed to verify email: ${(reason as Error).message}`,
245+
}),
246+
)

src/Chat/components/Error.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as React from 'react'
22
import styled from 'styled-components'
3+
import { ErrorInfo } from '../Twilio/api'
34

45
const Header = styled.div`
56
background-color: #fa0000;
@@ -13,7 +14,7 @@ const Text = styled.p`
1314
padding: 0.5rem 0.5rem 0.5rem 1rem;
1415
`
1516

16-
export const Error = ({ type, message }: { type: string; message: string }) => (
17+
export const Error = ({ type, message }: ErrorInfo) => (
1718
<Header>
1819
<Text>
1920
<strong>{type}:</strong> {message}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import * as React from 'react'
2+
import { useState } from 'react'
3+
import {
4+
FieldSet,
5+
InputWithButton,
6+
Label,
7+
Legend,
8+
TextInput,
9+
Wrapper,
10+
} from './elements'
11+
12+
const isValid = (code: string) => /[a-z]{8}-[a-z]{8}-[a-z]{8}/.test(code)
13+
14+
export const EnterConfirmationCode = ({
15+
onCode,
16+
email,
17+
}: {
18+
email: string
19+
onCode: (code: string) => void
20+
}) => {
21+
const [code, setCode] = useState('')
22+
return (
23+
<Wrapper>
24+
<form
25+
onSubmit={e => {
26+
e.preventDefault()
27+
}}
28+
>
29+
<Legend>Confirmation code</Legend>
30+
<p>
31+
Please enter the confirmation sent to your email <code>{email}</code>.
32+
</p>
33+
<FieldSet>
34+
<Label htmlFor="code">Code</Label>
35+
<InputWithButton>
36+
<TextInput
37+
type="text"
38+
id="code"
39+
required
40+
onChange={({ target: { value } }) => {
41+
setCode(value)
42+
}}
43+
value={code}
44+
/>
45+
<button
46+
disabled={!isValid(code)}
47+
onClick={() => {
48+
onCode(code)
49+
}}
50+
>
51+
confirm
52+
</button>
53+
</InputWithButton>
54+
</FieldSet>
55+
</form>
56+
</Wrapper>
57+
)
58+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import * as React from 'react'
2+
import { useState } from 'react'
3+
import {
4+
FieldSet,
5+
InputWithButton,
6+
Label,
7+
Legend,
8+
TextInput,
9+
Wrapper,
10+
} from './elements'
11+
import { Channel } from 'twilio-chat/lib/channel'
12+
13+
const isValid = (email: string) => /.@./.test(email)
14+
15+
export const EnterEmailForNotification = ({
16+
channel,
17+
onEmail,
18+
}: {
19+
channel: Channel
20+
onEmail: (email: string) => void
21+
}) => {
22+
const [email, setEmail] = useState(
23+
window.localStorage.getItem('dachat:notification:email') || '',
24+
)
25+
return (
26+
<Wrapper>
27+
<form
28+
onSubmit={e => {
29+
e.preventDefault()
30+
}}
31+
>
32+
<Legend>Notification settings</Legend>
33+
<p>
34+
You can enable email notifications for new messages in the channel{' '}
35+
<em>{channel.uniqueName}</em> when you are offline.
36+
</p>
37+
<FieldSet>
38+
<Label htmlFor="email">Email</Label>
39+
<InputWithButton>
40+
<TextInput
41+
type="email"
42+
id="email"
43+
required
44+
onChange={({ target: { value } }) => {
45+
setEmail(value)
46+
}}
47+
value={email}
48+
/>
49+
<button
50+
disabled={!isValid(email)}
51+
onClick={() => {
52+
onEmail(email)
53+
window.localStorage.setItem('dachat:notification:email', email)
54+
}}
55+
>
56+
subscribe
57+
</button>
58+
</InputWithButton>
59+
</FieldSet>
60+
</form>
61+
</Wrapper>
62+
)
63+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import styled from 'styled-components'
2+
3+
export const Wrapper = styled.div`
4+
border-right: 1px solid #ccc;
5+
border-left: 1px solid #ccc;
6+
border-bottom: 1px solid #ccc;
7+
padding: 0.5rem;
8+
background-color: #fff;
9+
`
10+
11+
export const Legend = styled.legend`
12+
font-weight: bold;
13+
`
14+
15+
export const FieldSet = styled.fieldset`
16+
padding: 0;
17+
border: 0;
18+
`
19+
20+
export const Label = styled.label`
21+
font-weight: bold;
22+
display: block;
23+
`
24+
25+
export const InputWithButton = styled.div`
26+
display: flex;
27+
`
28+
29+
export const TextInput = styled.input`
30+
flex-grow: 1;
31+
background-color: #fff;
32+
border: 1px solid;
33+
height: 28px;
34+
padding: 0 0.5rem;
35+
`

0 commit comments

Comments
 (0)