diff --git a/controllers/admin/config.go b/controllers/admin/config.go index b28e0d5ab..29aac58e0 100644 --- a/controllers/admin/config.go +++ b/controllers/admin/config.go @@ -435,6 +435,23 @@ func SetSocialHandles(w http.ResponseWriter, r *http.Request) { controllers.WriteSimpleResponse(w, true, "social handles updated") } +// SetChatDisabled will disable chat functionality. +func SetChatDisabled(w http.ResponseWriter, r *http.Request) { + if !requirePOST(w, r) { + return + } + + configValue, success := getValueFromRequest(w, r) + if !success { + controllers.WriteSimpleResponse(w, false, "unable to update chat disabled") + return + } + + data.SetChatDisabled(configValue.Value.(bool)) + + controllers.WriteSimpleResponse(w, true, "chat disabled status updated") +} + func requirePOST(w http.ResponseWriter, r *http.Request) bool { if r.Method != controllers.POST { controllers.WriteSimpleResponse(w, false, r.Method+" not supported") diff --git a/controllers/admin/serverConfig.go b/controllers/admin/serverConfig.go index 97ce35f9a..fb721dd0c 100644 --- a/controllers/admin/serverConfig.go +++ b/controllers/admin/serverConfig.go @@ -43,6 +43,7 @@ func GetServerConfig(w http.ResponseWriter, r *http.Request) { StreamKey: data.GetStreamKey(), WebServerPort: config.WebServerPort, RTMPServerPort: data.GetRTMPPortNumber(), + ChatDisabled: data.GetChatDisabled(), VideoSettings: videoSettings{ VideoQualityVariants: videoQualityVariants, LatencyLevel: data.GetStreamLatencyLevel().Level, @@ -71,6 +72,7 @@ type serverConfigAdminResponse struct { VideoSettings videoSettings `json:"videoSettings"` LatencyLevel int `json:"latencyLevel"` YP yp `json:"yp"` + ChatDisabled bool `json:"chatDisabled"` } type videoSettings struct { diff --git a/controllers/config.go b/controllers/config.go index cffb8d16d..a5016baf0 100644 --- a/controllers/config.go +++ b/controllers/config.go @@ -21,6 +21,7 @@ type webConfigResponse struct { ExtraPageContent string `json:"extraPageContent"` StreamTitle string `json:"streamTitle,omitempty"` // What's going on with the current stream SocialHandles []models.SocialHandle `json:"socialHandles"` + ChatDisabled bool `json:"chatDisabled"` } // GetWebConfig gets the status of the server. @@ -48,6 +49,7 @@ func GetWebConfig(w http.ResponseWriter, r *http.Request) { ExtraPageContent: pageContent, StreamTitle: data.GetStreamTitle(), SocialHandles: socialHandles, + ChatDisabled: data.GetChatDisabled(), } if err := json.NewEncoder(w).Encode(configuration); err != nil { diff --git a/core/chat/server.go b/core/chat/server.go index 3c2add7b0..9ce5c5ee8 100644 --- a/core/chat/server.go +++ b/core/chat/server.go @@ -135,6 +135,10 @@ func (s *server) Listen() { case c := <-s.delCh: s.removeClient(c) case msg := <-s.sendAllCh: + if data.GetChatDisabled() { + break + } + if !msg.Empty() { // set defaults before sending msg to anywhere msg.SetDefaults() diff --git a/core/data/config.go b/core/data/config.go index 31ed87c6f..6a629a58c 100644 --- a/core/data/config.go +++ b/core/data/config.go @@ -33,6 +33,7 @@ const s3StorageEnabledKey = "s3_storage_enabled" const s3StorageConfigKey = "s3_storage_config" const videoLatencyLevel = "video_latency_level" const videoStreamOutputVariantsKey = "video_stream_output_variants" +const chatDisabledKey = "chat_disabled" // GetExtraPageBodyContent will return the user-supplied body content. func GetExtraPageBodyContent() string { @@ -408,6 +409,21 @@ func SetStreamOutputVariants(variants []models.StreamOutputVariant) error { return _datastore.Save(configEntry) } +// SetChatDisabled will disable chat if set to true. +func SetChatDisabled(disabled bool) error { + return _datastore.SetBool(chatDisabledKey, disabled) +} + +// GetChatDisabled will return if chat is disabled. +func GetChatDisabled() bool { + disabled, err := _datastore.GetBool(chatDisabledKey) + if err == nil { + return disabled + } + + return false +} + // VerifySettings will perform a sanity check for specific settings values. func VerifySettings() error { if GetStreamKey() == "" { diff --git a/router/router.go b/router/router.go index 6fa042404..adc3c1f7c 100644 --- a/router/router.go +++ b/router/router.go @@ -109,6 +109,9 @@ func Start() error { // Server summary http.HandleFunc("/api/admin/config/serversummary", middleware.RequireAdminAuth(admin.SetServerSummary)) + // Disable chat + http.HandleFunc("/api/admin/config/chat/disable", middleware.RequireAdminAuth(admin.SetChatDisabled)) + // Return all webhooks http.HandleFunc("/api/admin/webhooks", middleware.RequireAdminAuth(admin.GetWebhooks)) diff --git a/webroot/js/app.js b/webroot/js/app.js index 581898029..7d65875d4 100644 --- a/webroot/js/app.js +++ b/webroot/js/app.js @@ -8,7 +8,11 @@ import UsernameForm from './components/chat/username.js'; import VideoPoster from './components/video-poster.js'; import Chat from './components/chat/chat.js'; import Websocket from './utils/websocket.js'; -import { parseSecondsToDurationString, hasTouchScreen, getOrientation } from './utils/helpers.js'; +import { + parseSecondsToDurationString, + hasTouchScreen, + getOrientation, +} from './utils/helpers.js'; import { addNewlines, @@ -50,6 +54,7 @@ export default class App extends Component { websocket: new Websocket(), displayChat: chatStorage === null ? true : chatStorage, chatInputEnabled: false, // chat input box state + chatDisabled: false, username: getLocalStorage(KEY_USERNAME) || generateUsername(), touchKeyboardActive: false, @@ -195,12 +200,7 @@ export default class App extends Component { if (!status) { return; } - const { - viewerCount, - online, - lastConnectTime, - streamTitle, - } = status; + const { viewerCount, online, lastConnectTime, streamTitle } = status; this.lastDisconnectTime = status.lastDisconnectTime; @@ -265,7 +265,9 @@ export default class App extends Component { } if (this.windowBlurred) { - document.title = ` 🔴 ${this.state.configData && this.state.configData.name}`; + document.title = ` 🔴 ${ + this.state.configData && this.state.configData.name + }`; } } @@ -289,7 +291,9 @@ export default class App extends Component { }); if (this.windowBlurred) { - document.title = ` 🟢 ${this.state.configData && this.state.configData.name}`; + document.title = ` 🟢 ${ + this.state.configData && this.state.configData.name + }`; } } @@ -302,7 +306,6 @@ export default class App extends Component { this.setState({ streamStatusMessage: `${MESSAGE_ONLINE} ${streamDurationString}`, }); - } handleUsernameChange(newName) { @@ -370,7 +373,7 @@ export default class App extends Component { handleSpaceBarPressed(e) { e.preventDefault(); - if(this.state.isPlaying) { + if (this.state.isPlaying) { this.setState({ isPlaying: false, }); @@ -384,7 +387,11 @@ export default class App extends Component { } handleKeyPressed(e) { - if (e.code === 'Space' && e.target === document.body && this.state.streamOnline) { + if ( + e.code === 'Space' && + e.target === document.body && + this.state.streamOnline + ) { this.handleSpaceBarPressed(e); } } @@ -408,7 +415,6 @@ export default class App extends Component { windowWidth, } = state; - const { version: appVersion, logo = TEMP_IMAGE, @@ -417,45 +423,52 @@ export default class App extends Component { tags = [], name, extraPageContent, + chatDisabled, } = configData; const bgUserLogo = { backgroundImage: `url(${logo})` }; - const tagList = (tags !== null && tags.length > 0) - ? tags.map( - (tag, index) => html` -
  • - ${tag} -
  • - ` - ) - : null; + const tagList = + tags !== null && tags.length > 0 + ? tags.map( + (tag, index) => html` +
  • + ${tag} +
  • + ` + ) + : null; - const viewerCountMessage = streamOnline && viewerCount > 0 ? ( - html`${viewerCount} ${pluralize('viewer', viewerCount)}` - ) : null; + const viewerCountMessage = + streamOnline && viewerCount > 0 + ? html`${viewerCount} ${pluralize('viewer', viewerCount)}` + : null; const mainClass = playerActive ? 'online' : ''; - const isPortrait = this.hasTouchScreen && orientation === ORIENTATION_PORTRAIT; + const isPortrait = + this.hasTouchScreen && orientation === ORIENTATION_PORTRAIT; const shortHeight = windowHeight <= HEIGHT_SHORT_WIDE && !isPortrait; const singleColMode = windowWidth <= WIDTH_SINGLE_COL && !shortHeight; + const shouldDisplayChat = displayChat && !chatDisabled; + const usernameStyle = chatDisabled ? 'none' : 'flex'; + const extraAppClasses = classNames({ - chat: displayChat, - 'no-chat': !displayChat, + chat: shouldDisplayChat, + 'no-chat': !shouldDisplayChat, 'single-col': singleColMode, - 'bg-gray-800': singleColMode && displayChat, + 'bg-gray-800': singleColMode && shouldDisplayChat, 'short-wide': shortHeight && windowWidth > WIDTH_SINGLE_COL, 'touch-screen': this.hasTouchScreen, 'touch-keyboard-active': touchKeyboardActive, }); - const poster = isPlaying ? null : html` - <${VideoPoster} offlineImage=${logo} active=${streamOnline} /> - `; + const poster = isPlaying + ? null + : html` <${VideoPoster} offlineImage=${logo} active=${streamOnline} /> `; return html`
    - + ${(streamOnline && streamTitle) ? streamTitle : name}${streamOnline && streamTitle ? streamTitle : name}
    <${UsernameForm} username=${username} @@ -538,9 +556,7 @@ export default class App extends Component { class="user-content-header border-b border-gray-500 border-solid" >

    - ${name} + ${name}

    ${streamOnline && streamTitle} @@ -573,11 +589,10 @@ export default class App extends Component { <${Chat} websocket=${websocket} username=${username} - chatInputEnabled=${chatInputEnabled} + chatInputEnabled=${chatInputEnabled && !chatDisabled} instanceTitle=${name} />

    `; } } - diff --git a/webroot/js/components/chat/chat-input.js b/webroot/js/components/chat/chat-input.js index 511e68241..5b473b653 100644 --- a/webroot/js/components/chat/chat-input.js +++ b/webroot/js/components/chat/chat-input.js @@ -5,8 +5,17 @@ const html = htm.bind(h); import { EmojiButton } from '/js/web_modules/@joeattardi/emoji-button.js'; import ContentEditable, { replaceCaret } from './content-editable.js'; -import { generatePlaceholderText, getCaretPosition, convertToText, convertOnPaste } from '../../utils/chat.js'; -import { getLocalStorage, setLocalStorage, classNames } from '../../utils/helpers.js'; +import { + generatePlaceholderText, + getCaretPosition, + convertToText, + convertOnPaste, +} from '../../utils/chat.js'; +import { + getLocalStorage, + setLocalStorage, + classNames, +} from '../../utils/helpers.js'; import { URL_CUSTOM_EMOJIS, KEY_CHAT_FIRST_MESSAGE_SENT, @@ -46,7 +55,9 @@ export default class ChatInput extends Component { this.handleSubmitChatButton = this.handleSubmitChatButton.bind(this); this.handlePaste = this.handlePaste.bind(this); - this.handleContentEditableChange = this.handleContentEditableChange.bind(this); + this.handleContentEditableChange = this.handleContentEditableChange.bind( + this + ); } componentDidMount() { @@ -55,13 +66,13 @@ export default class ChatInput extends Component { getCustomEmojis() { fetch(URL_CUSTOM_EMOJIS) - .then(response => { + .then((response) => { if (!response.ok) { throw new Error(`Network response was not ok ${response.ok}`); } return response.json(); }) - .then(json => { + .then((json) => { this.emojiPicker = new EmojiButton({ zIndex: 100, theme: 'owncast', // see chat.css @@ -75,7 +86,7 @@ export default class ChatInput extends Component { position: 'right-start', strategy: 'absolute', }); - this.emojiPicker.on('emoji', emoji => { + this.emojiPicker.on('emoji', (emoji) => { this.handleEmojiSelected(emoji); }); this.emojiPicker.on('hidden', () => { @@ -83,7 +94,7 @@ export default class ChatInput extends Component { replaceCaret(this.formMessageInput.current); }); }) - .catch(error => { + .catch((error) => { // this.handleNetworkingError(`Emoji Fetch: ${error}`); }); } @@ -98,9 +109,9 @@ export default class ChatInput extends Component { const { inputHTML } = this.state; let content = ''; if (emoji.url) { - const url = location.protocol + "//" + location.host + "/" + emoji.url; + const url = location.protocol + '//' + location.host + '/' + emoji.url; const name = url.split('\\').pop().split('/').pop(); - content = "\"""; + content = '' + name + ''; } else { content = emoji.emoji; } @@ -109,11 +120,11 @@ export default class ChatInput extends Component { inputHTML: inputHTML + content, }); // a hacky way add focus back into input field - setTimeout( () => { + setTimeout(() => { const input = this.formMessageInput.current; input.focus(); replaceCaret(input); - }, 100); + }, 100); } // autocomplete user names @@ -138,7 +149,10 @@ export default class ChatInput extends Component { return username.toLowerCase().startsWith(partial.toLowerCase()); }); - if (this.completionIndex === undefined || ++this.completionIndex >= possibilities.length) { + if ( + this.completionIndex === undefined || + ++this.completionIndex >= possibilities.length + ) { this.completionIndex = 0; } @@ -146,8 +160,12 @@ export default class ChatInput extends Component { this.suggestion = possibilities[this.completionIndex]; this.setState({ - inputHTML: inputHTML.substring(0, at + 1) + this.suggestion + ' ' + inputHTML.substring(position), - }) + inputHTML: + inputHTML.substring(0, at + 1) + + this.suggestion + + ' ' + + inputHTML.substring(position), + }); } return true; @@ -204,7 +222,7 @@ export default class ChatInput extends Component { const { key } = event; if (key === 'Control' || key === 'Shift') { - this.prepNewLine = false; + this.prepNewLine = false; } if (CHAT_KEY_MODIFIERS.includes(key)) { this.modifierKeyPressed = false; @@ -264,52 +282,61 @@ export default class ChatInput extends Component { render(props, state) { const { hasSentFirstChatMessage, inputCharsLeft, inputHTML } = state; - const { inputEnabled } = props; + const { inputEnabled, chatDisabled } = props; const emojiButtonStyle = { display: this.emojiPicker && inputCharsLeft > 0 ? 'block' : 'none', }; const extraClasses = classNames({ 'display-count': inputCharsLeft <= CHAT_CHAR_COUNT_BUFFER, }); - const placeholderText = generatePlaceholderText(inputEnabled, hasSentFirstChatMessage); - return ( - html` -
    + const placeholderText = generatePlaceholderText( + inputEnabled, + hasSentFirstChatMessage + ); + return html` +
    +
    + <${ContentEditable} + id="message-input" + class="appearance-none block w-full bg-transparent text-sm text-gray-700 h-full focus:outline-none" + placeholderText=${placeholderText} + innerRef=${this.formMessageInput} + html=${inputHTML} + disabled=${!inputEnabled} + onChange=${this.handleContentEditableChange} + onKeyDown=${this.handleMessageInputKeydown} + onKeyUp=${this.handleMessageInputKeyup} + onBlur=${this.handleMessageInputBlur} + onPaste=${this.handlePaste} + /> +
    +
    + -
    - <${ContentEditable} - id="message-input" - class="appearance-none block w-full bg-transparent text-sm text-gray-700 h-full focus:outline-none" - - placeholderText=${placeholderText} - innerRef=${this.formMessageInput} - html=${inputHTML} - disabled=${!inputEnabled} - onChange=${this.handleContentEditableChange} - onKeyDown=${this.handleMessageInputKeydown} - onKeyUp=${this.handleMessageInputKeyup} - onBlur=${this.handleMessageInputBlur} - - onPaste=${this.handlePaste} - /> -
    -
    - - - ${inputCharsLeft}/${CHAT_MAX_MESSAGE_LENGTH} -
    + ${inputCharsLeft}/${CHAT_MAX_MESSAGE_LENGTH} +
    - `); - } - + `; } +}