Skip to content

Commit

Permalink
feat(lib): add channel suggestion box (#125)
Browse files Browse the repository at this point in the history
* feat(lib): add channel suggestion box

* feat(lib): fix switching channels and channel id for direct conversations

* feat(lib): rm a comment
  • Loading branch information
piotr-suwala authored Sep 25, 2023
1 parent a3d3952 commit 681c1f7
Show file tree
Hide file tree
Showing 15 changed files with 159 additions and 117 deletions.
3 changes: 2 additions & 1 deletion lib/src/entities/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { MESSAGE_THREAD_ID_PREFIX } from "../constants"
import { ThreadChannel } from "./thread-channel"
import { MentionsUtils } from "../mentions-utils"
import { getErrorProxiedEntity, ErrorLogger } from "../error-logging"
import { cyrb53a } from "../hash"

type ChatConfig = {
saveDebugLog: boolean
Expand Down Expand Up @@ -690,7 +691,7 @@ export class Chat {

const sortedUsers = [this.user.id, user.id].sort()

const channelId = `direct.${sortedUsers[0]}&${sortedUsers[1]}`
const channelId = `direct.${cyrb53a(`${sortedUsers[0]}&${sortedUsers[1]}`)}`

const channel =
(await this.getChannel(channelId)) ||
Expand Down
15 changes: 15 additions & 0 deletions lib/src/hash.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export const cyrb53a = function (str: string, seed = 0) {
let h1 = 0xdeadbeef ^ seed,
h2 = 0x41c6ce57 ^ seed
for (let i = 0, ch; i < str.length; i++) {
ch = str.charCodeAt(i)
h1 = Math.imul(h1 ^ ch, 0x85ebca77)
h2 = Math.imul(h2 ^ ch, 0xc2b2ae3d)
}
h1 ^= Math.imul(h1 ^ (h2 >>> 15), 0x735a2d97)
h2 ^= Math.imul(h2 ^ (h1 >>> 15), 0xcaf649a9)
h1 ^= h2 >>> 16
h2 ^= h1 >>> 16

return 2097152 * (h2 >>> 0) + (h1 >>> 11)
}
10 changes: 8 additions & 2 deletions samples/react-native-group-chat/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -119,11 +119,17 @@ function App() {
const [chat, setChat] = useState<Chat | null>(null)
const [users, setUsers] = useState<User[]>([])
const [loading, setLoading] = useState(false)
const [currentChannel, setCurrentChannel] = useState<Channel>()
const [currentChannel, setCurrentChannel] = useState<Channel | null>()
const [currentChannelMembers, setCurrentChannelMembers] = useState<Membership[]>([])
const [userMemberships, setUserMemberships] = useState<Membership[]>([])

async function setCurrentChannelWithMembers(channel: Channel) {
async function setCurrentChannelWithMembers(channel: Channel | null) {
if (!channel) {
setCurrentChannelMembers([])
setCurrentChannel(null)
return
}

const { members } = await channel.getMembers()
setCurrentChannelMembers(members)
setCurrentChannel(channel)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
import React from "react"
import { View, StyleSheet } from "react-native"
import { ListItem } from "../list-item"
import { User } from "@pubnub/chat"
import { colorPalette } from "../../ui-components"
import { Channel, User } from "@pubnub/chat"

type UserSuggestionBoxProps = {
users: User[]
onUserSelect: (user: User) => void
type DataSuggestionBoxProps = {
data: Channel[] | User[]
onSelect: (element: Channel | User) => void
}

export function UserSuggestionBox({ users, onUserSelect }: UserSuggestionBoxProps) {
export function DataSuggestionBox({ data, onSelect }: DataSuggestionBoxProps) {
return (
<View style={styles.container}>
{users.map((user) => (
<ListItem title={user.name || user.id} key={user.id} onPress={() => onUserSelect(user)} />
{data.map((element) => (
<ListItem
title={element.name || element.id}
key={element.id}
onPress={() => onSelect(element)}
/>
))}
</View>
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./DataSuggestionBox"
2 changes: 1 addition & 1 deletion samples/react-native-group-chat/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export * from "./actions-menu"
export * from "./list-item"
export * from "./user-suggestion-box"
export * from "./data-suggestion-box"
export * from "./quote"
export * from "./avatar"
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,33 @@ import { Quote } from "../quote"
import { Text } from "../../ui-components"
import { Message, MixedTextTypedElement } from "@pubnub/chat"
import { ChatContext } from "../../context"
import { useNavigation } from "@react-navigation/native"

type MessageTextProps = {
onGoToMessage: (message: Message) => void
messageProps: Bubble<EnhancedIMessage>["props"]
}

export function MessageText({ onGoToMessage, messageProps }: MessageTextProps) {
const { chat } = useContext(ChatContext)
const { chat, setCurrentChannel } = useContext(ChatContext)
const navigation = useNavigation()

const openLink = (link: string) => {
Linking.openURL(link)
}

async function openChannel(channelId: string) {
if (!chat) return
const channel = await chat.getChannel(channelId)
if (!channel) {
alert("This channel no longer exists.")
return
}
navigation.pop()
setCurrentChannel(channel)
navigation.navigate("Chat")
}

const renderMessagePart = useCallback(
(messagePart: MixedTextTypedElement, index: number, userId: string | number) => {
// TODO make it look nice
Expand All @@ -31,14 +45,24 @@ export function MessageText({ onGoToMessage, messageProps }: MessageTextProps) {
}
if (messagePart.type === "plainLink") {
return (
<Text key={index} variant="body" onPress={() => openLink(messagePart.content.link)}>
<Text
key={index}
variant="body"
onPress={() => openLink(messagePart.content.link)}
color="sky150"
>
{messagePart.content.link}
</Text>
)
}
if (messagePart.type === "textLink") {
return (
<Text key={index} variant="body" onPress={() => openLink(messagePart.content.link)}>
<Text
key={index}
variant="body"
onPress={() => openLink(messagePart.content.link)}
color="sky150"
>
{messagePart.content.text}
</Text>
)
Expand All @@ -49,6 +73,7 @@ export function MessageText({ onGoToMessage, messageProps }: MessageTextProps) {
key={index}
variant="body"
onPress={() => openLink(`https://pubnub.com/${messagePart.content.id}`)}
color="sky150"
>
@{messagePart.content.name}
</Text>
Expand All @@ -59,7 +84,8 @@ export function MessageText({ onGoToMessage, messageProps }: MessageTextProps) {
<Text
key={index}
variant="body"
onPress={() => openLink(`https://pubnub.com/${messagePart.content.id}`)}
onPress={() => openChannel(messagePart.content.id)}
color="sky150"
>
#{messagePart.content.name}
</Text>
Expand Down

This file was deleted.

2 changes: 1 addition & 1 deletion samples/react-native-group-chat/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ type ChatContextParams = {
chat: null | Chat
setChat: (chat: Chat | null) => void
currentChannel?: Channel
setCurrentChannel: (channel: Channel) => void
setCurrentChannel: (channel: Channel | null) => void
currentChannelMembers: Membership[]
users: User[]
setUsers: (users: User[]) => void
Expand Down
57 changes: 31 additions & 26 deletions samples/react-native-group-chat/hooks/useCommonChatRenderers.tsx
Original file line number Diff line number Diff line change
@@ -1,48 +1,53 @@
import { FlatList, View, StyleSheet } from "react-native"
import React, { useCallback } from "react"
import { Message, MessageDraft, User } from "@pubnub/chat"
import React, { useCallback, useContext } from "react"
import { Channel, Message, MessageDraft, User } from "@pubnub/chat"
import { Text } from "../ui-components"
import { Bubble } from "react-native-gifted-chat"
import { EnhancedIMessage } from "../utils"
import { Quote, UserSuggestionBox } from "../components"
import { Quote, DataSuggestionBox } from "../components"
import { MessageText } from "../components/message-text"
import { ChatContext } from "../context"

type UseCommonChatRenderersProps = {
typingData: string[]
users: Map<string, User>
messageDraft: MessageDraft | null
lastAffectedNameOccurrenceIndex: number
setText: (text: string) => void
setShowSuggestedUsers: (value: boolean) => void
showSuggestedUsers: boolean
setShowSuggestedData: (value: boolean) => void
showSuggestedData: boolean
giftedChatRef: React.RefObject<FlatList<EnhancedIMessage>>
giftedChatMappedMessages: EnhancedIMessage[]
suggestedUsers: User[]
suggestedData: User[] | Channel[]
}

export function useCommonChatRenderers({
typingData,
users,
messageDraft,
lastAffectedNameOccurrenceIndex,
setText,
setShowSuggestedUsers,
setShowSuggestedData,
giftedChatRef,
giftedChatMappedMessages,
suggestedUsers,
showSuggestedUsers,
suggestedData,
showSuggestedData,
}: UseCommonChatRenderersProps) {
const handleUserToMention = useCallback(
(user: User) => {
const { getUser } = useContext(ChatContext)

const handleSuggestionSelect = useCallback(
(suggestion: User | Channel) => {
if (!messageDraft) {
return
}
if (suggestion instanceof User) {
messageDraft.addMentionedUser(suggestion, lastAffectedNameOccurrenceIndex)
} else {
messageDraft.addReferencedChannel(suggestion, lastAffectedNameOccurrenceIndex)
}

messageDraft.addMentionedUser(user, lastAffectedNameOccurrenceIndex)
setText(messageDraft.value)
setShowSuggestedUsers(false)
setShowSuggestedData(false)
},
[messageDraft, lastAffectedNameOccurrenceIndex]
[messageDraft, lastAffectedNameOccurrenceIndex, setText, setShowSuggestedData]
)

const scrollToMessage = useCallback(
Expand All @@ -62,7 +67,7 @@ export function useCommonChatRenderers({

giftedChatRef.current.scrollToIndex({ animated: true, index: messageIndex })
},
[giftedChatMappedMessages]
[giftedChatMappedMessages, giftedChatRef]
)

const renderChatFooter = useCallback(() => {
Expand All @@ -72,7 +77,7 @@ export function useCommonChatRenderers({

const quotedMessage = messageDraft.quotedMessage
let quotedMessageComponent = null
let userSuggestionComponent = null
let dataSuggestionComponent = null

if (quotedMessage) {
quotedMessageComponent = (
Expand All @@ -85,19 +90,19 @@ export function useCommonChatRenderers({
</View>
)
}
if (showSuggestedUsers) {
userSuggestionComponent = (
<UserSuggestionBox users={suggestedUsers} onUserSelect={handleUserToMention} />
if (showSuggestedData) {
dataSuggestionComponent = (
<DataSuggestionBox data={suggestedData} onSelect={handleSuggestionSelect} />
)
}

return (
<>
{quotedMessageComponent}
{userSuggestionComponent}
{dataSuggestionComponent}
</>
)
}, [messageDraft, showSuggestedUsers, scrollToMessage, suggestedUsers])
}, [messageDraft, showSuggestedData, scrollToMessage, suggestedData, handleSuggestionSelect])

const renderFooter = useCallback(() => {
if (!typingData.length) {
Expand All @@ -107,20 +112,20 @@ export function useCommonChatRenderers({
if (typingData.length === 1) {
return (
<View>
<Text variant="body">{users.get(typingData[0])?.name || typingData[0]} is typing...</Text>
<Text variant="body">{getUser(typingData[0])?.name || typingData[0]} is typing...</Text>
</View>
)
}

return (
<View>
<Text variant="body">
{typingData.map((typingPoint) => users.get(typingPoint)?.name || typingPoint).join(", ")}{" "}
{typingData.map((typingPoint) => getUser(typingPoint)?.name || typingPoint).join(", ")}{" "}
are typing...
</Text>
</View>
)
}, [typingData, users])
}, [getUser, typingData])

return {
renderFooter,
Expand Down
Loading

0 comments on commit 681c1f7

Please sign in to comment.