Require authentication to participate in chat (#4762)
* feat(chat): require authentication to participate in chat * fix: it's pretty much impossible to bypass the auth requirement, addressing review feedback anyway * feat(chat): render chat text input as disabled if chat auth is required * Commit updated API documentation --------- Co-authored-by: Owncast <owncast@owncast.online>
This commit is contained in:
@@ -618,6 +618,22 @@ export const ChatDisabled = {
|
||||
},
|
||||
};
|
||||
|
||||
export const AuthenticationRequired = {
|
||||
render: Template,
|
||||
|
||||
args: {
|
||||
loading: false,
|
||||
messages,
|
||||
usernameToHighlight: 'testuser',
|
||||
chatUserId: 'testuser',
|
||||
isModerator: false,
|
||||
showInput: true,
|
||||
chatAvailable: true,
|
||||
inputEnabled: false,
|
||||
inputDisabledPlaceholder: 'Authenticate to chat',
|
||||
},
|
||||
};
|
||||
|
||||
export const SingleMessage = {
|
||||
render: Template,
|
||||
|
||||
|
||||
@@ -34,6 +34,8 @@ export type ChatContainerProps = {
|
||||
focusInput?: boolean;
|
||||
desktop?: boolean;
|
||||
readonly?: boolean;
|
||||
inputEnabled?: boolean;
|
||||
inputDisabledPlaceholder?: string;
|
||||
};
|
||||
|
||||
let resizeWindowCallback: () => void;
|
||||
@@ -82,11 +84,15 @@ export const ChatContainer: FC<ChatContainerProps> = ({
|
||||
isModerator,
|
||||
showInput = true,
|
||||
height = 'auto',
|
||||
chatAvailable: chatEnabled,
|
||||
chatAvailable,
|
||||
desktop,
|
||||
focusInput = true,
|
||||
readonly = false,
|
||||
inputEnabled,
|
||||
inputDisabledPlaceholder,
|
||||
}) => {
|
||||
// If inputEnabled is explicitly set, use that; otherwise fall back to chatAvailable
|
||||
const chatInputEnabled = inputEnabled !== undefined ? inputEnabled : chatAvailable;
|
||||
const [showScrollToBottomButton, setShowScrollToBottomButton] = useState(false);
|
||||
const [isAtBottom, setIsAtBottom] = useState(false);
|
||||
|
||||
@@ -366,7 +372,11 @@ export const ChatContainer: FC<ChatContainerProps> = ({
|
||||
{MessagesTable}
|
||||
{showInput && (
|
||||
<div className={styles.chatTextField}>
|
||||
<ChatTextField enabled={chatEnabled} focusInput={focusInput} />
|
||||
<ChatTextField
|
||||
enabled={chatInputEnabled}
|
||||
focusInput={focusInput}
|
||||
disabledPlaceholder={inputDisabledPlaceholder}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{desktop && (
|
||||
|
||||
@@ -30,6 +30,7 @@ export type ChatTextFieldProps = {
|
||||
defaultText?: string;
|
||||
enabled: boolean;
|
||||
focusInput: boolean;
|
||||
disabledPlaceholder?: string;
|
||||
};
|
||||
|
||||
const characterLimit = 300;
|
||||
@@ -119,7 +120,12 @@ const getTextContent = node => {
|
||||
return text;
|
||||
};
|
||||
|
||||
export const ChatTextField: FC<ChatTextFieldProps> = ({ defaultText, enabled, focusInput }) => {
|
||||
export const ChatTextField: FC<ChatTextFieldProps> = ({
|
||||
defaultText,
|
||||
enabled,
|
||||
focusInput,
|
||||
disabledPlaceholder,
|
||||
}) => {
|
||||
const [inputDraft, setInputDraft] = useRecoilState(chatInputDraftAtom);
|
||||
const [characterCount, setCharacterCount] = useState(defaultText?.length);
|
||||
const websocketService = useRecoilValue<WebsocketService>(websocketServiceAtom);
|
||||
@@ -276,7 +282,9 @@ export const ChatTextField: FC<ChatTextFieldProps> = ({ defaultText, enabled, fo
|
||||
<ContentEditable
|
||||
id="chat-input-content-editable"
|
||||
html={defaultText || ''}
|
||||
placeholder={enabled ? 'Send a message to chat' : 'Chat is disabled'}
|
||||
placeholder={
|
||||
enabled ? 'Send a message to chat' : disabledPlaceholder || 'Chat is disabled'
|
||||
}
|
||||
disabled={!enabled}
|
||||
onKeyDown={onKeyDown}
|
||||
onContentChange={handleChange}
|
||||
|
||||
@@ -14,9 +14,17 @@ export type ChatModalProps = {
|
||||
messages: ChatMessage[];
|
||||
currentUser: CurrentUser;
|
||||
handleClose: () => void;
|
||||
inputEnabled?: boolean;
|
||||
inputDisabledPlaceholder?: string;
|
||||
};
|
||||
|
||||
export const ChatModal: FC<ChatModalProps> = ({ messages, currentUser, handleClose }) => {
|
||||
export const ChatModal: FC<ChatModalProps> = ({
|
||||
messages,
|
||||
currentUser,
|
||||
handleClose,
|
||||
inputEnabled = true,
|
||||
inputDisabledPlaceholder,
|
||||
}) => {
|
||||
if (!currentUser) {
|
||||
return null;
|
||||
}
|
||||
@@ -58,6 +66,8 @@ export const ChatModal: FC<ChatModalProps> = ({ messages, currentUser, handleClo
|
||||
chatUserId={id}
|
||||
isModerator={isModerator}
|
||||
chatAvailable
|
||||
inputEnabled={inputEnabled}
|
||||
inputDisabledPlaceholder={inputDisabledPlaceholder}
|
||||
focusInput={false}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
@@ -4,9 +4,11 @@ import MessageFilled from '@ant-design/icons/MessageFilled';
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import dynamic from 'next/dynamic';
|
||||
import classnames from 'classnames';
|
||||
import { useTranslation } from 'next-export-i18n';
|
||||
import ActionButtons from './ActionButtons';
|
||||
import { LOCAL_STORAGE_KEYS, getLocalStorage, setLocalStorage } from '../../../utils/localStorage';
|
||||
import { canPushNotificationsBeSupported } from '../../../utils/browserPushNotifications';
|
||||
import { Localization } from '../../../types/localization';
|
||||
|
||||
import {
|
||||
clientConfigStateAtom,
|
||||
@@ -19,6 +21,7 @@ import {
|
||||
serverStatusState,
|
||||
isChatAvailableSelector,
|
||||
visibleChatMessagesSelector,
|
||||
chatAuthenticatedAtom,
|
||||
} from '../../stores/ClientConfigStore';
|
||||
import { ClientConfig } from '../../../interfaces/client-config.model';
|
||||
|
||||
@@ -97,6 +100,7 @@ const ExternalModal = ({ externalActionToDisplay, setExternalActionToDisplay })
|
||||
};
|
||||
|
||||
export const Content: FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const appState = useRecoilValue<AppStateOptions>(appStateAtom);
|
||||
const clientConfig = useRecoilValue<ClientConfig>(clientConfigStateAtom);
|
||||
const chatState = useRecoilValue<ChatState>(chatStateAtom);
|
||||
@@ -106,6 +110,7 @@ export const Content: FC = () => {
|
||||
const messages = useRecoilValue<ChatMessage[]>(visibleChatMessagesSelector);
|
||||
const online = useRecoilValue<boolean>(isOnlineSelector);
|
||||
const isChatAvailable = useRecoilValue<boolean>(isChatAvailableSelector);
|
||||
const isUserAuthenticated = useRecoilValue<boolean>(chatAuthenticatedAtom);
|
||||
|
||||
const { viewerCount, lastConnectTime, lastDisconnectTime, streamTitle } =
|
||||
useRecoilValue<ServerStatus>(serverStatusState);
|
||||
@@ -118,6 +123,7 @@ export const Content: FC = () => {
|
||||
externalActions,
|
||||
offlineMessage,
|
||||
chatDisabled,
|
||||
chatRequireAuthentication,
|
||||
federation,
|
||||
notifications,
|
||||
} = clientConfig;
|
||||
@@ -219,6 +225,16 @@ export const Content: FC = () => {
|
||||
|
||||
const showChat = isChatAvailable && !chatDisabled && chatState === ChatState.VISIBLE;
|
||||
|
||||
// Determine if chat input should be enabled based on authentication requirements.
|
||||
// Moderators bypass the authentication requirement.
|
||||
const chatInputEnabled = !!(
|
||||
isChatAvailable &&
|
||||
(!chatRequireAuthentication || isUserAuthenticated || currentUser?.isModerator)
|
||||
);
|
||||
const chatInputDisabledMessage = chatRequireAuthentication
|
||||
? t(Localization.Frontend.Chat.authenticateToChat)
|
||||
: t(Localization.Frontend.chatDisabled);
|
||||
|
||||
return (
|
||||
<div className={styles.main}>
|
||||
<div className={styles.mainColumn}>
|
||||
@@ -325,6 +341,8 @@ export const Content: FC = () => {
|
||||
isModerator={currentUser.isModerator}
|
||||
chatAvailable={isChatAvailable}
|
||||
showInput={!!currentUser}
|
||||
inputEnabled={chatInputEnabled}
|
||||
inputDisabledPlaceholder={chatInputDisabledMessage}
|
||||
desktop
|
||||
/>
|
||||
)}
|
||||
@@ -351,6 +369,8 @@ export const Content: FC = () => {
|
||||
messages={messages}
|
||||
currentUser={currentUser}
|
||||
handleClose={() => setShowChatModal(false)}
|
||||
inputEnabled={chatInputEnabled}
|
||||
inputDisabledPlaceholder={chatInputDisabledMessage}
|
||||
/>
|
||||
)}
|
||||
{isMobile && isChatAvailable && !chatDisabled && (
|
||||
|
||||
Reference in New Issue
Block a user