diff --git a/gaming-livestream/package.json b/gaming-livestream/package.json index 304ad33b..d655b7f7 100644 --- a/gaming-livestream/package.json +++ b/gaming-livestream/package.json @@ -10,8 +10,8 @@ "emoji-mart": "^5.5.2", "react": "^19.0.0", "react-dom": "^19.0.0", - "stream-chat": "^8.56.0", - "stream-chat-react": "^12.12.0" + "stream-chat": "9.0.0-rc.15", + "stream-chat-react": "13.0.0-rc.2" }, "devDependencies": { "@types/react": "^19.0.8", diff --git a/gaming-livestream/src/components/Chat/ChannelContainer.tsx b/gaming-livestream/src/components/Chat/ChannelContainer.tsx index 856b49dc..400b6c49 100644 --- a/gaming-livestream/src/components/Chat/ChannelContainer.tsx +++ b/gaming-livestream/src/components/Chat/ChannelContainer.tsx @@ -4,11 +4,10 @@ import {MessageTimestampController} from "../../context/MessageTimestampControll import {GamingChatInner} from "./GamingChatInner"; import {Channel, useChatContext} from "stream-chat-react"; import React, {useEffect} from "react"; -import {StreamChatType} from "../../types"; export const ChannelContainer = () => { - const {channel, client, setActiveChannel} = useChatContext(); + const {channel, client, setActiveChannel} = useChatContext(); useEffect(() => { const loadChat = async () => { const channel = client.channel('gaming', 'gaming-demo', { name: 'Gaming Demo' }); diff --git a/gaming-livestream/src/components/Chat/GamingChatInner.tsx b/gaming-livestream/src/components/Chat/GamingChatInner.tsx index 0751a95d..2069b805 100644 --- a/gaming-livestream/src/components/Chat/GamingChatInner.tsx +++ b/gaming-livestream/src/components/Chat/GamingChatInner.tsx @@ -1,6 +1,6 @@ import React, {useCallback} from 'react'; -import {logChatPromiseExecution} from 'stream-chat'; -import {MessageInput, MessageList, MessageToSend, useChannelActionContext, Window} from 'stream-chat-react'; +import {type LocalMessage, logChatPromiseExecution, type Message, type SendMessageOptions} from 'stream-chat'; +import {MessageInput, MessageList, useChannelActionContext, Window} from 'stream-chat-react'; import {GamingChatHeader} from './GamingChatHeader'; import {GamingMessage} from './GamingMessage'; @@ -10,8 +10,6 @@ import {GamingThread} from './GamingThread'; import {useLayoutController} from '../../context/LayoutController'; import {useMessageTimestamp} from "../../context/MessageTimestampController"; -import type {StreamChatType} from '../../types'; - const NOTIFICATION_TEXT_FOR_COMMAND: Record = { '/ban': 'User banned', '/flag': 'User flagged', @@ -33,10 +31,15 @@ export const GamingChatInner = () => { const { publishAppNotification } = useLayoutController(); const {timestampEnabled, toggleTimestamp} = useMessageTimestamp(); - const overrideSubmitHandler = useCallback((message: MessageToSend) => { + const overrideSubmitHandler = useCallback(({localMessage, message, sendOptions}: { + cid: string; + localMessage: LocalMessage; + message: Message; + sendOptions: SendMessageOptions; + }) => { const { text } = message; publishAppNotification(getNotificationText(text)) - const sendMessagePromise = sendMessage(message); + const sendMessagePromise = sendMessage({localMessage, message, options: sendOptions}); logChatPromiseExecution(sendMessagePromise, 'send message'); }, [publishAppNotification, sendMessage]); diff --git a/gaming-livestream/src/components/Chat/GamingMessage.tsx b/gaming-livestream/src/components/Chat/GamingMessage.tsx index e3b2b0df..67c5c04a 100644 --- a/gaming-livestream/src/components/Chat/GamingMessage.tsx +++ b/gaming-livestream/src/components/Chat/GamingMessage.tsx @@ -10,11 +10,9 @@ import ReactionUpVote from '../../assets/icons/ReactionUpVote'; import {getColor} from '../../assets/data'; import {TimestampContextValue, useMessageTimestamp} from "../../context/MessageTimestampController"; +import type {LocalMessage} from "stream-chat"; -import type {StreamMessage} from 'stream-chat-react/dist/context/ChannelStateContext'; -import type {StreamChatType} from '../../types'; - -const getTimeStamp = (messageCreatedAt?: StreamMessage['created_at']) => { +const getTimeStamp = (messageCreatedAt?: LocalMessage['created_at']) => { if (!messageCreatedAt) return ''; const createdAt = new Date(messageCreatedAt); @@ -91,10 +89,10 @@ const MessageActions = ({downVote, upVote, onOpenThread}: MessageActionsProps) = type ReactionListProps = VotingStatsProps & Pick - & Pick, 'reply_count'>; + & Pick; const ReactionList = ({upVotes, downVotes, onOpenThread}: ReactionListProps) => { - const {message} = useMessageContext(); + const {message} = useMessageContext(); return (
@@ -108,7 +106,7 @@ const ReactionList = ({upVotes, downVotes, onOpenThread}: ReactionListProps) => } type MessageContentProps = {color?: string} - & Pick, 'message' | 'handleAction'> + & Pick & Pick; const MessageContent = ({color, message, handleAction, timestampEnabled}: MessageContentProps) => { @@ -132,8 +130,8 @@ const MessageContent = ({color, message, handleAction, timestampEnabled}: Messag } export const GamingMessage = () => { - const {openThread} = useChannelActionContext(); - const {handleAction, message} = useMessageContext(); + const {openThread} = useChannelActionContext(); + const {handleAction, message} = useMessageContext(); const {timestampEnabled} = useMessageTimestamp(); const [downVotes, setDownVotes] = useState(0); const [upVotes, setUpVotes] = useState(0); diff --git a/gaming-livestream/src/components/Chat/GamingMessageInput.tsx b/gaming-livestream/src/components/Chat/GamingMessageInput.tsx index 3f621cc6..14fa4800 100644 --- a/gaming-livestream/src/components/Chat/GamingMessageInput.tsx +++ b/gaming-livestream/src/components/Chat/GamingMessageInput.tsx @@ -1,19 +1,18 @@ import React from 'react'; import { - ChatAutoComplete, + TextareaComposer, useChannelStateContext, + useMessageComposerHasSendableData, useMessageInputContext, useTypingContext, } from 'stream-chat-react'; -import { EmojiPicker } from 'stream-chat-react/emojis'; +import {EmojiPicker} from 'stream-chat-react/emojis'; import SendIcon from '../../assets/icons/SendIcon'; import StarIcon from '../../assets/icons/StarIcon'; import EmojiIcon from '../../assets/icons/EmojiIcon'; -import { useLayoutController } from '../../context/LayoutController'; - -import type { StreamChatType } from '../../types'; +import {useLayoutController} from '../../context/LayoutController'; const TypingIndicator = () => (
@@ -26,16 +25,29 @@ const TypingIndicator = () => (
); +const SendMessageButton = () => { + const {handleSubmit} = useMessageInputContext(); + const hasSendableData = useMessageComposerHasSendableData(); + return ( + + ); +}; + export const GamingMessageInput = React.memo(() => { const { showUpgradePanel } = useLayoutController(); - const { thread } = useChannelStateContext(); - const { typing } = useTypingContext(); - const messageInput = useMessageInputContext(); + const { thread } = useChannelStateContext(); + const { typing } = useTypingContext(); return (
- + {!thread && ( {

68

{typing && !!Object.keys(typing).length && } - +
); diff --git a/gaming-livestream/src/components/Chat/GamingThreadHeader.tsx b/gaming-livestream/src/components/Chat/GamingThreadHeader.tsx index 92fb63ad..22d4d97c 100644 --- a/gaming-livestream/src/components/Chat/GamingThreadHeader.tsx +++ b/gaming-livestream/src/components/Chat/GamingThreadHeader.tsx @@ -1,9 +1,8 @@ import type { MouseEventHandler } from 'react'; import type { ThreadHeaderProps } from 'stream-chat-react'; -import type { StreamChatType } from '../../types'; -type GamingThreadHeaderProps = Pick, 'closeThread' | 'thread'> +type GamingThreadHeaderProps = Pick export const GamingThreadHeader = ({ closeThread, thread }: GamingThreadHeaderProps) => { const onCloseThread: MouseEventHandler = (event) => { diff --git a/gaming-livestream/src/components/Chat/index.tsx b/gaming-livestream/src/components/Chat/index.tsx index 1639005c..36adcd8e 100644 --- a/gaming-livestream/src/components/Chat/index.tsx +++ b/gaming-livestream/src/components/Chat/index.tsx @@ -13,7 +13,6 @@ import {useChecklist} from '../../hooks/useChecklistTasks'; import {useConnectUser} from '../../hooks/useConnectUser'; import {useLayoutController} from '../../context/LayoutController'; -import type {StreamChatType} from '../../types'; import {ChannelContainer} from "./ChannelContainer"; init({ data }); @@ -32,7 +31,7 @@ const userToConnect = { export const GamingChat = () => { const { memberListVisible, popUpText, upgradePanelVisible, chatVisible } = useLayoutController(); - const chatClient = useConnectUser(apiKey!, userToConnect, userToken); + const chatClient = useConnectUser(apiKey!, userToConnect, userToken); useChecklist({ chatClient, targetOrigin }); if (!chatClient) return null; diff --git a/gaming-livestream/src/hooks/useChecklistTasks.ts b/gaming-livestream/src/hooks/useChecklistTasks.ts index 9dba10e9..1168f88b 100644 --- a/gaming-livestream/src/hooks/useChecklistTasks.ts +++ b/gaming-livestream/src/hooks/useChecklistTasks.ts @@ -2,9 +2,6 @@ import { useEffect } from 'react'; import type { Event, StreamChat } from 'stream-chat'; -import type { StreamChatType } from '../types'; - - const notifyParent = (parent: string) => (message: any) => { window.parent.postMessage(message, parent); }; @@ -22,7 +19,7 @@ const [REACT_TO_MESSAGE, RUN_GIPHY, SEND_YOUTUBE, DRAG_DROP, START_THREAD, SEND_ ]; type ChecklistTaskProps = { - chatClient: StreamChat | null; + chatClient: StreamChat | null; targetOrigin?: string; }; @@ -34,7 +31,7 @@ export const useChecklist = (props: ChecklistTaskProps): void => { const notify = notifyParent(targetOrigin); const handleNewEvent = ( - props: Event, + props: Event, ) => { const { message, type } = props; diff --git a/gaming-livestream/src/hooks/useConnectUser.ts b/gaming-livestream/src/hooks/useConnectUser.ts index 6bed19d7..f998a4ba 100644 --- a/gaming-livestream/src/hooks/useConnectUser.ts +++ b/gaming-livestream/src/hooks/useConnectUser.ts @@ -1,7 +1,5 @@ import { useEffect, useState } from 'react'; import { - DefaultGenerics, - ExtendableGenerics, OwnUserResponse, StreamChat, TokenOrProvider, @@ -16,14 +14,14 @@ import { * @param userToConnect the user information. * @param userTokenOrProvider the user's token. */ -export const useConnectUser = ( +export const useConnectUser = ( apiKey: string, - userToConnect: OwnUserResponse | UserResponse, + userToConnect: OwnUserResponse | UserResponse, userTokenOrProvider: TokenOrProvider, ) => { - const [chatClient, setChatClient] = useState | null>(null); + const [chatClient, setChatClient] = useState(null); useEffect(() => { - const client = new StreamChat(apiKey, { + const client = new StreamChat(apiKey, { enableInsights: true, enableWSFallback: true, }); diff --git a/gaming-livestream/src/types.stream.d.ts b/gaming-livestream/src/types.stream.d.ts new file mode 100644 index 00000000..80d9a4d4 --- /dev/null +++ b/gaming-livestream/src/types.stream.d.ts @@ -0,0 +1,12 @@ +import 'stream-chat'; + +declare module 'stream-chat' { + interface CustomChannelData { + name: string; + } + + interface CustomUserData { + color: string; + userRole: UserRole; + } +} \ No newline at end of file diff --git a/gaming-livestream/src/types.ts b/gaming-livestream/src/types.ts index fa8bca7f..dfd6ca13 100644 --- a/gaming-livestream/src/types.ts +++ b/gaming-livestream/src/types.ts @@ -1,27 +1 @@ -import type { UR, LiteralStringForUnion } from 'stream-chat'; - export type UserRole = 'streamer' | 'moderator' | 'VIP' | 'user' | string; - -export type AttachmentType = UR; -export type ChannelType = UR; -export type CommandType = LiteralStringForUnion; -export type EventType = UR; -export type MemberType = UR; -export type MessageType = UR; -export type ReactionType = UR; -export type UserType = { color: string; userRole: UserRole }; -export type PollOptionType = UR; -export type PollType = UR; - -export type StreamChatType = { - attachmentType: AttachmentType; - channelType: ChannelType; - commandType: CommandType; - eventType: EventType; - memberType: MemberType; - messageType: MessageType; - reactionType: ReactionType; - userType: UserType; - pollOptionType: PollOptionType; - pollType: PollType; -}; diff --git a/gaming-livestream/yarn.lock b/gaming-livestream/yarn.lock index 8bb590e9..40d06007 100644 --- a/gaming-livestream/yarn.lock +++ b/gaming-livestream/yarn.lock @@ -2190,11 +2190,12 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== -"@types/jsonwebtoken@~9.0.0": - version "9.0.5" - resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-9.0.5.tgz#0bd9b841c9e6c5a937c17656e2368f65da025588" - integrity sha512-VRLSGzik+Unrup6BsouBeHsf4d1hOEgYWTm/7Nmw1sXoN1+tRly/Gy/po3yeahnP4jfnQWWAhQAqcNfH7ngOkA== +"@types/jsonwebtoken@^9.0.8": + version "9.0.9" + resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-9.0.9.tgz#a4c3a446c0ebaaf467a58398382616f416345fb3" + integrity sha512-uoe+GxEuHbvy12OUQct2X9JenKM3qAscquYymuQN4fMWG9DBQtykrQEFcAbVACF7qaLw9BePSodUL0kquqBJpQ== dependencies: + "@types/ms" "*" "@types/node" "*" "@types/linkifyjs@^2.1.3": @@ -2366,10 +2367,10 @@ resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.10.tgz#04ffa7f406ab628f7f7e97ca23e290cd8ab15efc" integrity sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA== -"@types/ws@^7.4.0": - version "7.4.7" - resolved "https://registry.yarnpkg.com/@types/ws/-/ws-7.4.7.tgz#f7c390a36f7a0679aa69de2d501319f4f8d9b702" - integrity sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww== +"@types/ws@^8.5.14": + version "8.18.1" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.18.1.tgz#48464e4bf2ddfd17db13d845467f6070ffea4aa9" + integrity sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg== dependencies: "@types/node" "*" @@ -6110,10 +6111,10 @@ isexe@^2.0.0: resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== -isomorphic-ws@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz#55fd4cd6c5e6491e76dc125938dd863f5cd4f2dc" - integrity sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w== +isomorphic-ws@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz#e5529148912ecb9b451b46ed44d53dae1ce04bbf" + integrity sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw== istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.0: version "3.2.2" @@ -6802,7 +6803,7 @@ jsonpointer@^5.0.0: resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-5.0.1.tgz#2110e0af0900fd37467b5907ecd13a7884a1b559" integrity sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ== -jsonwebtoken@~9.0.0: +jsonwebtoken@^9.0.2: version "9.0.2" resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz#65ff91f4abef1784697d40952bb1998c504caaf3" integrity sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ== @@ -6933,6 +6934,11 @@ linkifyjs@^4.1.0: resolved "https://registry.yarnpkg.com/linkifyjs/-/linkifyjs-4.1.2.tgz#48fadb05ddf5a5f7065510a385a500ca1ac4e65e" integrity sha512-1elJrH8MwUgr77Rgmx4JgB/nBgISYVoGossH6pAfCeHG+07TblTn6RWKx0MKozEMJU6NCFYHRih9M8ZtV3YZ+Q== +linkifyjs@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/linkifyjs/-/linkifyjs-4.2.0.tgz#9dd30222b9cbabec9c950e725ec00031c7fa3f08" + integrity sha512-pCj3PrQyATaoTYKHrgWRF3SJwsm61udVh+vuls/Rl6SptiDhgE7ziUIudAedRY9QEfynmM7/RmLEfPUyw1HPCw== + load-script@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/load-script/-/load-script-1.0.0.tgz#0491939e0bee5643ee494a7e3da3d2bac70c6ca4" @@ -9892,10 +9898,10 @@ stop-iteration-iterator@^1.0.0: dependencies: internal-slot "^1.0.4" -stream-chat-react@^12.12.0: - version "12.12.0" - resolved "https://registry.yarnpkg.com/stream-chat-react/-/stream-chat-react-12.12.0.tgz#4c8896ec00628660f4b5f31417e3e96fb8fa4a8d" - integrity sha512-fZ4CXG9HuycODMUnHExQH05mjANCxZkCzo1miF7WLRgHtzj+ghGBu8kRqO4qLcSMi7noSUmuCGFMY2/r7iKccQ== +stream-chat-react@13.0.0-rc.2: + version "13.0.0-rc.2" + resolved "https://registry.yarnpkg.com/stream-chat-react/-/stream-chat-react-13.0.0-rc.2.tgz#4e48ade6b66b816827fdacecf02632b23f7ff254" + integrity sha512-HYEjojijcwvucCfDebX1veWaMWmb5jI0/F7GNmALooy2eBt5ChBV2rM53SqjjN/AbSE4VGgLYWm3jLKH6AIHQQ== dependencies: "@braintree/sanitize-url" "^6.0.4" "@popperjs/core" "^2.11.5" @@ -9932,20 +9938,20 @@ stream-chat-react@^12.12.0: "@stream-io/transliterate" "^1.5.5" mml-react "^0.4.7" -stream-chat@^8.56.0: - version "8.56.0" - resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-8.56.0.tgz#2960282d0fdfedcf067328e36dce87190646dec5" - integrity sha512-wv0OIjNdoMcK25PCjVg9lMfezMCrKzPoG0L9eDeKo/ZbJNGY3hNhUXVsyzIDYLYGWU9aqXGhAKpuxbVfgwbnqg== +stream-chat@9.0.0-rc.15: + version "9.0.0-rc.15" + resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-9.0.0-rc.15.tgz#6a35f27c38be2e021a3e62abcffa40aaef84fc0a" + integrity sha512-n8IftP103jp8IVOUZA4mnOvEIbyzGTVjoL3rDm42apk8DRHz2/vdvgyTYUJysDNYh+Oxu5HwWObToKv1c/Bxdw== dependencies: - "@babel/runtime" "^7.16.3" - "@types/jsonwebtoken" "~9.0.0" - "@types/ws" "^7.4.0" + "@types/jsonwebtoken" "^9.0.8" + "@types/ws" "^8.5.14" axios "^1.6.0" base64-js "^1.5.1" form-data "^4.0.0" - isomorphic-ws "^4.0.1" - jsonwebtoken "~9.0.0" - ws "^7.5.10" + isomorphic-ws "^5.0.0" + jsonwebtoken "^9.0.2" + linkifyjs "^4.2.0" + ws "^8.18.1" string-length@^4.0.1: version "4.0.2" @@ -11356,7 +11362,7 @@ write-file-atomic@^3.0.0: signal-exit "^3.0.2" typedarray-to-buffer "^3.1.5" -ws@^7.4.6, ws@^7.5.10: +ws@^7.4.6: version "7.5.10" resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.10.tgz#58b5c20dc281633f6c19113f39b349bd8bd558d9" integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ== @@ -11366,6 +11372,11 @@ ws@^8.13.0: resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc" integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw== +ws@^8.18.1: + version "8.18.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.1.tgz#ea131d3784e1dfdff91adb0a4a116b127515e3cb" + integrity sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w== + xml-name-validator@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" diff --git a/social-messenger-ts/package.json b/social-messenger-ts/package.json index 142ab27e..53b0b583 100644 --- a/social-messenger-ts/package.json +++ b/social-messenger-ts/package.json @@ -12,8 +12,8 @@ "lodash.debounce": "^4.0.8", "react": "^19.0.0", "react-dom": "^19.0.0", - "stream-chat": "^8.56.0", - "stream-chat-react": "^12.12.0" + "stream-chat": "9.0.0-rc.15", + "stream-chat-react": "13.0.0-rc.2" }, "devDependencies": { "@types/jest": "^27.5.2", diff --git a/social-messenger-ts/src/App.tsx b/social-messenger-ts/src/App.tsx index bfafc632..1c7655ff 100644 --- a/social-messenger-ts/src/App.tsx +++ b/social-messenger-ts/src/App.tsx @@ -1,5 +1,5 @@ -import React, { useState } from 'react'; -import type { ChannelFilters, ChannelOptions, ChannelSort } from 'stream-chat'; +import React, {useEffect, useState} from 'react'; +import type { ChannelFilters, ChannelOptions, ChannelSort, TextComposerMiddleware } from 'stream-chat'; import { Channel, Chat, @@ -9,7 +9,7 @@ import { useCreateChatClient, } from 'stream-chat-react'; import clsx from 'clsx'; -import { EmojiPicker } from 'stream-chat-react/emojis'; +import { createTextComposerEmojiMiddleware, EmojiPicker } from 'stream-chat-react/emojis'; import data from '@emoji-mart/data'; import { init, SearchIndex } from 'emoji-mart'; @@ -25,11 +25,10 @@ import { SendButton, } from './components'; -import { GiphyContextProvider, useThemeContext } from './context'; +import { useThemeContext } from './context'; import { useChecklist, useMobileView, useUpdateAppHeightOnResize } from './hooks'; -import type { StreamChatGenerics } from './types'; init({ data }); @@ -45,6 +44,8 @@ type AppProps = { }; }; +const noop = () => null; + const EmojiPickerWithTheme = () => { const { theme } = useThemeContext(); @@ -55,7 +56,7 @@ const App = (props: AppProps) => { const { apiKey, userToConnect, userToken, targetOrigin, channelListOptions } = props; const [isCreating, setIsCreating] = useState(false); - const chatClient = useCreateChatClient({ + const chatClient = useCreateChatClient({ apiKey, userData: userToConnect, tokenOrProvider: userToken, @@ -66,6 +67,23 @@ const App = (props: AppProps) => { useChecklist(chatClient, targetOrigin); useUpdateAppHeightOnResize(); + useEffect(() => { + if (!chatClient) return; + + chatClient.setMessageComposerSetupFunction(({ composer }) => { + composer.textComposer.middlewareExecutor.insert({ + middleware: [ + createTextComposerEmojiMiddleware(SearchIndex) as TextComposerMiddleware, + ], + position: { before: 'stream-io/text-composer/mentions-middleware' }, + unique: true, + }); + composer.updateConfig({ + linkPreviews: {enabled: true}, + }); + }); + }, [chatClient]); + if (!chatClient) { return null; // render nothing until connection to the backend is established } @@ -82,21 +100,16 @@ const App = (props: AppProps) => { onPreviewSelect={() => setIsCreating(false)} /> null} + TypingIndicator={noop} EmojiPicker={EmojiPickerWithTheme} emojiSearchIndex={SearchIndex} - enrichURLForPreview > {isCreating && ( setIsCreating(false)} /> )} - - - + diff --git a/social-messenger-ts/src/assets/index.ts b/social-messenger-ts/src/assets/index.ts index 451ce1ae..ea53f600 100644 --- a/social-messenger-ts/src/assets/index.ts +++ b/social-messenger-ts/src/assets/index.ts @@ -1,7 +1,5 @@ import type { ChannelMemberResponse } from 'stream-chat'; -import type { StreamChatGenerics } from '../types'; - import avatar1 from './userImages/photo-1438761681033-6461ffad8d80.jpeg'; import avatar2 from './userImages/photo-1463453091185-61582044d556.jpeg'; import avatar3 from './userImages/photo-1503467913725-8484b65b0715.jpeg'; @@ -72,7 +70,7 @@ export const getImage = (userId: string) => { return staticImages[index]; }; -export const getCleanImage = (member: ChannelMemberResponse) => { +export const getCleanImage = (member: ChannelMemberResponse) => { let cleanImage = member.user?.image || ''; const cleanIndex = staticImages.indexOf(cleanImage); if (cleanIndex === -1) { diff --git a/social-messenger-ts/src/components/ChannelInner/ChannelInner.tsx b/social-messenger-ts/src/components/ChannelInner/ChannelInner.tsx index ca450289..7020456a 100644 --- a/social-messenger-ts/src/components/ChannelInner/ChannelInner.tsx +++ b/social-messenger-ts/src/components/ChannelInner/ChannelInner.tsx @@ -1,18 +1,8 @@ import React from 'react'; -import { logChatPromiseExecution } from 'stream-chat'; -import { - MessageInput, - MessageList, - Thread, - useChannelActionContext, - Window, -} from 'stream-chat-react'; +import { MessageInput, MessageList, Thread, Window } from 'stream-chat-react'; import { encodeToMp3 } from 'stream-chat-react/mp3-encoder'; -import { MessagingChannelHeader } from '../../components'; -import { useGiphyContext } from '../../context'; -import type { StreamChatGenerics } from '../../types'; -import { MessageInputProps } from 'stream-chat-react/dist/components/MessageInput/MessageInput'; +import { MessagingChannelHeader} from '../../components'; export type ChannelInnerProps = { toggleMobile: () => void; @@ -21,42 +11,6 @@ export type ChannelInnerProps = { const ChannelInner = (props: ChannelInnerProps) => { const { theme, toggleMobile } = props; - const { giphyState, setGiphyState } = useGiphyContext(); - - const { sendMessage } = useChannelActionContext(); - - const overrideSubmitHandler: MessageInputProps['overrideSubmitHandler'] = ( - message, - _, - ...restSendParams - ) => { - let updatedMessage; - - if (message.attachments?.length && message.text?.startsWith('/giphy')) { - const updatedText = message.text.replace('/giphy', ''); - updatedMessage = { ...message, text: updatedText }; - } - - if (giphyState) { - const updatedText = `/giphy ${message.text}`; - updatedMessage = { ...message, text: updatedText }; - } - - if (sendMessage) { - const newMessage = updatedMessage || message; - const parentMessage = newMessage.parent; - - const messageToSend = { - ...newMessage, - parent: parentMessage, - }; - - const sendMessagePromise = sendMessage(messageToSend, ...restSendParams); - logChatPromiseExecution(sendMessagePromise, 'send message'); - } - - setGiphyState(false); - }; const actions = ['delete', 'edit', 'flag', 'markUnread', 'mute', 'react', 'reply']; @@ -67,7 +21,6 @@ const ChannelInner = (props: ChannelInnerProps) => { ) => ({ + searchQuery: state.searchQuery, + users: state.items, +}) -const UserResult = ({ user }: { user: UserResponse }) => ( +const UserResult = ({ user }: { user: UserResponse }) => (
  • {user.online &&
    } @@ -27,72 +29,23 @@ type Props = { const CreateChannel = (props: Props) => { const { onClose, toggleMobile } = props; - const { client, setActiveChannel } = useChatContext(); + const { client, setActiveChannel } = useChatContext(); const [focusedUser, setFocusedUser] = useState(); - const [inputText, setInputText] = useState(''); - const [resultsOpen, setResultsOpen] = useState(false); - const [searchEmpty, setSearchEmpty] = useState(false); - const [searching, setSearching] = useState(false); - const [selectedUsers, setSelectedUsers] = useState[]>([]); - const [users, setUsers] = useState[]>([]); + const [selectedUsers, setSelectedUsers] = useState([]); const inputRef = useRef(null); - const clearState = () => { - setInputText(''); - setResultsOpen(false); - setSearchEmpty(false); - }; - - useEffect(() => { - const clickListener = () => { - if (resultsOpen) clearState(); - }; - - document.addEventListener('click', clickListener); - - return () => document.removeEventListener('click', clickListener); - }, [resultsOpen]); - - const findUsers = async () => { - if (searching) return; - setSearching(true); - - try { - const response = await client.queryUsers( - { - id: { $ne: client.userID as string }, - $and: [{ name: { $autocomplete: inputText } }], - }, - { id: 1 }, - { limit: 6 }, - ); - - if (!response.users.length) { - setSearchEmpty(true); - } else { - setSearchEmpty(false); - setUsers(response.users); - } - - setResultsOpen(true); - } catch (error) { - console.log({ error }); - } - - setSearching(false); - }; + const searchSource = useMemo( + () => { + const source = new UserSearchSource(client, {debounceMs: 100, pageSize: 6}); + source.activate(); + return source; + }, + [client], + ); - const findUsersDebounce = _debounce(findUsers, 100, { - trailing: true, - }); - - useEffect(() => { - if (inputText) { - findUsersDebounce(); - } - }, [inputText]); // eslint-disable-line react-hooks/exhaustive-deps + const {searchQuery, users} = useStateStore(searchSource.state, searchSourceStateSelector) const createChannel = async () => { const selectedUsersIds = selectedUsers.map((u) => u.id); @@ -107,32 +60,34 @@ const CreateChannel = (props: Props) => { setActiveChannel?.(conversation); setSelectedUsers([]); - setUsers([]); + searchSource.resetStateAndActivate(); onClose(); }; - const addUser = (addedUser: UserResponse) => { - const isAlreadyAdded = selectedUsers.find((user) => user.id === addedUser.id); - if (isAlreadyAdded) return; + const addUser = useCallback((addedUser: UserResponse) => { + setSelectedUsers((selected) => { + const alreadyPresent = selected.find((user) => user.id === addedUser.id); + if (alreadyPresent) return selected; + inputRef.current?.focus(); + searchSource.resetStateAndActivate(); + return [...selected, addedUser] + }); + }, [searchSource]); - setSelectedUsers([...selectedUsers, addedUser]); - setResultsOpen(false); - setInputText(''); - if (inputRef.current) { - inputRef.current.focus(); - } - }; + const removeUser = useCallback((user: UserResponse) => { + setSelectedUsers((selected) => { + inputRef.current?.focus(); + return selected.filter((item) => item.id !== user.id); - const removeUser = (user: UserResponse) => { - const newUsers = selectedUsers.filter((item) => item.id !== user.id); - setSelectedUsers(newUsers); - if (inputRef.current) { - inputRef.current.focus(); - } - }; + }); + }, []); - const handleKeyDown = useCallback( - (event: KeyboardEvent) => { + // todo: where to activate searchSource? + + + useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + const users: UserResponse[] = searchSource.items ?? [] // check for up(ArrowUp) or down(ArrowDown) key if (event.key === 'ArrowUp') { setFocusedUser((prevFocused) => { @@ -148,19 +103,15 @@ const CreateChannel = (props: Props) => { } if (event.key === 'Enter') { event.preventDefault(); - if (focusedUser !== undefined) { - addUser(users[focusedUser]); - return setFocusedUser(undefined); - } + setFocusedUser((focuseUser) => { + if (focuseUser) addUser(users[focuseUser]); + return undefined + }); } - }, - [users, focusedUser], // eslint-disable-line - ); - - useEffect(() => { + } document.addEventListener('keydown', handleKeyDown, false); return () => document.removeEventListener('keydown', handleKeyDown); - }, [handleKeyDown]); + }, [addUser, searchSource]); return (
    @@ -186,8 +137,8 @@ const CreateChannel = (props: Props) => { setInputText(e.target.value)} + value={searchQuery} + onChange={(e) => searchSource.search(e.target.value)} placeholder={!selectedUsers.length ? 'Start typing for suggestions' : ''} type='text' className='messaging-create-channel__input' @@ -202,10 +153,10 @@ const CreateChannel = (props: Props) => { Start chat - {inputText && ( + {searchQuery && (
      - {!!users?.length && !searchEmpty && ( + {!!users?.length ? (
      {users.map((user, i) => (
      {
      ))}
      - )} - {searchEmpty && ( + ) : (
      { inputRef.current?.focus(); - clearState(); + searchSource.resetStateAndActivate(); }} className='messaging-create-channel__user-result empty' > diff --git a/social-messenger-ts/src/components/MessagingChannelHeader/MessagingChannelHeader.tsx b/social-messenger-ts/src/components/MessagingChannelHeader/MessagingChannelHeader.tsx index 5e6c7381..8b010f0f 100644 --- a/social-messenger-ts/src/components/MessagingChannelHeader/MessagingChannelHeader.tsx +++ b/social-messenger-ts/src/components/MessagingChannelHeader/MessagingChannelHeader.tsx @@ -6,8 +6,6 @@ import { TypingIndicator } from '../TypingIndicator/TypingIndicator'; import { ChannelInfoIcon, ChannelSaveIcon, HamburgerIcon } from '../../assets'; import { AvatarGroup } from '../'; -import type { StreamChatGenerics } from '../../types'; - type Props = { theme: string; toggleMobile: () => void; @@ -15,8 +13,8 @@ type Props = { const MessagingChannelHeader = (props: Props) => { const { theme, toggleMobile } = props; - const { client } = useChatContext(); - const { channel } = useChannelStateContext(); + const { client } = useChatContext(); + const { channel } = useChannelStateContext(); const [channelName, setChannelName] = useState(channel.data?.name || ''); const [isEditing, setIsEditing] = useState(false); const [title, setTitle] = useState(''); diff --git a/social-messenger-ts/src/components/MessagingChannelListHeader/MessagingChannelListHeader.tsx b/social-messenger-ts/src/components/MessagingChannelListHeader/MessagingChannelListHeader.tsx index 24c87ee2..dca1e066 100644 --- a/social-messenger-ts/src/components/MessagingChannelListHeader/MessagingChannelListHeader.tsx +++ b/social-messenger-ts/src/components/MessagingChannelListHeader/MessagingChannelListHeader.tsx @@ -4,8 +4,6 @@ import { Avatar, useChatContext } from 'stream-chat-react'; import { CreateChannelIcon } from '../../assets'; import streamLogo from '../../assets/ProfilePic_LogoMark_GrdntOnWt.png'; -import type { StreamChatGenerics } from '../../types'; - type Props = { onCreateChannel?: () => void; }; @@ -13,7 +11,7 @@ type Props = { const MessagingChannelListHeader = React.memo((props: Props) => { const { onCreateChannel } = props; - const { client } = useChatContext(); + const { client } = useChatContext(); const { id, image = streamLogo as string, name = 'Example User' } = client.user || {}; diff --git a/social-messenger-ts/src/components/MessagingChannelPreview/MessagingChannelPreview.tsx b/social-messenger-ts/src/components/MessagingChannelPreview/MessagingChannelPreview.tsx index 3bf51747..f415564c 100644 --- a/social-messenger-ts/src/components/MessagingChannelPreview/MessagingChannelPreview.tsx +++ b/social-messenger-ts/src/components/MessagingChannelPreview/MessagingChannelPreview.tsx @@ -8,7 +8,6 @@ import { AvatarGroup } from '../'; import type { MouseEventHandler} from 'react'; import type { Channel, ChannelMemberResponse } from 'stream-chat'; -import type { StreamChatGenerics } from '../../types'; const getTimeStamp = (channel: Channel) => { let lastHours = channel.state.last_message_at?.getHours(); @@ -52,7 +51,7 @@ type MessagingChannelPreviewProps = ChannelPreviewUIComponentProps & { const MessagingChannelPreview = (props: MessagingChannelPreviewProps) => { const { channel, setActiveChannel, onClick, latestMessage, } = props; - const { channel: activeChannel, client } = useChatContext(); + const { channel: activeChannel, client } = useChatContext(); const members = Object.values(channel.state.members).filter( ({ user }) => user?.id !== client.userID, diff --git a/social-messenger-ts/src/components/TypingIndicator/TypingIndicator.tsx b/social-messenger-ts/src/components/TypingIndicator/TypingIndicator.tsx index 12536c0e..9b2b93a1 100644 --- a/social-messenger-ts/src/components/TypingIndicator/TypingIndicator.tsx +++ b/social-messenger-ts/src/components/TypingIndicator/TypingIndicator.tsx @@ -2,12 +2,10 @@ import { useChatContext, useTypingContext } from 'stream-chat-react'; import './TypingIndicator.css'; -import type { StreamChatGenerics } from '../../types'; - export const TypingIndicator = () => { - const { client } = useChatContext(); + const { client } = useChatContext(); - const {typing} = useTypingContext(); + const {typing} = useTypingContext(); if (!client || !typing || !Object.values(typing).length ) return null; const users = Object.values(typing) diff --git a/social-messenger-ts/src/context/Giphy.tsx b/social-messenger-ts/src/context/Giphy.tsx deleted file mode 100644 index 3629e671..00000000 --- a/social-messenger-ts/src/context/Giphy.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import React, { Dispatch, PropsWithChildren, SetStateAction, useContext, useState } from 'react'; - -export type GiphyContextValue = { - giphyState: boolean; - setGiphyState: Dispatch>; -}; - -const GiphyContext = React.createContext(null); - -/** - * A convenience wrapper around React's default Context.Provider. - * - * @param props the props. - * @constructor - */ -export const GiphyContextProvider = (props: PropsWithChildren) => { - const [giphyState, setGiphyState] = useState(false); - return ( - - {props.children} - - ); -}; - -/** - * A convenience hook for consuming GiphyContext value. - */ -export const useGiphyContext = () => { - return useContext(GiphyContext) as GiphyContextValue; -}; diff --git a/social-messenger-ts/src/context/index.ts b/social-messenger-ts/src/context/index.ts index 9a599729..33c95c52 100644 --- a/social-messenger-ts/src/context/index.ts +++ b/social-messenger-ts/src/context/index.ts @@ -1,2 +1 @@ -export * from './Giphy'; export * from './Theme'; \ No newline at end of file diff --git a/social-messenger-ts/src/hooks/useChecklist.ts b/social-messenger-ts/src/hooks/useChecklist.ts index d27b63ca..a201d05d 100644 --- a/social-messenger-ts/src/hooks/useChecklist.ts +++ b/social-messenger-ts/src/hooks/useChecklist.ts @@ -1,6 +1,5 @@ import { useEffect } from 'react'; import type { Event, StreamChat } from 'stream-chat'; -import type { StreamChatGenerics } from '../types'; const notifyParent = (parent: string) => (message: any) => { window.parent.postMessage(message, parent); @@ -23,7 +22,7 @@ export const useChecklist = (chatClient: StreamChat | null, targetOrigin: string useEffect(() => { const notify = notifyParent(targetOrigin); - const handleNewEvent = (props: Event) => { + const handleNewEvent = (props: Event) => { const { message, type } = props; switch (type) { diff --git a/social-messenger-ts/src/index.tsx b/social-messenger-ts/src/index.tsx index bd02ea57..630a3723 100644 --- a/social-messenger-ts/src/index.tsx +++ b/social-messenger-ts/src/index.tsx @@ -8,7 +8,6 @@ import { getImage } from './assets'; import { getChannelListOptions } from './channelListOptions'; import { ThemeContextProvider } from './context'; import { UserResponse } from 'stream-chat'; -import { StreamChatGenerics } from './types'; if (process.env.NODE_ENV === 'production') { Sentry.init({ @@ -29,7 +28,7 @@ const noChannelNameFilter = urlParams.get('no_channel_name_filter') || true; const skipNameImageSet = urlParams.get('skip_name_image_set') || false; const channelListOptions = getChannelListOptions(!!noChannelNameFilter, user); -const userToConnect: UserResponse = { +const userToConnect: UserResponse = { id: user!, name: skipNameImageSet ? undefined : user!, image: skipNameImageSet ? undefined : getImage(user!), diff --git a/social-messenger-ts/src/types.stream.d.ts b/social-messenger-ts/src/types.stream.d.ts new file mode 100644 index 00000000..55089a0f --- /dev/null +++ b/social-messenger-ts/src/types.stream.d.ts @@ -0,0 +1,8 @@ +import 'stream-chat'; + +declare module 'stream-chat' { + interface CustomChannelData { + name?: string; + demo?: string; + } +} \ No newline at end of file diff --git a/social-messenger-ts/src/types.ts b/social-messenger-ts/src/types.ts deleted file mode 100644 index 0e542cab..00000000 --- a/social-messenger-ts/src/types.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { LiteralStringForUnion, UR } from 'stream-chat'; - -export type AttachmentType = {}; -export type ChannelType = { demo?: string }; -export type CommandType = LiteralStringForUnion; -export type EventType = {}; -export type MemberType = UR; -export type MessageType = {}; -export type ReactionType = {}; -export type UserType = { image?: string }; - -export type StreamChatGenerics = { - attachmentType: AttachmentType; - channelType: ChannelType; - commandType: CommandType; - eventType: EventType; - memberType: MemberType; - messageType: MessageType; - reactionType: ReactionType; - userType: UserType; - pollOptionType: Record; - pollType: Record; -}; diff --git a/social-messenger-ts/yarn.lock b/social-messenger-ts/yarn.lock index ee9cdc10..b65b82f4 100644 --- a/social-messenger-ts/yarn.lock +++ b/social-messenger-ts/yarn.lock @@ -2167,11 +2167,12 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== -"@types/jsonwebtoken@~9.0.0": - version "9.0.5" - resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-9.0.5.tgz#0bd9b841c9e6c5a937c17656e2368f65da025588" - integrity sha512-VRLSGzik+Unrup6BsouBeHsf4d1hOEgYWTm/7Nmw1sXoN1+tRly/Gy/po3yeahnP4jfnQWWAhQAqcNfH7ngOkA== +"@types/jsonwebtoken@^9.0.8": + version "9.0.9" + resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-9.0.9.tgz#a4c3a446c0ebaaf467a58398382616f416345fb3" + integrity sha512-uoe+GxEuHbvy12OUQct2X9JenKM3qAscquYymuQN4fMWG9DBQtykrQEFcAbVACF7qaLw9BePSodUL0kquqBJpQ== dependencies: + "@types/ms" "*" "@types/node" "*" "@types/linkifyjs@^2.1.3": @@ -2360,10 +2361,10 @@ resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.10.tgz#04ffa7f406ab628f7f7e97ca23e290cd8ab15efc" integrity sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA== -"@types/ws@^7.4.0": - version "7.4.7" - resolved "https://registry.yarnpkg.com/@types/ws/-/ws-7.4.7.tgz#f7c390a36f7a0679aa69de2d501319f4f8d9b702" - integrity sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww== +"@types/ws@^8.5.14": + version "8.18.1" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.18.1.tgz#48464e4bf2ddfd17db13d845467f6070ffea4aa9" + integrity sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg== dependencies: "@types/node" "*" @@ -5932,10 +5933,10 @@ isexe@^2.0.0: resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== -isomorphic-ws@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz#55fd4cd6c5e6491e76dc125938dd863f5cd4f2dc" - integrity sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w== +isomorphic-ws@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz#e5529148912ecb9b451b46ed44d53dae1ce04bbf" + integrity sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw== istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.0: version "3.2.2" @@ -6615,7 +6616,7 @@ jsonpointer@^5.0.0: resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-5.0.1.tgz#2110e0af0900fd37467b5907ecd13a7884a1b559" integrity sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ== -jsonwebtoken@~9.0.0: +jsonwebtoken@^9.0.2: version "9.0.2" resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz#65ff91f4abef1784697d40952bb1998c504caaf3" integrity sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ== @@ -6741,6 +6742,11 @@ linkifyjs@^4.1.0: resolved "https://registry.yarnpkg.com/linkifyjs/-/linkifyjs-4.1.2.tgz#48fadb05ddf5a5f7065510a385a500ca1ac4e65e" integrity sha512-1elJrH8MwUgr77Rgmx4JgB/nBgISYVoGossH6pAfCeHG+07TblTn6RWKx0MKozEMJU6NCFYHRih9M8ZtV3YZ+Q== +linkifyjs@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/linkifyjs/-/linkifyjs-4.2.0.tgz#9dd30222b9cbabec9c950e725ec00031c7fa3f08" + integrity sha512-pCj3PrQyATaoTYKHrgWRF3SJwsm61udVh+vuls/Rl6SptiDhgE7ziUIudAedRY9QEfynmM7/RmLEfPUyw1HPCw== + load-script@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/load-script/-/load-script-1.0.0.tgz#0491939e0bee5643ee494a7e3da3d2bac70c6ca4" @@ -9621,10 +9627,10 @@ statuses@2.0.1: resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== -stream-chat-react@^12.12.0: - version "12.12.0" - resolved "https://registry.yarnpkg.com/stream-chat-react/-/stream-chat-react-12.12.0.tgz#4c8896ec00628660f4b5f31417e3e96fb8fa4a8d" - integrity sha512-fZ4CXG9HuycODMUnHExQH05mjANCxZkCzo1miF7WLRgHtzj+ghGBu8kRqO4qLcSMi7noSUmuCGFMY2/r7iKccQ== +stream-chat-react@13.0.0-rc.2: + version "13.0.0-rc.2" + resolved "https://registry.yarnpkg.com/stream-chat-react/-/stream-chat-react-13.0.0-rc.2.tgz#4e48ade6b66b816827fdacecf02632b23f7ff254" + integrity sha512-HYEjojijcwvucCfDebX1veWaMWmb5jI0/F7GNmALooy2eBt5ChBV2rM53SqjjN/AbSE4VGgLYWm3jLKH6AIHQQ== dependencies: "@braintree/sanitize-url" "^6.0.4" "@popperjs/core" "^2.11.5" @@ -9661,20 +9667,20 @@ stream-chat-react@^12.12.0: "@stream-io/transliterate" "^1.5.5" mml-react "^0.4.7" -stream-chat@^8.56.0: - version "8.56.0" - resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-8.56.0.tgz#2960282d0fdfedcf067328e36dce87190646dec5" - integrity sha512-wv0OIjNdoMcK25PCjVg9lMfezMCrKzPoG0L9eDeKo/ZbJNGY3hNhUXVsyzIDYLYGWU9aqXGhAKpuxbVfgwbnqg== +stream-chat@9.0.0-rc.15: + version "9.0.0-rc.15" + resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-9.0.0-rc.15.tgz#6a35f27c38be2e021a3e62abcffa40aaef84fc0a" + integrity sha512-n8IftP103jp8IVOUZA4mnOvEIbyzGTVjoL3rDm42apk8DRHz2/vdvgyTYUJysDNYh+Oxu5HwWObToKv1c/Bxdw== dependencies: - "@babel/runtime" "^7.16.3" - "@types/jsonwebtoken" "~9.0.0" - "@types/ws" "^7.4.0" + "@types/jsonwebtoken" "^9.0.8" + "@types/ws" "^8.5.14" axios "^1.6.0" base64-js "^1.5.1" form-data "^4.0.0" - isomorphic-ws "^4.0.1" - jsonwebtoken "~9.0.0" - ws "^7.5.10" + isomorphic-ws "^5.0.0" + jsonwebtoken "^9.0.2" + linkifyjs "^4.2.0" + ws "^8.18.1" string-length@^4.0.1: version "4.0.2" @@ -11020,7 +11026,7 @@ write-file-atomic@^3.0.0: signal-exit "^3.0.2" typedarray-to-buffer "^3.1.5" -ws@^7.4.6, ws@^7.5.10: +ws@^7.4.6: version "7.5.10" resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.10.tgz#58b5c20dc281633f6c19113f39b349bd8bd558d9" integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ== @@ -11030,6 +11036,11 @@ ws@^8.13.0: resolved "https://registry.yarnpkg.com/ws/-/ws-8.14.2.tgz#6c249a806eb2db7a20d26d51e7709eab7b2e6c7f" integrity sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g== +ws@^8.18.1: + version "8.18.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.1.tgz#ea131d3784e1dfdff91adb0a4a116b127515e3cb" + integrity sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w== + xml-name-validator@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" diff --git a/team-ts/package.json b/team-ts/package.json index 82c0c7de..d8789ac9 100644 --- a/team-ts/package.json +++ b/team-ts/package.json @@ -19,8 +19,8 @@ "react-dom": "^19.0.0", "react-dropzone": "^14.2.3", "react-popper": "^2.3.0", - "stream-chat": "^8.56.0", - "stream-chat-react": "^12.12.0" + "stream-chat": "9.0.0-rc.15", + "stream-chat-react": "13.0.0-rc.2" }, "devDependencies": { "@types/emoji-mart": "^3.0.9", diff --git a/team-ts/src/App.tsx b/team-ts/src/App.tsx index a47479f2..6506f45d 100644 --- a/team-ts/src/App.tsx +++ b/team-ts/src/App.tsx @@ -9,7 +9,12 @@ import {Sidebar} from './components/Sidebar/Sidebar'; import {WorkspaceController} from './context/WorkspaceController'; -import type {StreamChatType} from './types'; +import { + createDraftGiphyCommandInjectionMiddleware, + createGiphyCommandInjectionMiddleware +} from "./middleware/composition/giphyCommandInjectionMiddleware"; +import {createGiphyCommandControlMiddleware} from "./middleware/textComposition/giphyCommandControl"; +import {createFormattingMarkdownInjectionMiddleware} from "./middleware/textComposition/formattingMarkdownInjection"; const urlParams = new URLSearchParams(window.location.search); @@ -19,12 +24,43 @@ const theme = urlParams.get('theme') || 'light'; const userToken = urlParams.get('user_token') || process.env.REACT_APP_USER_TOKEN; const targetOrigin = urlParams.get('target_origin') || process.env.REACT_APP_TARGET_ORIGIN; -const client = StreamChat.getInstance(apiKey!, { enableInsights: true, enableWSFallback: true }); +const client = StreamChat.getInstance(apiKey!, { enableInsights: true, enableWSFallback: true }); client.connectUser({ id: user!, name: user, image: getRandomImage() }, userToken); const App = () => { useChecklist({ chatClient: client, targetOrigin: targetOrigin! }); + useEffect(() => { + if (!client) return; + + client.setMessageComposerSetupFunction(({ composer }) => { + composer.compositionMiddlewareExecutor.insert({ + middleware: [ + createGiphyCommandInjectionMiddleware(composer) + ], + position: {after: 'stream-io/message-composer-middleware/attachments'} + }); + composer.draftCompositionMiddlewareExecutor.insert({ + middleware: [ + createDraftGiphyCommandInjectionMiddleware(composer) + ], + position: {after: 'stream-io/message-composer-middleware/draft-attachments'} + }); + composer.textComposer.middlewareExecutor.insert({ + middleware: [ + createGiphyCommandControlMiddleware(composer), + ], + position: {before: 'stream-io/text-composer/pre-validation-middleware'} + }) + composer.textComposer.middlewareExecutor.insert({ + middleware: [ + createFormattingMarkdownInjectionMiddleware(composer), + ], + position: {after: 'stream-io/text-composer/pre-validation-middleware'} + }) + }); + }, []); + useEffect(() => { const handleColorChange = (color: string) => { const root = document.documentElement; diff --git a/team-ts/src/ChecklistTasks.ts b/team-ts/src/ChecklistTasks.ts index 5fadc4a7..9121171c 100644 --- a/team-ts/src/ChecklistTasks.ts +++ b/team-ts/src/ChecklistTasks.ts @@ -2,8 +2,6 @@ import { useEffect } from 'react'; import type { Event, StreamChat } from 'stream-chat'; -import type { StreamChatType } from './types'; - const notifyParent = (parent: string) => (message: any) => { window.parent.postMessage(message, parent); }; @@ -22,7 +20,7 @@ const [REACT_TO_MESSAGE, RUN_GIPHY, SEND_YOUTUBE, DRAG_DROP, START_THREAD, SEND_ ]; type ChecklistTaskProps = { - chatClient: StreamChat; + chatClient: StreamChat; targetOrigin: string; }; @@ -33,7 +31,7 @@ export const useChecklist = (props: ChecklistTaskProps): void => { const notify = notifyParent(targetOrigin); const handleNewEvent = ( - props: Event, + props: Event, ) => { const { message, type } = props; diff --git a/team-ts/src/components/AdminPanel/AdminPanel.tsx b/team-ts/src/components/AdminPanel/AdminPanel.tsx index d62e97a6..7a89c436 100644 --- a/team-ts/src/components/AdminPanel/AdminPanel.tsx +++ b/team-ts/src/components/AdminPanel/AdminPanel.tsx @@ -4,10 +4,9 @@ import { AdminPanelForm, FormValues } from './context/AdminPanelFormContext'; import { CreateChannel } from './CreateChannel'; import { EditChannel } from './EditChannel'; import { useChatContext } from 'stream-chat-react'; -import { StreamChatType } from '../../types'; export const AdminPanel = () => { - const { client, channel } = useChatContext(); + const { client, channel } = useChatContext(); const { displayWorkspace, activeWorkspace } = useWorkspaceController(); const onSubmit = useCallback(() => displayWorkspace('Chat'), [displayWorkspace]); diff --git a/team-ts/src/components/AdminPanel/UserList.tsx b/team-ts/src/components/AdminPanel/UserList.tsx index 3b07817f..a7d4242e 100644 --- a/team-ts/src/components/AdminPanel/UserList.tsx +++ b/team-ts/src/components/AdminPanel/UserList.tsx @@ -3,7 +3,6 @@ import { Avatar, useChatContext } from 'stream-chat-react'; import type { UserResponse } from 'stream-chat'; -import type { StreamChatType } from '../../types'; import { useAdminPanelFormState } from './context/AdminPanelFormContext'; import { ValidationError } from './ValidationError'; @@ -30,7 +29,7 @@ const ListContainer = (props: { children: React.ReactNode }) => { type UserItemProps = { index: number; - user: UserResponse; + user: UserResponse; }; const MOCKED_LAST_ACTIVE_STRINGS = [ @@ -73,10 +72,10 @@ const LOAD_STATE_NOTIFICATION: Record = { export const UserList = () => { - const { client, channel } = useChatContext(); + const { client, channel } = useChatContext(); const { createChannelType } = useAdminPanelFormState(); const [loadState, setLoadState] = useState(null); - const [users, setUsers] = useState[] | undefined>(); + const [users, setUsers] = useState(); const channelMembers = useMemo(() => channel?.state.members ? Object.keys(channel.state.members) @@ -91,6 +90,8 @@ export const UserList = () => { try { const response = await client.queryUsers( + // FIXME: How to query non-member users? + // @ts-ignore { id: { $nin: channelMembers } }, { id: 1 }, { limit: 8 }, diff --git a/team-ts/src/components/AdminPanel/context/AdminPanelFormContext.tsx b/team-ts/src/components/AdminPanel/context/AdminPanelFormContext.tsx index fe90472d..ef399065 100644 --- a/team-ts/src/components/AdminPanel/context/AdminPanelFormContext.tsx +++ b/team-ts/src/components/AdminPanel/context/AdminPanelFormContext.tsx @@ -10,7 +10,6 @@ import { } from 'react'; import { Workspace } from '../../../context/WorkspaceController'; import { useChatContext } from 'stream-chat-react'; -import { StreamChatType } from '../../../types'; type UpsertChannelParams = { name: string, members: string[] }; @@ -63,7 +62,7 @@ const getUpsertAction = (workspace: Workspace): UpsertAction | undefined => { }; export const AdminPanelForm = ({ children, defaultValues, workspace, onSubmit }: PropsWithChildren) => { - const { client, channel, setActiveChannel } = useChatContext(); + const { client, channel, setActiveChannel } = useChatContext(); const [name, setChannelName] = useState(defaultValues.name); const [members, setMembers] = useState(defaultValues.members); const [errors, setErrors] = useState({ name: null, members: null }); diff --git a/team-ts/src/components/ChannelContainer/ChannelContainer.tsx b/team-ts/src/components/ChannelContainer/ChannelContainer.tsx index d68a6b5e..5a1e5c47 100644 --- a/team-ts/src/components/ChannelContainer/ChannelContainer.tsx +++ b/team-ts/src/components/ChannelContainer/ChannelContainer.tsx @@ -8,7 +8,6 @@ import {TeamTypingIndicator} from '../TeamTypingIndicator/TeamTypingIndicator'; import { ThreadHeader } from '../TeamChannelHeader/ThreadHeader'; import { TeamMessage } from '../TeamMessage/TeamMessage'; -import { GiphyInMessageFlagProvider } from '../../context/GiphyInMessageFlagContext'; import { useWorkspaceController } from '../../context/WorkspaceController'; import data from '@emoji-mart/data'; @@ -37,9 +36,7 @@ export const ChannelContainer = () => { TypingIndicator={TeamTypingIndicator} emojiSearchIndex={SearchIndex} > - -
      ); diff --git a/team-ts/src/components/ChannelContainer/ChannelInner.tsx b/team-ts/src/components/ChannelContainer/ChannelInner.tsx index 6681c7f7..cb0c1f75 100644 --- a/team-ts/src/components/ChannelContainer/ChannelInner.tsx +++ b/team-ts/src/components/ChannelContainer/ChannelInner.tsx @@ -1,28 +1,18 @@ -import React, { useCallback } from 'react'; -import { logChatPromiseExecution, MessageResponse } from 'stream-chat'; +import React from 'react'; import { defaultPinPermissions, MessageInput, MessageList, - MessageToSend, PinEnabledUserRoles, Thread, - useChannelActionContext, Window, } from 'stream-chat-react'; -import { PinnedMessageList } from '../PinnedMessageList/PinnedMessageList'; -import { TeamChannelHeader } from '../TeamChannelHeader/TeamChannelHeader'; -import { ThreadMessageInput } from '../TeamMessageInput/ThreadMessageInput'; - -import { useGiphyInMessageContext } from '../../context/GiphyInMessageFlagContext'; - -import type { StreamChatType } from '../../types'; +import {PinnedMessageList} from '../PinnedMessageList/PinnedMessageList'; +import {TeamChannelHeader} from '../TeamChannelHeader/TeamChannelHeader'; +import {ThreadMessageInput} from "../TeamMessageInput/TeamMessageInput"; export const ChannelInner = () => { - const {inputHasGiphyMessage, clearGiphyFlag} = useGiphyInMessageContext(); - const { sendMessage } = useChannelActionContext(); - // todo: migrate to channel capabilities once migration guide is available const teamPermissions: PinEnabledUserRoles = { ...defaultPinPermissions.team, user: true }; const messagingPermissions: PinEnabledUserRoles = { @@ -36,37 +26,14 @@ export const ChannelInner = () => { messaging: messagingPermissions, }; - const overrideSubmitHandler = useCallback((message: MessageToSend) => { - let updatedMessage = { - attachments: message.attachments, - mentioned_users: message.mentioned_users, - parent_id: message.parent?.id, - parent: message.parent as MessageResponse, - text: message.text, - }; - - const isReply = !!updatedMessage.parent_id; - - if (inputHasGiphyMessage(isReply)) { - const updatedText = `/giphy ${message.text}`; - updatedMessage = { ...updatedMessage, text: updatedText }; - } - - if (sendMessage) { - const sendMessagePromise = sendMessage(updatedMessage); - logChatPromiseExecution(sendMessagePromise, 'send message'); - clearGiphyFlag(isReply); - } - }, [inputHasGiphyMessage, sendMessage, clearGiphyFlag]); - return ( <> - + - + ); diff --git a/team-ts/src/components/ChannelPreview/ChannelPreview.tsx b/team-ts/src/components/ChannelPreview/ChannelPreview.tsx index ed58ff18..4f4287b3 100644 --- a/team-ts/src/components/ChannelPreview/ChannelPreview.tsx +++ b/team-ts/src/components/ChannelPreview/ChannelPreview.tsx @@ -7,14 +7,12 @@ import { TeamChannelPreview } from './TeamChannelPreview'; import { useWorkspaceController } from '../../context/WorkspaceController'; -import type { StreamChatType } from '../../types'; - -type TeamChannelPreviewProps = ChannelPreviewUIComponentProps & { +type TeamChannelPreviewProps = ChannelPreviewUIComponentProps & { type: string; }; export const ChannelPreview = ({ channel, type }: TeamChannelPreviewProps) => { - const { channel: activeChannel, setActiveChannel } = useChatContext(); + const { channel: activeChannel, setActiveChannel } = useChatContext(); const { displayWorkspace } = useWorkspaceController(); const handleClick = useCallback(() => { diff --git a/team-ts/src/components/ChannelPreview/DirectMessagingChannelPreview.tsx b/team-ts/src/components/ChannelPreview/DirectMessagingChannelPreview.tsx index 042933a7..10d3a938 100644 --- a/team-ts/src/components/ChannelPreview/DirectMessagingChannelPreview.tsx +++ b/team-ts/src/components/ChannelPreview/DirectMessagingChannelPreview.tsx @@ -1,11 +1,9 @@ import { Avatar, ChannelPreviewUIComponentProps, useChatContext } from 'stream-chat-react'; -import { StreamChatType } from '../../types'; - -type DirectMessagingChannelPreviewProps = Pick, 'channel'>; +type DirectMessagingChannelPreviewProps = Pick; export const DirectMessagingChannelPreview = ({channel}: DirectMessagingChannelPreviewProps) => { - const { client } = useChatContext(); + const { client } = useChatContext(); const members = Object.values(channel.state.members).filter( ({ user }) => user?.id !== client.userID, diff --git a/team-ts/src/components/ChannelSearch/ChannelSearch.tsx b/team-ts/src/components/ChannelSearch/ChannelSearch.tsx index 7e2f0052..84c6c3da 100644 --- a/team-ts/src/components/ChannelSearch/ChannelSearch.tsx +++ b/team-ts/src/components/ChannelSearch/ChannelSearch.tsx @@ -9,17 +9,15 @@ import { ResultsDropdown } from './ResultsDropdown'; import { SearchIcon } from '../../assets'; -import type { StreamChatType } from '../../types'; - export const ChannelSearch = () => { - const { client, setActiveChannel } = useChatContext(); + const { client, setActiveChannel } = useChatContext(); const [allChannels, setAllChannels] = useState | undefined>(); const [teamChannels, setTeamChannels] = useState< - | Channel[] + | Channel[] | undefined >(); - const [directChannels, setDirectChannels] = useState[] | undefined>(); + const [directChannels, setDirectChannels] = useState(); const [focused, setFocused] = useState(); const [focusedId, setFocusedId] = useState(''); @@ -80,7 +78,7 @@ export const ChannelSearch = () => { }, [allChannels, focused]); const setChannel = ( - channel: Channel, + channel: Channel, ) => { setQuery(''); setActiveChannel(channel); @@ -98,21 +96,16 @@ export const ChannelSearch = () => { ); const userResponse = client.queryUsers( - { - id: { $ne: client.userID || '' }, - $and: [ - { name: { $autocomplete: text } }, - ], - }, + { name: { $autocomplete: text } }, { id: 1 }, { limit: 5 }, ); const [channels, { users }] = await Promise.all([channelResponse, userResponse]); - + const otherUsers = users.filter((user) => user.id !== client.userID); if (channels.length) setTeamChannels(channels); - if (users.length) setDirectChannels(users); - setAllChannels([...channels, ...users]); + if (otherUsers.length) setDirectChannels(otherUsers); + setAllChannels([...channels, ...otherUsers]); } catch (event) { setQuery(''); } diff --git a/team-ts/src/components/ChannelSearch/ResultsDropdown.tsx b/team-ts/src/components/ChannelSearch/ResultsDropdown.tsx index 2c63de11..91e51996 100644 --- a/team-ts/src/components/ChannelSearch/ResultsDropdown.tsx +++ b/team-ts/src/components/ChannelSearch/ResultsDropdown.tsx @@ -5,8 +5,6 @@ import { channelByUser, ChannelOrUserType, isChannel } from './utils'; import type { Channel, UserResponse } from 'stream-chat'; -import type { StreamChatType } from '../../types'; - type SearchResultProps = Pick & { result: ChannelOrUserType; }; @@ -14,10 +12,10 @@ type SearchResultProps = Pick const SearchResult = (props: SearchResultProps) => { const { focusedId, result, setChannel } = props; - const { client, setActiveChannel } = useChatContext(); + const { client, setActiveChannel } = useChatContext(); if (isChannel(result)) { - const channel = result as Channel; + const channel = result as Channel; return (
      {
      ); } else { - const user = result as UserResponse; + const user = result as UserResponse; return (
      { }; type ResultsDropdownProps = { - teamChannels?: Channel[]; - directChannels?: UserResponse[]; + teamChannels?: Channel[]; + directChannels?: UserResponse[]; focusedId: string; loading: boolean; setChannel: ( - channel: Channel, + channel: Channel, ) => void; setQuery: React.Dispatch>; }; @@ -94,7 +92,7 @@ export const ResultsDropdown = (props: ResultsDropdownProps) => { No direct messages found

      ) : ( - directChannels?.map((user: UserResponse, i) => ( + directChannels?.map((user: UserResponse, i) => ( )) )} diff --git a/team-ts/src/components/ChannelSearch/utils.tsx b/team-ts/src/components/ChannelSearch/utils.tsx index 58964ff7..6386d011 100644 --- a/team-ts/src/components/ChannelSearch/utils.tsx +++ b/team-ts/src/components/ChannelSearch/utils.tsx @@ -1,27 +1,27 @@ import type { Channel, ChannelFilters, StreamChat, UserResponse } from 'stream-chat'; -import type { StreamChatType } from '../../types'; + export type ChannelOrUserType = - | Channel - | UserResponse; + | Channel + | UserResponse; export const isChannel = ( channel: ChannelOrUserType, -): channel is Channel => - (channel as Channel).cid !== undefined; +): channel is Channel => + (channel as Channel).cid !== undefined; type Props = { - client: StreamChat; + client: StreamChat; setActiveChannel: ( - newChannel?: Channel, + newChannel?: Channel, watchers?: { limit?: number; offset?: number; }, event?: React.SyntheticEvent, ) => void; - user: UserResponse; + user: UserResponse; }; export const channelByUser = async (props: Props) => { diff --git a/team-ts/src/components/EmptyChannel/EmptyChannel.tsx b/team-ts/src/components/EmptyChannel/EmptyChannel.tsx index 78aab3e6..6cae97b6 100644 --- a/team-ts/src/components/EmptyChannel/EmptyChannel.tsx +++ b/team-ts/src/components/EmptyChannel/EmptyChannel.tsx @@ -2,10 +2,8 @@ import { Avatar, useChatContext } from 'stream-chat-react'; import { HashIcon } from './HashIcon'; -import type { StreamChatType } from '../../types'; - export const EmptyChannel = () => { - const { channel, client } = useChatContext(); + const { channel, client } = useChatContext(); const members = Object.values(channel?.state?.members || {}).filter( ({ user }) => user?.id !== client.userID, diff --git a/team-ts/src/components/PinnedMessageList/PinnedMessageList.tsx b/team-ts/src/components/PinnedMessageList/PinnedMessageList.tsx index 7dfeea95..ed55ec2f 100644 --- a/team-ts/src/components/PinnedMessageList/PinnedMessageList.tsx +++ b/team-ts/src/components/PinnedMessageList/PinnedMessageList.tsx @@ -1,14 +1,14 @@ import {DialogManagerProvider, Message, useChannelStateContext } from 'stream-chat-react'; import { CloseThreadButton } from '../TeamChannelHeader/CloseThreadButton'; -import type { StreamChatType } from '../../types'; + import { TeamMessage } from '../TeamMessage/TeamMessage'; import { useWorkspaceController } from '../../context/WorkspaceController'; export const PinnedMessageList = () => { const { pinnedMessageListOpen, togglePinnedMessageListOpen } = useWorkspaceController(); - const { channel } = useChannelStateContext(); + const { channel } = useChannelStateContext(); if (!pinnedMessageListOpen) return null; diff --git a/team-ts/src/components/Sidebar/Sidebar.tsx b/team-ts/src/components/Sidebar/Sidebar.tsx index 200ea78e..5258d087 100644 --- a/team-ts/src/components/Sidebar/Sidebar.tsx +++ b/team-ts/src/components/Sidebar/Sidebar.tsx @@ -13,14 +13,14 @@ import { CompanyLogo } from './icons'; import type { Channel, ChannelFilters } from 'stream-chat'; import { ChannelSort } from 'stream-chat'; -import { StreamChatType } from '../../types'; + const filters: ChannelFilters[] = [ { type: 'team', demo: 'team' }, { type: 'messaging', demo: 'team' }, ]; const options = { state: true, watch: true, presence: true, limit: 3 }; -const sort: ChannelSort = { last_message_at: -1, updated_at: -1 }; +const sort: ChannelSort = { last_message_at: -1, updated_at: -1 }; const FakeCompanySelectionBar = () => (
      diff --git a/team-ts/src/components/TeamChannelHeader/TeamChannelHeader.tsx b/team-ts/src/components/TeamChannelHeader/TeamChannelHeader.tsx index 96fa38fa..87d32d17 100644 --- a/team-ts/src/components/TeamChannelHeader/TeamChannelHeader.tsx +++ b/team-ts/src/components/TeamChannelHeader/TeamChannelHeader.tsx @@ -6,13 +6,13 @@ import { PinIcon } from '../../assets'; import { ChannelInfoIcon } from './ChannelInfoIcon'; import { useWorkspaceController } from '../../context/WorkspaceController'; -import type { StreamChatType } from '../../types'; + export const TeamChannelHeader = () => { const { displayWorkspace } = useWorkspaceController(); - const { client } = useChatContext(); - const { channel, watcher_count } = useChannelStateContext(); - const { closeThread } = useChannelActionContext(); + const { client } = useChatContext(); + const { channel, watcher_count } = useChannelStateContext(); + const { closeThread } = useChannelActionContext(); const { togglePinnedMessageListOpen } = useWorkspaceController(); const teamHeader = `# ${channel?.data?.name || channel?.data?.id || 'random'}`; diff --git a/team-ts/src/components/TeamMessage/PinIndicator.tsx b/team-ts/src/components/TeamMessage/PinIndicator.tsx index e3c3a52b..f8683f14 100644 --- a/team-ts/src/components/TeamMessage/PinIndicator.tsx +++ b/team-ts/src/components/TeamMessage/PinIndicator.tsx @@ -1,11 +1,10 @@ -import { StreamMessage } from 'stream-chat-react'; - import { PinIcon } from '../../assets'; +import type {LocalMessage} from "stream-chat"; + -import { StreamChatType } from '../../types'; export type PinIndicatorProps = { - message?: StreamMessage; + message?: LocalMessage; }; export const PinIndicator = ({ message }: PinIndicatorProps) => { diff --git a/team-ts/src/components/TeamMessage/TeamMessage.tsx b/team-ts/src/components/TeamMessage/TeamMessage.tsx index 32191b11..4ce3a56c 100644 --- a/team-ts/src/components/TeamMessage/TeamMessage.tsx +++ b/team-ts/src/components/TeamMessage/TeamMessage.tsx @@ -27,7 +27,6 @@ import {PinIndicator} from './PinIndicator'; import {useWorkspaceController} from '../../context/WorkspaceController'; -import type {StreamChatType} from '../../types'; import {ErrorIcon} from "./icons"; export const TeamMessage = () => { @@ -47,8 +46,8 @@ export const TeamMessage = () => { onUserHover, renderText = defaultRenderText, threadList, - } = useMessageContext('MessageTeam'); - const { Attachment } = useComponentContext('MessageTeam'); + } = useMessageContext('MessageTeam'); + const { Attachment } = useComponentContext('MessageTeam'); const { t, userLanguage } = useTranslationContext('MessageTeam'); @@ -106,7 +105,6 @@ export const TeamMessage = () => {
      ); @@ -233,10 +231,10 @@ export const TeamMessage = () => { diff --git a/team-ts/src/components/TeamMessageInput/EmojiPicker.tsx b/team-ts/src/components/TeamMessageInput/EmojiPicker.tsx index c2d611cc..69f9d0b6 100644 --- a/team-ts/src/components/TeamMessageInput/EmojiPicker.tsx +++ b/team-ts/src/components/TeamMessageInput/EmojiPicker.tsx @@ -3,7 +3,7 @@ import { usePopper } from 'react-popper'; import { useMessageInputContext } from 'stream-chat-react'; import Picker from '@emoji-mart/react'; -import { StreamChatType } from '../../types'; + import { MessageInputControlButton } from './MessageInputControls'; // similar to EmojiPicker from "stream-chat-react/emojis" @@ -15,7 +15,7 @@ export const EmojiPicker = () => { placement: 'bottom-start', }); const [emojiPickerIsOpen, setEmojiPickerIsOpen] = useState(false); - const { insertText, textareaRef } = useMessageInputContext(); + const { insertText, textareaRef } = useMessageInputContext(); useEffect(() => { if (!popperElement || !referenceElement) return; diff --git a/team-ts/src/components/TeamMessageInput/MessageInputControls.tsx b/team-ts/src/components/TeamMessageInput/MessageInputControls.tsx index 06e5976c..08b25106 100644 --- a/team-ts/src/components/TeamMessageInput/MessageInputControls.tsx +++ b/team-ts/src/components/TeamMessageInput/MessageInputControls.tsx @@ -1,6 +1,8 @@ import React, { forwardRef, MouseEventHandler } from 'react'; import clsx from 'clsx'; -import { MessageInputControlType } from './hooks/useMessageInputCompositionControls'; + + +import {MessageInputControlType} from "../../types.stream"; type MessageInputControlButtonProps = { onClick: MouseEventHandler; @@ -85,7 +87,7 @@ const ControlsIcons: Record = { code: CodeSnippetIcon, emoji: EmojiIcon, italics: ItalicsIcon, - 'strike-through': StrikeThroughIcon, + 'strikethrough': StrikeThroughIcon, }; diff --git a/team-ts/src/components/TeamMessageInput/TeamMessageInput.tsx b/team-ts/src/components/TeamMessageInput/TeamMessageInput.tsx index 42b064f2..892cec6a 100644 --- a/team-ts/src/components/TeamMessageInput/TeamMessageInput.tsx +++ b/team-ts/src/components/TeamMessageInput/TeamMessageInput.tsx @@ -1,40 +1,52 @@ import clsx from 'clsx'; -import { useMemo } from 'react'; +import {useMemo} from 'react'; import { AttachmentPreviewList, - ChatAutoComplete, SendButton, - useChannelStateContext, + TextareaComposer, + useAttachmentManagerState, useComponentContext, + useMessageComposer, + useMessageComposerHasSendableData, useMessageInputContext, + useStateStore, } from 'stream-chat-react'; -import { useDropzone } from 'react-dropzone'; +import {useDropzone} from 'react-dropzone'; -import { GiphyBadge } from './GiphyBadge'; -import { MessageInputControlButton } from './MessageInputControls'; -import { EmojiPicker } from './EmojiPicker'; +import {GiphyBadge} from './GiphyBadge'; +import {MessageInputControlButton} from './MessageInputControls'; +import {EmojiPicker} from './EmojiPicker'; +import {useMessageInputCompositionControls} from './hooks/useMessageInputCompositionControls'; +import type {CustomDataManagerState, MessageComposerConfig} from "stream-chat"; +import {SendButtonIcon} from "./SendButtonIcon"; -import { useGiphyInMessageContext } from '../../context/GiphyInMessageFlagContext'; -import { useMessageInputCompositionControls } from './hooks/useMessageInputCompositionControls'; +const attachmentManagerConfigStateSelector = (state: MessageComposerConfig) => ({ + acceptedFiles: state.attachments.acceptedFiles, + multipleUploads: state.attachments.maxNumberOfFilesPerMessage > 1, +}); -import type { StreamChatType } from '../../types'; +const customComposerDataSelector = (state: CustomDataManagerState) => ({ + activeFormatting: state.custom.activeFormatting, + isComposingGiphyText: state.custom.command === 'giphy', +}); export const TeamMessageInput = () => { const { TypingIndicator } = useComponentContext(); - const { acceptedFiles = [], multipleUploads } = useChannelStateContext(); - const { handleSubmit, numberOfUploads, text, uploadNewFiles, maxFilesLeft, isUploadEnabled } = - useMessageInputContext(); - const { isComposingGiphyMessage } = useGiphyInMessageContext(); + const { handleSubmit } = useMessageInputContext(); const { - formatting, - handleBoldButtonClick, - handleCodeButtonClick, - handleItalicsButtonClick, - handleStrikeThroughButtonClick, - onChange, + formatter, placeholder, } = useMessageInputCompositionControls(); + const messageComposer = useMessageComposer(); + const { acceptedFiles, multipleUploads } = useStateStore( + messageComposer.configState, + attachmentManagerConfigStateSelector, + ); + const { activeFormatting, isComposingGiphyText } = useStateStore(messageComposer.customDataManager.state, customComposerDataSelector) + const {isUploadEnabled } = useAttachmentManagerState(); + const hasSendableData = useMessageComposerHasSendableData(); + const accept = useMemo( () => @@ -47,12 +59,11 @@ export const TeamMessageInput = () => { const { getRootProps, isDragActive, isDragReject } = useDropzone({ accept, - disabled: !isUploadEnabled || maxFilesLeft === 0, + disabled: !isUploadEnabled, multiple: multipleUploads, noClick: true, - onDrop: uploadNewFiles, + onDrop: messageComposer.attachmentManager.uploadFiles, }); - return (
      {isDragActive && ( @@ -67,35 +78,35 @@ export const TeamMessageInput = () => { )}
      - {!!numberOfUploads && } +
      - {isComposingGiphyMessage() && !numberOfUploads && } - + {isComposingGiphyText && } + - +
      @@ -103,3 +114,27 @@ export const TeamMessageInput = () => {
      ); }; + +export const ThreadMessageInput = () => { + const { handleSubmit } = useMessageInputContext(); + const messageComposer = useMessageComposer(); + const hasSendableData = useMessageComposerHasSendableData(); + const { isComposingGiphyText } = useStateStore(messageComposer.customDataManager.state, customComposerDataSelector) + return ( +
      +
      + {isComposingGiphyText && } + + + + +
      +
      + ); +}; \ No newline at end of file diff --git a/team-ts/src/components/TeamMessageInput/ThreadMessageInput.tsx b/team-ts/src/components/TeamMessageInput/ThreadMessageInput.tsx deleted file mode 100644 index 28a159a8..00000000 --- a/team-ts/src/components/TeamMessageInput/ThreadMessageInput.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { useCallback } from 'react'; -import { ChatAutoComplete, useMessageInputContext } from 'stream-chat-react'; - -import { GiphyBadge } from './GiphyBadge'; -import { SendButtonIcon } from './SendButtonIcon'; -import { EmojiPicker } from './EmojiPicker'; - -import { useGiphyInMessageContext } from '../../context/GiphyInMessageFlagContext'; - -import type { StreamChatType } from '../../types'; - -export const ThreadMessageInput = () => { - const { isComposingGiphyReply, clearGiphyFlagThread, setComposeGiphyReplyFlag } = - useGiphyInMessageContext(); - - const messageInput = useMessageInputContext(); - - const onChange: React.ChangeEventHandler = useCallback( - (event) => { - const deletePressed = - event.nativeEvent instanceof InputEvent && - event.nativeEvent.inputType === 'deleteContentBackward'; - - if (messageInput.text.length === 1 && deletePressed) { - clearGiphyFlagThread(); - } - - if (messageInput.text.startsWith('/giphy') && !isComposingGiphyReply()) { - console.log('replacing'); - event.target.value = event.target.value.replace('/giphy', ''); - setComposeGiphyReplyFlag(); - } - - messageInput.handleChange(event); - }, - [clearGiphyFlagThread, messageInput, setComposeGiphyReplyFlag, isComposingGiphyReply], - ); - - return ( -
      -
      - {isComposingGiphyReply() && } - - - - -
      -
      - ); -}; diff --git a/team-ts/src/components/TeamMessageInput/hooks/useMessageInputCompositionControls.ts b/team-ts/src/components/TeamMessageInput/hooks/useMessageInputCompositionControls.ts index 5b28700c..7c39cc4b 100644 --- a/team-ts/src/components/TeamMessageInput/hooks/useMessageInputCompositionControls.ts +++ b/team-ts/src/components/TeamMessageInput/hooks/useMessageInputCompositionControls.ts @@ -1,21 +1,28 @@ -import { useCallback, useMemo, useState } from 'react'; -import { useChannelStateContext, useChatContext, useMessageInputContext } from 'stream-chat-react'; - -import { useGiphyInMessageContext } from '../../../context/GiphyInMessageFlagContext'; - -import { StreamChatType } from '../../../types'; - -export type MessageInputControlType = 'emoji' | 'bold' | 'italics' | 'code' | 'strike-through'; +import {useCallback, useMemo} from 'react'; +import {useChannelStateContext, useChatContext, useMessageComposer, useMessageInputContext} from 'stream-chat-react'; +import type {MessageInputFormattingType} from "../../../types.stream"; + +const mdToFormattingType: Record = { + '**': 'bold', + '*': 'italics', + '~~': 'strikethrough', + '``': 'code', +} + +export const formattingTypeToMarkdown: Record = { + bold: '**', + code: '`', + italics: '*', + strikethrough: '~~', +} export const useMessageInputCompositionControls = () => { - const { client } = useChatContext(); + const { client } = useChatContext(); const { channel, - } = useChannelStateContext(); - const messageInput = useMessageInputContext(); - const { isComposingGiphyMessage, clearGiphyFlagMainInput, setComposeGiphyMessageFlag } = useGiphyInMessageContext(); - const [formatting, setFormatting] = useState(null); - + } = useChannelStateContext(); + const {customDataManager, textComposer} = useMessageComposer(); + const {textareaRef} = useMessageInputContext(); const placeholder = useMemo(() => { let dynamicPart = 'the group'; @@ -36,83 +43,44 @@ export const useMessageInputCompositionControls = () => { }, [channel.type, channel.state.members, channel?.data?.id, channel?.data?.name, client.userID]); - const onChange: React.ChangeEventHandler = useCallback( - (event) => { - const { value } = event.target; - - const deletePressed = - event.nativeEvent instanceof InputEvent && - event.nativeEvent.inputType === 'deleteContentBackward'; - - if (messageInput.text.length === 1 && deletePressed) { - clearGiphyFlagMainInput(); - } - - if (!isComposingGiphyMessage() && messageInput.text.startsWith('/giphy') && !messageInput.numberOfUploads) { - event.target.value = value.replace('/giphy', ''); - setComposeGiphyMessageFlag(); - } - - if (formatting === 'bold') { - if (deletePressed) { - event.target.value = `${value.slice(0, value.length - 2)}**`; - } else { - event.target.value = `**${value.replace(/\**/g, '')}**`; - } - } else if (formatting === 'code') { - if (deletePressed) { - event.target.value = `${value.slice(0, value.length - 1)}\``; - } else { - event.target.value = `\`${value.replace(/`/g, '')}\``; - } - } else if (formatting === 'italics') { - if (deletePressed) { - event.target.value = `${value.slice(0, value.length - 1)}*`; - } else { - event.target.value = `*${value.replace(/\*/g, '')}*`; - } - } else if (formatting === 'strike-through') { - if (deletePressed) { - event.target.value = `${value.slice(0, value.length - 2)}~~`; - } else { - event.target.value = `~~${value.replace(/~~/g, '')}~~`; - } - } - - messageInput.handleChange(event); - }, - [ - formatting, - messageInput, - clearGiphyFlagMainInput, - isComposingGiphyMessage, - setComposeGiphyMessageFlag, - ], - ); - - const handleBoldButtonClick = useCallback(() => { - setFormatting((prev) => prev === 'bold' ? null : 'bold'); - }, []); - - const handleItalicsButtonClick = useCallback(() => { - setFormatting((prev) => prev === 'italics' ? null : 'italics') - }, []); - - const handleStrikeThroughButtonClick = useCallback(() => { - setFormatting((prev) => prev === 'strike-through' ? null : 'strike-through') - }, []); + const handleFormattingButtonClick = useCallback((wrappingMarkdown: string) => { + const textarea = textareaRef.current; + if (!textarea) return; + + const {activeFormatting} = customDataManager.customComposerData; + if (!activeFormatting) { + textComposer.wrapSelection({head: wrappingMarkdown, tail: wrappingMarkdown}); + customDataManager.setCustomData({activeFormatting: mdToFormattingType[wrappingMarkdown]}); + textarea.selectionStart = textComposer.selection.start; + textarea.selectionEnd = textComposer.selection.end; + textarea.focus(); + return; + } + const activeMarkdown = formattingTypeToMarkdown[activeFormatting]; + const newSelection = { + start: textComposer.selection.start + activeMarkdown.length, + end: textComposer.selection.end + + activeMarkdown.length, + }; + textarea.selectionStart = newSelection.start; + textarea.selectionEnd = newSelection.end; + if (wrappingMarkdown === activeMarkdown) { + customDataManager.setCustomData({activeFormatting: null}); + } else { + customDataManager.setCustomData({activeFormatting: mdToFormattingType[wrappingMarkdown]}); + textComposer.wrapSelection({head: wrappingMarkdown, selection: newSelection, tail: wrappingMarkdown}); + } + textarea.focus(); + }, [customDataManager, textareaRef, textComposer]) - const handleCodeButtonClick = useCallback(() => { - setFormatting((prev) => prev === 'code' ? null : 'code') - }, []); + const formatter = useMemo void>>(() => ({ + bold: () => handleFormattingButtonClick('**'), + italics: () => handleFormattingButtonClick('*'), + 'strikethrough': () => handleFormattingButtonClick('~~'), + code: () => handleFormattingButtonClick('`'), + }), [handleFormattingButtonClick]); return { - formatting, - handleBoldButtonClick, - handleCodeButtonClick, - handleItalicsButtonClick, - handleStrikeThroughButtonClick, + formatter, placeholder, - onChange, } } \ No newline at end of file diff --git a/team-ts/src/components/TeamTypingIndicator/TeamTypingIndicator.tsx b/team-ts/src/components/TeamTypingIndicator/TeamTypingIndicator.tsx index f513029a..3321e135 100644 --- a/team-ts/src/components/TeamTypingIndicator/TeamTypingIndicator.tsx +++ b/team-ts/src/components/TeamTypingIndicator/TeamTypingIndicator.tsx @@ -1,11 +1,11 @@ import { useChatContext, useTypingContext } from 'stream-chat-react'; -import type { StreamChatType } from '../../types'; + export const TeamTypingIndicator = () => { - const { client } = useChatContext(); + const { client } = useChatContext(); - const { typing } = useTypingContext(); + const { typing } = useTypingContext(); if (!client || !typing) return null; diff --git a/team-ts/src/middleware/composition/giphyCommandInjectionMiddleware.ts b/team-ts/src/middleware/composition/giphyCommandInjectionMiddleware.ts new file mode 100644 index 00000000..3317436e --- /dev/null +++ b/team-ts/src/middleware/composition/giphyCommandInjectionMiddleware.ts @@ -0,0 +1,57 @@ +import { + MessageComposer, + MessageComposerMiddlewareValueState, + type MessageDraftComposerMiddlewareValueState, + MiddlewareHandlerParams +} from "stream-chat"; + +export const createGiphyCommandInjectionMiddleware = (composer: MessageComposer) => ({ + id: 'demo-team/message-composer-middleware/giphy-command-injection', + compose: ({ + input, + nextHandler, + }: MiddlewareHandlerParams) => { + const {custom: {command}} = composer.customDataManager.state.getLatestValue(); + const {attachments, text} = input.state.localMessage; + const injection = command && `/${command}`; + if (command !== 'giphy' || !injection || text?.startsWith(injection) || attachments?.length) return nextHandler(input); + const enrichedText = `${injection} ${text}` + return nextHandler({ + ...input, + state: { + ...input.state, + localMessage: { + ...input.state.localMessage, + text: enrichedText, + }, + message: { + ...input.state.message, + text: enrichedText, + }, + }, + }); + } +}); +export const createDraftGiphyCommandInjectionMiddleware = (composer: MessageComposer) => ({ + id: 'demo-team/message-composer-middleware/draft-giphy-command-injection', + compose: ({ + input, + nextHandler, + }: MiddlewareHandlerParams) => { + const {custom: {command}} = composer.customDataManager.state.getLatestValue(); + const text = input.state.draft.text; + const injection = command && `/${command}`; + if (command !== 'giphy' || !injection || text?.startsWith(injection)) return nextHandler(input); + const enrichedText = `${injection} ${text}` + return nextHandler({ + ...input, + state: { + ...input.state, + draft: { + ...input.state.draft, + text: enrichedText, + }, + }, + }); + } +}); \ No newline at end of file diff --git a/team-ts/src/middleware/textComposition/formattingMarkdownInjection.ts b/team-ts/src/middleware/textComposition/formattingMarkdownInjection.ts new file mode 100644 index 00000000..225536f2 --- /dev/null +++ b/team-ts/src/middleware/textComposition/formattingMarkdownInjection.ts @@ -0,0 +1,23 @@ +import {MessageComposer, TextComposerMiddlewareParams, UserSuggestion} from "stream-chat"; +import {formattingTypeToMarkdown} from "../../components/TeamMessageInput/hooks/useMessageInputCompositionControls"; + + +export const createFormattingMarkdownInjectionMiddleware = (composer: MessageComposer) => { + return ({ + id: 'demo-team/text-composer/formatting-markdown-injection', + onChange: ({input, nextHandler}: TextComposerMiddlewareParams) => { + const formattingMarkdown = composer.customDataManager.customComposerData.activeFormatting; + if (!formattingMarkdown) return nextHandler(input); + const markdown = formattingTypeToMarkdown[formattingMarkdown]; + if (input.state.text.length || !markdown) return nextHandler(input); + return nextHandler({ + ...input, + state: { + ...input.state, + text: `${markdown}${input.state.text}${markdown}`, + selection: {start: markdown.length, end: markdown.length + input.state.text.length}, + } + }); + }, + }); +}; \ No newline at end of file diff --git a/team-ts/src/middleware/textComposition/giphyCommandControl.ts b/team-ts/src/middleware/textComposition/giphyCommandControl.ts new file mode 100644 index 00000000..7f8de0eb --- /dev/null +++ b/team-ts/src/middleware/textComposition/giphyCommandControl.ts @@ -0,0 +1,55 @@ +import {CommandSuggestion, MessageComposer, TextComposerMiddlewareParams, UserSuggestion} from "stream-chat"; + +export const createGiphyCommandControlMiddleware = (composer: MessageComposer) => { + const trigger = '/giphy '; + return ({ + id: 'demo-team/text-composer/giphy-command-control', + onChange: ({input, nextHandler}: TextComposerMiddlewareParams) => { + const {attachmentManager} = composer; + const hasNonUploadAttachments = attachmentManager.attachments.filter((att) => !att.localMetadata.uploadState).length > 0; + const hasUploadAttachments = attachmentManager.uploadsInProgressCount + attachmentManager.successfulUploadsCount > 0; + if (hasUploadAttachments || hasNonUploadAttachments) { + return nextHandler(input); + } + + const {customDataManager} = composer; + if (!input.state.text && customDataManager.customComposerData.command) { + customDataManager.setCustomData({command: null}) + return nextHandler(input); + } + if (!input.state.text.startsWith(trigger) || customDataManager.customComposerData.command) { + return nextHandler(input); + } + + customDataManager.setCustomData({command: 'giphy'}); + const newText = input.state.text.slice(trigger.length); + return nextHandler({ + ...input, + state: { + ...input.state, + text: newText, + selection: {start: input.state.selection.start - trigger.length, end: input.state.selection.end - trigger.length}, + } + }); + }, + onSuggestionItemSelect: ({ + input, + nextHandler, + selectedSuggestion, + }: TextComposerMiddlewareParams) => { + if (selectedSuggestion?.name !== 'giphy') + return nextHandler(input); + + composer.customDataManager.setCustomData({command: 'giphy'}); + const newText = input.state.text.slice(trigger.length); + return nextHandler({ + ...input, + state: { + ...input.state, + text: newText, + selection: {start: input.state.selection.start - trigger.length, end: input.state.selection.end - trigger.length}, + } + }); + } + }); +}; \ No newline at end of file diff --git a/team-ts/src/types.stream.d.ts b/team-ts/src/types.stream.d.ts new file mode 100644 index 00000000..a1f15945 --- /dev/null +++ b/team-ts/src/types.stream.d.ts @@ -0,0 +1,17 @@ +import 'stream-chat'; + + +export type MessageInputFormattingType = 'bold' | 'italics' | 'code' | 'strikethrough'; +export type MessageInputControlType = 'emoji' | MessageInputFormattingType; + +declare module 'stream-chat' { + interface CustomChannelData { + name?: string; + demo?: string; + } + + interface CustomMessageComposerData { + command: 'giphy' | null; + activeFormatting: MessageInputFormattingType | null; + } +} \ No newline at end of file diff --git a/team-ts/src/types.ts b/team-ts/src/types.ts deleted file mode 100644 index 88953d4a..00000000 --- a/team-ts/src/types.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type { UR, LiteralStringForUnion } from 'stream-chat'; - -export type TeamAttachmentType = UR; -export type TeamChannelType = UR; -export type TeamCommandType = LiteralStringForUnion; -export type TeamEventType = UR; -export type MemberType = UR; -export type TeamMessageType = UR; -export type TeamReactionType = UR; -export type TeamUserType = { image?: string }; -export type TeamPollOptionType = UR; -export type TeamPollType = UR; - -export type StreamChatType = { - attachmentType: TeamAttachmentType; - channelType: TeamChannelType; - commandType: TeamCommandType; - eventType: TeamEventType; - memberType: MemberType; - messageType: TeamMessageType; - reactionType: TeamReactionType; - userType: TeamUserType; - pollOptionType: TeamPollOptionType; - pollType: TeamPollType; -}; diff --git a/team-ts/yarn.lock b/team-ts/yarn.lock index 06e7b79c..dd28493d 100644 --- a/team-ts/yarn.lock +++ b/team-ts/yarn.lock @@ -2216,11 +2216,12 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== -"@types/jsonwebtoken@~9.0.0": - version "9.0.5" - resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-9.0.5.tgz#0bd9b841c9e6c5a937c17656e2368f65da025588" - integrity sha512-VRLSGzik+Unrup6BsouBeHsf4d1hOEgYWTm/7Nmw1sXoN1+tRly/Gy/po3yeahnP4jfnQWWAhQAqcNfH7ngOkA== +"@types/jsonwebtoken@^9.0.8": + version "9.0.9" + resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-9.0.9.tgz#a4c3a446c0ebaaf467a58398382616f416345fb3" + integrity sha512-uoe+GxEuHbvy12OUQct2X9JenKM3qAscquYymuQN4fMWG9DBQtykrQEFcAbVACF7qaLw9BePSodUL0kquqBJpQ== dependencies: + "@types/ms" "*" "@types/node" "*" "@types/linkifyjs@^2.1.3": @@ -2404,10 +2405,10 @@ resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.10.tgz#04ffa7f406ab628f7f7e97ca23e290cd8ab15efc" integrity sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA== -"@types/ws@^7.4.0": - version "7.4.7" - resolved "https://registry.yarnpkg.com/@types/ws/-/ws-7.4.7.tgz#f7c390a36f7a0679aa69de2d501319f4f8d9b702" - integrity sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww== +"@types/ws@^8.5.14": + version "8.18.1" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.18.1.tgz#48464e4bf2ddfd17db13d845467f6070ffea4aa9" + integrity sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg== dependencies: "@types/node" "*" @@ -6165,10 +6166,10 @@ isexe@^2.0.0: resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== -isomorphic-ws@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz#55fd4cd6c5e6491e76dc125938dd863f5cd4f2dc" - integrity sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w== +isomorphic-ws@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz#e5529148912ecb9b451b46ed44d53dae1ce04bbf" + integrity sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw== istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.0: version "3.2.2" @@ -6872,7 +6873,7 @@ jsonpointer@^5.0.0: resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-5.0.1.tgz#2110e0af0900fd37467b5907ecd13a7884a1b559" integrity sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ== -jsonwebtoken@~9.0.0: +jsonwebtoken@^9.0.2: version "9.0.2" resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz#65ff91f4abef1784697d40952bb1998c504caaf3" integrity sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ== @@ -7003,6 +7004,11 @@ linkifyjs@^4.1.0: resolved "https://registry.yarnpkg.com/linkifyjs/-/linkifyjs-4.1.2.tgz#48fadb05ddf5a5f7065510a385a500ca1ac4e65e" integrity sha512-1elJrH8MwUgr77Rgmx4JgB/nBgISYVoGossH6pAfCeHG+07TblTn6RWKx0MKozEMJU6NCFYHRih9M8ZtV3YZ+Q== +linkifyjs@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/linkifyjs/-/linkifyjs-4.2.0.tgz#9dd30222b9cbabec9c950e725ec00031c7fa3f08" + integrity sha512-pCj3PrQyATaoTYKHrgWRF3SJwsm61udVh+vuls/Rl6SptiDhgE7ziUIudAedRY9QEfynmM7/RmLEfPUyw1HPCw== + load-script@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/load-script/-/load-script-1.0.0.tgz#0491939e0bee5643ee494a7e3da3d2bac70c6ca4" @@ -9972,10 +9978,10 @@ stop-iteration-iterator@^1.0.0: dependencies: internal-slot "^1.0.4" -stream-chat-react@^12.12.0: - version "12.12.0" - resolved "https://registry.yarnpkg.com/stream-chat-react/-/stream-chat-react-12.12.0.tgz#4c8896ec00628660f4b5f31417e3e96fb8fa4a8d" - integrity sha512-fZ4CXG9HuycODMUnHExQH05mjANCxZkCzo1miF7WLRgHtzj+ghGBu8kRqO4qLcSMi7noSUmuCGFMY2/r7iKccQ== +stream-chat-react@13.0.0-rc.2: + version "13.0.0-rc.2" + resolved "https://registry.yarnpkg.com/stream-chat-react/-/stream-chat-react-13.0.0-rc.2.tgz#4e48ade6b66b816827fdacecf02632b23f7ff254" + integrity sha512-HYEjojijcwvucCfDebX1veWaMWmb5jI0/F7GNmALooy2eBt5ChBV2rM53SqjjN/AbSE4VGgLYWm3jLKH6AIHQQ== dependencies: "@braintree/sanitize-url" "^6.0.4" "@popperjs/core" "^2.11.5" @@ -10012,20 +10018,20 @@ stream-chat-react@^12.12.0: "@stream-io/transliterate" "^1.5.5" mml-react "^0.4.7" -stream-chat@^8.56.0: - version "8.56.0" - resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-8.56.0.tgz#2960282d0fdfedcf067328e36dce87190646dec5" - integrity sha512-wv0OIjNdoMcK25PCjVg9lMfezMCrKzPoG0L9eDeKo/ZbJNGY3hNhUXVsyzIDYLYGWU9aqXGhAKpuxbVfgwbnqg== +stream-chat@9.0.0-rc.15: + version "9.0.0-rc.15" + resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-9.0.0-rc.15.tgz#6a35f27c38be2e021a3e62abcffa40aaef84fc0a" + integrity sha512-n8IftP103jp8IVOUZA4mnOvEIbyzGTVjoL3rDm42apk8DRHz2/vdvgyTYUJysDNYh+Oxu5HwWObToKv1c/Bxdw== dependencies: - "@babel/runtime" "^7.16.3" - "@types/jsonwebtoken" "~9.0.0" - "@types/ws" "^7.4.0" + "@types/jsonwebtoken" "^9.0.8" + "@types/ws" "^8.5.14" axios "^1.6.0" base64-js "^1.5.1" form-data "^4.0.0" - isomorphic-ws "^4.0.1" - jsonwebtoken "~9.0.0" - ws "^7.5.10" + isomorphic-ws "^5.0.0" + jsonwebtoken "^9.0.2" + linkifyjs "^4.2.0" + ws "^8.18.1" string-length@^4.0.1: version "4.0.2" @@ -11431,7 +11437,7 @@ write-file-atomic@^3.0.0: signal-exit "^3.0.2" typedarray-to-buffer "^3.1.5" -ws@^7.4.6, ws@^7.5.10: +ws@^7.4.6: version "7.5.10" resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.10.tgz#58b5c20dc281633f6c19113f39b349bd8bd558d9" integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ== @@ -11441,6 +11447,11 @@ ws@^8.13.0: resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc" integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw== +ws@^8.18.1: + version "8.18.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.1.tgz#ea131d3784e1dfdff91adb0a4a116b127515e3cb" + integrity sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w== + xml-name-validator@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" diff --git a/virtual-event/package.json b/virtual-event/package.json index aa8dfa91..cef5fe29 100644 --- a/virtual-event/package.json +++ b/virtual-event/package.json @@ -12,8 +12,8 @@ "lottie-web": "^5.9.6", "react": "^19.0.0", "react-dom": "^19.0.0", - "stream-chat": "^8.56.0", - "stream-chat-react": "^12.12.0" + "stream-chat": "9.0.0-rc.15", + "stream-chat-react": "13.0.0-rc.2" }, "devDependencies": { "@types/emoji-mart": "^3.0.8", diff --git a/virtual-event/src/components/Chat/ChannelInner.tsx b/virtual-event/src/components/Chat/ChannelInner.tsx index afd8834a..8531a967 100644 --- a/virtual-event/src/components/Chat/ChannelInner.tsx +++ b/virtual-event/src/components/Chat/ChannelInner.tsx @@ -1,12 +1,8 @@ -import { MessageInput, VirtualizedMessageList, Window } from 'stream-chat-react'; +import {MessageInput, VirtualizedMessageList, Window} from 'stream-chat-react'; -import { ThreadInner } from './ThreadInner'; - -import { useOverrideSubmit } from '../../hooks/useOverrideSubmit'; +import {ThreadInner} from './ThreadInner'; export const ChannelInner = () => { - const overrideSubmitHandler = useOverrideSubmit(); - return ( <> @@ -15,7 +11,7 @@ export const ChannelInner = () => { hideDeletedMessages separateGiphyPreview /> - + diff --git a/virtual-event/src/components/Chat/ChatContainer.tsx b/virtual-event/src/components/Chat/ChatContainer.tsx index 3095148b..3f9c0ba1 100644 --- a/virtual-event/src/components/Chat/ChatContainer.tsx +++ b/virtual-event/src/components/Chat/ChatContainer.tsx @@ -1,4 +1,4 @@ -import React, {useState} from 'react'; +import React, {useEffect, useState} from 'react'; import {Channel as StreamChannel, UserResponse} from 'stream-chat'; import {Channel, Chat} from 'stream-chat-react'; @@ -16,16 +16,19 @@ import {MessageInputUI} from './MessageInputUI'; import {ParticipantProfile} from './ParticipantProfile'; import {ParticipantSearch} from './ParticipantSearch'; import {Snackbar} from './Snackbar'; -import {SuggestionListItem} from './SuggestionList'; import {ThreadHeader} from './ThreadHeader'; import {UserActionsModal} from './UserActionsModal'; import {useEventContext} from '../../contexts/EventContext'; -import {GiphyContextProvider} from '../../contexts/GiphyContext'; import {useInitChat} from '../../hooks/useInitChat'; +import { + createDraftGiphyCommandInjectionMiddleware, + createGiphyCommandInjectionMiddleware +} from "../../middleware/composition/giphyCommandInjectionMiddleware"; +import {createGiphyCommandControlMiddleware} from "../../middleware/textComposition/giphyCommandControl"; +import {CustomSuggestionList} from "./SuggestionList"; -import {StreamChatType} from '../../types'; init({ data }); @@ -48,6 +51,31 @@ export const ChatContainer = () => { const { chatClient, currentChannel, dmUnread, eventUnread, globalUnread, qaUnread } = useInitChat(); + useEffect(() => { + if (!chatClient) return; + + chatClient.setMessageComposerSetupFunction(({ composer }) => { + composer.compositionMiddlewareExecutor.insert({ + middleware: [ + createGiphyCommandInjectionMiddleware(composer) + ], + position: {after: 'stream-io/message-composer-middleware/attachments'} + }); + composer.draftCompositionMiddlewareExecutor.insert({ + middleware: [ + createDraftGiphyCommandInjectionMiddleware(composer) + ], + position: {after: 'stream-io/message-composer-middleware/draft-attachments'} + }); + composer.textComposer.middlewareExecutor.insert({ + middleware: [ + createGiphyCommandControlMiddleware(composer), + ], + position: {before: 'stream-io/text-composer/pre-validation-middleware'} + }) + }); + }, [chatClient]); + if (!chatClient) return null; return ( @@ -66,7 +94,6 @@ export const ChatContainer = () => { )}
      - {searching && ( { /> ) : ( currentChannel && ( - - AutocompleteSuggestionItem={SuggestionListItem} + { ) )} -
      diff --git a/virtual-event/src/components/Chat/DMChannel.tsx b/virtual-event/src/components/Chat/DMChannel.tsx index 86a51686..96de2750 100644 --- a/virtual-event/src/components/Chat/DMChannel.tsx +++ b/virtual-event/src/components/Chat/DMChannel.tsx @@ -7,10 +7,10 @@ import {EmptyStateIndicatorChannel} from './EmptyStateIndicators'; import {GiphyPreview} from './GiphyPreview'; import {MessageUI} from './MessageUI'; import {MessageInputUI} from './MessageInputUI'; -import {SuggestionListItem} from './SuggestionList'; +import {CustomSuggestionList} from './SuggestionList'; import {ThreadHeader} from './ThreadHeader'; import {UserActionsDropdown} from './UserActionsDropdown'; -import {StreamChatType} from '../../types'; + import {useBoolState} from '../../hooks/useBoolState'; import {ChannelHeader} from './ChannelHeader'; @@ -48,8 +48,8 @@ export const DMChannel: React.FC = (props) => { closeDropdown={closeDropdown} /> )} - - AutocompleteSuggestionItem={SuggestionListItem} + } GiphyPreviewMessage={GiphyPreview} diff --git a/virtual-event/src/components/Chat/EmojiPicker.tsx b/virtual-event/src/components/Chat/EmojiPicker.tsx index 5fd26b53..4179bb6c 100644 --- a/virtual-event/src/components/Chat/EmojiPicker.tsx +++ b/virtual-event/src/components/Chat/EmojiPicker.tsx @@ -3,7 +3,7 @@ import { usePopper } from 'react-popper'; import { useMessageInputContext } from 'stream-chat-react'; import Picker from '@emoji-mart/react'; -import { StreamChatType } from '../../types'; + import { EmojiPickerIcon } from '../../assets'; import { useEventContext } from '../../contexts/EventContext'; @@ -16,7 +16,7 @@ export const EmojiPicker = () => { placement: 'top-end', }); const [emojiPickerIsOpen, setEmojiPickerIsOpen] = useState(false); - const { insertText, textareaRef, cooldownRemaining } = useMessageInputContext(); + const { insertText, textareaRef, cooldownRemaining } = useMessageInputContext(); const { mode } = useEventContext(); useEffect(() => { @@ -39,6 +39,7 @@ export const EmojiPicker = () => { {emojiPickerIsOpen && (
      > = (props) => { + +const CustomAttachmentActions: React.FC = (props) => { const { actionHandler, actions } = props; const handleClick = async ( @@ -65,7 +65,7 @@ const CustomCard: React.FC = (props) => { return ; }; -export const GiphyPreview: React.FC> = (props) => { +export const GiphyPreview: React.FC = (props) => { const { message } = props; const handleAction = useActionHandler(message); @@ -75,7 +75,7 @@ export const GiphyPreview: React.FC> = return (
      - + { +const textComposerStateSelector = (state: TextComposerState) => ({ + text: state.text, +}); + +const customComposerDataSelector = (state: CustomDataManagerState) => ({ + isComposingGiphyText: state.custom.command === 'giphy', +}); + +const CharacterCounter = () => { + const {textComposer} = useMessageComposer(); + const {text} = useStateStore(textComposer.state, textComposerStateSelector); + return
      {269 - text.length}
      ; +} + +const SendButton = () => { const { - closeCommandsList, cooldownInterval, cooldownRemaining, - handleChange, handleSubmit, - numberOfUploads, - openCommandsList, setCooldownRemaining, - text, } = useMessageInputContext(); + const {customDataManager, textComposer} = useMessageComposer(); + const hasSendableData = useMessageComposerHasSendableData(); + const {text} = useStateStore(textComposer.state, textComposerStateSelector); + const {isComposingGiphyText} = useStateStore(customDataManager.state, customComposerDataSelector); + return ( + + ); +} +export const MessageInputUI = () => { + const { + cooldownRemaining, + } = useMessageInputContext(); const { chatType } = useEventContext(); - const { giphyState, setGiphyState } = useGiphyContext(); + const {customDataManager, textComposer} = useMessageComposer(); - const [commandsOpen, setCommandsOpen] = useState(false); - - useEffect(() => { - const handleClick = () => { - closeCommandsList(); - setCommandsOpen(false); - }; - - if (commandsOpen) document.addEventListener('click', handleClick); - return () => document.removeEventListener('click', handleClick); - }, [commandsOpen]); // eslint-disable-line - - const onChange: React.ChangeEventHandler = useCallback( - (event) => { - const { value } = event.target; - - const deletePressed = - event.nativeEvent instanceof InputEvent && - event.nativeEvent.inputType === 'deleteContentBackward'; - - if (text.length === 1 && deletePressed) { - setGiphyState(false); - } - - if (!giphyState && text.startsWith('/giphy') && !numberOfUploads) { - event.target.value = value.replace('/giphy', ''); - setGiphyState(true); - } - handleChange(event); - }, - [text, giphyState, numberOfUploads, handleChange], // eslint-disable-line + const {isComposingGiphyText} = useStateStore(customDataManager.state, customComposerDataSelector); + const commandSearchSource = useMemo(() => new CommandSearchSource(textComposer.channel), + [textComposer] ); - const handleCommandsClick = () => { - openCommandsList(); - setGiphyState(false); - setCommandsOpen(true); - }; - return (
      -
      - {giphyState && !numberOfUploads && } - +
      + {isComposingGiphyText && } + {chatType !== 'qa' && ( <> -
      null : handleCommandsClick} - role='button' + disabled={!!cooldownRemaining} + onClick={() => { + textComposer.setText('/'); + commandSearchSource.resetStateAndActivate(); + commandSearchSource.search(); + textComposer.setSuggestions({query: '', searchSource: commandSearchSource, trigger: '/'}) + }} > -
      - {!giphyState && } + + {!isComposingGiphyText && } )}
      - +
      ); }; diff --git a/virtual-event/src/components/Chat/MessageUI.tsx b/virtual-event/src/components/Chat/MessageUI.tsx index 58cde07a..810a0fe4 100644 --- a/virtual-event/src/components/Chat/MessageUI.tsx +++ b/virtual-event/src/components/Chat/MessageUI.tsx @@ -6,7 +6,6 @@ import { MessageRepliesCountButton, MessageUIComponentProps, SimpleReactionsList, - StreamMessage, useChannelActionContext, useChannelStateContext, useChatContext, @@ -21,8 +20,9 @@ import { useEventContext } from '../../contexts/EventContext'; import { useOnClickOutside } from '../../hooks/useOnClickOutside'; import { useBoolState } from '../../hooks/useBoolState'; +import {formatMessage, LocalMessage} from "stream-chat"; + -import { StreamChatType } from '../../types'; type OptionsProps = { dropdownOpen: boolean; @@ -123,8 +123,8 @@ const ReactionSelector = React.forwardRef ); const UpvoteButton = () => { - const { client } = useChatContext(); - const { message } = useMessageContext(); + const { client } = useChatContext(); + const { message } = useMessageContext(); const userUpVoted = client.userID && message.up_votes?.includes(client.userID); @@ -132,25 +132,18 @@ const UpvoteButton = () => { async (event: React.MouseEvent) => { event.stopPropagation(); - const mentionIDs = message.mentioned_users?.map(({ id }) => id); - let updatedUpVotes; - - if (!message.up_votes) { + if (!message.up_votes && client.userID) { return await client.updateMessage({ ...message, - mentioned_users: mentionIDs, up_votes: [client.userID], }); - } else if (client.userID && message.up_votes.includes(client.userID)) { - updatedUpVotes = message.up_votes.filter((userID: string) => userID !== client.userID); - } else { - updatedUpVotes = [...message.up_votes, client.userID]; } const updatedMessage = { ...message, - mentioned_users: mentionIDs, - up_votes: updatedUpVotes, + up_votes: (client.userID && message.up_votes.includes(client.userID) + ? message.up_votes.filter((userID: string) => userID !== client.userID) + : [...message.up_votes, client.userID]) as string[], }; return await client.updateMessage(updatedMessage); @@ -186,8 +179,8 @@ const searchRequestLock = Promise.resolve(); const OpenThreadButton = () => { const { openThread } = useChannelActionContext(); const { channel, thread } = useChannelStateContext(); - const { handleOpenThread, message } = useMessageContext(); - const [threadParent, setThreadParent] = useState(); + const { handleOpenThread, message } = useMessageContext(); + const [threadParent, setThreadParent] = useState(); const customOpenThread = useCallback( (event: BaseSyntheticEvent) => { @@ -205,7 +198,7 @@ const OpenThreadButton = () => { const foundMessage = results[0]?.message; if (foundMessage) { - setThreadParent(foundMessage); + setThreadParent(formatMessage(foundMessage)); } } catch (err) { console.log(err); @@ -233,7 +226,7 @@ const OpenThreadButton = () => { }; export const MessageUI: React.FC< - MessageUIComponentProps & { + MessageUIComponentProps & { setMessageActionUser?: React.Dispatch>; } > = (props) => { @@ -241,7 +234,7 @@ export const MessageUI: React.FC< const { messages } = useChannelStateContext(); const { chatType, themeModalOpen } = useEventContext(); - const { message } = useMessageContext(); + const { message } = useMessageContext(); const [dropdownOpen, setDropdownOpen] = useState(false); const [showOptions, setShowOptions] = useState(false); @@ -304,7 +297,7 @@ export const MessageUI: React.FC<
      {message.text}
      {!!message.attachments?.length && ( - attachments={message.attachments} /> + )} diff --git a/virtual-event/src/components/Chat/ParticipantProfile.tsx b/virtual-event/src/components/Chat/ParticipantProfile.tsx index 3c3c3f36..716740c5 100644 --- a/virtual-event/src/components/Chat/ParticipantProfile.tsx +++ b/virtual-event/src/components/Chat/ParticipantProfile.tsx @@ -1,5 +1,5 @@ import React, { useCallback, useState } from 'react'; -import type { Channel, UserResponse, ExtendableGenerics } from 'stream-chat'; +import type { Channel, UserResponse } from 'stream-chat'; import { Avatar, ReactEventHandler, useChatContext } from 'stream-chat-react'; import { UserActionsDropdown } from './UserActionsDropdown'; @@ -9,15 +9,15 @@ import { useEventContext } from '../../contexts/EventContext'; import { useBoolState } from '../../hooks/useBoolState'; -import { StreamChatType } from '../../types'; -type Props = { - participantProfile: UserResponse; + +type Props = { + participantProfile: UserResponse; setDmChannel: React.Dispatch>; setParticipantProfile: React.Dispatch>; }; -export const ParticipantProfile = (props: Props) => { +export const ParticipantProfile = (props: Props) => { const { participantProfile, setDmChannel, setParticipantProfile } = props; const { id, image, name, online, title } = participantProfile; diff --git a/virtual-event/src/components/Chat/ParticipantSearch.tsx b/virtual-event/src/components/Chat/ParticipantSearch.tsx index beefe339..883c3efb 100644 --- a/virtual-event/src/components/Chat/ParticipantSearch.tsx +++ b/virtual-event/src/components/Chat/ParticipantSearch.tsx @@ -13,7 +13,7 @@ import { SkeletonLoader } from './DMChannelList'; import { SearchResult } from './SearchResult'; import { ClearSearchButton, CloseX, SearchIcon } from '../../assets'; -import { StreamChatType } from '../../types'; + type ParticipantSearchProps = { setDmChannel: React.Dispatch>; @@ -40,12 +40,12 @@ export const ParticipantSearch = (props: ParticipantSearchProps) => { try { const { users } = await client.queryUsers( - { id: { $ne: client.userID || '' } }, + { }, { id: 1, last_active: -1 }, { limit: 10 }, ); - - if (users.length) setParticipants(users); + const otherParticipants = users.filter((user) => user.id !== client.userID); + if (otherParticipants.length) setParticipants(otherParticipants); } catch (err) { console.log(err); } @@ -63,8 +63,8 @@ export const ParticipantSearch = (props: ParticipantSearchProps) => { }; const onSelectResult = ( - params: ChannelSearchFunctionParams, - result: ChannelOrUserResponse, + params: ChannelSearchFunctionParams, + result: ChannelOrUserResponse, ) => handleSelectResult(result); const extraParams: ChannelSearchProps['searchQueryParams'] = { diff --git a/virtual-event/src/components/Chat/SearchResult.tsx b/virtual-event/src/components/Chat/SearchResult.tsx index e1401944..4c41ec61 100644 --- a/virtual-event/src/components/Chat/SearchResult.tsx +++ b/virtual-event/src/components/Chat/SearchResult.tsx @@ -1,9 +1,9 @@ import React from 'react'; import { Avatar, isChannel, SearchResultItemProps } from 'stream-chat-react'; -import { StreamChatType } from '../../types'; -export const SearchResult: React.FC> = (props) => { + +export const SearchResult: React.FC = (props) => { const { focusedUser, index, result, selectResult } = props; const focused = focusedUser === index; diff --git a/virtual-event/src/components/Chat/SuggestionList.tsx b/virtual-event/src/components/Chat/SuggestionList.tsx index 0f46fdef..d1afafd5 100644 --- a/virtual-event/src/components/Chat/SuggestionList.tsx +++ b/virtual-event/src/components/Chat/SuggestionList.tsx @@ -1,17 +1,14 @@ import React from 'react'; -import {Avatar, SuggestionCommand, SuggestionItemProps, SuggestionUser,} from 'stream-chat-react'; +import type {CommandResponse} from "stream-chat"; +import { + Avatar, + EmojiSearchIndexResult, + SuggestionList, + type SuggestionListItemComponentProps, SuggestionListProps, UserItemProps, +} from 'stream-chat-react'; import {Ban, Flag, Giphy, Mute, Unban, Unmute} from '../../assets'; -type Emoji = { id: string; native: string; name: string }; - -const isEmoji = (output: SuggestionItem): output is Emoji => (output as Emoji).native != null; - -const isMention = (output: SuggestionItem): output is SuggestionUser => - (output as SuggestionUser).id != null && (output as SuggestionUser).native == null; - -const isEmojiOrMention = (output: SuggestionItem): output is Emoji | SuggestionUser => - (output as Emoji | SuggestionUser).id != null; const getCommandIcon = (name?: string) => { let description; @@ -49,35 +46,51 @@ const getCommandIcon = (name?: string) => { return { description, Icon }; }; -type SuggestionItem = Emoji | SuggestionUser | SuggestionCommand; -export const SuggestionListItem = React.forwardRef( - (props: SuggestionItemProps, ref: React.Ref) => { - const { item, onClickHandler, onSelectHandler, selected } = props; +const CommandSuggestionItemComponent = (props: SuggestionListItemComponentProps) => { + const entity = props.entity as CommandResponse; + if (!entity) return null; + + const { description, Icon } = getCommandIcon(entity.name); + return ( +
      + {Icon &&
      } +
      {entity.name}
      +
      {description}
      +
      + ); +} + +const MentionSuggestionItemComponent = (props: SuggestionListItemComponentProps) => { + const entity = props.entity as UserItemProps['entity']; + if (!entity) return null; - const selectItem = () => onSelectHandler(item); + return ( +
      + +
      {entity.name}
      +
      + ); +} - const { description, Icon } = getCommandIcon(item.name); +const EmojiSuggestionItemComponent = (props: SuggestionListItemComponentProps) => { + const entity = props.entity as EmojiSearchIndexResult; + if (!entity) return null; + const displayText = `${entity.native} ${entity.name || entity.id};`; + return ( +
      +
      {displayText}
      +
      + ); +} - const itemName = isEmojiOrMention(item) ? item.name || item.id : item.name; - const displayText = isEmoji(item) ? `${item.native}- ${itemName}` : itemName; +const suggestionItemComponents: Record< + string, + React.ComponentType +> = { + '/': CommandSuggestionItemComponent, + ':': EmojiSuggestionItemComponent, + '@': MentionSuggestionItemComponent, +} - return ( -
      onClickHandler(event, item)} - onMouseEnter={selectItem} - ref={ref} - role='button' - tabIndex={0} - > - {!isEmojiOrMention(item) && ( -
      {Icon ? : null}
      - )} - {isMention(item) ? : null} -
      {displayText}
      -
      {description}
      -
      - ); - }, -); +export const CustomSuggestionList = (props: SuggestionListProps) => diff --git a/virtual-event/src/components/Chat/ThreadInner.tsx b/virtual-event/src/components/Chat/ThreadInner.tsx index 983e7066..cbb9da18 100644 --- a/virtual-event/src/components/Chat/ThreadInner.tsx +++ b/virtual-event/src/components/Chat/ThreadInner.tsx @@ -5,13 +5,11 @@ import { ThreadMessageInputUI } from './ThreadMessageInputUI'; import { ThreadInputContext } from '../../contexts/ThreadInputContext'; import { useBoolState } from '../../hooks/useBoolState'; -import { useOverrideSubmit } from '../../hooks/useOverrideSubmit'; export const ThreadInner = () => { const { thread } = useChannelStateContext(); const {state: checked, off: uncheck, toggle: toggleCheckedFooter} = useBoolState(); - const threadOverrideSubmitHandler = useOverrideSubmit(checked); useEffect(() => { if (!thread) { @@ -23,7 +21,6 @@ export const ThreadInner = () => { return ( { useEffect(() => { const initChat = async () => { - const client = StreamChat.getInstance(apiKey); + const client = StreamChat.getInstance(apiKey); if (process.env.REACT_APP_CHAT_SERVER_ENDPOINT) { client.setBaseURL(process.env.REACT_APP_CHAT_SERVER_ENDPOINT); diff --git a/virtual-event/src/hooks/useOverrideSubmit.ts b/virtual-event/src/hooks/useOverrideSubmit.ts deleted file mode 100644 index 8f503ae7..00000000 --- a/virtual-event/src/hooks/useOverrideSubmit.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { MessageToSend, useChannelActionContext } from 'stream-chat-react'; -import { useCallback } from 'react'; - -import { useGiphyContext } from '../contexts/GiphyContext'; - -export const useOverrideSubmit = (showInChannel?: boolean) => { - const { sendMessage } = useChannelActionContext(); - const { giphyState, setGiphyState } = useGiphyContext(); - - return useCallback( - async (message: MessageToSend) => { - let updatedMessage; - if (message.attachments?.length && message.text?.startsWith('/giphy')) { - const updatedText = message.text.replace('/giphy', ''); - updatedMessage = { ...message, text: updatedText }; - } - - if (giphyState) { - const updatedText = `/giphy ${message.text}`; - updatedMessage = { ...message, text: updatedText }; - } - - const messageToSend = updatedMessage || message; - - try { - await sendMessage(messageToSend, { show_in_channel: showInChannel }); - } catch (err) { - console.log(err); - } - - setGiphyState(false); - }, - [showInChannel, giphyState, sendMessage, setGiphyState], - ); -}; diff --git a/virtual-event/src/middleware/composition/giphyCommandInjectionMiddleware.ts b/virtual-event/src/middleware/composition/giphyCommandInjectionMiddleware.ts new file mode 100644 index 00000000..4c55ab29 --- /dev/null +++ b/virtual-event/src/middleware/composition/giphyCommandInjectionMiddleware.ts @@ -0,0 +1,57 @@ +import type { + MessageComposer, + MessageComposerMiddlewareValueState, + MessageDraftComposerMiddlewareValueState, + MiddlewareHandlerParams +} from "stream-chat"; + +export const createGiphyCommandInjectionMiddleware = (composer: MessageComposer) => ({ + id: 'demo-team/message-composer-middleware/giphy-command-injection', + compose: ({ + input, + nextHandler, + }: MiddlewareHandlerParams) => { + const {custom: {command}} = composer.customDataManager.state.getLatestValue(); + const {attachments, text} = input.state.localMessage; + const injection = command && `/${command}`; + if (command !== 'giphy' || !injection || text?.startsWith(injection) || attachments?.length) return nextHandler(input); + const enrichedText = `${injection} ${text}` + return nextHandler({ + ...input, + state: { + ...input.state, + localMessage: { + ...input.state.localMessage, + text: enrichedText, + }, + message: { + ...input.state.message, + text: enrichedText, + }, + }, + }); + } +}); +export const createDraftGiphyCommandInjectionMiddleware = (composer: MessageComposer) => ({ + id: 'demo-team/message-composer-middleware/draft-giphy-command-injection', + compose: ({ + input, + nextHandler, + }: MiddlewareHandlerParams) => { + const {custom: {command}} = composer.customDataManager.state.getLatestValue(); + const text = input.state.draft.text; + const injection = command && `/${command}`; + if (command !== 'giphy' || !injection || text?.startsWith(injection)) return nextHandler(input); + const enrichedText = `${injection} ${text}` + return nextHandler({ + ...input, + state: { + ...input.state, + draft: { + ...input.state.draft, + text: enrichedText, + }, + }, + }); + } +}); \ No newline at end of file diff --git a/virtual-event/src/middleware/textComposition/giphyCommandControl.ts b/virtual-event/src/middleware/textComposition/giphyCommandControl.ts new file mode 100644 index 00000000..a4e610b2 --- /dev/null +++ b/virtual-event/src/middleware/textComposition/giphyCommandControl.ts @@ -0,0 +1,57 @@ +import {CommandSuggestion, MessageComposer, TextComposerMiddlewareParams, UserSuggestion} from "stream-chat"; + +export const createGiphyCommandControlMiddleware = (composer: MessageComposer) => { + const trigger = '/giphy '; + return ({ + id: 'demo-team/text-composer/giphy-command-control', + onChange: ({input, nextHandler}: TextComposerMiddlewareParams) => { + const {attachmentManager} = composer; + const hasNonUploadAttachments = attachmentManager.attachments.filter((att) => !att.localMetadata.uploadState).length > 0; + const hasUploadAttachments = attachmentManager.uploadsInProgressCount + attachmentManager.successfulUploadsCount > 0; + if (hasUploadAttachments || hasNonUploadAttachments) { + return nextHandler(input); + } + + const {customDataManager} = composer; + if (!input.state.text && customDataManager.customComposerData.command) { + customDataManager.setCustomData({command: null}) + return nextHandler(input); + } + if (!input.state.text.startsWith(trigger) || customDataManager.customComposerData.command) { + return nextHandler(input); + } + + customDataManager.setCustomData({command: 'giphy'}); + const newText = input.state.text.slice(trigger.length); + return nextHandler({ + ...input, + state: { + ...input.state, + text: newText, + selection: {start: input.state.selection.start - trigger.length, end: input.state.selection.end - trigger.length}, + } + }); + }, + onSuggestionItemSelect: ({ + input, + nextHandler, + selectedSuggestion, + }: TextComposerMiddlewareParams) => { + if (selectedSuggestion?.name !== 'giphy') + return nextHandler(input); + + composer.customDataManager.setCustomData({command: 'giphy'}); + const newText = input.state.text.slice(trigger.length); + return nextHandler({ + ...input, + state: { + ...input.state, + text: newText, + selection: {start: input.state.selection.start - trigger.length, end: input.state.selection.end - trigger.length}, + suggestions: undefined + }, + status: 'complete' + }); + } + }); +}; \ No newline at end of file diff --git a/virtual-event/src/styles/Chat/MessageInput/MessageInput.theme.scss b/virtual-event/src/styles/Chat/MessageInput/MessageInput.theme.scss index 63b7ccf1..35c3898a 100644 --- a/virtual-event/src/styles/Chat/MessageInput/MessageInput.theme.scss +++ b/virtual-event/src/styles/Chat/MessageInput/MessageInput.theme.scss @@ -17,6 +17,8 @@ } .input-ui-input-commands-button { + background: none; + border: none; &:hover { cursor: pointer; } diff --git a/virtual-event/src/types.stream.d.ts b/virtual-event/src/types.stream.d.ts new file mode 100644 index 00000000..bf53553c --- /dev/null +++ b/virtual-event/src/types.stream.d.ts @@ -0,0 +1,21 @@ +import 'stream-chat'; + +declare module 'stream-chat' { + interface CustomChannelData { + name?: string; + demo?: string; + } + + interface CustomUserData { + title: string; + } + + interface CustomMessageData { + show_in_channel: boolean; + up_votes: string[] + } + + interface CustomMessageComposerData { + command: 'giphy' | null; + } +} \ No newline at end of file diff --git a/virtual-event/src/types.ts b/virtual-event/src/types.ts index 0bce7fe9..74ac1919 100644 --- a/virtual-event/src/types.ts +++ b/virtual-event/src/types.ts @@ -1,27 +1,3 @@ import { PropsWithChildren } from 'react'; -import { LiteralStringForUnion, UR } from 'stream-chat'; export type PropsWithChildrenOnly = PropsWithChildren>; -type AttachmentType = UR; -type ChannelType = UR; -type CommandType = LiteralStringForUnion; -type EventType = UR; -type MemberType = UR; -type MessageType = UR & { up_votes?: string[]; show_in_channel?: boolean }; -type ReactionType = UR; -export type UserType = UR & { image?: string; title?: string }; -export type PollOptionType = UR; -export type PollType = UR; - -export type StreamChatType = { - attachmentType: AttachmentType; - channelType: ChannelType; - commandType: CommandType; - eventType: EventType; - memberType: MemberType; - messageType: MessageType; - reactionType: ReactionType; - userType: UserType; - pollOptionType: PollOptionType; - pollType: PollType; -}; diff --git a/virtual-event/yarn.lock b/virtual-event/yarn.lock index 9d0742bb..81d92fbf 100644 --- a/virtual-event/yarn.lock +++ b/virtual-event/yarn.lock @@ -2093,11 +2093,12 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== -"@types/jsonwebtoken@~9.0.0": - version "9.0.7" - resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-9.0.7.tgz#e49b96c2b29356ed462e9708fc73b833014727d2" - integrity sha512-ugo316mmTYBl2g81zDFnZ7cfxlut3o+/EQdaP7J8QN2kY6lJ22hmQYCK5EHcJHbrW+dkCGSCPgbG8JtYj6qSrg== +"@types/jsonwebtoken@^9.0.8": + version "9.0.9" + resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-9.0.9.tgz#a4c3a446c0ebaaf467a58398382616f416345fb3" + integrity sha512-uoe+GxEuHbvy12OUQct2X9JenKM3qAscquYymuQN4fMWG9DBQtykrQEFcAbVACF7qaLw9BePSodUL0kquqBJpQ== dependencies: + "@types/ms" "*" "@types/node" "*" "@types/linkifyjs@^2.1.3": @@ -2256,13 +2257,6 @@ resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.11.tgz#11af57b127e32487774841f7a4e54eab166d03c4" integrity sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA== -"@types/ws@^7.4.0": - version "7.4.7" - resolved "https://registry.yarnpkg.com/@types/ws/-/ws-7.4.7.tgz#f7c390a36f7a0679aa69de2d501319f4f8d9b702" - integrity sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww== - dependencies: - "@types/node" "*" - "@types/ws@^8.5.1": version "8.5.3" resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.3.tgz#7d25a1ffbecd3c4f2d35068d0b283c037003274d" @@ -2270,6 +2264,13 @@ dependencies: "@types/node" "*" +"@types/ws@^8.5.14": + version "8.18.1" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.18.1.tgz#48464e4bf2ddfd17db13d845467f6070ffea4aa9" + integrity sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg== + dependencies: + "@types/node" "*" + "@types/yargs-parser@*": version "21.0.0" resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b" @@ -5646,10 +5647,10 @@ isexe@^2.0.0: resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== -isomorphic-ws@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz#55fd4cd6c5e6491e76dc125938dd863f5cd4f2dc" - integrity sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w== +isomorphic-ws@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz#e5529148912ecb9b451b46ed44d53dae1ce04bbf" + integrity sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw== istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.0: version "3.2.0" @@ -6304,7 +6305,7 @@ jsonpointer@^5.0.0: resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-5.0.1.tgz#2110e0af0900fd37467b5907ecd13a7884a1b559" integrity sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ== -jsonwebtoken@~9.0.0: +jsonwebtoken@^9.0.2: version "9.0.2" resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz#65ff91f4abef1784697d40952bb1998c504caaf3" integrity sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ== @@ -6413,6 +6414,11 @@ linkifyjs@^4.1.0: resolved "https://registry.yarnpkg.com/linkifyjs/-/linkifyjs-4.1.3.tgz#0edbc346428a7390a23ea2e5939f76112c9ae07f" integrity sha512-auMesunaJ8yfkHvK4gfg1K0SaKX/6Wn9g2Aac/NwX+l5VdmFZzo/hdPGxEOETj+ryRa4/fiOPjeeKURSAJx1sg== +linkifyjs@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/linkifyjs/-/linkifyjs-4.2.0.tgz#9dd30222b9cbabec9c950e725ec00031c7fa3f08" + integrity sha512-pCj3PrQyATaoTYKHrgWRF3SJwsm61udVh+vuls/Rl6SptiDhgE7ziUIudAedRY9QEfynmM7/RmLEfPUyw1HPCw== + load-script@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/load-script/-/load-script-1.0.0.tgz#0491939e0bee5643ee494a7e3da3d2bac70c6ca4" @@ -9285,10 +9291,10 @@ statuses@2.0.1: resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== -stream-chat-react@^12.12.0: - version "12.12.0" - resolved "https://registry.yarnpkg.com/stream-chat-react/-/stream-chat-react-12.12.0.tgz#4c8896ec00628660f4b5f31417e3e96fb8fa4a8d" - integrity sha512-fZ4CXG9HuycODMUnHExQH05mjANCxZkCzo1miF7WLRgHtzj+ghGBu8kRqO4qLcSMi7noSUmuCGFMY2/r7iKccQ== +stream-chat-react@13.0.0-rc.2: + version "13.0.0-rc.2" + resolved "https://registry.yarnpkg.com/stream-chat-react/-/stream-chat-react-13.0.0-rc.2.tgz#4e48ade6b66b816827fdacecf02632b23f7ff254" + integrity sha512-HYEjojijcwvucCfDebX1veWaMWmb5jI0/F7GNmALooy2eBt5ChBV2rM53SqjjN/AbSE4VGgLYWm3jLKH6AIHQQ== dependencies: "@braintree/sanitize-url" "^6.0.4" "@popperjs/core" "^2.11.5" @@ -9325,20 +9331,20 @@ stream-chat-react@^12.12.0: "@stream-io/transliterate" "^1.5.5" mml-react "^0.4.7" -stream-chat@^8.56.0: - version "8.56.0" - resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-8.56.0.tgz#2960282d0fdfedcf067328e36dce87190646dec5" - integrity sha512-wv0OIjNdoMcK25PCjVg9lMfezMCrKzPoG0L9eDeKo/ZbJNGY3hNhUXVsyzIDYLYGWU9aqXGhAKpuxbVfgwbnqg== +stream-chat@9.0.0-rc.15: + version "9.0.0-rc.15" + resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-9.0.0-rc.15.tgz#6a35f27c38be2e021a3e62abcffa40aaef84fc0a" + integrity sha512-n8IftP103jp8IVOUZA4mnOvEIbyzGTVjoL3rDm42apk8DRHz2/vdvgyTYUJysDNYh+Oxu5HwWObToKv1c/Bxdw== dependencies: - "@babel/runtime" "^7.16.3" - "@types/jsonwebtoken" "~9.0.0" - "@types/ws" "^7.4.0" + "@types/jsonwebtoken" "^9.0.8" + "@types/ws" "^8.5.14" axios "^1.6.0" base64-js "^1.5.1" form-data "^4.0.0" - isomorphic-ws "^4.0.1" - jsonwebtoken "~9.0.0" - ws "^7.5.10" + isomorphic-ws "^5.0.0" + jsonwebtoken "^9.0.2" + linkifyjs "^4.2.0" + ws "^8.18.1" string-length@^4.0.1: version "4.0.2" @@ -10563,10 +10569,10 @@ ws@^7.4.6: resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591" integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q== -ws@^7.5.10: - version "7.5.10" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.10.tgz#58b5c20dc281633f6c19113f39b349bd8bd558d9" - integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ== +ws@^8.18.1: + version "8.18.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.1.tgz#ea131d3784e1dfdff91adb0a4a116b127515e3cb" + integrity sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w== ws@^8.4.2: version "8.9.0"