mirror of
				https://github.com/owncast/owncast.git
				synced 2025-11-04 05:17:27 +08:00 
			
		
		
		
	Wire up chat message props. Add username highlighting. Closes #1921
This commit is contained in:
		
							
								
								
									
										118
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										118
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							@ -1,118 +0,0 @@
 | 
				
			|||||||
{
 | 
					 | 
				
			||||||
  "name": "owncast",
 | 
					 | 
				
			||||||
  "lockfileVersion": 2,
 | 
					 | 
				
			||||||
  "requires": true,
 | 
					 | 
				
			||||||
  "packages": {
 | 
					 | 
				
			||||||
    "": {
 | 
					 | 
				
			||||||
      "dependencies": {
 | 
					 | 
				
			||||||
        "fetch-retry": "^5.0.2",
 | 
					 | 
				
			||||||
        "isomorphic-fetch": "^3.0.0"
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "node_modules/fetch-retry": {
 | 
					 | 
				
			||||||
      "version": "5.0.2",
 | 
					 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/fetch-retry/-/fetch-retry-5.0.2.tgz",
 | 
					 | 
				
			||||||
      "integrity": "sha512-57Hmu+1kc6pKFUGVIobT7qw3NeAzY/uNN26bSevERLVvf6VGFR/ooDCOFBHMNDgAxBiU2YJq1D0vFzc6U1DcPw=="
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "node_modules/isomorphic-fetch": {
 | 
					 | 
				
			||||||
      "version": "3.0.0",
 | 
					 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz",
 | 
					 | 
				
			||||||
      "integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==",
 | 
					 | 
				
			||||||
      "dependencies": {
 | 
					 | 
				
			||||||
        "node-fetch": "^2.6.1",
 | 
					 | 
				
			||||||
        "whatwg-fetch": "^3.4.1"
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "node_modules/node-fetch": {
 | 
					 | 
				
			||||||
      "version": "2.6.7",
 | 
					 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
 | 
					 | 
				
			||||||
      "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
 | 
					 | 
				
			||||||
      "dependencies": {
 | 
					 | 
				
			||||||
        "whatwg-url": "^5.0.0"
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      "engines": {
 | 
					 | 
				
			||||||
        "node": "4.x || >=6.0.0"
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      "peerDependencies": {
 | 
					 | 
				
			||||||
        "encoding": "^0.1.0"
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      "peerDependenciesMeta": {
 | 
					 | 
				
			||||||
        "encoding": {
 | 
					 | 
				
			||||||
          "optional": true
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "node_modules/tr46": {
 | 
					 | 
				
			||||||
      "version": "0.0.3",
 | 
					 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
 | 
					 | 
				
			||||||
      "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o="
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "node_modules/webidl-conversions": {
 | 
					 | 
				
			||||||
      "version": "3.0.1",
 | 
					 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
 | 
					 | 
				
			||||||
      "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE="
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "node_modules/whatwg-fetch": {
 | 
					 | 
				
			||||||
      "version": "3.6.2",
 | 
					 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz",
 | 
					 | 
				
			||||||
      "integrity": "sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA=="
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "node_modules/whatwg-url": {
 | 
					 | 
				
			||||||
      "version": "5.0.0",
 | 
					 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
 | 
					 | 
				
			||||||
      "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=",
 | 
					 | 
				
			||||||
      "dependencies": {
 | 
					 | 
				
			||||||
        "tr46": "~0.0.3",
 | 
					 | 
				
			||||||
        "webidl-conversions": "^3.0.0"
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  "dependencies": {
 | 
					 | 
				
			||||||
    "fetch-retry": {
 | 
					 | 
				
			||||||
      "version": "5.0.2",
 | 
					 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/fetch-retry/-/fetch-retry-5.0.2.tgz",
 | 
					 | 
				
			||||||
      "integrity": "sha512-57Hmu+1kc6pKFUGVIobT7qw3NeAzY/uNN26bSevERLVvf6VGFR/ooDCOFBHMNDgAxBiU2YJq1D0vFzc6U1DcPw=="
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "isomorphic-fetch": {
 | 
					 | 
				
			||||||
      "version": "3.0.0",
 | 
					 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz",
 | 
					 | 
				
			||||||
      "integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==",
 | 
					 | 
				
			||||||
      "requires": {
 | 
					 | 
				
			||||||
        "node-fetch": "^2.6.1",
 | 
					 | 
				
			||||||
        "whatwg-fetch": "^3.4.1"
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "node-fetch": {
 | 
					 | 
				
			||||||
      "version": "2.6.7",
 | 
					 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
 | 
					 | 
				
			||||||
      "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
 | 
					 | 
				
			||||||
      "requires": {
 | 
					 | 
				
			||||||
        "whatwg-url": "^5.0.0"
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "tr46": {
 | 
					 | 
				
			||||||
      "version": "0.0.3",
 | 
					 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
 | 
					 | 
				
			||||||
      "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o="
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "webidl-conversions": {
 | 
					 | 
				
			||||||
      "version": "3.0.1",
 | 
					 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
 | 
					 | 
				
			||||||
      "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE="
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "whatwg-fetch": {
 | 
					 | 
				
			||||||
      "version": "3.6.2",
 | 
					 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz",
 | 
					 | 
				
			||||||
      "integrity": "sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA=="
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "whatwg-url": {
 | 
					 | 
				
			||||||
      "version": "5.0.0",
 | 
					 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
 | 
					 | 
				
			||||||
      "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=",
 | 
					 | 
				
			||||||
      "requires": {
 | 
					 | 
				
			||||||
        "tr46": "~0.0.3",
 | 
					 | 
				
			||||||
        "webidl-conversions": "^3.0.0"
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,6 +0,0 @@
 | 
				
			|||||||
{
 | 
					 | 
				
			||||||
  "dependencies": {
 | 
					 | 
				
			||||||
    "fetch-retry": "^5.0.2",
 | 
					 | 
				
			||||||
    "isomorphic-fetch": "^3.0.0"
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,6 +1,6 @@
 | 
				
			|||||||
import { Spin } from 'antd';
 | 
					import { Spin } from 'antd';
 | 
				
			||||||
import { Virtuoso } from 'react-virtuoso';
 | 
					import { Virtuoso } from 'react-virtuoso';
 | 
				
			||||||
import { useRef } from 'react';
 | 
					import { useMemo, useRef } from 'react';
 | 
				
			||||||
import { LoadingOutlined } from '@ant-design/icons';
 | 
					import { LoadingOutlined } from '@ant-design/icons';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { MessageType, NameChangeEvent } from '../../../interfaces/socket-events';
 | 
					import { MessageType, NameChangeEvent } from '../../../interfaces/socket-events';
 | 
				
			||||||
@ -12,10 +12,13 @@ import ChatActionMessage from '../ChatActionMessage';
 | 
				
			|||||||
interface Props {
 | 
					interface Props {
 | 
				
			||||||
  messages: ChatMessage[];
 | 
					  messages: ChatMessage[];
 | 
				
			||||||
  loading: boolean;
 | 
					  loading: boolean;
 | 
				
			||||||
 | 
					  usernameToHighlight: string;
 | 
				
			||||||
 | 
					  chatUserId: string;
 | 
				
			||||||
 | 
					  isModerator: boolean;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default function ChatContainer(props: Props) {
 | 
					export default function ChatContainer(props: Props) {
 | 
				
			||||||
  const { messages, loading } = props;
 | 
					  const { messages, loading, usernameToHighlight, chatUserId, isModerator } = props;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const chatContainerRef = useRef(null);
 | 
					  const chatContainerRef = useRef(null);
 | 
				
			||||||
  const spinIcon = <LoadingOutlined style={{ fontSize: '32px' }} spin />;
 | 
					  const spinIcon = <LoadingOutlined style={{ fontSize: '32px' }} spin />;
 | 
				
			||||||
@ -31,7 +34,14 @@ export default function ChatContainer(props: Props) {
 | 
				
			|||||||
  const getViewForMessage = message => {
 | 
					  const getViewForMessage = message => {
 | 
				
			||||||
    switch (message.type) {
 | 
					    switch (message.type) {
 | 
				
			||||||
      case MessageType.CHAT:
 | 
					      case MessageType.CHAT:
 | 
				
			||||||
        return <ChatUserMessage message={message} showModeratorMenu={false} />;
 | 
					        return (
 | 
				
			||||||
 | 
					          <ChatUserMessage
 | 
				
			||||||
 | 
					            message={message}
 | 
				
			||||||
 | 
					            showModeratorMenu={isModerator} // Moderators have access to an additional menu
 | 
				
			||||||
 | 
					            highlightString={usernameToHighlight} // What to highlight in the message
 | 
				
			||||||
 | 
					            renderAsPersonallySent={message.user?.id === chatUserId} // The local user sent this message
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
      case MessageType.NAME_CHANGE:
 | 
					      case MessageType.NAME_CHANGE:
 | 
				
			||||||
        return getNameChangeViewForMessage(message);
 | 
					        return getNameChangeViewForMessage(message);
 | 
				
			||||||
      default:
 | 
					      default:
 | 
				
			||||||
@ -39,12 +49,8 @@ export default function ChatContainer(props: Props) {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  const MessagesTable = useMemo(
 | 
				
			||||||
    <div>
 | 
					    () => (
 | 
				
			||||||
      <div className={s.chatHeader}>
 | 
					 | 
				
			||||||
        <span>stream chat</span>
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
      <Spin spinning={loading} indicator={spinIcon} />
 | 
					 | 
				
			||||||
      <Virtuoso
 | 
					      <Virtuoso
 | 
				
			||||||
        style={{ height: '80vh' }}
 | 
					        style={{ height: '80vh' }}
 | 
				
			||||||
        ref={chatContainerRef}
 | 
					        ref={chatContainerRef}
 | 
				
			||||||
@ -53,6 +59,18 @@ export default function ChatContainer(props: Props) {
 | 
				
			|||||||
        itemContent={(index, message) => getViewForMessage(message)}
 | 
					        itemContent={(index, message) => getViewForMessage(message)}
 | 
				
			||||||
        followOutput="smooth"
 | 
					        followOutput="smooth"
 | 
				
			||||||
      />
 | 
					      />
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					    [messages, usernameToHighlight, chatUserId, isModerator],
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div>
 | 
				
			||||||
 | 
					      <div className={s.chatHeader}>
 | 
				
			||||||
 | 
					        <span>stream chat</span>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      <Spin spinning={loading} indicator={spinIcon}>
 | 
				
			||||||
 | 
					        {MessagesTable}
 | 
				
			||||||
 | 
					      </Spin>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -12,5 +12,20 @@
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
  .message {
 | 
					  .message {
 | 
				
			||||||
    color: var(--color-owncast-grey-100);
 | 
					    color: var(--color-owncast-grey-100);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    mark {
 | 
				
			||||||
 | 
					      color: white;
 | 
				
			||||||
 | 
					      padding: 0.1em 0.4em;
 | 
				
			||||||
 | 
					      border-radius: 0.5em 0.3em;
 | 
				
			||||||
 | 
					      background: transparent;
 | 
				
			||||||
 | 
					      background-image: linear-gradient(
 | 
				
			||||||
 | 
					        to right,
 | 
				
			||||||
 | 
					        rgba(255, 225, 0, 0.1),
 | 
				
			||||||
 | 
					        rgba(255, 225, 0, 0.358) 4%,
 | 
				
			||||||
 | 
					        rgba(255, 225, 0, 0.3)
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      -webkit-box-decoration-break: clone;
 | 
				
			||||||
 | 
					      box-decoration-break: clone;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,15 +1,25 @@
 | 
				
			|||||||
/* eslint-disable react/no-danger */
 | 
					/* eslint-disable react/no-danger */
 | 
				
			||||||
import { useEffect, useState } from 'react';
 | 
					import { useEffect, useState } from 'react';
 | 
				
			||||||
 | 
					import { Highlight } from 'react-highlighter-ts';
 | 
				
			||||||
 | 
					import he from 'he';
 | 
				
			||||||
import { ChatMessage } from '../../../interfaces/chat-message.model';
 | 
					import { ChatMessage } from '../../../interfaces/chat-message.model';
 | 
				
			||||||
import { formatTimestamp, formatMessageText } from './messageFmt';
 | 
					import { formatTimestamp } from './messageFmt';
 | 
				
			||||||
import s from './ChatUserMessage.module.scss';
 | 
					import s from './ChatUserMessage.module.scss';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface Props {
 | 
					interface Props {
 | 
				
			||||||
  message: ChatMessage;
 | 
					  message: ChatMessage;
 | 
				
			||||||
  showModeratorMenu: boolean;
 | 
					  showModeratorMenu: boolean;
 | 
				
			||||||
 | 
					  highlightString: string;
 | 
				
			||||||
 | 
					  renderAsPersonallySent: boolean;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default function ChatUserMessage({ message, showModeratorMenu }: Props) {
 | 
					export default function ChatUserMessage({
 | 
				
			||||||
 | 
					  message,
 | 
				
			||||||
 | 
					  highlightString,
 | 
				
			||||||
 | 
					  showModeratorMenu,
 | 
				
			||||||
 | 
					  // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
				
			||||||
 | 
					  renderAsPersonallySent, // Move the border to the right and render a background
 | 
				
			||||||
 | 
					}: Props) {
 | 
				
			||||||
  const { body, user, timestamp } = message;
 | 
					  const { body, user, timestamp } = message;
 | 
				
			||||||
  const { displayName, displayColor } = user;
 | 
					  const { displayName, displayColor } = user;
 | 
				
			||||||
  const color = `hsl(${displayColor}, 100%, 70%)`;
 | 
					  const color = `hsl(${displayColor}, 100%, 70%)`;
 | 
				
			||||||
@ -17,7 +27,7 @@ export default function ChatUserMessage({ message, showModeratorMenu }: Props) {
 | 
				
			|||||||
  const [formattedMessage, setFormattedMessage] = useState<string>(body);
 | 
					  const [formattedMessage, setFormattedMessage] = useState<string>(body);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  useEffect(() => {
 | 
					  useEffect(() => {
 | 
				
			||||||
    setFormattedMessage(formatMessageText(body));
 | 
					    setFormattedMessage(he.decode(body));
 | 
				
			||||||
  }, [message]);
 | 
					  }, [message]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
@ -25,7 +35,9 @@ export default function ChatUserMessage({ message, showModeratorMenu }: Props) {
 | 
				
			|||||||
      <div className={s.user} style={{ color }}>
 | 
					      <div className={s.user} style={{ color }}>
 | 
				
			||||||
        {displayName}
 | 
					        {displayName}
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
      <div className={s.message} dangerouslySetInnerHTML={{ __html: formattedMessage }} />
 | 
					      <Highlight search={highlightString}>
 | 
				
			||||||
 | 
					        <div className={s.message}>{formattedMessage}</div>
 | 
				
			||||||
 | 
					      </Highlight>
 | 
				
			||||||
      {showModeratorMenu && <div>Moderator menu</div>}
 | 
					      {showModeratorMenu && <div>Moderator menu</div>}
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
 | 
				
			|||||||
@ -47,6 +47,16 @@ export const chatDisplayNameAtom = atom<string>({
 | 
				
			|||||||
  default: null,
 | 
					  default: null,
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const chatUserIdAtom = atom<string>({
 | 
				
			||||||
 | 
					  key: 'chatUserIdAtom',
 | 
				
			||||||
 | 
					  default: null,
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const isChatModeratorAtom = atom<boolean>({
 | 
				
			||||||
 | 
					  key: 'isModeratorAtom',
 | 
				
			||||||
 | 
					  default: false,
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const accessTokenAtom = atom<string>({
 | 
					export const accessTokenAtom = atom<string>({
 | 
				
			||||||
  key: 'accessTokenAtom',
 | 
					  key: 'accessTokenAtom',
 | 
				
			||||||
  default: null,
 | 
					  default: null,
 | 
				
			||||||
@ -135,6 +145,8 @@ export function ClientConfigStore() {
 | 
				
			|||||||
  const [appState, appStateSend, appStateService] = useMachine(appStateModel);
 | 
					  const [appState, appStateSend, appStateService] = useMachine(appStateModel);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const setChatDisplayName = useSetRecoilState<string>(chatDisplayNameAtom);
 | 
					  const setChatDisplayName = useSetRecoilState<string>(chatDisplayNameAtom);
 | 
				
			||||||
 | 
					  const setChatUserId = useSetRecoilState<string>(chatUserIdAtom);
 | 
				
			||||||
 | 
					  const setIsChatModerator = useSetRecoilState<boolean>(isChatModeratorAtom);
 | 
				
			||||||
  const setClientConfig = useSetRecoilState<ClientConfig>(clientConfigStateAtom);
 | 
					  const setClientConfig = useSetRecoilState<ClientConfig>(clientConfigStateAtom);
 | 
				
			||||||
  const setServerStatus = useSetRecoilState<ServerStatus>(serverStatusState);
 | 
					  const setServerStatus = useSetRecoilState<ServerStatus>(serverStatusState);
 | 
				
			||||||
  const setClockSkew = useSetRecoilState<Number>(clockSkewAtom);
 | 
					  const setClockSkew = useSetRecoilState<Number>(clockSkewAtom);
 | 
				
			||||||
@ -236,7 +248,12 @@ export function ClientConfigStore() {
 | 
				
			|||||||
        resetAndReAuth();
 | 
					        resetAndReAuth();
 | 
				
			||||||
        break;
 | 
					        break;
 | 
				
			||||||
      case MessageType.CONNECTED_USER_INFO:
 | 
					      case MessageType.CONNECTED_USER_INFO:
 | 
				
			||||||
        handleConnectedClientInfoMessage(message as ConnectedClientInfoEvent, setChatDisplayName);
 | 
					        handleConnectedClientInfoMessage(
 | 
				
			||||||
 | 
					          message as ConnectedClientInfoEvent,
 | 
				
			||||||
 | 
					          setChatDisplayName,
 | 
				
			||||||
 | 
					          setChatUserId,
 | 
				
			||||||
 | 
					          setIsChatModerator,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
        break;
 | 
					        break;
 | 
				
			||||||
      case MessageType.CHAT:
 | 
					      case MessageType.CHAT:
 | 
				
			||||||
        handleChatMessage(message as ChatEvent, chatMessages, setChatMessages);
 | 
					        handleChatMessage(message as ChatEvent, chatMessages, setChatMessages);
 | 
				
			||||||
 | 
				
			|||||||
@ -3,8 +3,12 @@ import { ConnectedClientInfoEvent } from '../../../interfaces/socket-events';
 | 
				
			|||||||
export default function handleConnectedClientInfoMessage(
 | 
					export default function handleConnectedClientInfoMessage(
 | 
				
			||||||
  message: ConnectedClientInfoEvent,
 | 
					  message: ConnectedClientInfoEvent,
 | 
				
			||||||
  setChatDisplayName: (string) => void,
 | 
					  setChatDisplayName: (string) => void,
 | 
				
			||||||
 | 
					  setChatUserId: (number) => void,
 | 
				
			||||||
 | 
					  setIsChatModerator: (boolean) => void,
 | 
				
			||||||
) {
 | 
					) {
 | 
				
			||||||
  const { user } = message;
 | 
					  const { user } = message;
 | 
				
			||||||
  const { displayName } = user;
 | 
					  const { id, displayName, scopes } = user;
 | 
				
			||||||
  setChatDisplayName(displayName);
 | 
					  setChatDisplayName(displayName);
 | 
				
			||||||
 | 
					  setChatUserId(id);
 | 
				
			||||||
 | 
					  setIsChatModerator(scopes.includes('moderator'));
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -4,6 +4,8 @@ import { Layout, Tabs, Spin } from 'antd';
 | 
				
			|||||||
import {
 | 
					import {
 | 
				
			||||||
  clientConfigStateAtom,
 | 
					  clientConfigStateAtom,
 | 
				
			||||||
  chatMessagesAtom,
 | 
					  chatMessagesAtom,
 | 
				
			||||||
 | 
					  chatDisplayNameAtom,
 | 
				
			||||||
 | 
					  chatUserIdAtom,
 | 
				
			||||||
  isChatVisibleSelector,
 | 
					  isChatVisibleSelector,
 | 
				
			||||||
  serverStatusState,
 | 
					  serverStatusState,
 | 
				
			||||||
  appStateAtom,
 | 
					  appStateAtom,
 | 
				
			||||||
@ -43,6 +45,8 @@ export default function ContentComponent() {
 | 
				
			|||||||
  const isChatVisible = useRecoilValue<boolean>(isChatVisibleSelector);
 | 
					  const isChatVisible = useRecoilValue<boolean>(isChatVisibleSelector);
 | 
				
			||||||
  const messages = useRecoilValue<ChatMessage[]>(chatMessagesAtom);
 | 
					  const messages = useRecoilValue<ChatMessage[]>(chatMessagesAtom);
 | 
				
			||||||
  const online = useRecoilValue<boolean>(isOnlineSelector);
 | 
					  const online = useRecoilValue<boolean>(isOnlineSelector);
 | 
				
			||||||
 | 
					  const chatDisplayName = useRecoilValue<string>(chatDisplayNameAtom);
 | 
				
			||||||
 | 
					  const chatUserId = useRecoilValue<string>(chatUserIdAtom);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const { extraPageContent, version, socialHandles, name, title, tags, summary } = clientConfig;
 | 
					  const { extraPageContent, version, socialHandles, name, title, tags, summary } = clientConfig;
 | 
				
			||||||
  const { viewerCount, lastConnectTime, lastDisconnectTime } = status;
 | 
					  const { viewerCount, lastConnectTime, lastDisconnectTime } = status;
 | 
				
			||||||
@ -125,7 +129,13 @@ export default function ContentComponent() {
 | 
				
			|||||||
          </Tabs>
 | 
					          </Tabs>
 | 
				
			||||||
          {isChatVisible && (
 | 
					          {isChatVisible && (
 | 
				
			||||||
            <div className={`${s.mobileChat}`}>
 | 
					            <div className={`${s.mobileChat}`}>
 | 
				
			||||||
              <ChatContainer messages={messages} loading={appState.chatLoading} />
 | 
					              <ChatContainer
 | 
				
			||||||
 | 
					                messages={messages}
 | 
				
			||||||
 | 
					                loading={appState.chatLoading}
 | 
				
			||||||
 | 
					                usernameToHighlight={chatDisplayName}
 | 
				
			||||||
 | 
					                chatUserId={chatUserId}
 | 
				
			||||||
 | 
					                isModerator={false}
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
              <ChatTextField />
 | 
					              <ChatTextField />
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
          )}
 | 
					          )}
 | 
				
			||||||
 | 
				
			|||||||
@ -4,16 +4,29 @@ import { ChatMessage } from '../../../interfaces/chat-message.model';
 | 
				
			|||||||
import { ChatContainer, ChatTextField } from '../../chat';
 | 
					import { ChatContainer, ChatTextField } from '../../chat';
 | 
				
			||||||
import s from './Sidebar.module.scss';
 | 
					import s from './Sidebar.module.scss';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { chatMessagesAtom, appStateAtom } from '../../stores/ClientConfigStore';
 | 
					import {
 | 
				
			||||||
 | 
					  chatMessagesAtom,
 | 
				
			||||||
 | 
					  appStateAtom,
 | 
				
			||||||
 | 
					  chatDisplayNameAtom,
 | 
				
			||||||
 | 
					  chatUserIdAtom,
 | 
				
			||||||
 | 
					} from '../../stores/ClientConfigStore';
 | 
				
			||||||
import { AppStateOptions } from '../../stores/application-state';
 | 
					import { AppStateOptions } from '../../stores/application-state';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default function Sidebar() {
 | 
					export default function Sidebar() {
 | 
				
			||||||
  const messages = useRecoilValue<ChatMessage[]>(chatMessagesAtom);
 | 
					  const messages = useRecoilValue<ChatMessage[]>(chatMessagesAtom);
 | 
				
			||||||
  const appState = useRecoilValue<AppStateOptions>(appStateAtom);
 | 
					  const appState = useRecoilValue<AppStateOptions>(appStateAtom);
 | 
				
			||||||
 | 
					  const chatDisplayName = useRecoilValue<string>(chatDisplayNameAtom);
 | 
				
			||||||
 | 
					  const chatUserId = useRecoilValue<string>(chatUserIdAtom);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <Sider className={s.root} collapsedWidth={0} width={320}>
 | 
					    <Sider className={s.root} collapsedWidth={0} width={320}>
 | 
				
			||||||
      <ChatContainer messages={messages} loading={appState.chatLoading} />
 | 
					      <ChatContainer
 | 
				
			||||||
 | 
					        messages={messages}
 | 
				
			||||||
 | 
					        loading={appState.chatLoading}
 | 
				
			||||||
 | 
					        usernameToHighlight={chatDisplayName}
 | 
				
			||||||
 | 
					        chatUserId={chatUserId}
 | 
				
			||||||
 | 
					        isModerator={false}
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
      <ChatTextField />
 | 
					      <ChatTextField />
 | 
				
			||||||
    </Sider>
 | 
					    </Sider>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										18
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										18
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							@ -20,6 +20,7 @@
 | 
				
			|||||||
        "chartkick": "4.1.1",
 | 
					        "chartkick": "4.1.1",
 | 
				
			||||||
        "classnames": "2.3.1",
 | 
					        "classnames": "2.3.1",
 | 
				
			||||||
        "date-fns": "2.28.0",
 | 
					        "date-fns": "2.28.0",
 | 
				
			||||||
 | 
					        "he": "^1.2.0",
 | 
				
			||||||
        "lodash": "4.17.21",
 | 
					        "lodash": "4.17.21",
 | 
				
			||||||
        "markdown-it": "12.3.2",
 | 
					        "markdown-it": "12.3.2",
 | 
				
			||||||
        "next": "^12.1.5",
 | 
					        "next": "^12.1.5",
 | 
				
			||||||
@ -34,6 +35,7 @@
 | 
				
			|||||||
        "react-contenteditable": "^3.3.6",
 | 
					        "react-contenteditable": "^3.3.6",
 | 
				
			||||||
        "react-crossfade-img": "^1.0.0",
 | 
					        "react-crossfade-img": "^1.0.0",
 | 
				
			||||||
        "react-dom": "17.0.2",
 | 
					        "react-dom": "17.0.2",
 | 
				
			||||||
 | 
					        "react-highlighter-ts": "^2.2.0",
 | 
				
			||||||
        "react-hotkeys-hook": "^3.4.6",
 | 
					        "react-hotkeys-hook": "^3.4.6",
 | 
				
			||||||
        "react-linkify": "1.0.0-alpha",
 | 
					        "react-linkify": "1.0.0-alpha",
 | 
				
			||||||
        "react-markdown": "8.0.0",
 | 
					        "react-markdown": "8.0.0",
 | 
				
			||||||
@ -27300,6 +27302,14 @@
 | 
				
			|||||||
        "react-dom": "^16.6.0 || ^17.0.0 || ^18.0.0"
 | 
					        "react-dom": "^16.6.0 || ^17.0.0 || ^18.0.0"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/react-highlighter-ts": {
 | 
				
			||||||
 | 
					      "version": "2.2.0",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/react-highlighter-ts/-/react-highlighter-ts-2.2.0.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-wZgYrOq6bO1O1szjyduvqeiuV1DuZbKb22FvbclXEhyG5rw9vFaJhdO420RwBvDkD4DvFnDLX4hFuAOg0aQkXA==",
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "react": "^17.0.1"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/react-hotkeys-hook": {
 | 
					    "node_modules/react-hotkeys-hook": {
 | 
				
			||||||
      "version": "3.4.6",
 | 
					      "version": "3.4.6",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/react-hotkeys-hook/-/react-hotkeys-hook-3.4.6.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/react-hotkeys-hook/-/react-hotkeys-hook-3.4.6.tgz",
 | 
				
			||||||
@ -53410,6 +53420,14 @@
 | 
				
			|||||||
        "shallowequal": "^1.1.0"
 | 
					        "shallowequal": "^1.1.0"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "react-highlighter-ts": {
 | 
				
			||||||
 | 
					      "version": "2.2.0",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/react-highlighter-ts/-/react-highlighter-ts-2.2.0.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-wZgYrOq6bO1O1szjyduvqeiuV1DuZbKb22FvbclXEhyG5rw9vFaJhdO420RwBvDkD4DvFnDLX4hFuAOg0aQkXA==",
 | 
				
			||||||
 | 
					      "requires": {
 | 
				
			||||||
 | 
					        "react": "^17.0.1"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "react-hotkeys-hook": {
 | 
					    "react-hotkeys-hook": {
 | 
				
			||||||
      "version": "3.4.6",
 | 
					      "version": "3.4.6",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/react-hotkeys-hook/-/react-hotkeys-hook-3.4.6.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/react-hotkeys-hook/-/react-hotkeys-hook-3.4.6.tgz",
 | 
				
			||||||
 | 
				
			|||||||
@ -24,6 +24,7 @@
 | 
				
			|||||||
    "chartkick": "4.1.1",
 | 
					    "chartkick": "4.1.1",
 | 
				
			||||||
    "classnames": "2.3.1",
 | 
					    "classnames": "2.3.1",
 | 
				
			||||||
    "date-fns": "2.28.0",
 | 
					    "date-fns": "2.28.0",
 | 
				
			||||||
 | 
					    "he": "^1.2.0",
 | 
				
			||||||
    "lodash": "4.17.21",
 | 
					    "lodash": "4.17.21",
 | 
				
			||||||
    "markdown-it": "12.3.2",
 | 
					    "markdown-it": "12.3.2",
 | 
				
			||||||
    "next": "^12.1.5",
 | 
					    "next": "^12.1.5",
 | 
				
			||||||
@ -38,6 +39,7 @@
 | 
				
			|||||||
    "react-contenteditable": "^3.3.6",
 | 
					    "react-contenteditable": "^3.3.6",
 | 
				
			||||||
    "react-crossfade-img": "^1.0.0",
 | 
					    "react-crossfade-img": "^1.0.0",
 | 
				
			||||||
    "react-dom": "17.0.2",
 | 
					    "react-dom": "17.0.2",
 | 
				
			||||||
 | 
					    "react-highlighter-ts": "^2.2.0",
 | 
				
			||||||
    "react-hotkeys-hook": "^3.4.6",
 | 
					    "react-hotkeys-hook": "^3.4.6",
 | 
				
			||||||
    "react-linkify": "1.0.0-alpha",
 | 
					    "react-linkify": "1.0.0-alpha",
 | 
				
			||||||
    "react-markdown": "8.0.0",
 | 
					    "react-markdown": "8.0.0",
 | 
				
			||||||
 | 
				
			|||||||
@ -32,7 +32,13 @@ const AddMessagesChatExample = args => {
 | 
				
			|||||||
      <button type="button" onClick={() => setChatMessages([...chatMessages, chatMessages[0]])}>
 | 
					      <button type="button" onClick={() => setChatMessages([...chatMessages, chatMessages[0]])}>
 | 
				
			||||||
        Add message
 | 
					        Add message
 | 
				
			||||||
      </button>
 | 
					      </button>
 | 
				
			||||||
      <ChatContainer messages={chatMessages} loading={loading} />
 | 
					      <ChatContainer
 | 
				
			||||||
 | 
					        messages={chatMessages}
 | 
				
			||||||
 | 
					        loading={loading}
 | 
				
			||||||
 | 
					        usernameToHighlight={null}
 | 
				
			||||||
 | 
					        chatUserId={null}
 | 
				
			||||||
 | 
					        isModerator={false}
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
				
			|||||||
@ -92,3 +92,10 @@ FromAuthenticatedUser.args = {
 | 
				
			|||||||
  message: authenticatedUserMessage,
 | 
					  message: authenticatedUserMessage,
 | 
				
			||||||
  showModeratorMenu: false,
 | 
					  showModeratorMenu: false,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const WithStringHighlighted = Template.bind({});
 | 
				
			||||||
 | 
					WithStringHighlighted.args = {
 | 
				
			||||||
 | 
					  message: standardMessage,
 | 
				
			||||||
 | 
					  showModeratorMenu: false,
 | 
				
			||||||
 | 
					  highlightString: 'message',
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user