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

Commit

Permalink
feat: handle nicks in client
Browse files Browse the repository at this point in the history
and propagate changes to all
  • Loading branch information
coderbyheart committed Jan 28, 2020
1 parent f85335d commit 7ad0c81
Show file tree
Hide file tree
Showing 6 changed files with 154 additions and 55 deletions.
16 changes: 3 additions & 13 deletions src/Chat/SlashCommands.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,6 @@ import {
VerifyTokenQueryResult,
VerifyTokenVariables,
} from '../graphql/verifyTokenQuery'
import {
updateNickMutation,
UpdateNickMutationResult,
UpdateNickVariables,
} from '../graphql/updateNickMutation'

export enum SlashCommand {
HELP = 'help',
Expand Down Expand Up @@ -42,10 +37,12 @@ export const SlashCommandHandler = ({
updateMessages,
token,
onSwitchChannel,
onChangeNick,
}: {
apollo: ApolloClient<NormalizedCacheObject>
updateMessages: UpdateMessages
onSwitchChannel: (channel: string) => void
onChangeNick: (nick: string) => void
token: string
}) => (cmd: SlashCommand, arg?: string) => {
switch (cmd) {
Expand Down Expand Up @@ -76,14 +73,7 @@ export const SlashCommandHandler = ({
onSwitchChannel(arg as string)
break
case SlashCommand.NICK:
apollo
.mutate<UpdateNickMutationResult, UpdateNickVariables>({
mutation: updateNickMutation,
variables: { token, nick: arg as string },
})
.catch(err => {
console.error(err)
})
onChangeNick(arg as string)
break
case SlashCommand.ME:
apollo
Expand Down
120 changes: 107 additions & 13 deletions src/Chat/Twilio/ChannelView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
} from '../components/MessageItem'
import { StatusItem, Status } from '../components/StatusItem'
import { Channel } from 'twilio-chat/lib/channel'
import { Client, User } from 'twilio-chat'
import { Message } from 'twilio-chat/lib/message'
import { Member } from 'twilio-chat/lib/member'
import { v4 } from 'uuid'
Expand All @@ -24,6 +25,10 @@ import {
SendButton,
OtherChannelHeader,
} from '../components/ChannelView'
import { UserDescriptor } from 'twilio-chat/lib/userdescriptor'

type AuthorMap = { [key: string]: User }
type AuthorNicks = { [key: string]: string | undefined }

export const ChannelView = ({
channelConnection,
Expand All @@ -34,14 +39,16 @@ export const ChannelView = ({
joinedChannels,
selectedChannel,
onCloseChannel,
onChangeNick,
}: {
channelConnection?: Channel
channelConnection?: { channel: Channel; client: Client }
identity: string
apollo: ApolloClient<NormalizedCacheObject>
token: string
selectedChannel: string
joinedChannels: string[]
onSwitchChannel: (channel: string) => void
onChangeNick: (nick: string) => void
onCloseChannel: (channel: string) => void
}) => {
const storageKey = `DAChat:minimized`
Expand Down Expand Up @@ -71,11 +78,22 @@ export const ChannelView = ({
],
})

const [authorSubscriptions, setAuthorSubscriptions] = useState<AuthorMap>({})
const [authorNicks, setAuthorNicks] = useState<AuthorNicks>({})
useEffect(() => {
;() => {
Object.values(authorSubscriptions).map(async author =>
author.unsubscribe(),
)
}
})

const onSlashCommand = SlashCommandHandler({
apollo,
updateMessages,
token,
onSwitchChannel,
onChangeNick,
})
const sendMessage = (channelConnection: Channel) => {
const [cmd, ...args] = message.split(' ')
Expand Down Expand Up @@ -157,15 +175,54 @@ export const ChannelView = ({
}))
}

const userChangedNickHandler = ({
updateReasons,
user,
}: User.UpdatedEventArgs) => {
if (updateReasons.includes('friendlyName')) {
updateMessages(prevMessages => ({
...prevMessages,
messages: [
...prevMessages.messages,
{
sid: v4(),
status: {
message: (
<>
User <em>{authorNicks[user.identity] || user.identity}</em>{' '}
changed their name to <em>{user.friendlyName}</em>.
</>
),
timestamp: new Date(),
},
},
],
}))
setAuthorNicks(authorNicks => ({
...authorNicks,
[user.identity]: user.friendlyName,
}))
}
}

useEffect(() => {
if (channelConnection) {
channelConnection.on('messageAdded', newMessageHandler)
channelConnection.on('memberJoined', memberJoinedHandler)
channelConnection.on('memberLeft', memberLeftHandler)
channelConnection.channel.on('messageAdded', newMessageHandler)
channelConnection.channel.on('memberJoined', memberJoinedHandler)
channelConnection.channel.on('memberLeft', memberLeftHandler)
return () => {
channelConnection.removeListener('messageAdded', newMessageHandler)
channelConnection.removeListener('memberJoined', memberJoinedHandler)
channelConnection.removeListener('memberLeft', memberLeftHandler)
channelConnection.channel.removeListener(
'messageAdded',
newMessageHandler,
)
channelConnection.channel.removeListener(
'memberJoined',
memberJoinedHandler,
)
channelConnection.channel.removeListener(
'memberLeft',
memberLeftHandler,
)
}
}
return () => {
Expand Down Expand Up @@ -204,7 +261,7 @@ export const ChannelView = ({
}
channel
.getMessages(10, messages.lastIndex && messages.lastIndex - 1)
.then(messages => {
.then(async messages => {
setInitialLoad(false)
updateMessages(prevMessages => ({
...prevMessages,
Expand All @@ -214,6 +271,39 @@ export const ChannelView = ({
],
lastIndex: messages.items[0]?.index ?? undefined,
}))
return Promise.all(
[...new Set(messages.items.map(message => message.author))]
.filter(
identity => !Object.keys(authorSubscriptions).includes(identity),
)
.map(async author =>
channelConnection?.client.getUserDescriptor(author),
),
)
})
.then(async newAuthorDescriptors =>
Promise.all(
(newAuthorDescriptors.filter(
f => f,
) as UserDescriptor[]).map(async a => a.subscribe()),
),
)
.then(async newAuthorSubscriptions => {
const subs = newAuthorSubscriptions.reduce((authors, user) => {
user.on('updated', userChangedNickHandler)
return {
...authors,
[user.identity]: user,
}
}, {} as AuthorMap)
setAuthorSubscriptions(subs)
setAuthorNicks({
...authorNicks,
...Object.values(subs).reduce(
(nicks, user) => ({ ...nicks, [user.identity]: user.friendlyName }),
{},
),
})
})
.catch(err => {
console.error(err)
Expand All @@ -223,7 +313,7 @@ export const ChannelView = ({

useEffect(() => {
if (channelConnection) {
loadOlderMessages(channelConnection, false)
loadOlderMessages(channelConnection.channel, false)
}
}, [channelConnection])

Expand Down Expand Up @@ -298,7 +388,7 @@ export const ChannelView = ({
<MessageList ref={messageListRef}>
{channelConnection && (
<TextButton
onClick={() => loadOlderMessages(channelConnection)}
onClick={() => loadOlderMessages(channelConnection.channel)}
>
Load older messages
</TextButton>
Expand All @@ -317,7 +407,11 @@ export const ChannelView = ({
) : (
<MessageItem
key={m.sid}
message={m.message}
message={{
...m.message,
from: m.message.from,
}}
nick={authorNicks[m.message.from]}
onRendered={() => {
// Scroll to the last item in the list
// if not at beginning
Expand Down Expand Up @@ -347,14 +441,14 @@ export const ChannelView = ({
disabled={!channelConnection}
onKeyUp={({ keyCode }) => {
if (keyCode === 13 && message.length > 0) {
channelConnection && sendMessage(channelConnection)
channelConnection && sendMessage(channelConnection.channel)
}
}}
/>
<SendButton
disabled={!channelConnection || !(message.length > 0)}
onClick={() =>
channelConnection && sendMessage(channelConnection)
channelConnection && sendMessage(channelConnection.channel)
}
>
send
Expand Down
19 changes: 18 additions & 1 deletion src/Chat/Twilio/TwilioChat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { ApolloClient, NormalizedCacheObject } from '@apollo/client'
import { Error } from '../components/Error'
import { ChannelView } from './ChannelView'
import { Channel } from 'twilio-chat/lib/channel'
import { Client } from 'twilio-chat'
import { ChatWidget } from '../components/ChatWidget'
import { connectToChannel } from './api'
import { isLeft } from 'fp-ts/lib/Either'
Expand All @@ -23,7 +24,7 @@ export const TwilioChat = ({
const [error, setError] = useState<{ type: string; message: string }>()
const [selectedChannel, setSelectedChannel] = useState<string>(context)
const [channelConnection, setConnectedChannel] = useState<
Channel | undefined
{ channel: Channel; client: Client } | undefined
>()
const [joinedChannels, setJoinedChannels] = useState<string[]>([
...new Set(['general', 'random', context]),
Expand Down Expand Up @@ -75,6 +76,22 @@ export const TwilioChat = ({
})
})
}}
onChangeNick={nick => {
console.log(`Changing nick ...`, nick)
if (channelConnection) {
channelConnection.client.user
.updateFriendlyName(nick)
.then(() => {
console.log(`Updated nick to ${nick}.`)
})
.catch(error => {
setError({
type: 'InternalError',
message: error.message,
})
})
}
}}
onCloseChannel={channel => {
setJoinedChannels(joinedChannels.filter(c => c !== channel))
}}
Expand Down
31 changes: 23 additions & 8 deletions src/Chat/Twilio/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
ChatTokenMutationResult,
ChatTokenVariables,
} from '../../graphql/createChatTokenMutation'
import * as Twilio from 'twilio-chat'
import { Client } from 'twilio-chat'
import { Channel } from 'twilio-chat/lib/channel'
import { Either } from 'fp-ts/lib/Either'
import { tryCatch, chain } from 'fp-ts/lib/TaskEither'
Expand Down Expand Up @@ -50,15 +50,15 @@ const createChatToken = ({
)

const createClient = (chatToken: string) =>
tryCatch<ErrorInfo, Twilio.Client>(
async () => Twilio.Client.create(chatToken),
tryCatch<ErrorInfo, Client>(
async () => Client.create(chatToken),
reason => ({
type: 'IntegrationError',
message: `Creating chat client failed: ${(reason as Error).message}`,
}),
)

const fetchSubscribedChannels = (client: Twilio.Client) =>
const fetchSubscribedChannels = (client: Client) =>
tryCatch<ErrorInfo, Paginator<Channel>>(
async () => client.getSubscribedChannels(),
reason => ({
Expand All @@ -73,7 +73,7 @@ const joinChannel = ({
client,
channel,
}: {
client: Twilio.Client
client: Client
channel: string
}) => () =>
tryCatch<ErrorInfo, Channel>(
Expand All @@ -92,6 +92,21 @@ const maybeAlreadyJoinedChannel = (context: string) => (
): Option<Channel> =>
fromNullable(channels.items.find(({ uniqueName }) => uniqueName === context))

export const authenticateClient = ({
apollo,
deviceId,
token,
}: {
deviceId: string
token: string
apollo: ApolloClient<NormalizedCacheObject>
}) =>
pipe(
TE.right({ apollo, deviceId, token }),
chain(createChatToken),
chain(createClient),
)

export const connectToChannel = async ({
apollo,
context,
Expand All @@ -102,16 +117,16 @@ export const connectToChannel = async ({
deviceId: string
token: string
apollo: ApolloClient<NormalizedCacheObject>
}): Promise<Either<ErrorInfo, Channel>> =>
}): Promise<Either<ErrorInfo, { client: Client; channel: Channel }>> =>
pipe(
TE.right({ apollo, deviceId, token }),
chain(createChatToken),
chain(createClient),
chain(authenticateClient),
chain(client =>
pipe(
fetchSubscribedChannels(client),
TE.map(maybeAlreadyJoinedChannel(context)),
getOrElse(joinChannel({ client, channel: context })),
TE.map(channel => ({ client, channel })),
),
),
)()
4 changes: 3 additions & 1 deletion src/Chat/components/MessageItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,11 @@ export const stringToColor = (str: string) => {

export const MessageItem = ({
message: { from, message, timestamp, fromUser },
nick,
onRendered,
}: {
message: Message
nick?: string
onRendered: () => void
}) => {
const V = fromUser ? UserMessageView : MessageView
Expand All @@ -138,7 +140,7 @@ export const MessageItem = ({

return (
<V style={stringToColor(from)}>
<From>{from}</From>
<From>{nick || from}</From>
<Text>{message}</Text>
<Meta>
<Timestamp from={timestamp} />
Expand Down
Loading

0 comments on commit 7ad0c81

Please sign in to comment.