2023-02-06 21:33:09 +00:00
// console.log("BareRTC!");
2023-01-11 06:38:48 +00:00
2023-01-27 06:54:02 +00:00
// WebRTC configuration.
const configuration = {
2023-06-14 04:57:31 +00:00
iceServers : TURN . URLs . map ( val => {
let row = {
urls : val ,
} ;
if ( val . indexOf ( 'turn:' ) === 0 ) {
row . username = TURN . Username ;
row . credential = TURN . Credential ;
}
return row ;
} )
2023-01-27 06:54:02 +00:00
} ;
2023-03-22 04:29:24 +00:00
const FileUploadMaxSize = 1024 * 1024 * 8 ; // 8 MB
function setModalImage ( url ) {
let $modalImg = document . querySelector ( "#modalImage" ) ,
$modal = document . querySelector ( "#photo-modal" ) ;
$modalImg . src = url ;
$modal . classList . add ( "is-active" ) ;
return false ;
}
2023-04-20 05:00:31 +00:00
// Popped-out video drag functions.
2023-01-27 06:54:02 +00:00
2023-01-11 06:38:48 +00:00
const app = Vue . createApp ( {
delimiters : [ '[[' , ']]' ] ,
data ( ) {
return {
2023-02-10 07:03:06 +00:00
// busy: false, // TODO: not used
2023-02-11 06:46:39 +00:00
disconnect : false , // don't try to reconnect (e.g. kicked)
2023-02-10 07:03:06 +00:00
windowFocused : true , // browser tab is active
windowFocusedAt : new Date ( ) ,
2023-01-11 06:38:48 +00:00
2023-02-20 05:36:36 +00:00
// Disconnect spamming: don't retry too many times.
2023-03-31 04:26:45 +00:00
disconnectLimit : 3 ,
2023-02-20 05:36:36 +00:00
disconnectCount : 0 ,
2023-02-05 08:53:50 +00:00
// Website configuration provided by chat.html template.
config : {
channels : PublicChannels ,
2023-02-06 01:42:09 +00:00
website : WebsiteURL ,
2023-02-11 06:46:39 +00:00
permitNSFW : PermitNSFW ,
2023-08-13 04:35:41 +00:00
webhookURLs : WebhookURLs ,
2023-03-25 05:10:44 +00:00
fontSizeClasses : [
2023-07-09 20:41:40 +00:00
[ "x-2" , "Very small chat room text" ] ,
[ "x-1" , "50% smaller chat room text" ] ,
2023-03-25 05:10:44 +00:00
[ "" , "Default size" ] ,
[ "x1" , "50% larger chat room text" ] ,
[ "x2" , "2x larger chat room text" ] ,
[ "x3" , "3x larger chat room text" ] ,
[ "x4" , "4x larger chat room text" ] ,
] ,
2023-08-15 02:59:35 +00:00
imageDisplaySettings : [
2023-08-17 05:40:17 +00:00
[ "show" , "Always show images in chat" ] ,
[ "collapse" , "Collapse images in chat, clicking to expand (default)" ] ,
2023-08-15 02:59:35 +00:00
[ "hide" , "Never show images shared in chat" ] ,
] ,
2023-08-13 04:35:41 +00:00
reportClassifications : [
"It's spam" ,
"It's abusive (racist, homophobic, etc.)" ,
2023-08-13 05:07:11 +00:00
"It's malicious (e.g. link to a malware website, phishing)" ,
"It's illegal (e.g. controlled substances, violence)" ,
"It's child abuse (CP, CSAM, pedophilia, etc.)" ,
2023-08-13 04:35:41 +00:00
"Other (please describe)" ,
] ,
2023-02-06 21:27:29 +00:00
sounds : {
available : SoundEffects ,
settings : DefaultSounds ,
ready : false ,
audioContext : null ,
audioTracks : { } ,
2023-07-01 03:00:21 +00:00
} ,
reactions : [
2023-07-01 04:48:09 +00:00
[ '❤️' , '👍' , '😂' , '😉' , '😢' , '😡' , '🥰' ] ,
2023-07-09 20:41:40 +00:00
[ '😘' , '👎' , '☹️' , '😭' , '🤔' , '🙄' , '🤩' ] ,
2023-07-01 18:39:08 +00:00
[ '👋' , '🔥' , '😈' , '🍑' , '🍆' , '💦' , '🍌' ] ,
2023-07-01 04:48:09 +00:00
[ '😋' , '⭐' , '😇' , '😴' , '😱' , '👀' , '🎃' ] ,
2023-07-09 20:41:40 +00:00
[ '🤮' , '🥳' , '🙏' , '🤦' , '💩' , '🤯' , '💯' ] ,
[ '😏' , '🙈' , '🙉' , '🙊' , '☀️' , '🌈' , '🎂' ] ,
2023-07-30 17:32:08 +00:00
] ,
// Cached blocklist for the current user sent by your website.
CachedBlocklist : CachedBlocklist ,
2023-02-06 01:42:09 +00:00
} ,
// User JWT settings if available.
jwt : {
token : UserJWTToken ,
valid : UserJWTValid ,
claims : UserJWTClaims
2023-02-05 08:53:50 +00:00
} ,
2023-01-27 04:34:58 +00:00
channel : "lobby" ,
username : "" , //"test",
2023-02-25 00:47:28 +00:00
autoLogin : false , // e.g. from JWT auth
2023-01-11 06:38:48 +00:00
message : "" ,
2023-02-10 07:03:06 +00:00
typingNotifDebounce : null ,
2023-03-28 04:13:04 +00:00
status : "online" , // away/idle status
// Idle detection variables
idleTimeout : null ,
2023-07-23 22:49:04 +00:00
idleThreshold : 300 , // number of seconds you must be idle
2023-01-11 06:38:48 +00:00
// WebSocket connection.
ws : {
conn : null ,
connected : false ,
} ,
2023-01-27 04:34:58 +00:00
// Who List for the room.
whoList : [ ] ,
2023-03-23 03:21:04 +00:00
whoTab : 'online' ,
2023-08-05 19:15:16 +00:00
whoSort : 'a-z' ,
2023-02-06 01:42:09 +00:00
whoMap : { } , // map username to wholist entry
2023-03-23 03:21:04 +00:00
muted : { } , // muted usernames for client side state
2023-01-27 04:34:58 +00:00
// My video feed.
webcam : {
busy : false ,
active : false ,
elem : null , // <video id="localVideo"> element
stream : null , // MediaStream object
2023-02-06 04:26:00 +00:00
muted : false , // our outgoing mic is muted, not by default
2023-08-12 01:56:22 +00:00
autoMute : false , // mute our mic automatically when going live (user option)
2023-02-11 06:46:39 +00:00
nsfw : false , // user has flagged their camera to be NSFW
2023-04-01 02:46:42 +00:00
mutual : false , // user wants viewers to share their own videos
mutualOpen : false , // user wants to open video mutually
2023-02-06 04:26:00 +00:00
// Who all is watching me? map of users.
watching : { } ,
2023-02-10 07:03:06 +00:00
// Scaling setting for the videos drawer, so the user can
// embiggen the webcam sizes so a suitable size.
videoScale : "" ,
videoScaleOptions : [
[ "" , "Default size" ] ,
[ "x1" , "50% larger videos" ] ,
[ "x2" , "2x larger videos" ] ,
[ "x3" , "3x larger videos" ] ,
[ "x4" , "4x larger videos (not recommended)" ] ,
] ,
2023-04-19 05:18:12 +00:00
// Available cameras and microphones for the Settings modal.
2023-08-18 01:45:43 +00:00
gettingDevices : false , // busy indicator for refreshing devices
2023-04-19 05:18:12 +00:00
videoDevices : [ ] ,
videoDeviceID : null ,
audioDevices : [ ] ,
audioDeviceID : null ,
2023-08-19 01:54:45 +00:00
// After we get a device selected, remember it (by name) so that we
// might hopefully re-select it by default IF we are able to enumerate
// devices before they go on camera the first time.
preferredDeviceNames : {
video : null ,
audio : null ,
} ,
2023-01-27 04:34:58 +00:00
} ,
2023-07-01 01:41:06 +00:00
// Video flag constants (sync with values in messages.go)
VideoFlag : {
Active : 1 << 0 ,
NSFW : 1 << 1 ,
Muted : 1 << 2 ,
IsTalking : 1 << 3 ,
MutualRequired : 1 << 4 ,
MutualOpen : 1 << 5 ,
} ,
2023-01-27 06:54:02 +00:00
// WebRTC sessions with other users.
WebRTC : {
// Streams per username.
streams : { } ,
2023-02-06 04:26:00 +00:00
muted : { } , // muted bool per username
2023-08-05 02:24:42 +00:00
booted : { } , // booted bool per username
2023-04-20 05:00:31 +00:00
poppedOut : { } , // popped-out video per username
2023-01-27 06:54:02 +00:00
// RTCPeerConnections per username.
pc : { } ,
2023-08-09 00:51:52 +00:00
// Video stream freeze detection.
frozenStreamInterval : { } , // map usernames to intervals
frozenStreamDetected : { } , // map usernames to bools
// Debounce connection attempts since now every click = try to connect.
debounceOpens : { } , // map usernames to bools
2023-01-27 06:54:02 +00:00
} ,
2023-01-11 06:38:48 +00:00
// Chat history.
history : [ ] ,
2023-02-05 08:53:50 +00:00
channels : {
// There will be values here like:
// "lobby": {
// "history": [],
// "updated": timestamp,
// "unread": 4,
// },
// "@username": {
// "history": [],
// ...
// }
} ,
2023-01-27 04:34:58 +00:00
historyScrollbox : null ,
2023-03-25 04:56:40 +00:00
autoscroll : true , // scroll to bottom on new messages
2023-03-25 05:10:44 +00:00
fontSizeClass : "" , // font size magnification
2023-08-17 05:40:17 +00:00
imageDisplaySetting : "collapse" , // image show/hide setting
2023-07-23 01:30:45 +00:00
scrollback : 1000 , // scrollback buffer (messages to keep per channel)
2023-01-11 06:38:48 +00:00
DMs : { } ,
2023-07-01 03:00:21 +00:00
messageReactions : {
// Will look like:
// "123": { (message ID)
// "❤️": [ (reaction emoji)
// "username" // users who reacted
// ]
// }
} ,
2023-01-11 06:38:48 +00:00
2023-02-10 07:03:06 +00:00
// Responsive CSS controls for mobile.
2023-02-05 08:53:50 +00:00
responsive : {
leftDrawerOpen : false ,
rightDrawerOpen : false ,
nodes : {
// DOM nodes for the CSS grid cells.
$container : null ,
$left : null ,
$center : null ,
$right : null ,
}
} ,
2023-01-11 06:38:48 +00:00
loginModal : {
visible : false ,
} ,
2023-02-06 21:27:29 +00:00
settingsModal : {
visible : false ,
2023-07-23 01:30:45 +00:00
tab : 'prefs' , // selected setting tab
2023-02-06 21:27:29 +00:00
} ,
2023-02-11 06:46:39 +00:00
nsfwModalCast : {
visible : false ,
} ,
nsfwModalView : {
visible : false ,
dontShowAgain : false ,
user : null , // staged User we wanted to open
} ,
2023-08-13 04:35:41 +00:00
reportModal : {
visible : false ,
busy : false ,
message : { } ,
origMessage : { } , // pointer, so we can set the "reported" flag
classification : "It's spam" ,
comment : "" ,
} ,
2023-01-11 06:38:48 +00:00
}
} ,
mounted ( ) {
2023-03-25 05:10:44 +00:00
this . setupConfig ( ) ; // localSettings persisted settings
2023-03-28 04:13:04 +00:00
this . setupIdleDetection ( ) ;
2023-02-06 21:27:29 +00:00
2023-01-27 04:34:58 +00:00
this . webcam . elem = document . querySelector ( "#localVideo" ) ;
this . historyScrollbox = document . querySelector ( "#chatHistory" ) ;
2023-02-05 08:53:50 +00:00
this . responsive . nodes = {
$container : document . querySelector ( ".chat-container" ) ,
$left : document . querySelector ( ".left-column" ) ,
$center : document . querySelector ( ".chat-column" ) ,
$right : document . querySelector ( ".right-column" ) ,
} ;
2023-02-10 07:03:06 +00:00
// Reset CSS overrides for responsive display on any window size change. In effect,
// making the chat panel the current screen again on phone rotation.
2023-02-05 08:53:50 +00:00
window . addEventListener ( "resize" , ( ) => {
this . resetResponsiveCSS ( ) ;
} ) ;
2023-02-10 07:03:06 +00:00
// Listen for window focus/unfocus events. Being on a different browser tab, for
// sound effect alert purposes, counts as not being "in" that chat channel when
// a message comes in.
window . addEventListener ( "focus" , ( ) => {
this . windowFocused = true ;
this . windowFocusedAt = new Date ( ) ;
} ) ;
window . addEventListener ( "blur" , ( ) => {
this . windowFocused = false ;
2023-06-12 02:21:06 +00:00
} ) ;
// Set up sound effects on first page interaction.
window . addEventListener ( "click" , ( ) => {
this . setupSounds ( ) ;
} ) ;
2023-06-12 03:33:26 +00:00
window . addEventListener ( "keydown" , ( ) => {
this . setupSounds ( ) ;
} ) ;
2023-02-10 07:03:06 +00:00
2023-02-05 08:53:50 +00:00
for ( let channel of this . config . channels ) {
this . initHistory ( channel . ID ) ;
}
2023-02-06 04:26:00 +00:00
this . ChatClient ( "Welcome to BareRTC!" ) ;
2023-01-11 06:38:48 +00:00
2023-02-06 01:42:09 +00:00
// Auto login with JWT token?
// TODO: JWT validation on the WebSocket as well.
if ( this . jwt . valid && this . jwt . claims . sub ) {
this . username = this . jwt . claims . sub ;
2023-02-25 00:47:28 +00:00
this . autoLogin = true ;
2023-02-06 01:42:09 +00:00
}
// Scrub JWT token from query string parameters.
history . pushState ( null , "" , location . href . split ( "?" ) [ 0 ] ) ;
2023-02-25 00:47:28 +00:00
// XX: always show login dialog to test if this helps iOS devices.
2023-03-14 04:26:37 +00:00
// this.loginModal.visible = true;
2023-01-11 06:38:48 +00:00
if ( ! this . username ) {
this . loginModal . visible = true ;
2023-01-27 04:34:58 +00:00
} else {
this . signIn ( ) ;
2023-01-11 06:38:48 +00:00
}
2023-03-14 04:26:37 +00:00
} ,
watch : {
2023-07-09 19:33:02 +00:00
"webcam.videoScale" : function ( ) {
2023-03-14 04:26:37 +00:00
document . querySelectorAll ( ".video-feeds > .feed" ) . forEach ( node => {
node . style . width = null ;
node . style . height = null ;
} ) ;
2023-07-09 19:33:02 +00:00
localStorage . videoScale = this . webcam . videoScale ;
2023-03-14 04:26:37 +00:00
} ,
2023-03-25 05:10:44 +00:00
fontSizeClass ( ) {
// Store the setting persistently.
localStorage . fontSizeClass = this . fontSizeClass ;
} ,
2023-08-15 02:59:35 +00:00
imageDisplaySetting ( ) {
localStorage . imageDisplaySetting = this . imageDisplaySetting ;
} ,
2023-07-23 01:30:45 +00:00
scrollback ( ) {
localStorage . scrollback = this . scrollback ;
} ,
2023-03-28 04:13:04 +00:00
status ( ) {
// Send presence updates to the server.
this . sendMe ( ) ;
2023-08-12 01:42:06 +00:00
} ,
// Webcam preferences that the user can edit while live.
"webcam.nsfw" : function ( ) {
if ( this . webcam . active ) {
this . sendMe ( ) ;
}
} ,
"webcam.mutual" : function ( ) {
if ( this . webcam . active ) {
this . sendMe ( ) ;
}
} ,
"webcam.mutualOpen" : function ( ) {
if ( this . webcam . active ) {
this . sendMe ( ) ;
}
} ,
2023-01-11 06:38:48 +00:00
} ,
2023-02-05 08:53:50 +00:00
computed : {
chatHistory ( ) {
if ( this . channels [ this . channel ] == undefined ) {
return [ ] ;
}
let history = this . channels [ this . channel ] . history ;
// How channels work:
// - Everything going to a public channel like "lobby" goes
// into the "lobby" channel in the front-end
// - Direct messages are different: they are all addressed
// "to" the channel of the current @user, but they are
// divided into DM threads based on the username.
if ( this . channel . indexOf ( "@" ) === 0 ) {
// DM thread, divide them by sender.
// let username = this.channel.substring(1);
// return history.filter(v => {
// return v.username === username;
// });
}
return history ;
} ,
2023-07-23 01:30:45 +00:00
activeDMs ( ) {
// List your currently open DM threads, sorted by most recent.
let result = [ ] ;
for ( let channel of Object . keys ( this . channels ) ) {
// @mentions only
if ( channel . indexOf ( "@" ) !== 0 ) {
continue ;
}
result . push ( {
channel : channel ,
name : channel . substring ( 1 ) ,
updated : this . channels [ channel ] . updated ,
unread : this . channels [ channel ] . unread ,
} ) ;
}
result . sort ( ( a , b ) => b . updated - a . updated ) ;
return result ;
} ,
2023-02-05 08:53:50 +00:00
channelName ( ) {
// Return a suitable channel title.
if ( this . channel . indexOf ( "@" ) === 0 ) {
// A DM, return it directly as is.
return this . channel ;
}
// Find the friendly name from our config.
for ( let channel of this . config . channels ) {
if ( channel . ID === this . channel ) {
return channel . Name ;
}
}
return this . channel ;
} ,
2023-02-06 01:42:09 +00:00
isDM ( ) {
// Is the current channel a DM?
return this . channel . indexOf ( "@" ) === 0 ;
} ,
2023-07-30 17:32:08 +00:00
canUploadFile ( ) {
// Public channels: OK
if ( ! this . channel . indexOf ( '@' ) === 0 ) {
return true ;
}
// User is an admin?
if ( this . jwt . claims . op ) {
return true ;
}
// User is in a DM thread with an admin?
if ( this . isDM ) {
let partner = this . normalizeUsername ( this . channel ) ;
if ( this . whoMap [ partner ] != undefined && this . whoMap [ partner ] . op ) {
return true ;
}
}
return ! this . isDM ;
} ,
2023-06-24 20:08:15 +00:00
isOp ( ) {
// Returns if the current user has operator rights
return this . jwt . claims . op ;
} ,
2023-07-01 01:41:06 +00:00
myVideoFlag ( ) {
// Compute the current user's video status flags.
let status = 0 ;
2023-08-09 00:51:52 +00:00
if ( ! this . webcam . active ) return 0 ; // unset all flags if not active now
2023-07-01 01:41:06 +00:00
if ( this . webcam . active ) status |= this . VideoFlag . Active ;
if ( this . webcam . muted ) status |= this . VideoFlag . Muted ;
if ( this . webcam . nsfw ) status |= this . VideoFlag . NSFW ;
if ( this . webcam . mutual ) status |= this . VideoFlag . MutualRequired ;
if ( this . webcam . mutualOpen ) status |= this . VideoFlag . MutualOpen ;
return status ;
} ,
2023-08-05 19:15:16 +00:00
sortedWhoList ( ) {
let result = [ ... this . whoList ] ;
switch ( this . whoSort ) {
case "broadcasting" :
result . sort ( ( a , b ) => {
return ( b . video & this . VideoFlag . Active ) - ( a . video & this . VideoFlag . Active ) ;
} ) ;
break ;
case "nsfw" :
result . sort ( ( a , b ) => {
let left = ( a . video & ( this . VideoFlag . Active | this . VideoFlag . NSFW ) ) ,
right = ( b . video & ( this . VideoFlag . Active | this . VideoFlag . NSFW ) ) ;
return right - left ;
} ) ;
break ;
case "status" :
result . sort ( ( a , b ) => {
if ( a . status === b . status ) return 0 ;
return b . status < a . status ? - 1 : 1 ;
} ) ;
break ;
case "op" :
result . sort ( ( a , b ) => {
return b . op - a . op ;
} ) ;
break ;
2023-08-06 02:38:04 +00:00
case "emoji" :
result . sort ( ( a , b ) => {
if ( a . emoji === b . emoji ) return 0 ;
return a . emoji < b . emoji ? - 1 : 1 ;
} )
break ;
2023-08-07 04:06:27 +00:00
case "login" :
result . sort ( ( a , b ) => {
return b . loginAt - a . loginAt ;
} ) ;
break ;
2023-08-06 02:38:04 +00:00
case "gender" :
result . sort ( ( a , b ) => {
if ( a . gender === b . gender ) return 0 ;
let left = a . gender || 'z' ,
right = b . gender || 'z' ;
return left < right ? - 1 : 1 ;
} )
break ;
2023-08-05 19:15:16 +00:00
case "z-a" :
result = result . reverse ( ) ;
}
// Default ordering from ChatServer = a-z
return result ;
} ,
2023-02-05 08:53:50 +00:00
} ,
2023-01-11 06:38:48 +00:00
methods : {
2023-03-25 05:10:44 +00:00
// Load user prefs from localStorage, called on startup
setupConfig ( ) {
if ( localStorage . fontSizeClass != undefined ) {
this . fontSizeClass = localStorage . fontSizeClass ;
}
2023-04-01 03:25:53 +00:00
2023-07-09 19:33:02 +00:00
if ( localStorage . videoScale != undefined ) {
this . webcam . videoScale = localStorage . videoScale ;
}
2023-08-15 02:59:35 +00:00
if ( localStorage . imageDisplaySetting != undefined ) {
this . imageDisplaySetting = localStorage . imageDisplaySetting ;
}
2023-07-23 01:30:45 +00:00
if ( localStorage . scrollback != undefined ) {
this . scrollback = parseInt ( localStorage . scrollback ) ;
}
2023-08-19 01:54:45 +00:00
// Stored user preferred device names for webcam/audio.
if ( localStorage . preferredDeviceNames != undefined ) {
let dev = JSON . parse ( localStorage . preferredDeviceNames ) ;
this . webcam . preferredDeviceNames . video = dev . video ;
this . webcam . preferredDeviceNames . audio = dev . audio ;
}
2023-04-01 03:25:53 +00:00
// Webcam mutality preferences from last broadcast.
if ( localStorage . videoMutual === "true" ) {
this . webcam . mutual = true ;
}
if ( localStorage . videoMutualOpen === "true" ) {
this . webcam . mutualOpen = true ;
}
2023-08-12 01:56:22 +00:00
if ( localStorage . videoAutoMute === "true" ) {
this . webcam . autoMute = true ;
}
2023-03-25 05:10:44 +00:00
} ,
2023-01-11 06:38:48 +00:00
signIn ( ) {
this . loginModal . visible = false ;
this . dial ( ) ;
} ,
2023-06-15 03:45:54 +00:00
// Normalize a DM channel name into a username (remove the @ prefix)
normalizeUsername ( channel ) {
return channel . replace ( /^@+/ , '' ) ;
} ,
2023-01-27 06:54:02 +00:00
/ * *
* Chat API Methods ( WebSocket packets sent / received )
* /
2023-01-11 06:38:48 +00:00
sendMessage ( ) {
if ( ! this . message ) {
return ;
}
if ( ! this . ws . connected ) {
2023-01-27 04:34:58 +00:00
this . ChatClient ( "You are not connected to the server." ) ;
2023-01-11 06:38:48 +00:00
return ;
}
2023-08-09 00:51:52 +00:00
// DEBUGGING: fake set the freeze indicator.
let match = this . message . match ( /^\/freeze (.+?)$/i ) ;
if ( match ) {
let username = match [ 1 ] ;
this . WebRTC . frozenStreamDetected [ username ] = true ;
this . ChatClient ( ` DEBUG: Marked ${ username } stream as frozen. ` ) ;
this . message = "" ;
return ;
}
2023-02-06 21:33:09 +00:00
// console.debug("Send message: %s", this.message);
2023-01-11 06:38:48 +00:00
this . ws . conn . send ( JSON . stringify ( {
action : "message" ,
2023-02-05 08:53:50 +00:00
channel : this . channel ,
2023-01-11 06:38:48 +00:00
message : this . message ,
} ) ) ;
this . message = "" ;
} ,
2023-02-10 07:03:06 +00:00
sendTypingNotification ( ) {
// TODO
} ,
2023-07-01 03:00:21 +00:00
// Emoji reactions
sendReact ( message , emoji ) {
this . ws . conn . send ( JSON . stringify ( {
action : 'react' ,
msgID : message . msgID ,
message : emoji ,
} ) ) ;
} ,
onReact ( msg ) {
// Search all channels for this message ID and append the reaction.
let msgID = msg . msgID ,
who = msg . username ,
emoji = msg . message ;
if ( this . messageReactions [ msgID ] == undefined ) {
this . messageReactions [ msgID ] = { } ;
}
if ( this . messageReactions [ msgID ] [ emoji ] == undefined ) {
this . messageReactions [ msgID ] [ emoji ] = [ ] ;
}
2023-07-01 18:39:08 +00:00
// if they double sent the same reaction, it counts as a removal
let unreact = false ;
for ( let i = 0 ; i < this . messageReactions [ msgID ] [ emoji ] . length ; i ++ ) {
let reactor = this . messageReactions [ msgID ] [ emoji ] [ i ] ;
if ( reactor === who ) {
this . messageReactions [ msgID ] [ emoji ] . splice ( i , 1 ) ;
unreact = true ;
}
}
// if this emoji reaction is empty, clean it up
if ( unreact ) {
if ( this . messageReactions [ msgID ] [ emoji ] . length === 0 ) {
delete ( this . messageReactions [ msgID ] [ emoji ] ) ;
}
return ;
2023-07-01 03:00:21 +00:00
}
this . messageReactions [ msgID ] [ emoji ] . push ( who ) ;
} ,
2023-01-27 04:34:58 +00:00
// Sync the current user state (such as video broadcasting status) to
// the backend, which will reload everybody's Who List.
sendMe ( ) {
this . ws . conn . send ( JSON . stringify ( {
action : "me" ,
2023-07-01 01:41:06 +00:00
video : this . myVideoFlag ,
2023-03-28 04:13:04 +00:00
status : this . status ,
2023-01-27 04:34:58 +00:00
} ) ) ;
} ,
onMe ( msg ) {
// We have had settings pushed to us by the server, such as a change
// in our choice of username.
if ( this . username != msg . username ) {
this . ChatServer ( ` Your username has been changed to ${ msg . username } . ` ) ;
2023-01-27 06:54:02 +00:00
this . username = msg . username ;
2023-01-27 04:34:58 +00:00
}
2023-02-11 06:46:39 +00:00
// The server can set our webcam NSFW flag.
2023-07-01 01:41:06 +00:00
let myNSFW = this . webcam . nsfw ;
let theirNSFW = ( msg . video & this . VideoFlag . NSFW ) > 0 ;
if ( myNSFW != theirNSFW ) {
this . webcam . nsfw = theirNSFW ;
2023-02-11 06:46:39 +00:00
}
2023-07-30 17:32:08 +00:00
// Note: Me events only come when we join the server or a moderator has
// flagged our video. This is as good of an "on connected" handler as we
// get, so push over our cached blocklist from the website now.
this . bulkMuteUsers ( ) ;
2023-02-05 08:53:50 +00:00
} ,
// WhoList updates.
onWho ( msg ) {
this . whoList = msg . whoList ;
2023-02-06 01:42:09 +00:00
this . whoMap = { } ;
2023-02-05 08:53:50 +00:00
2023-03-29 01:12:49 +00:00
if ( this . whoList == undefined ) {
this . whoList = [ ] ;
}
2023-02-05 08:53:50 +00:00
// If we had a camera open with any of these and they have gone
// off camera, close our side of the connection.
for ( let row of this . whoList ) {
2023-02-06 01:42:09 +00:00
this . whoMap [ row . username ] = row ;
2023-02-05 08:53:50 +00:00
if ( this . WebRTC . streams [ row . username ] != undefined &&
2023-07-01 01:41:06 +00:00
! ( row . video & this . VideoFlag . Active ) ) {
2023-02-06 23:02:23 +00:00
this . closeVideo ( row . username , "offerer" ) ;
2023-02-05 08:53:50 +00:00
}
}
2023-02-06 04:26:00 +00:00
2023-08-12 01:42:06 +00:00
// Hang up on mutual cameras, if they changed their setting while we
// are already watching them.
this . unMutualVideo ( ) ;
2023-02-06 04:26:00 +00:00
// Has the back-end server forgotten we are on video? This can
// happen if we disconnect/reconnect while we were streaming.
2023-07-01 01:41:06 +00:00
if ( this . webcam . active && ! ( this . whoMap [ this . username ] ? . video & this . VideoFlag . Active ) ) {
2023-02-06 04:26:00 +00:00
this . sendMe ( ) ;
}
2023-01-27 04:34:58 +00:00
} ,
2023-03-23 03:21:04 +00:00
// Mute or unmute a user.
muteUser ( username ) {
2023-06-15 03:45:54 +00:00
username = this . normalizeUsername ( username ) ;
2023-03-23 03:21:04 +00:00
let mute = this . muted [ username ] == undefined ;
2023-08-10 03:49:41 +00:00
// If the user is muted because they were blocked on your main website (CachedBlocklist),
// do not allow a temporary unmute in chat: make them live with their choice.
if ( this . config . CachedBlocklist . length > 0 ) {
for ( let user of this . config . CachedBlocklist ) {
if ( user === username ) {
this . ChatClient (
` You can not unmute <strong> ${ username } </strong> because you have blocked them on the main website. ` +
` To unmute them, you will need to unblock them on the website and then reload the chat room. `
) ;
return ;
}
}
}
2023-03-23 03:21:04 +00:00
if ( mute ) {
2023-07-01 18:39:08 +00:00
if ( ! window . confirm (
` Do you want to mute ${ username } ? If muted, you will no longer see their ` +
` chat messages or any DMs they send you going forward. Also, ${ username } will ` +
` not be able to see whether your webcam is active until you unmute them. `
) ) {
return ;
}
2023-03-23 03:21:04 +00:00
this . muted [ username ] = true ;
} else {
2023-07-30 18:25:44 +00:00
if ( ! window . confirm (
` Do you want to remove your mute on ${ username } ? If you un-mute them, you ` +
` will be able to see their chat messages or DMs going forward, but most importantly, ` +
` they may be able to watch your webcam now if you are broadcasting! \n \n ` +
` Note: currently you can only re-mute them the next time you see one of their ` +
` chat messages, or you can only boot them off your cam after they have already ` +
` opened it. If you are concerned about this, click Cancel and do not remove ` +
` the mute on ${ username } . `
) ) {
return ;
}
2023-03-23 03:21:04 +00:00
delete this . muted [ username ] ;
}
2023-06-15 04:06:57 +00:00
// Hang up videos both ways.
this . closeVideo ( username ) ;
2023-03-23 03:21:04 +00:00
this . sendMute ( username , mute ) ;
if ( mute ) {
this . ChatClient (
` You have muted <strong> ${ username } </strong> and will no longer see their chat messages, ` +
` and they will not see whether your webcam is active. You may unmute them via the Who Is Online list. ` ) ;
} else {
this . ChatClient (
` You have unmuted <strong> ${ username } </strong> and can see their chat messages from now on. ` ,
) ;
}
} ,
sendMute ( username , mute ) {
this . ws . conn . send ( JSON . stringify ( {
action : mute ? "mute" : "unmute" ,
username : username ,
} ) ) ;
} ,
isMutedUser ( username ) {
2023-06-15 03:45:54 +00:00
return this . muted [ this . normalizeUsername ( username ) ] != undefined ;
2023-03-23 03:21:04 +00:00
} ,
2023-07-30 17:32:08 +00:00
bulkMuteUsers ( ) {
// On page load, if the website sent you a CachedBlocklist, mute all
// of these users in bulk when the server connects.
// this.ChatClient("BulkMuteUsers: sending our blocklist " + this.config.CachedBlocklist);
if ( this . config . CachedBlocklist . length === 0 ) {
return ; // nothing to do
}
// Set the client side mute.
let blocklist = this . config . CachedBlocklist ;
for ( let username of blocklist ) {
this . muted [ username ] = true ;
}
// Send the username list to the server.
this . ws . conn . send ( JSON . stringify ( {
action : "blocklist" ,
usernames : blocklist ,
} ) )
} ,
2023-03-23 03:21:04 +00:00
2023-01-27 06:54:02 +00:00
// Send a video request to access a user's camera.
sendOpen ( username ) {
this . ws . conn . send ( JSON . stringify ( {
action : "open" ,
username : username ,
} ) ) ;
} ,
2023-03-23 03:21:04 +00:00
sendBoot ( username ) {
this . ws . conn . send ( JSON . stringify ( {
action : "boot" ,
username : username ,
} ) ) ;
} ,
2023-01-27 06:54:02 +00:00
onOpen ( msg ) {
// Response for the opener to begin WebRTC connection.
this . startWebRTC ( msg . username , true ) ;
} ,
onRing ( msg ) {
2023-08-05 02:24:42 +00:00
// Admin moderation feature: if the user has booted an admin off their camera, do not
// notify if the admin re-opens their camera.
if ( this . isBootedAdmin ( msg . username ) ) {
this . startWebRTC ( msg . username , false ) ;
return ;
}
2023-01-27 06:54:02 +00:00
this . ChatServer ( ` ${ msg . username } has opened your camera. ` ) ;
this . startWebRTC ( msg . username , false ) ;
} ,
2023-02-05 05:00:01 +00:00
onUserExited ( msg ) {
// A user has logged off the server. Clean up any WebRTC connections.
2023-02-05 08:53:50 +00:00
this . closeVideo ( msg . username ) ;
2023-02-05 05:00:01 +00:00
} ,
2023-01-27 06:54:02 +00:00
2023-01-27 04:34:58 +00:00
// Handle messages sent in chat.
onMessage ( msg ) {
2023-02-10 07:03:06 +00:00
// Play sound effects if this is not the active channel or the window is not focused.
2023-02-06 21:27:29 +00:00
if ( msg . channel . indexOf ( "@" ) === 0 ) {
2023-02-10 07:03:06 +00:00
if ( msg . channel !== this . channel || ! this . windowFocused ) {
2023-02-06 21:27:29 +00:00
this . playSound ( "DM" ) ;
}
2023-02-10 07:03:06 +00:00
} else if ( msg . channel !== this . channel || ! this . windowFocused ) {
2023-02-06 21:27:29 +00:00
this . playSound ( "Chat" ) ;
}
2023-01-27 04:34:58 +00:00
this . pushHistory ( {
2023-02-05 08:53:50 +00:00
channel : msg . channel ,
2023-01-27 04:34:58 +00:00
username : msg . username ,
message : msg . message ,
2023-06-24 20:08:15 +00:00
messageID : msg . msgID ,
2023-01-27 04:34:58 +00:00
} ) ;
} ,
2023-06-24 20:08:15 +00:00
// A user deletes their message for everybody
onTakeback ( msg ) {
// Search all channels for this message ID and remove it.
for ( let channel of Object . keys ( this . channels ) ) {
for ( let i = 0 ; i < this . channels [ channel ] . history . length ; i ++ ) {
let cmp = this . channels [ channel ] . history [ i ] ;
if ( cmp . msgID === msg . msgID ) {
this . channels [ channel ] . history . splice ( i , 1 ) ;
return ;
}
}
}
console . error ( "Got a takeback for msgID %d but did not find it!" , msg . msgID ) ;
} ,
2023-02-06 01:42:09 +00:00
// User logged in or out.
onPresence ( msg ) {
// TODO: make a dedicated leave event
2023-06-12 03:33:26 +00:00
let isLeave = false ;
2023-02-06 01:42:09 +00:00
if ( msg . message . indexOf ( "has exited the room!" ) > - 1 ) {
// Clean up data about this user.
this . onUserExited ( msg ) ;
2023-02-06 21:27:29 +00:00
this . playSound ( "Leave" ) ;
2023-06-12 03:33:26 +00:00
isLeave = true ;
2023-02-06 21:27:29 +00:00
} else {
this . playSound ( "Enter" ) ;
2023-02-06 01:42:09 +00:00
}
2023-06-12 03:33:26 +00:00
// Push it to the history of all public channels (not leaves).
if ( ! isLeave ) {
for ( let channel of this . config . channels ) {
this . pushHistory ( {
channel : channel . ID ,
action : msg . action ,
username : msg . username ,
message : msg . message ,
} ) ;
}
2023-02-06 21:27:29 +00:00
}
2023-06-12 03:33:26 +00:00
// Push also to any DM channels for this user (leave events do push to DM thread for visibility).
2023-02-06 21:27:29 +00:00
let channel = "@" + msg . username ;
if ( this . channels [ channel ] != undefined ) {
this . pushHistory ( {
channel : channel ,
action : msg . action ,
username : msg . username ,
message : msg . message ,
2023-02-06 01:42:09 +00:00
} ) ;
}
} ,
2023-01-11 06:38:48 +00:00
// Dial the WebSocket connection.
dial ( ) {
2023-02-09 04:01:06 +00:00
this . ChatClient ( "Establishing connection to server..." ) ;
2023-02-05 05:00:01 +00:00
const proto = location . protocol === 'https:' ? 'wss' : 'ws' ;
const conn = new WebSocket ( ` ${ proto } :// ${ location . host } /ws ` ) ;
2023-01-11 06:38:48 +00:00
conn . addEventListener ( "close" , ev => {
2023-02-06 01:42:09 +00:00
// Lost connection to server - scrub who list.
this . onWho ( { whoList : [ ] } ) ;
2023-03-23 03:21:04 +00:00
this . muted = { } ;
2023-02-06 01:42:09 +00:00
2023-01-11 06:38:48 +00:00
this . ws . connected = false ;
2023-01-27 04:34:58 +00:00
this . ChatClient ( ` WebSocket Disconnected code: ${ ev . code } , reason: ${ ev . reason } ` ) ;
2023-01-11 06:38:48 +00:00
2023-02-20 05:36:36 +00:00
this . disconnectCount ++ ;
if ( this . disconnectCount > this . disconnectLimit ) {
this . ChatClient ( ` It seems there's a problem connecting to the server. Please try some other time. Note that iPhones and iPads currently have issues connecting to the chat room in general. ` ) ;
return ;
}
2023-02-11 06:46:39 +00:00
if ( ! this . disconnect ) {
if ( ev . code !== 1001 ) {
this . ChatClient ( "Reconnecting in 5s" ) ;
setTimeout ( this . dial , 5000 ) ;
}
2023-01-11 06:38:48 +00:00
}
} ) ;
conn . addEventListener ( "open" , ev => {
this . ws . connected = true ;
2023-01-27 04:34:58 +00:00
this . ChatClient ( "Websocket connected!" ) ;
2023-01-11 06:38:48 +00:00
// Tell the server our username.
this . ws . conn . send ( JSON . stringify ( {
action : "login" ,
username : this . username ,
2023-02-06 01:42:09 +00:00
jwt : this . jwt . token ,
2023-01-11 06:38:48 +00:00
} ) ) ;
} ) ;
conn . addEventListener ( "message" , ev => {
if ( typeof ev . data !== "string" ) {
console . error ( "unexpected message type" , typeof ev . data ) ;
return ;
}
let msg = JSON . parse ( ev . data ) ;
2023-02-06 21:27:29 +00:00
try {
// Cast timestamp to date.
msg . at = new Date ( msg . at ) ;
} catch ( e ) {
console . error ( "Parsing timestamp '%s' on msg: %s" , msg . at , e ) ;
}
2023-01-27 04:34:58 +00:00
switch ( msg . action ) {
case "who" :
2023-02-05 08:53:50 +00:00
this . onWho ( msg ) ;
2023-01-27 04:34:58 +00:00
break ;
case "me" :
this . onMe ( msg ) ;
break ;
case "message" :
this . onMessage ( msg ) ;
break ;
2023-06-24 20:08:15 +00:00
case "takeback" :
this . onTakeback ( msg ) ;
break ;
2023-07-01 03:00:21 +00:00
case "react" :
this . onReact ( msg ) ;
break ;
2023-01-27 04:34:58 +00:00
case "presence" :
2023-02-06 01:42:09 +00:00
this . onPresence ( msg ) ;
2023-01-27 04:34:58 +00:00
break ;
2023-01-27 06:54:02 +00:00
case "ring" :
this . onRing ( msg ) ;
break ;
case "open" :
this . onOpen ( msg ) ;
break ;
case "candidate" :
this . onCandidate ( msg ) ;
break ;
case "sdp" :
this . onSDP ( msg ) ;
break ;
2023-02-06 04:26:00 +00:00
case "watch" :
this . onWatch ( msg ) ;
break ;
case "unwatch" :
this . onUnwatch ( msg ) ;
break ;
2023-01-27 06:54:02 +00:00
case "error" :
this . pushHistory ( {
2023-02-06 01:42:09 +00:00
channel : msg . channel ,
2023-01-27 06:54:02 +00:00
username : msg . username || 'Internal Server Error' ,
message : msg . message ,
2023-02-06 01:42:09 +00:00
isChatServer : true ,
2023-01-27 06:54:02 +00:00
} ) ;
2023-02-06 21:27:29 +00:00
break ;
2023-02-11 06:46:39 +00:00
case "disconnect" :
this . disconnect = true ;
break ;
2023-02-06 21:27:29 +00:00
case "ping" :
2023-04-20 02:55:39 +00:00
// New JWT token?
if ( msg . jwt ) {
this . jwt . token = msg . jwt ;
}
// Reset disconnect retry counter: if we were on long enough to get
// a ping, we're well connected and can reconnect no matter how many
// times the chat server is rebooted.
this . disconnectCount = 0 ;
2023-02-06 21:27:29 +00:00
break ;
2023-01-27 04:34:58 +00:00
default :
console . error ( "Unexpected action: %s" , JSON . stringify ( msg ) ) ;
}
2023-01-11 06:38:48 +00:00
} ) ;
this . ws . conn = conn ;
} ,
2023-01-27 06:54:02 +00:00
/ * *
* WebRTC concerns .
* /
// Start WebRTC with the other username.
startWebRTC ( username , isOfferer ) {
2023-02-05 08:53:50 +00:00
// this.ChatClient(`Begin WebRTC with ${username}.`);
2023-01-27 06:54:02 +00:00
let pc = new RTCPeerConnection ( configuration ) ;
2023-02-06 23:02:23 +00:00
// Store uni-directional PeerConnections:
// - If we are reading video from the other (offerer)
// - If we are sending video to the other (answerer)
if ( this . WebRTC . pc [ username ] == undefined ) {
this . WebRTC . pc [ username ] = { } ;
}
if ( isOfferer ) {
this . WebRTC . pc [ username ] . offerer = pc ;
} else {
this . WebRTC . pc [ username ] . answerer = pc ;
}
// Keep a pointer to the current channel being established (for candidate/SDP).
this . WebRTC . pc [ username ] . connecting = pc ;
2023-01-27 06:54:02 +00:00
2023-02-05 05:00:01 +00:00
// Create a data channel so we have something to connect over even if
// the local user is not broadcasting their own camera.
let dataChannel = pc . createDataChannel ( "data" ) ;
dataChannel . addEventListener ( "open" , event => {
// beginTransmission(dataChannel);
} )
2023-01-27 06:54:02 +00:00
// 'onicecandidate' notifies us whenever an ICE agent needs to deliver a
// message to the other peer through the signaling server.
pc . onicecandidate = event => {
if ( event . candidate ) {
this . ws . conn . send ( JSON . stringify ( {
action : "candidate" ,
username : username ,
2023-02-25 01:42:38 +00:00
candidate : JSON . stringify ( event . candidate ) ,
2023-01-27 06:54:02 +00:00
} ) ) ;
}
} ;
// If the user is offerer let the 'negotiationneeded' event create the offer.
if ( isOfferer ) {
pc . onnegotiationneeded = ( ) => {
pc . createOffer ( ) . then ( this . localDescCreated ( pc , username ) ) . catch ( this . ChatClient ) ;
} ;
}
// When a remote stream arrives.
pc . ontrack = event => {
const stream = event . streams [ 0 ] ;
// Do we already have it?
2023-02-05 08:53:50 +00:00
// this.ChatClient(`Received a video stream from ${username}.`);
2023-01-27 06:54:02 +00:00
if ( this . WebRTC . streams [ username ] == undefined ||
this . WebRTC . streams [ username ] . id !== stream . id ) {
this . WebRTC . streams [ username ] = stream ;
}
2023-02-05 05:00:01 +00:00
window . requestAnimationFrame ( ( ) => {
let $ref = document . getElementById ( ` videofeed- ${ username } ` ) ;
$ref . srcObject = stream ;
} ) ;
2023-02-06 04:26:00 +00:00
// Inform them they are being watched.
this . sendWatch ( username , true ) ;
2023-08-09 00:51:52 +00:00
// Set a mute video handler to detect freezes.
stream . getVideoTracks ( ) . forEach ( videoTrack => {
let freezeDetected = ( ) => {
console . log ( "FREEZE DETECTED:" , username ) ;
2023-08-11 03:01:38 +00:00
// Wait some seconds to see if the stream has recovered on its own
2023-08-09 00:51:52 +00:00
setTimeout ( ( ) => {
// Flag it as likely frozen.
if ( videoTrack . muted ) {
this . WebRTC . frozenStreamDetected [ username ] = true ;
}
2023-08-11 03:01:38 +00:00
} , 7500 ) ; // 7.5s
2023-08-09 00:51:52 +00:00
} ;
console . log ( "Apply onmute handler for" , username ) ;
videoTrack . onmute = freezeDetected ;
// Double check for frozen streams on an interval.
this . WebRTC . frozenStreamInterval [ username ] = setInterval ( ( ) => {
if ( videoTrack . muted ) freezeDetected ( ) ;
} , 3000 ) ;
} )
2023-01-27 06:54:02 +00:00
} ;
// If we were already broadcasting video, send our stream to
// the connecting user.
2023-02-05 05:00:01 +00:00
// TODO: currently both users need to have video on for the connection
// to succeed - if offerer doesn't addTrack it won't request a video channel
// and so the answerer (who has video) won't actually send its
2023-02-05 08:53:50 +00:00
if ( ! isOfferer && this . webcam . active ) {
// this.ChatClient(`Sharing our video stream to ${username}.`);
2023-02-05 05:00:01 +00:00
let stream = this . webcam . stream ;
stream . getTracks ( ) . forEach ( track => {
pc . addTrack ( track , stream )
} ) ;
2023-01-27 06:54:02 +00:00
}
2023-04-01 02:46:42 +00:00
// If we are the offerer, and this member wants to auto-open our camera
// then add our own stream to the connection.
2023-07-01 01:41:06 +00:00
if ( isOfferer && ( this . whoMap [ username ] . video & this . VideoFlag . MutualOpen ) && this . webcam . active ) {
2023-04-01 02:46:42 +00:00
let stream = this . webcam . stream ;
stream . getTracks ( ) . forEach ( track => {
pc . addTrack ( track , stream )
} ) ;
}
2023-01-27 06:54:02 +00:00
// If we are the offerer, begin the connection.
if ( isOfferer ) {
2023-02-05 08:53:50 +00:00
pc . createOffer ( {
offerToReceiveVideo : true ,
offerToReceiveAudio : true ,
} ) . then ( this . localDescCreated ( pc , username ) ) . catch ( this . ChatClient ) ;
2023-01-27 06:54:02 +00:00
}
} ,
// Common handler function for
localDescCreated ( pc , username ) {
return ( desc ) => {
2023-02-05 08:53:50 +00:00
// this.ChatClient(`setLocalDescription ${JSON.stringify(desc)}`);
2023-01-27 06:54:02 +00:00
pc . setLocalDescription (
new RTCSessionDescription ( desc ) ,
( ) => {
this . ws . conn . send ( JSON . stringify ( {
action : "sdp" ,
username : username ,
2023-02-25 01:42:38 +00:00
description : JSON . stringify ( pc . localDescription ) ,
2023-01-27 06:54:02 +00:00
} ) ) ;
} ,
console . error ,
)
} ;
} ,
// Handle inbound WebRTC signaling messages proxied by the websocket.
onCandidate ( msg ) {
2023-02-06 23:02:23 +00:00
if ( this . WebRTC . pc [ msg . username ] == undefined || ! this . WebRTC . pc [ msg . username ] . connecting ) {
2023-02-05 05:00:01 +00:00
return ;
}
2023-02-06 23:02:23 +00:00
let pc = this . WebRTC . pc [ msg . username ] . connecting ;
2023-01-27 06:54:02 +00:00
2023-02-25 01:42:38 +00:00
// XX: WebRTC candidate/SDP messages JSON stringify their inner payload so that the
// Go back-end server won't re-order their json keys (Safari on Mac OS is very sensitive
// to the keys being re-ordered during the handshake, in ways that NO OTHER BROWSER cares
// about at all). Re-parse the JSON stringified object here.
let candidate = JSON . parse ( msg . candidate ) ;
2023-01-27 06:54:02 +00:00
// Add the new ICE candidate.
pc . addIceCandidate (
new RTCIceCandidate (
2023-02-25 01:42:38 +00:00
candidate ,
2023-02-05 08:53:50 +00:00
( ) => { } ,
2023-01-27 06:54:02 +00:00
console . error ,
)
) ;
} ,
onSDP ( msg ) {
2023-02-06 23:02:23 +00:00
if ( this . WebRTC . pc [ msg . username ] == undefined || ! this . WebRTC . pc [ msg . username ] . connecting ) {
2023-02-05 05:00:01 +00:00
return ;
}
2023-02-06 23:02:23 +00:00
let pc = this . WebRTC . pc [ msg . username ] . connecting ;
2023-02-25 01:42:38 +00:00
// XX: WebRTC candidate/SDP messages JSON stringify their inner payload so that the
// Go back-end server won't re-order their json keys (Safari on Mac OS is very sensitive
// to the keys being re-ordered during the handshake, in ways that NO OTHER BROWSER cares
// about at all). Re-parse the JSON stringified object here.
let message = JSON . parse ( msg . description ) ;
2023-01-27 06:54:02 +00:00
// Add the new ICE candidate.
2023-02-05 08:53:50 +00:00
// this.ChatClient(`Received a Remote Description from ${msg.username}: ${JSON.stringify(msg.description)}.`);
2023-02-05 05:00:01 +00:00
pc . setRemoteDescription ( new RTCSessionDescription ( message ) , ( ) => {
2023-01-27 06:54:02 +00:00
// When receiving an offer let's answer it.
if ( pc . remoteDescription . type === 'offer' ) {
pc . createAnswer ( ) . then ( this . localDescCreated ( pc , msg . username ) ) . catch ( this . ChatClient ) ;
}
} , console . error ) ;
} ,
2023-02-06 04:26:00 +00:00
onWatch ( msg ) {
// The user has our video feed open now.
2023-08-05 02:24:42 +00:00
if ( this . isBootedAdmin ( msg . username ) ) return ;
2023-02-06 04:26:00 +00:00
this . webcam . watching [ msg . username ] = true ;
} ,
onUnwatch ( msg ) {
// The user has closed our video feed.
delete ( this . webcam . watching [ msg . username ] ) ;
} ,
sendWatch ( username , watching ) {
// Send the watch or unwatch message to backend.
this . ws . conn . send ( JSON . stringify ( {
action : watching ? "watch" : "unwatch" ,
username : username ,
} ) ) ;
} ,
2023-07-23 01:30:45 +00:00
isWatchingMe ( username ) {
// Return whether the user is watching your camera
return this . webcam . watching [ username ] === true ;
} ,
2023-01-27 06:54:02 +00:00
/ * *
* Front - end web app concerns .
* /
2023-02-06 21:27:29 +00:00
// Settings modal.
showSettings ( ) {
this . settingsModal . visible = true ;
} ,
hideSettings ( ) {
this . settingsModal . visible = false ;
} ,
2023-02-05 08:53:50 +00:00
// Set active chat room.
setChannel ( channel ) {
this . channel = typeof ( channel ) === "string" ? channel : channel . ID ;
2023-06-24 18:12:02 +00:00
this . scrollHistory ( this . channel , true ) ;
2023-02-05 08:53:50 +00:00
this . channels [ this . channel ] . unread = 0 ;
2023-02-06 01:42:09 +00:00
// Responsive CSS: switch back to chat panel upon selecting a channel.
this . openChatPanel ( ) ;
2023-02-12 00:02:48 +00:00
// Edit hyperlinks to open in a new window.
this . makeLinksExternal ( ) ;
2023-02-05 08:53:50 +00:00
} ,
hasUnread ( channel ) {
if ( this . channels [ channel ] == undefined ) {
return 0 ;
}
return this . channels [ channel ] . unread ;
} ,
2023-07-18 03:38:07 +00:00
hasAnyUnread ( ) {
// Returns total unread count (for mobile responsive view to show in the left drawer button)
let count = 0 ;
for ( let channel of Object . keys ( this . channels ) ) {
count += this . channels [ channel ] . unread ;
}
return count ;
} ,
anyUnreadDMs ( ) {
// Returns true if any unread messages are DM threads
for ( let channel of Object . keys ( this . channels ) ) {
if ( channel . indexOf ( "@" ) === 0 && this . channels [ channel ] . unread > 0 ) {
return true ;
}
}
return false ;
} ,
2023-02-05 08:53:50 +00:00
openDMs ( user ) {
let channel = "@" + user . username ;
this . initHistory ( channel ) ;
this . setChannel ( channel ) ;
2023-02-06 01:42:09 +00:00
// Responsive CSS: switch back to chat panel upon opening a DM.
this . openChatPanel ( ) ;
} ,
openProfile ( user ) {
let url = this . profileURLForUsername ( user . username ) ;
if ( url ) {
window . open ( url ) ;
}
} ,
avatarURL ( user ) {
// Resolve the avatar URL of this user.
if ( user . avatar . match ( /^https?:/i ) ) {
return user . avatar ;
} else if ( user . avatar . indexOf ( "/" ) === 0 ) {
return this . config . website . replace ( /\/+$/ , "" ) + user . avatar ;
}
return "" ;
} ,
2023-06-13 01:16:27 +00:00
isUsernameOnline ( username ) {
return this . whoMap [ username ] != undefined ;
} ,
2023-02-06 01:42:09 +00:00
avatarForUsername ( username ) {
if ( this . whoMap [ username ] != undefined && this . whoMap [ username ] . avatar ) {
return this . avatarURL ( this . whoMap [ username ] ) ;
}
return null ;
} ,
profileURLForUsername ( username ) {
if ( ! username ) return ;
username = username . replace ( /^@/ , "" ) ;
if ( this . whoMap [ username ] != undefined && this . whoMap [ username ] . profileURL ) {
let url = this . whoMap [ username ] . profileURL ;
if ( url . match ( /^https?:/i ) ) {
return url ;
} else if ( url . indexOf ( "/" ) === 0 ) {
// Subdirectory relative to our WebsiteURL
return this . config . website . replace ( /\/+$/ , "" ) + url ;
} else {
this . ChatClient ( "Didn't know how to open profile URL: " + url ) ;
}
return url ;
}
return null ;
2023-02-05 08:53:50 +00:00
} ,
2023-04-19 05:18:12 +00:00
nicknameForUsername ( username ) {
if ( ! username ) return ;
username = username . replace ( /^@/ , "" ) ;
if ( this . whoMap [ username ] != undefined && this . whoMap [ username ] . profileURL ) {
let nick = this . whoMap [ username ] . nickname ;
if ( nick ) {
return nick ;
}
2023-06-12 03:33:26 +00:00
} else if ( this . whoMap [ username ] == undefined && username !== 'ChatServer' && username !== 'ChatClient' ) {
// User is not even logged in! Add this note to their name
username += " (offline)" ;
2023-04-19 05:18:12 +00:00
}
return username ;
} ,
2023-08-09 00:51:52 +00:00
isUsernameCamNSFW ( username ) {
// returns true if the username is broadcasting and NSFW, false otherwise.
// (used for the color coding of their nickname on their video box - so assumed they are broadcasting)
if ( this . whoMap [ username ] != undefined && this . whoMap [ username ] . video & this . VideoFlag . NSFW ) {
return true ;
}
return false ;
} ,
2023-02-05 08:53:50 +00:00
leaveDM ( ) {
// Validate we're in a DM currently.
if ( this . channel . indexOf ( "@" ) !== 0 ) return ;
if ( ! window . confirm (
"Do you want to close this chat thread? Your conversation history will " +
"be forgotten on your computer, but your chat partner may still have " +
"your chat thread open on their end."
) ) {
return ;
}
let channel = this . channel ;
this . setChannel ( this . config . channels [ 0 ] . ID ) ;
delete ( this . channels [ channel ] ) ;
} ,
2023-06-24 20:08:15 +00:00
/* Take back messages (for everyone) or remove locally */
takeback ( msg ) {
if ( ! window . confirm (
"Do you want to take this message back? Doing so will remove this message from everybody's view in the chat room."
) ) return ;
this . ws . conn . send ( JSON . stringify ( {
action : "takeback" ,
msgID : msg . msgID ,
} ) ) ;
} ,
removeMessage ( msg ) {
if ( ! window . confirm (
"Do you want to remove this message from your view? This will delete the message only for you, but others in this chat thread may still see it."
) ) return ;
this . onTakeback ( {
msgID : msg . msgID ,
} )
} ,
2023-07-01 03:00:21 +00:00
/* message reaction emojis */
hasReactions ( msg ) {
return this . messageReactions [ msg . msgID ] != undefined ;
} ,
getReactions ( msg ) {
if ( ! this . hasReactions ( msg ) ) return [ ] ;
return this . messageReactions [ msg . msgID ] ;
} ,
2023-07-01 18:39:08 +00:00
iReacted ( msg , emoji ) {
// test whether the current user has reacted
if ( this . messageReactions [ msg . msgID ] != undefined && this . messageReactions [ msg . msgID ] [ emoji ] != undefined ) {
for ( let reactor of this . messageReactions [ msg . msgID ] [ emoji ] ) {
if ( reactor === this . username ) {
return true ;
}
}
}
return false ;
} ,
2023-07-01 03:00:21 +00:00
2023-02-05 08:53:50 +00:00
activeChannels ( ) {
// List of current channels, unread indicators etc.
let result = [ ] ;
for ( let channel of this . config . channels ) {
let data = {
ID : channel . ID ,
Name : channel . Name ,
} ;
if ( this . channels [ channel ] != undefined ) {
data . Unread = this . channels [ channel ] . unread ;
data . Updated = this . channels [ channel ] . updated ;
}
result . push ( data ) ;
}
return result ;
} ,
2023-01-27 04:34:58 +00:00
// Start broadcasting my webcam.
2023-07-09 19:33:02 +00:00
// - force=true to skip the NSFW modal prompt (this param is passed by the button in that modal)
// - changeCamera=true to re-negotiate WebRTC connections with a new camera device (invoked by the Settings modal)
startVideo ( { force = false , changeCamera = false } ) {
2023-01-27 04:34:58 +00:00
if ( this . webcam . busy ) return ;
2023-08-18 02:15:26 +00:00
// Before they go on cam the first time, ATTEMPT to get their device names.
// - If they had never granted permission, we won't get the names of
// the devices and no big deal.
// - If they had given permission before, we can present a nicer experience
// for them and enumerate their devices before they go on originally.
2023-08-18 02:36:33 +00:00
if ( ! changeCamera && ! force ) {
2023-08-18 02:15:26 +00:00
// Initial broadcast: did they select device IDs?
this . getDevices ( ) ;
}
2023-02-11 06:46:39 +00:00
// If we are running in PermitNSFW mode, show the user the modal.
if ( this . config . permitNSFW && ! force ) {
this . nsfwModalCast . visible = true ;
return ;
}
2023-07-09 19:33:02 +00:00
let mediaParams = {
2023-01-27 04:34:58 +00:00
audio : true ,
2023-07-09 19:33:02 +00:00
video : {
width : { max : 1280 } ,
height : { max : 720 } ,
} ,
} ;
2023-08-18 02:15:26 +00:00
// Name the specific devices chosen by the user.
if ( this . webcam . videoDeviceID ) {
2023-07-09 19:33:02 +00:00
mediaParams . video . deviceId = { exact : this . webcam . videoDeviceID } ;
2023-08-18 02:15:26 +00:00
}
if ( this . webcam . audioDeviceID ) {
2023-07-09 19:33:02 +00:00
mediaParams . audio = {
deviceId : { exact : this . webcam . audioDeviceID } ,
} ;
}
this . webcam . busy = true ;
navigator . mediaDevices . getUserMedia ( mediaParams ) . then ( stream => {
2023-01-27 04:34:58 +00:00
this . webcam . active = true ;
this . webcam . elem . srcObject = stream ;
this . webcam . stream = stream ;
2023-04-01 03:25:53 +00:00
// Save our mutuality prefs.
localStorage . videoMutual = this . webcam . mutual ;
localStorage . videoMutualOpen = this . webcam . mutualOpen ;
2023-08-12 01:56:22 +00:00
localStorage . videoAutoMute = this . webcam . autoMute ;
// Auto-mute our camera? Two use cases:
// 1. The user marked their cam as muted but then changed video device,
// so we set the mute to match their preference as shown on their UI.
// 2. The user opted to auto-mute their camera from the get to on their
// NSFW broadcast modal popup.
if ( this . webcam . muted || this . webcam . autoMute ) {
// Mute their audio tracks.
this . webcam . stream . getAudioTracks ( ) . forEach ( track => {
track . enabled = false ;
} ) ;
// Set their front-end mute toggle to match (in case of autoMute).
this . webcam . muted = true ;
}
2023-04-01 03:25:53 +00:00
2023-01-27 04:34:58 +00:00
// Tell backend the camera is ready.
this . sendMe ( ) ;
2023-04-19 05:18:12 +00:00
// Record the selected device IDs.
this . webcam . videoDeviceID = stream . getVideoTracks ( ) [ 0 ] . getSettings ( ) . deviceId ;
this . webcam . audioDeviceID = stream . getAudioTracks ( ) [ 0 ] . getSettings ( ) . deviceId ;
console . log ( "device IDs:" , this . webcam . videoDeviceID , this . webcam . audioDeviceID ) ;
// Collect video and audio devices to let the user change them in their settings.
2023-08-19 01:54:45 +00:00
this . getDevices ( ) . then ( ( ) => {
// Store their names on localStorage in case we can reselect them by name
// on the user's next visit.
this . storePreferredDeviceNames ( ) ;
} ) ;
2023-07-09 19:33:02 +00:00
// If we have changed devices, reconnect everybody's WebRTC channels for your existing watchers.
if ( changeCamera ) {
this . updateWebRTCStreams ( ) ;
}
2023-01-27 04:34:58 +00:00
} ) . catch ( err => {
this . ChatClient ( ` Webcam error: ${ err } ` ) ;
} ) . finally ( ( ) => {
this . webcam . busy = false ;
} )
} ,
2023-04-19 05:18:12 +00:00
getDevices ( ) {
// Collect video and audio devices.
if ( ! navigator . mediaDevices ? . enumerateDevices ) {
console . log ( "enumerateDevices() not supported." ) ;
return ;
}
2023-08-18 01:45:43 +00:00
this . webcam . gettingDevices = true ;
2023-08-19 01:54:45 +00:00
return navigator . mediaDevices . enumerateDevices ( ) . then ( devices => {
2023-04-19 05:18:12 +00:00
this . webcam . videoDevices = [ ] ;
this . webcam . audioDevices = [ ] ;
devices . forEach ( device => {
2023-08-18 02:15:26 +00:00
// If we can't get the device label, disregard it.
// It can happen if the user has not yet granted permission.
2023-08-18 02:36:33 +00:00
if ( ! device . label ) {
return ;
} ;
2023-08-18 02:15:26 +00:00
2023-04-19 05:18:12 +00:00
if ( device . kind === 'videoinput' ) {
2023-07-09 19:33:02 +00:00
// console.log(`Video device ${device.deviceId} ${device.label}`);
2023-04-19 05:18:12 +00:00
this . webcam . videoDevices . push ( {
id : device . deviceId ,
label : device . label ,
} ) ;
} else if ( device . kind === 'audioinput' ) {
2023-07-09 19:33:02 +00:00
// console.log(`Audio device ${device.deviceId} ${device.label}`);
2023-04-19 05:18:12 +00:00
this . webcam . audioDevices . push ( {
id : device . deviceId ,
label : device . label ,
} ) ;
}
2023-08-19 01:54:45 +00:00
} ) ;
// If we don't have the user's current active device IDs (e.g., they have
// not yet started their video the first time), see if we can pre-select
// by their preferred device names.
if ( ! this . webcam . videoDeviceID && this . webcam . preferredDeviceNames . video ) {
for ( let dev of this . webcam . videoDevices ) {
if ( dev . label === this . webcam . preferredDeviceNames . video ) {
this . webcam . videoDeviceID = dev . id ;
}
}
}
if ( ! this . webcam . audioDeviceID && this . webcam . preferredDeviceNames . audio ) {
for ( let dev of this . webcam . audioDevices ) {
if ( dev . label === this . webcam . preferredDeviceNames . audio ) {
this . webcam . audioDeviceID = dev . id ;
}
}
}
2023-04-19 05:18:12 +00:00
} ) . catch ( err => {
this . ChatClient ( ` Error listing your cameras and microphones: ${ err . name } : ${ err . message } ` ) ;
2023-08-18 01:45:43 +00:00
} ) . finally ( ( ) => {
// In the settings modal, let the spinner spin for a moment.
setTimeout ( ( ) => {
this . webcam . gettingDevices = false ;
} , 500 ) ;
2023-04-19 05:18:12 +00:00
} )
} ,
2023-08-19 01:54:45 +00:00
storePreferredDeviceNames ( ) {
// This function looks up the names of the user's selected video/audio device.
// When they come back later, IF we are able to enumerate devices before they
// go on for the first time, we might pre-select their last device by name.
// NOTE: on iOS apparently device IDs shuffle every single time so only names
// may be reliable!
if ( this . webcam . videoDeviceID ) {
for ( let dev of this . webcam . videoDevices ) {
if ( dev . id === this . webcam . videoDeviceID && dev . label ) {
this . webcam . preferredDeviceNames . video = dev . label ;
}
}
}
if ( this . webcam . audioDeviceID ) {
for ( let dev of this . webcam . audioDevices ) {
if ( dev . id === this . webcam . audioDeviceID && dev . label ) {
this . webcam . preferredDeviceNames . audio = dev . label ;
}
}
}
// Put them on localStorage.
localStorage . preferredDeviceNames = JSON . stringify ( this . webcam . preferredDeviceNames ) ;
} ,
2023-01-27 04:34:58 +00:00
2023-07-09 19:33:02 +00:00
// Replace your video/audio streams for your watchers (on camera changes)
updateWebRTCStreams ( ) {
console . log ( "Re-negotiating video and audio channels to your watchers." ) ;
for ( let username of Object . keys ( this . WebRTC . pc ) ) {
let pc = this . WebRTC . pc [ username ] ;
if ( pc . answerer != undefined ) {
let oldTracks = pc . answerer . getSenders ( ) ;
let newTracks = this . webcam . stream . getTracks ( ) ;
// Remove and replace the tracks.
for ( let old of oldTracks ) {
if ( old . track . kind === 'audio' ) {
for ( let replace of newTracks ) {
if ( replace . kind === 'audio' ) {
old . replaceTrack ( replace ) ;
}
}
}
else if ( old . track . kind === 'video' ) {
for ( let replace of newTracks ) {
if ( replace . kind === 'video' ) {
old . replaceTrack ( replace ) ;
}
}
}
}
}
}
} ,
2023-01-27 06:54:02 +00:00
// Begin connecting to someone else's webcam.
2023-08-09 00:51:52 +00:00
openVideoByUsername ( username , force ) {
if ( this . whoMap [ username ] != undefined ) {
this . openVideo ( this . whoMap [ username ] , force ) ;
return ;
}
this . ChatClient ( "Couldn't open video by username: not found." ) ;
} ,
2023-02-11 06:46:39 +00:00
openVideo ( user , force ) {
2023-01-27 06:54:02 +00:00
if ( user . username === this . username ) {
this . ChatClient ( "You can already see your own webcam." ) ;
return ;
}
2023-06-15 04:06:57 +00:00
// If we have muted the target, we shouldn't view their video.
if ( this . isMutedUser ( user . username ) ) {
this . ChatClient ( ` You have muted <strong> ${ user . username } </strong> and so should not see their camera. ` ) ;
return ;
}
2023-02-11 06:46:39 +00:00
// Is the target user NSFW? Go thru the modal.
let dontShowAgain = localStorage [ "skip-nsfw-modal" ] == "true" ;
2023-07-01 01:41:06 +00:00
if ( ( user . video & this . VideoFlag . NSFW ) && ! dontShowAgain && ! force ) {
2023-02-11 06:46:39 +00:00
this . nsfwModalView . user = user ;
this . nsfwModalView . visible = true ;
return ;
}
if ( this . nsfwModalView . dontShowAgain ) {
// user doesn't want to see the modal again.
localStorage [ "skip-nsfw-modal" ] = "true" ;
}
2023-08-09 00:55:22 +00:00
// Debounce so we don't spam too much for the same user.
if ( this . WebRTC . debounceOpens [ user . username ] ) return ;
this . WebRTC . debounceOpens [ user . username ] = true ;
setTimeout ( ( ) => {
delete ( this . WebRTC . debounceOpens [ user . username ] ) ;
} , 5000 ) ;
2023-02-05 08:53:50 +00:00
// Camera is already open? Then disconnect the connection.
2023-02-06 23:02:23 +00:00
if ( this . WebRTC . pc [ user . username ] != undefined && this . WebRTC . pc [ user . username ] . offerer != undefined ) {
this . closeVideo ( user . username , "offerer" ) ;
2023-02-05 05:00:01 +00:00
}
2023-04-01 02:46:42 +00:00
// If this user requests mutual viewership...
2023-07-01 01:41:06 +00:00
if ( ( user . video & this . VideoFlag . MutualRequired ) && ! this . webcam . active ) {
2023-04-01 02:46:42 +00:00
this . ChatClient (
` <strong> ${ user . username } </strong> has requested that you should share your own camera too before opening theirs. `
) ;
return ;
}
2023-01-27 06:54:02 +00:00
this . sendOpen ( user . username ) ;
2023-02-06 04:26:00 +00:00
// Responsive CSS -> go to chat panel to see the camera
this . openChatPanel ( ) ;
2023-06-25 14:36:26 +00:00
// Send some feedback to the chat window.
this . ChatClient (
2023-08-07 04:53:52 +00:00
` A request was sent to open <strong> ${ user . username } </strong>'s camera which should (hopefully) appear on your screen soon. ` ,
2023-06-25 14:36:26 +00:00
) ;
2023-01-27 06:54:02 +00:00
} ,
2023-02-06 23:02:23 +00:00
closeVideo ( username , name ) {
2023-08-09 02:09:49 +00:00
// Clean up any lingering camera freeze states.
delete ( this . WebRTC . frozenStreamDetected [ username ] ) ;
if ( this . WebRTC . frozenStreamInterval [ username ] ) {
clearInterval ( this . WebRTC . frozenStreamInterval ) ;
delete ( this . WebRTC . frozenStreamInterval [ username ] ) ;
}
2023-02-06 23:02:23 +00:00
if ( name === "offerer" ) {
// We are closing another user's video stream.
delete ( this . WebRTC . streams [ username ] ) ;
2023-07-01 01:41:06 +00:00
delete ( this . WebRTC . muted [ username ] ) ;
delete ( this . WebRTC . poppedOut [ username ] ) ;
2023-02-06 23:02:23 +00:00
if ( this . WebRTC . pc [ username ] != undefined && this . WebRTC . pc [ username ] . offerer != undefined ) {
this . WebRTC . pc [ username ] . offerer . close ( ) ;
delete ( this . WebRTC . pc [ username ] ) ;
}
// Inform backend we have closed it.
this . sendWatch ( username , false ) ;
return ;
} else if ( name === "answerer" ) {
// We have turned off our camera, kick off viewers.
if ( this . WebRTC . pc [ username ] != undefined && this . WebRTC . pc [ username ] . answerer != undefined ) {
this . WebRTC . pc [ username ] . answerer . close ( ) ;
delete ( this . WebRTC . pc [ username ] ) ;
}
return ;
}
2023-02-05 08:53:50 +00:00
// A user has logged off the server. Clean up any WebRTC connections.
delete ( this . WebRTC . streams [ username ] ) ;
2023-02-06 04:26:00 +00:00
delete ( this . webcam . watching [ username ] ) ;
2023-02-05 08:53:50 +00:00
if ( this . WebRTC . pc [ username ] != undefined ) {
2023-02-06 23:02:23 +00:00
if ( this . WebRTC . pc [ username ] . offerer ) {
this . WebRTC . pc [ username ] . offerer . close ( ) ;
}
if ( this . WebRTC . pc [ username ] . answerer ) {
this . WebRTC . pc [ username ] . answerer . close ( ) ;
}
2023-02-05 08:53:50 +00:00
delete ( this . WebRTC . pc [ username ] ) ;
2023-07-01 01:41:06 +00:00
delete ( this . WebRTC . muted [ username ] ) ;
delete ( this . WebRTC . poppedOut [ username ] ) ;
2023-02-05 08:53:50 +00:00
}
2023-02-06 04:26:00 +00:00
2023-08-09 00:51:52 +00:00
// Clean up any lingering camera freeze states.
delete ( this . WebRTC . frozenStreamDetected [ username ] ) ;
if ( this . WebRTC . frozenStreamInterval [ username ] ) {
clearInterval ( this . WebRTC . frozenStreamInterval ) ;
delete ( this . WebRTC . frozenStreamInterval [ username ] ) ;
}
2023-02-06 04:26:00 +00:00
// Inform backend we have closed it.
this . sendWatch ( username , false ) ;
} ,
2023-04-01 02:46:42 +00:00
unMutualVideo ( ) {
// If we had our camera on to watch a video of someone who wants mutual cameras,
// and then we turn ours off: we should unfollow the ones with mutual video.
2023-08-12 01:58:05 +00:00
if ( this . webcam . active ) return ;
2023-04-01 02:46:42 +00:00
for ( let row of this . whoList ) {
let username = row . username ;
2023-07-01 01:41:06 +00:00
if ( ( row . video & this . VideoFlag . MutualRequired ) && this . WebRTC . pc [ username ] != undefined ) {
2023-04-01 02:46:42 +00:00
this . closeVideo ( username ) ;
}
}
} ,
2023-07-01 01:41:06 +00:00
webcamIconClass ( user ) {
// Return the icon to show on a video button.
// - Usually a video icon
// - May be a crossed-out video if isVideoNotAllowed
// - Or an eyeball for cameras already opened
if ( user . username === this . username && this . webcam . active ) {
return 'fa-eye' ; // user sees their own self camera always
}
// Already opened?
if ( this . WebRTC . pc [ user . username ] != undefined && this . WebRTC . streams [ user . username ] != undefined ) {
return 'fa-eye' ;
}
if ( this . isVideoNotAllowed ( user ) ) return 'fa-video-slash' ;
return 'fa-video' ;
} ,
2023-06-15 04:06:57 +00:00
isVideoNotAllowed ( user ) {
// Returns whether the video button to open a user's cam will be not allowed (crossed out)
// Mutual video sharing is required on this camera, and ours is not active
2023-07-01 01:41:06 +00:00
if ( ( user . video & this . VideoFlag . Active ) && ( user . video & this . VideoFlag . MutualRequired ) && ! this . webcam . active ) {
2023-06-15 04:06:57 +00:00
return true ;
}
// We have muted them and it wouldn't be appropriate to still watch their video but not get their messages.
if ( this . isMutedUser ( user . username ) ) {
return true ;
}
return false ;
} ,
2023-02-06 04:26:00 +00:00
2023-08-13 06:09:46 +00:00
// Show who watches our video.
showViewers ( ) {
// TODO: for now, ChatClient is our bro.
let users = Object . keys ( this . webcam . watching ) ;
if ( users . length === 0 ) {
this . ChatClient ( "There is currently nobody viewing your camera." ) ;
} else {
this . ChatClient ( "Your current webcam viewers are:<br><br>" + users . join ( ", " ) ) ;
}
// Also focus the Watching list.
this . whoTab = 'watching' ;
// TODO: if mobile, show the panel - this width matches
// the media query in chat.css
if ( screen . width < 1024 ) {
this . openWhoPanel ( ) ;
}
} ,
// Boot someone off your video.
2023-03-23 03:21:04 +00:00
bootUser ( username ) {
if ( ! window . confirm (
` Kick ${ username } off your camera? This will also prevent them ` +
` from seeing that your camera is active for the remainder of your ` +
` chat session. ` ) ) {
return ;
}
this . sendBoot ( username ) ;
2023-08-05 02:24:42 +00:00
this . WebRTC . booted [ username ] = true ;
2023-03-23 03:21:04 +00:00
// Close the WebRTC peer connection.
if ( this . WebRTC . pc [ username ] != undefined ) {
this . closeVideo ( username , "answerer" ) ;
}
this . ChatClient (
` You have booted ${ username } off your camera. They will no longer be able ` +
` to connect to your camera, or even see that your camera is active at all -- ` +
` to them it appears as though you had turned yours off.<br><br>This will be ` +
` in place for the remainder of your current chat session. `
) ;
2023-02-05 08:53:50 +00:00
} ,
2023-08-05 02:24:42 +00:00
isBootedAdmin ( username ) {
return ( this . WebRTC . booted [ username ] === true || this . muted [ username ] === true ) &&
this . whoMap [ username ] != undefined &&
this . whoMap [ username ] . op ;
} ,
2023-01-27 06:54:02 +00:00
2023-01-27 04:34:58 +00:00
// Stop broadcasting.
stopVideo ( ) {
2023-02-05 08:53:50 +00:00
// Close all WebRTC sessions.
for ( username of Object . keys ( this . WebRTC . pc ) ) {
2023-02-06 23:02:23 +00:00
this . closeVideo ( username , "answerer" ) ;
2023-02-05 08:53:50 +00:00
}
2023-04-01 02:46:42 +00:00
// Hang up on mutual cameras.
this . unMutualVideo ( ) ;
2023-07-08 19:08:46 +00:00
// Close the local camera devices completely.
this . webcam . stream . getTracks ( ) . forEach ( track => {
track . stop ( ) ;
} ) ;
// Reset all front-end state.
this . webcam . elem . srcObject = null ;
this . webcam . stream = null ;
this . webcam . active = false ;
this . webcam . muted = false ;
this . whoTab = "online" ;
2023-01-27 04:34:58 +00:00
// Tell backend our camera state.
this . sendMe ( ) ;
} ,
2023-02-06 04:26:00 +00:00
// Mute my microphone if broadcasting.
muteMe ( ) {
this . webcam . muted = ! this . webcam . muted ;
this . webcam . stream . getAudioTracks ( ) . forEach ( track => {
track . enabled = ! this . webcam . muted ;
} ) ;
2023-07-01 01:41:06 +00:00
// Communicate our local mute to others.
this . sendMe ( ) ;
} ,
isSourceMuted ( username ) {
// See if the webcam broadcaster muted their mic at the source
if ( this . whoMap [ username ] != undefined && this . whoMap [ username ] . video & this . VideoFlag . Muted ) {
return true ;
}
return false ;
2023-02-06 04:26:00 +00:00
} ,
isMuted ( username ) {
return this . WebRTC . muted [ username ] === true ;
} ,
muteVideo ( username ) {
this . WebRTC . muted [ username ] = ! this . isMuted ( username ) ;
// Find the <video> tag to mute it.
let $ref = document . getElementById ( ` videofeed- ${ username } ` ) ;
if ( $ref ) {
$ref . muted = this . WebRTC . muted [ username ] ;
}
} ,
2023-04-20 05:00:31 +00:00
// Pop out a user's video.
popoutVideo ( username ) {
this . WebRTC . poppedOut [ username ] = ! this . WebRTC . poppedOut [ username ] ;
// If not popped out, reset CSS positioning.
window . requestAnimationFrame ( this . makeDraggableVideos ) ;
} ,
// Outside of Vue, attach draggable video scripts to DOM.
makeDraggableVideos ( ) {
let $panel = document . querySelector ( "#video-feeds" ) ;
interact ( '.popped-in' ) . unset ( ) ;
// Give popped out videos to the root of the DOM so they can
// be dragged anywhere on the page.
window . requestAnimationFrame ( ( ) => {
document . querySelectorAll ( '.popped-out' ) . forEach ( node => {
// $panel.removeChild(node);
document . body . appendChild ( node ) ;
} ) ;
document . querySelectorAll ( '.popped-in' ) . forEach ( node => {
// document.body.removeChild(node);
$panel . appendChild ( node ) ;
node . style . top = null ;
node . style . left = null ;
node . setAttribute ( 'data-x' , 0 ) ;
node . setAttribute ( 'data-y' , 0 ) ;
} ) ;
} ) ;
interact ( '.popped-out' ) . draggable ( {
// enable inertial throwing
inertia : true ,
// keep the element within the area of it's parent
modifiers : [
interact . modifiers . restrictRect ( {
restriction : 'parent' ,
endOnly : true
} )
] ,
listeners : {
// call this function on every dragmove event
move ( event ) {
let target = event . target ;
let x = ( parseFloat ( target . getAttribute ( 'data-x' ) ) || 0 ) + event . dx
let y = ( parseFloat ( target . getAttribute ( 'data-y' ) ) || 0 ) + event . dy
target . style . top = ` ${ y } px ` ;
target . style . left = ` ${ x } px ` ;
target . setAttribute ( 'data-x' , x ) ;
target . setAttribute ( 'data-y' , y ) ;
} ,
// call this function on every dragend event
end ( event ) {
console . log (
'moved a distance of ' +
( Math . sqrt ( Math . pow ( event . pageX - event . x0 , 2 ) +
Math . pow ( event . pageY - event . y0 , 2 ) | 0 ) )
. toFixed ( 2 ) + 'px' )
}
}
} ) . resizable ( {
edges : { left : true , right : true , bottom : true , right : true } ,
listeners : {
move ( event ) {
var target = event . target
var x = ( parseFloat ( target . getAttribute ( 'data-x' ) ) || 0 )
var y = ( parseFloat ( target . getAttribute ( 'data-y' ) ) || 0 )
// update the element's style
target . style . width = event . rect . width + 'px'
target . style . height = event . rect . height + 'px'
// translate when resizing from top or left edges
x += event . deltaRect . left
y += event . deltaRect . top
target . style . top = ` ${ y } px ` ;
target . style . left = ` ${ x } px ` ;
target . setAttribute ( 'data-x' , x )
target . setAttribute ( 'data-y' , y )
}
} ,
modifiers : [
// keep the edges inside the parent
interact . modifiers . restrictEdges ( {
outer : 'parent'
} ) ,
// minimum size
interact . modifiers . restrictSize ( {
min : { width : 100 , height : 50 }
} )
] ,
inertia : true
} )
} ,
2023-02-05 08:53:50 +00:00
initHistory ( channel ) {
if ( this . channels [ channel ] == undefined ) {
this . channels [ channel ] = {
history : [ ] ,
updated : Date . now ( ) ,
unread : 0 ,
} ;
}
} ,
2023-06-24 20:08:15 +00:00
pushHistory ( { channel , username , message , action = "message" , isChatServer , isChatClient , messageID } ) {
2023-02-05 08:53:50 +00:00
// Default channel = your current channel.
if ( ! channel ) {
channel = this . channel ;
}
// Initialize this channel's history?
this . initHistory ( channel ) ;
2023-08-15 02:59:35 +00:00
// Image handling per the user's preference.
if ( message . indexOf ( "<img" ) > - 1 ) {
if ( this . imageDisplaySetting === "hide" ) {
return ;
} else if ( this . imageDisplaySetting === "collapse" ) {
// Put a collapser link.
let collapseID = ` collapse- ${ messageID } ` ;
message = `
< a href = "#" id = "img-show-${collapseID}"
class = "button is-outlined is-small is-info"
onclick = " document . querySelector ( '#img-${collapseID}' ) . style . display = 'block' ;
document . querySelector ( '#img-show-${collapseID}' ) . style . display = 'none' ;
return false " >
< i class = "fa fa-image mr-1" > < / i >
Image attachment - click to expand
< / a >
< div id = "img-${collapseID}" style = "display: none" > $ { message } < / d i v > ` ;
}
}
2023-02-05 08:53:50 +00:00
// Append the message.
2023-07-23 01:30:45 +00:00
this . channels [ channel ] . updated = new Date ( ) . getTime ( ) ;
2023-02-05 08:53:50 +00:00
this . channels [ channel ] . history . push ( {
2023-01-27 04:34:58 +00:00
action : action ,
2023-08-13 04:35:41 +00:00
channel : channel ,
2023-01-11 06:38:48 +00:00
username : username ,
message : message ,
2023-06-24 20:08:15 +00:00
msgID : messageID ,
2023-02-10 07:03:06 +00:00
at : new Date ( ) ,
2023-01-27 04:34:58 +00:00
isChatServer ,
isChatClient ,
2023-01-11 06:38:48 +00:00
} ) ;
2023-07-23 01:30:45 +00:00
// Trim the history per the scrollback buffer.
if ( this . scrollback > 0 && this . channels [ channel ] . history . length > this . scrollback ) {
this . channels [ channel ] . history = this . channels [ channel ] . history . slice (
- this . scrollback ,
this . channels [ channel ] . history . length + 1 ,
) ;
}
2023-06-24 18:12:02 +00:00
this . scrollHistory ( channel ) ;
2023-02-05 08:53:50 +00:00
// Mark unread notifiers if this is not our channel.
if ( this . channel !== channel ) {
2023-02-06 01:42:09 +00:00
// Don't notify about presence broadcasts.
2023-03-17 02:02:59 +00:00
if ( action !== "presence" && ! isChatServer ) {
2023-02-06 01:42:09 +00:00
this . channels [ channel ] . unread ++ ;
}
2023-02-05 08:53:50 +00:00
}
2023-02-12 00:02:48 +00:00
// Edit hyperlinks to open in a new window.
this . makeLinksExternal ( ) ;
2023-01-27 04:34:58 +00:00
} ,
2023-06-24 18:12:02 +00:00
scrollHistory ( channel , force ) {
if ( ! this . autoscroll && ! force ) return ;
2023-03-25 04:56:40 +00:00
2023-01-27 04:34:58 +00:00
window . requestAnimationFrame ( ( ) => {
2023-06-24 18:12:02 +00:00
// Only scroll if it's the current channel.
if ( channel !== this . channel ) return ;
2023-01-27 04:34:58 +00:00
this . historyScrollbox . scroll ( {
top : this . historyScrollbox . scrollHeight ,
behavior : 'smooth' ,
} ) ;
} ) ;
} ,
2023-02-05 08:53:50 +00:00
// Responsive CSS controls for mobile. Notes for maintenance:
// - The chat.css has responsive CSS to hide the left/right panels
// and set the grid-template-columns for devices < 1024px width
// - These functions override w/ style tags to force the drawer to
// be visible and change the grid-template-columns.
// - On window resize (e.g. device rotation) or when closing one
// of the side drawers, we reset our CSS overrides to default so
// the main chat window reappears.
openChannelsPanel ( ) {
// Open the left drawer
let $container = this . responsive . nodes . $container ,
$drawer = this . responsive . nodes . $left ;
$container . style . gridTemplateColumns = "1fr 0 0" ;
$drawer . style . display = "block" ;
} ,
openWhoPanel ( ) {
// Open the right drawer
let $container = this . responsive . nodes . $container ,
$drawer = this . responsive . nodes . $right ;
$container . style . gridTemplateColumns = "0 0 1fr" ;
$drawer . style . display = "block" ;
} ,
openChatPanel ( ) {
this . resetResponsiveCSS ( ) ;
} ,
resetResponsiveCSS ( ) {
let $container = this . responsive . nodes . $container ,
$left = this . responsive . nodes . $left ,
$right = this . responsive . nodes . $right ;
$left . style . removeProperty ( "display" ) ;
$right . style . removeProperty ( "display" ) ;
$container . style . removeProperty ( "grid-template-columns" ) ;
} ,
2023-01-27 04:34:58 +00:00
// Send a chat message as ChatServer
ChatServer ( message ) {
this . pushHistory ( {
username : "ChatServer" ,
message : message ,
isChatServer : true ,
} ) ;
} ,
ChatClient ( message ) {
this . pushHistory ( {
username : "ChatClient" ,
message : message ,
isChatClient : true ,
} ) ;
} ,
2023-02-06 21:27:29 +00:00
// Format a datetime nicely for chat timestamp.
prettyDate ( date ) {
2023-08-13 04:35:41 +00:00
if ( date == undefined ) return '' ;
2023-02-06 21:27:29 +00:00
let hours = date . getHours ( ) ,
minutes = String ( date . getMinutes ( ) ) . padStart ( 2 , '0' ) ,
ampm = hours >= 11 ? "pm" : "am" ;
2023-02-10 07:03:06 +00:00
let hour = hours % 12 || 12 ;
2023-04-19 05:34:45 +00:00
return ` ${ ( hour ) } : ${ minutes } ${ ampm } ` ;
2023-02-06 21:27:29 +00:00
} ,
2023-08-06 02:38:04 +00:00
// CSS classes for the profile button (color coded genders)
profileButtonClass ( user ) {
let gender = ( user . gender || "" ) . toLowerCase ( ) ;
if ( gender . indexOf ( "m" ) === 0 ) {
return "has-text-gender-male" ;
} else if ( gender . indexOf ( "f" ) === 0 ) {
return "has-text-gender-female" ;
} else if ( gender . length > 0 ) {
return "has-text-gender-other" ;
}
return "" ;
} ,
2023-03-22 04:29:24 +00:00
/ * *
* Image sharing in chat
* /
// The image upload button handler.
uploadFile ( ) {
let input = document . createElement ( 'input' ) ;
input . type = 'file' ;
input . accept = 'image/*' ;
input . onchange = e => {
let file = e . target . files [ 0 ] ;
if ( file . size > FileUploadMaxSize ) {
this . ChatClient ( ` Please share an image smaller than ${ FileUploadMaxSize / 1024 / 1024 } MB in size! ` ) ;
return ;
}
this . ChatClient ( ` <em>Uploading file to chat: ${ file . name } - ${ file . size } bytes, ${ file . type } format.</em> ` ) ;
// Get image file data.
let reader = new FileReader ( ) ;
let rawData = new ArrayBuffer ( ) ;
reader . onload = e => {
rawData = e . target . result ;
let fileByteArray = [ ] ,
u8array = new Uint8Array ( rawData ) ;
for ( let i = 0 ; i < u8array . length ; i ++ ) {
fileByteArray . push ( u8array [ i ] ) ;
}
let msg = JSON . stringify ( {
action : "file" ,
channel : this . channel ,
message : file . name ,
bytes : fileByteArray , //btoa(fileByteArray),
} ) ;
// Send it to the chat server.
this . ws . conn . send ( msg ) ;
} ;
reader . readAsArrayBuffer ( file ) ;
} ;
input . click ( ) ;
} ,
2023-02-06 21:27:29 +00:00
/ * *
* Sound effect concerns .
* /
setupSounds ( ) {
2023-06-12 02:21:06 +00:00
// Note: setupSounds had to be called on a page gesture (mouse click) before browsers
// allow it to set up the AudioContext. If we've successfully set one up before, exit
// this function immediately.
if ( this . config . sounds . audioContext ) {
2023-06-12 03:33:26 +00:00
if ( this . config . sounds . audioContext . state === 'suspended' ) {
this . config . sounds . audioContext . resume ( ) ;
}
2023-06-12 02:21:06 +00:00
return ;
}
2023-02-09 05:23:08 +00:00
try {
if ( AudioContext ) {
this . config . sounds . audioContext = new AudioContext ( ) ;
} else {
this . config . sounds . audioContext = window . AudioContext || window . webkitAudioContext ;
}
} catch { }
2023-02-06 21:27:29 +00:00
if ( ! this . config . sounds . audioContext ) {
console . error ( "Couldn't set up AudioContext! No sound effects will be supported." ) ;
return ;
}
// Create <audio> elements for all the sounds.
for ( let effect of this . config . sounds . available ) {
if ( ! effect . filename ) continue ; // 'Quiet' has no audio
let elem = document . createElement ( "audio" ) ;
elem . autoplay = false ;
elem . src = ` /static/sfx/ ${ effect . filename } ` ;
document . body . appendChild ( elem ) ;
let track = this . config . sounds . audioContext . createMediaElementSource ( elem ) ;
track . connect ( this . config . sounds . audioContext . destination ) ;
this . config . sounds . audioTracks [ effect . name ] = elem ;
}
// Apply the user's saved preferences if any.
for ( let setting of Object . keys ( this . config . sounds . settings ) ) {
if ( localStorage [ ` sound: ${ setting } ` ] != undefined ) {
this . config . sounds . settings [ setting ] = localStorage [ ` sound: ${ setting } ` ] ;
}
}
} ,
playSound ( event ) {
let filename = this . config . sounds . settings [ event ] ;
// Do we have an audio track?
if ( this . config . sounds . audioTracks [ filename ] != undefined ) {
let track = this . config . sounds . audioTracks [ filename ] ;
track . play ( ) ;
}
} ,
setSoundPref ( event ) {
this . playSound ( event ) ;
// Store the user's setting in localStorage.
localStorage [ ` sound: ${ event } ` ] = this . config . sounds . settings [ event ] ;
} ,
2023-02-12 00:02:48 +00:00
2023-03-28 04:13:04 +00:00
// Make all links in chat open in new windows
2023-02-12 00:02:48 +00:00
makeLinksExternal ( ) {
window . requestAnimationFrame ( ( ) => {
let $history = document . querySelector ( "#chatHistory" ) ;
// Make all <a> links appearing in chat into external links.
( $history . querySelectorAll ( "a" ) || [ ] ) . forEach ( node => {
let href = node . attributes . href ,
target = node . attributes . target ;
if ( href == undefined || target != undefined ) return ;
node . target = "_blank" ;
} ) ;
} )
} ,
2023-03-28 04:13:04 +00:00
/ *
* Idle Detection methods
* /
setupIdleDetection ( ) {
window . addEventListener ( "keypress" , this . deidle ) ;
window . addEventListener ( "mousemove" , this . deidle ) ;
} ,
// Common "de-idle" event handler
deidle ( e ) {
if ( this . status === "idle" ) {
this . status = "online" ;
}
if ( this . idleTimeout !== null ) {
clearTimeout ( this . idleTimeout ) ;
}
this . idleTimeout = setTimeout ( this . goIdle , 1000 * this . idleThreshold ) ;
} ,
goIdle ( ) {
// only if we aren't already set on away
if ( this . status === "online" ) {
this . status = "idle" ;
}
2023-08-13 04:35:41 +00:00
} ,
/ *
* Webhook methods
* /
isWebhookEnabled ( name ) {
for ( let webhook of this . config . webhookURLs ) {
if ( webhook . Name === name && webhook . Enabled ) {
return true ;
}
}
return false ;
} ,
reportMessage ( message ) {
// User is reporting a message on chat.
if ( message . reported ) {
if ( ! window . confirm ( "You have already reported this message. Do you want to report it again?" ) ) return ;
}
// Clone the message.
let clone = Object . assign ( { } , message ) ;
// Sub out attached images.
clone . message = clone . message . replace ( /<img .+?>/g , "[inline image]" ) ;
this . reportModal . message = clone ;
this . reportModal . origMessage = message ;
this . reportModal . classification = this . config . reportClassifications [ 0 ] ;
this . reportModal . comment = "" ;
this . reportModal . visible = true ;
} ,
doReport ( ) {
// Submit the queued up report.
if ( this . reportModal . busy ) return ;
this . reportModal . busy = true ;
let msg = this . reportModal . message ;
this . ws . conn . send ( JSON . stringify ( {
action : "report" ,
channel : msg . channel ,
username : msg . username ,
timestamp : "" + msg . at ,
reason : this . reportModal . classification ,
message : msg . message ,
comment : this . reportModal . comment ,
} ) ) ;
this . reportModal . busy = false ;
this . reportModal . visible = false ;
// Set the "reported" flag.
this . reportModal . origMessage . reported = true ;
} ,
2023-01-11 06:38:48 +00:00
}
} ) ;
2023-06-24 18:12:02 +00:00
app . mount ( "#BareRTC-App" ) ;