You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
340 lines
8.9 KiB
340 lines
8.9 KiB
import { h, Component } from '/js/web_modules/preact.js'; |
|
import htm from '/js/web_modules/htm.js'; |
|
const html = htm.bind(h); |
|
import UsernameForm from './components/chat/username.js'; |
|
import Chat from './components/chat/chat.js'; |
|
import Websocket, { |
|
CALLBACKS, |
|
SOCKET_MESSAGE_TYPES, |
|
} from './utils/websocket.js'; |
|
import { registerChat } from './chat/register.js'; |
|
import { |
|
getLocalStorage, |
|
setLocalStorage, |
|
} from './utils/helpers.js'; |
|
import { |
|
CHAT_MAX_MESSAGE_LENGTH, |
|
EST_SOCKET_PAYLOAD_BUFFER, |
|
KEY_EMBED_CHAT_ACCESS_TOKEN, |
|
KEY_ACCESS_TOKEN, |
|
KEY_USERNAME, |
|
TIMER_DISABLE_CHAT_AFTER_OFFLINE, |
|
URL_STATUS, |
|
URL_CONFIG, |
|
TIMER_STATUS_UPDATE, |
|
} from './utils/constants.js'; |
|
|
|
|
|
export default class StandaloneChat extends Component { |
|
constructor(props, context) { |
|
super(props, context); |
|
|
|
this.state = { |
|
websocket: null, |
|
canChat: false, |
|
chatEnabled: true, // always true for standalone chat |
|
chatInputEnabled: false, // chat input box state |
|
accessToken: null, |
|
username: null, |
|
isRegistering: false, |
|
streamOnline: false, // stream is active/online |
|
lastDisconnectTime: null, |
|
configData: { |
|
loading: true, |
|
}, |
|
}; |
|
this.disableChatInputTimer = null; |
|
this.hasConfiguredChat = false; |
|
|
|
this.handleUsernameChange = this.handleUsernameChange.bind(this); |
|
this.handleOfflineMode = this.handleOfflineMode.bind(this); |
|
this.handleOnlineMode = this.handleOnlineMode.bind(this); |
|
this.handleFormFocus = this.handleFormFocus.bind(this); |
|
this.handleFormBlur = this.handleFormBlur.bind(this); |
|
this.getStreamStatus = this.getStreamStatus.bind(this); |
|
this.getConfig = this.getConfig.bind(this); |
|
this.disableChatInput = this.disableChatInput.bind(this); |
|
this.setupChatAuth = this.setupChatAuth.bind(this); |
|
this.disableChat = this.disableChat.bind(this); |
|
|
|
// user events |
|
this.handleWebsocketMessage = this.handleWebsocketMessage.bind(this); |
|
|
|
this.getConfig(); |
|
|
|
this.getStreamStatus(); |
|
this.statusTimer = setInterval(this.getStreamStatus, TIMER_STATUS_UPDATE); |
|
} |
|
|
|
// fetch /config data |
|
getConfig() { |
|
fetch(URL_CONFIG) |
|
.then((response) => { |
|
if (!response.ok) { |
|
throw new Error(`Network response was not ok ${response.ok}`); |
|
} |
|
return response.json(); |
|
}) |
|
.then((json) => { |
|
this.setConfigData(json); |
|
}) |
|
.catch((error) => { |
|
this.handleNetworkingError(`Fetch config: ${error}`); |
|
}); |
|
} |
|
|
|
// fetch stream status |
|
getStreamStatus() { |
|
fetch(URL_STATUS) |
|
.then((response) => { |
|
if (!response.ok) { |
|
throw new Error(`Network response was not ok ${response.ok}`); |
|
} |
|
return response.json(); |
|
}) |
|
.then((json) => { |
|
this.updateStreamStatus(json); |
|
}) |
|
.catch((error) => { |
|
this.handleOfflineMode(); |
|
this.handleNetworkingError(`Stream status: ${error}`); |
|
}); |
|
} |
|
|
|
setConfigData(data = {}) { |
|
const { chatDisabled } = data; |
|
|
|
// If this is the first time setting the config |
|
// then setup chat if it's enabled. |
|
const chatBlocked = getLocalStorage('owncast_chat_blocked'); |
|
if (!chatBlocked && !this.hasConfiguredChat && !chatDisabled) { |
|
this.setupChatAuth(); |
|
} |
|
|
|
this.hasConfiguredChat = true; |
|
|
|
this.setState({ |
|
canChat: !chatBlocked, |
|
configData: { |
|
...data, |
|
}, |
|
}); |
|
} |
|
|
|
// handle UI things from stream status result |
|
updateStreamStatus(status = {}) { |
|
const { streamOnline: curStreamOnline } = this.state; |
|
|
|
if (!status) { |
|
return; |
|
} |
|
const { |
|
online, |
|
lastDisconnectTime, |
|
} = status; |
|
|
|
if (status.online && !curStreamOnline) { |
|
// stream has just come online. |
|
this.handleOnlineMode(); |
|
} else if (!status.online && curStreamOnline) { |
|
// stream has just flipped offline. |
|
this.handleOfflineMode(); |
|
} |
|
|
|
this.setState({ |
|
lastDisconnectTime, |
|
streamOnline: online, |
|
}); |
|
} |
|
|
|
// stop status timer and disable chat after some time. |
|
handleOfflineMode() { |
|
const remainingChatTime = |
|
TIMER_DISABLE_CHAT_AFTER_OFFLINE - |
|
(Date.now() - new Date(this.state.lastDisconnectTime)); |
|
const countdown = remainingChatTime < 0 ? 0 : remainingChatTime; |
|
this.disableChatInputTimer = setTimeout(this.disableChatInput, countdown); |
|
this.setState({ |
|
streamOnline: false, |
|
}); |
|
|
|
} |
|
|
|
handleOnlineMode() { |
|
clearTimeout(this.disableChatInputTimer); |
|
this.disableChatInputTimer = null; |
|
|
|
this.setState({ |
|
streamOnline: true, |
|
chatInputEnabled: true, |
|
}); |
|
} |
|
|
|
handleUsernameChange(newName) { |
|
this.setState({ |
|
username: newName, |
|
}); |
|
this.sendUsernameChange(newName); |
|
} |
|
|
|
|
|
disableChatInput() { |
|
this.setState({ |
|
chatInputEnabled: false, |
|
}); |
|
} |
|
|
|
handleNetworkingError(error) { |
|
console.error(`>>> App Error: ${error}`); |
|
} |
|
|
|
handleWebsocketMessage(e) { |
|
if (e.type === SOCKET_MESSAGE_TYPES.ERROR_USER_DISABLED) { |
|
// User has been actively disabled on the backend. Turn off chat for them. |
|
this.handleBlockedChat(); |
|
} else if ( |
|
e.type === SOCKET_MESSAGE_TYPES.ERROR_NEEDS_REGISTRATION && |
|
!this.isRegistering |
|
) { |
|
// User needs an access token, so start the user auth flow. |
|
this.state.websocket.shutdown(); |
|
this.setState({ websocket: null }); |
|
this.setupChatAuth(true); |
|
} else if (e.type === SOCKET_MESSAGE_TYPES.ERROR_MAX_CONNECTIONS_EXCEEDED) { |
|
// Chat server cannot support any more chat clients. Turn off chat for them. |
|
this.disableChat(); |
|
} else if (e.type === SOCKET_MESSAGE_TYPES.CONNECTED_USER_INFO) { |
|
// When connected the user will return an event letting us know what our |
|
// user details are so we can display them properly. |
|
const { user } = e; |
|
const { displayName } = user; |
|
|
|
this.setState({ username: displayName }); |
|
} |
|
} |
|
|
|
handleBlockedChat() { |
|
setLocalStorage('owncast_chat_blocked', true); |
|
this.disableChat(); |
|
} |
|
|
|
handleFormFocus() { |
|
if (this.hasTouchScreen) { |
|
this.setState({ |
|
touchKeyboardActive: true, |
|
}); |
|
} |
|
} |
|
|
|
handleFormBlur() { |
|
if (this.hasTouchScreen) { |
|
this.setState({ |
|
touchKeyboardActive: false, |
|
}); |
|
} |
|
} |
|
|
|
disableChat() { |
|
this.state.websocket.shutdown(); |
|
this.setState({ websocket: null, canChat: false }); |
|
} |
|
|
|
async setupChatAuth(force) { |
|
const { readonly } = this.props; |
|
var accessToken = readonly ? getLocalStorage(KEY_EMBED_CHAT_ACCESS_TOKEN) : getLocalStorage(KEY_ACCESS_TOKEN); |
|
var randomIntArray = new Uint32Array(1); |
|
window.crypto.getRandomValues(randomIntArray); |
|
var username = readonly ? 'chat-embed-' + randomIntArray[0] : getLocalStorage(KEY_USERNAME); |
|
|
|
if (!accessToken || force) { |
|
try { |
|
this.isRegistering = true; |
|
const registration = await registerChat(username); |
|
accessToken = registration.accessToken; |
|
username = registration.displayName; |
|
|
|
if (readonly) { |
|
setLocalStorage(KEY_EMBED_CHAT_ACCESS_TOKEN, accessToken); |
|
} else { |
|
setLocalStorage(KEY_ACCESS_TOKEN, accessToken); |
|
setLocalStorage(KEY_USERNAME, username); |
|
} |
|
|
|
this.isRegistering = false; |
|
} catch (e) { |
|
console.error('registration error:', e); |
|
} |
|
} |
|
|
|
if (this.state.websocket) { |
|
this.state.websocket.shutdown(); |
|
this.setState({ |
|
websocket: null, |
|
}); |
|
} |
|
|
|
// Without a valid access token he websocket connection will be rejected. |
|
const websocket = new Websocket(accessToken); |
|
websocket.addListener( |
|
CALLBACKS.RAW_WEBSOCKET_MESSAGE_RECEIVED, |
|
this.handleWebsocketMessage |
|
); |
|
|
|
this.setState({ |
|
username, |
|
websocket, |
|
accessToken, |
|
}); |
|
} |
|
|
|
sendUsernameChange(newName) { |
|
const nameChange = { |
|
type: SOCKET_MESSAGE_TYPES.NAME_CHANGE, |
|
newName, |
|
}; |
|
this.state.websocket.send(nameChange); |
|
} |
|
|
|
render(props, state) { |
|
const { |
|
username, |
|
websocket, |
|
accessToken, |
|
chatInputEnabled, |
|
configData, |
|
} = state; |
|
|
|
const { |
|
chatDisabled, |
|
maxSocketPayloadSize, |
|
customStyles, |
|
name, |
|
} = configData; |
|
|
|
const { readonly } = props; |
|
return this.state.websocket ? |
|
html`${!readonly ? |
|
html`<style> |
|
${customStyles} |
|
</style> |
|
<header class="flex flex-row-reverse fixed z-10 w-full bg-gray-900"> |
|
<${UsernameForm} |
|
username=${username} |
|
onUsernameChange=${this.handleUsernameChange} |
|
onFocus=${this.handleFormFocus} |
|
onBlur=${this.handleFormBlur} |
|
/> |
|
</header>` : ''} |
|
<${Chat} |
|
websocket=${websocket} |
|
username=${username} |
|
accessToken=${accessToken} |
|
readonly=${readonly} |
|
instanceTitle=${name} |
|
chatInputEnabled=${chatInputEnabled && !chatDisabled} |
|
inputMaxBytes=${maxSocketPayloadSize - EST_SOCKET_PAYLOAD_BUFFER || |
|
CHAT_MAX_MESSAGE_LENGTH} |
|
/>` |
|
: null; |
|
} |
|
}
|
|
|