Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Contribute to GitLab
Sign in
Toggle navigation
S
SHMCamStudio
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
1
Merge Requests
1
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
SexHackMe
SHMCamStudio
Commits
93089ca1
Commit
93089ca1
authored
Jun 22, 2025
by
nextime
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
update chat.html
parent
b8a231ca
Changes
1
Hide whitespace changes
Inline
Side-by-side
Showing
1 changed file
with
1039 additions
and
748 deletions
+1039
-748
chat.html
templates/chat.html
+1039
-748
No files found.
templates/chat.html
View file @
93089ca1
<!DOCTYPE html>
<!DOCTYPE html>
<html
lang=
"en"
>
<html
lang=
"en"
>
<head>
<head>
<meta
charset=
"UTF-8"
/>
<meta
charset=
"UTF-8"
>
<meta
name=
"viewport"
content=
"width=device-width, initial-scale=1"
/>
<meta
name=
"viewport"
content=
"width=device-width, initial-scale=1.0"
>
<title>
Responsive Chat with Context Menus and Fixed Private Chat
</title>
<title>
SHMCamStudio
</title>
<style>
<link
href=
"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap"
rel=
"stylesheet"
>
@import
url('https://fonts.googleapis.com/css2?family=Poppins&display=swap')
;
<style>
/* Reset and base styles */
/* Reset and base */
*
{
*
{
margin
:
0
;
box-sizing
:
border-box
;
padding
:
0
;
}
box-sizing
:
border-box
;
body
{
font-family
:
'Inter'
,
Arial
,
sans-serif
;
margin
:
0
;
}
font-family
:
'Poppins'
,
sans-serif
;
body
{
background
:
#121212
;
background
:
#0a1122
;
color
:
#e0e0e0
;
color
:
#ffffff
;
display
:
flex
;
display
:
flex
;
min-height
:
100vh
;
flex-direction
:
column
;
user-select
:
none
;
min-height
:
100vh
;
-webkit-font-smoothing
:
antialiased
;
overflow
:
hidden
;
-moz-osx-font-smoothing
:
grayscale
;
}
}
/* Top bar */
/* Container Layout */
.top-bar
{
.container
{
height
:
60px
;
display
:
grid
;
background
:
linear-gradient
(
to
right
,
#3b4d66
,
#6a1b9a
);
grid-template-columns
:
1
fr
280px
;
display
:
flex
;
grid-template-rows
:
1
fr
;
align-items
:
center
;
gap
:
0
;
padding
:
0
20px
;
width
:
100%
;
gap
:
10px
;
height
:
100vh
;
border-bottom
:
2px
solid
#3b82f6
;
background
:
#1f1f1f
;
}
overflow
:
hidden
;
.top-bar
img
{
}
height
:
40px
;
width
:
auto
;
/* User List Panel */
margin-right
:
10px
;
.user-list
{
}
background
:
#20232a
;
.top-bar
span
{
border-left
:
1px
solid
#2e2e3a
;
font-size
:
20px
;
display
:
flex
;
font-weight
:
600
;
flex-direction
:
column
;
}
user-select
:
none
;
.top-bar
a
{
min-width
:
280px
;
color
:
#3b82f6
;
}
text-decoration
:
none
;
}
.user-list-header
{
.top-bar
a
:hover
{
padding
:
16px
;
text-decoration
:
underline
;
font-weight
:
700
;
}
font-size
:
1.2rem
;
border-bottom
:
1px
solid
#2e2e3a
;
/* Desktop layout: 3 columns */
flex-shrink
:
0
;
.container
{
}
display
:
flex
;
height
:
calc
(
100vh
-
60px
);
.users
{
width
:
100%
;
flex-grow
:
1
;
}
overflow-y
:
auto
;
padding
:
12px
8px
;
.left-column
{
-webkit-overflow-scrolling
:
touch
;
width
:
25%
;
}
min-width
:
20%
;
max-width
:
45%
;
.user-item
{
background
:
linear-gradient
(
to
bottom
,
#2a2a2a
,
#1f1f1f
);
padding
:
10px
12px
;
padding
:
10px
;
margin-bottom
:
8px
;
border-right
:
2px
solid
#111111
;
border-radius
:
8px
;
height
:
100%
;
cursor
:
pointer
;
display
:
flex
;
display
:
flex
;
flex-direction
:
column
;
align-items
:
center
;
overflow-x
:
hidden
;
gap
:
12px
;
overflow-y
:
auto
;
transition
:
background-color
0.3s
ease
;
position
:
relative
;
user-select
:
none
;
}
}
.user-item
:hover
,
.resize-handle
{
.user-item
:focus
{
position
:
absolute
;
background-color
:
#393d4d
;
right
:
-2px
;
outline
:
none
;
top
:
0
;
}
width
:
4px
;
.user-avatar
{
height
:
100%
;
flex-shrink
:
0
;
background
:
#111111
;
width
:
32px
;
cursor
:
col-resize
;
height
:
32px
;
z-index
:
10
;
border-radius
:
50%
;
}
background
:
linear-gradient
(
135deg
,
#6c5ce7
,
#0984e3
);
color
:
white
;
.center-column
{
font-weight
:
700
;
flex
:
1
;
font-size
:
1rem
;
padding
:
10px
;
display
:
flex
;
background
:
linear-gradient
(
to
bottom
,
#1c1c1c
,
#121212
);
align-items
:
center
;
display
:
flex
;
justify-content
:
center
;
flex-direction
:
column
;
user-select
:
none
;
}
}
.user-name
{
.right-column
{
flex-grow
:
1
;
flex
:
0
0
20%
;
font-size
:
0.95rem
;
background
:
linear-gradient
(
to
bottom
,
#2a2a2a
,
#1f1f1f
);
color
:
#dcdde1
;
padding
:
10px
;
white-space
:
nowrap
;
border-left
:
2px
solid
#111111
;
overflow
:
hidden
;
overflow-y
:
auto
;
text-overflow
:
ellipsis
;
}
user-select
:
text
;
}
/* Video window */
.user-status
{
.video-container
{
width
:
10px
;
position
:
relative
;
height
:
10px
;
background
:
#000
;
border-radius
:
50%
;
border-radius
:
8px
;
background-color
:
#44bd32
;
overflow
:
hidden
;
box-shadow
:
0
0
4px
#44bd32
aa
;
flex-shrink
:
0
;
flex-shrink
:
0
;
width
:
100%
;
}
}
video
{
/* Main Chat Panel */
width
:
100%
;
.chat-panel
{
height
:
auto
;
display
:
flex
;
aspect-ratio
:
16
/
9
;
flex-direction
:
column
;
display
:
block
;
background
:
#181a20
;
}
user-select
:
none
;
.video-controls
{
min-width
:
0
;
position
:
absolute
;
}
top
:
10px
;
left
:
10px
;
.chat-header
{
display
:
flex
;
padding
:
16px
24px
;
gap
:
10px
;
border-bottom
:
1px
solid
#2e2e3a
;
opacity
:
0
;
font-weight
:
700
;
transition
:
opacity
0.3s
;
font-size
:
1.3rem
;
}
background
:
linear-gradient
(
135deg
,
#6c5ce7
,
#0984e3
);
.video-container
:hover
.video-controls
{
color
:
white
;
opacity
:
1
;
user-select
:
none
;
}
flex-shrink
:
0
;
.video-controls
select
,
.video-controls
button
{
}
padding
:
5px
;
border
:
none
;
.chat-messages
{
border-radius
:
20px
;
flex-grow
:
1
;
background
:
#3b82f6
;
overflow-y
:
auto
;
color
:
#fff
;
padding
:
20px
24px
;
cursor
:
pointer
;
scrollbar-width
:
thin
;
}
scrollbar-color
:
#394264
transparent
;
.video-controls
select
{
background
:
#121217
;
flex
:
1
;
user-select
:
text
;
min-width
:
100px
;
-webkit-overflow-scrolling
:
touch
;
max-width
:
150px
;
min-height
:
0
;
}
}
.video-controls
button
{
.chat-messages
::-webkit-scrollbar
{
min-width
:
60px
;
width
:
8px
;
}
}
.video-stats
{
.chat-messages
::-webkit-scrollbar-thumb
{
position
:
absolute
;
background-color
:
#394264
;
bottom
:
0
;
border-radius
:
4px
;
width
:
100%
;
}
background
:
rgba
(
0
,
0
,
0
,
0.7
);
padding
:
5px
;
.message
{
font-size
:
12px
;
max-width
:
60%
;
border-radius
:
0
0
8px
8px
;
margin-bottom
:
16px
;
}
padding
:
12px
16px
;
border-radius
:
12px
;
/* Online/offline button */
line-height
:
1.4
;
.status-button
{
font-size
:
0.95rem
;
width
:
100%
;
word-wrap
:
break-word
;
padding
:
12px
;
box-shadow
:
0
2px
5px
rgb
(
0
0
0
/
0.3
);
margin
:
10px
0
;
user-select
:
text
;
font-size
:
18px
;
display
:
flex
;
font-weight
:
600
;
align-items
:
center
;
border
:
none
;
gap
:
8px
;
border-radius
:
20px
;
}
cursor
:
pointer
;
.message.user
{
text-align
:
center
;
background
:
linear-gradient
(
135deg
,
#0984e3
,
#6c5ce7
);
flex-shrink
:
0
;
color
:
white
;
}
align-self
:
flex-end
;
.status-button.online
{
border-bottom-right-radius
:
2px
;
background
:
#dc3545
;
justify-content
:
flex-end
;
}
}
.status-button.offline
{
.message.other
{
background
:
#28a745
;
background
:
#2d2f3a
;
}
color
:
#ddd
;
.status-menu
{
align-self
:
flex-start
;
display
:
none
;
border-bottom-left-radius
:
2px
;
position
:
absolute
;
}
background
:
#2a2a2a
;
.msg-username
{
border
:
1px
solid
#3b82f6
;
font-weight
:
700
;
padding
:
10px
;
cursor
:
pointer
;
z-index
:
100
;
user-select
:
text
;
border-radius
:
8px
;
color
:
#a2d2ff
;
}
}
.status-menu
button
{
display
:
block
;
.chat-input-container
{
width
:
100%
;
display
:
flex
;
padding
:
8px
;
padding
:
12px
24px
;
background
:
none
;
border-top
:
1px
solid
#2e2e3a
;
border
:
none
;
background
:
#20232a
;
color
:
#fff
;
align-items
:
center
;
text-align
:
left
;
user-select
:
none
;
cursor
:
pointer
;
flex-shrink
:
0
;
border-radius
:
20px
;
}
}
.chat-input
{
.status-menu
button
:hover
{
flex-grow
:
1
;
background
:
#3b82f6
;
padding
:
10px
16px
;
}
font-size
:
1rem
;
border-radius
:
24px
;
/* Tabs */
border
:
none
;
.tabs
{
background-color
:
#2d2f3a
;
display
:
flex
;
color
:
#eee
;
border-bottom
:
2px
solid
#111111
;
outline
:
none
;
margin-top
:
10px
;
transition
:
background-color
0.2s
ease
;
flex-shrink
:
0
;
}
overflow-x
:
hidden
;
.chat-input
::placeholder
{
}
color
:
#888
;
.tab
{
}
padding
:
8px
12px
;
.chat-input
:focus
{
cursor
:
pointer
;
background-color
:
#424559
;
background
:
#2a2a2a
;
}
border-radius
:
8px
8px
0
0
;
.send-btn
{
margin-right
:
5px
;
margin-left
:
12px
;
font-size
:
14px
;
background
:
linear-gradient
(
135deg
,
#6c5ce7
,
#0984e3
);
white-space
:
nowrap
;
border
:
none
;
}
padding
:
10px
18px
;
.tab.active
{
color
:
white
;
background
:
#3b82f6
;
font-weight
:
700
;
}
font-size
:
1rem
;
.tab-content
{
border-radius
:
24px
;
display
:
none
;
cursor
:
pointer
;
padding
:
15px
;
user-select
:
none
;
background
:
#2a2a2a
;
transition
:
background-color
0.3s
ease
;
border-radius
:
0
0
8px
8px
;
}
overflow-y
:
auto
;
.send-btn
:hover
{
flex-grow
:
1
;
background
:
linear-gradient
(
135deg
,
#5353c3
,
#0761c7
);
max-height
:
100%
;
}
overflow-x
:
hidden
;
}
/* Private Chat Popup */
.tab-content.active
{
.private-chat-popup
{
display
:
block
;
position
:
fixed
;
}
bottom
:
16px
;
right
:
16px
;
/* Chat window */
width
:
320px
;
.chat-window
{
max-height
:
480px
;
flex
:
1
;
background
:
#292b37
;
background
:
#121212
;
border-radius
:
12px
;
border-radius
:
8px
;
box-shadow
:
0
8px
24px
rgba
(
0
,
0
,
0
,
0.7
);
padding
:
15px
;
display
:
flex
;
overflow-y
:
auto
;
flex-direction
:
column
;
margin-bottom
:
10px
;
overflow
:
hidden
;
}
z-index
:
1000
;
.message
{
font-size
:
0.9rem
;
margin
:
8px
0
;
animation
:
fadeInUp
0.3s
ease
forwards
;
padding
:
10px
;
user-select
:
none
;
font-size
:
16px
;
resize
:
both
;
}
overflow
:
auto
;
.message.me
{
}
background
:
linear-gradient
(
to
right
,
#2563eb
,
#4b0082
);
.private-chat-header
{
margin-left
:
20%
;
background
:
linear-gradient
(
135deg
,
#6c5ce7
,
#0984e3
);
text-align
:
right
;
padding
:
12px
16px
;
border-radius
:
10px
10px
0px
10px
;
font-weight
:
700
;
}
color
:
white
;
.message.me
.sender
{
display
:
flex
;
color
:
#93c5fd
;
justify-content
:
space-between
;
font-weight
:
600
;
align-items
:
center
;
}
flex-shrink
:
0
;
.message.me
.content
{
cursor
:
move
;
color
:
#f0f9ff
;
}
}
.private-chat-close
{
.message.other
{
cursor
:
pointer
;
background
:
linear-gradient
(
to
right
,
#1e293b
,
#2a1b3d
);
font-weight
:
700
;
margin-right
:
20%
;
font-size
:
1.2rem
;
border-radius
:
10px
10px
10px
0px
;
line-height
:
0
;
}
user-select
:
none
;
.message.notify-system
{
transition
:
color
0.3s
ease
;
background
:
linear-gradient
(
to
right
,
#1f2937
,
#2a1b3d
);
background
:
none
;
margin
:
8px
10%
;
border
:
none
;
text-align
:
center
;
color
:
white
;
border-radius
:
8px
;
}
}
.private-chat-close
:hover
{
.message.notify-platform
{
color
:
#ffd32a
;
background
:
linear-gradient
(
to
right
,
#1f2937
,
#2a1b3d
);
}
margin
:
8px
10%
;
.private-chat-messages
{
text-align
:
center
;
flex-grow
:
1
;
border-radius
:
8px
;
padding
:
12px
16px
;
}
overflow-y
:
auto
;
.message.tip
{
background
:
#20212a
;
background
:
linear-gradient
(
to
right
,
#064e3b
,
#1b3d2a
);
scrollbar-width
:
thin
;
margin
:
8px
10%
;
scrollbar-color
:
#52526a
transparent
;
text-align
:
center
;
-webkit-overflow-scrolling
:
touch
;
border-radius
:
8px
8px
0
8px
;
user-select
:
text
;
}
min-height
:
120px
;
.message.tip
.content
{
max-height
:
320px
;
color
:
#d1fae5
;
}
}
.private-chat-messages
::-webkit-scrollbar
{
.message
.sender
{
width
:
6px
;
color
:
#60a5fa
;
}
font-weight
:
600
;
.private-chat-messages
::-webkit-scrollbar-thumb
{
cursor
:
pointer
;
background-color
:
#52526a
;
}
border-radius
:
4px
;
.message
.content
{
}
color
:
#a3bffa
;
.private-message
{
}
margin-bottom
:
12px
;
.message
img
{
background
:
#3a3c4d
;
max-width
:
100px
;
padding
:
10px
14px
;
border-radius
:
8px
;
border-radius
:
10px
;
}
color
:
#e4e6f0
;
.message
a
{
max-width
:
90%
;
color
:
#00b7ff
;
word-wrap
:
break-word
;
text-decoration
:
underline
;
}
}
.private-message.user
{
.chat-input-container
{
background
:
linear-gradient
(
135deg
,
#0984e3
,
#6c5ce7
);
display
:
flex
;
color
:
white
;
gap
:
10px
;
align-self
:
flex-end
;
align-items
:
stretch
;
border-bottom-right-radius
:
2px
;
}
}
.chat-input
{
.private-message.other
{
display
:
flex
;
background
:
#3a3c4d
;
gap
:
10px
;
color
:
#ddd
;
position
:
relative
;
align-self
:
flex-start
;
flex
:
1
;
border-bottom-left-radius
:
2px
;
}
}
.platform-selector
{
.private-chat-input-container
{
height
:
40px
;
display
:
flex
;
padding
:
8px
;
padding
:
10px
14px
;
border
:
none
;
border-top
:
1px
solid
#44475a
;
border-radius
:
20px
;
background
:
#2b2e42
;
background
:
#3b82f6
;
align-items
:
center
;
color
:
#fff
;
flex-shrink
:
0
;
cursor
:
pointer
;
}
font-size
:
14px
;
.private-chat-input
{
width
:
120px
;
flex-grow
:
1
;
}
padding
:
8px
14px
;
.chat-input
.emoji-button
{
font-size
:
0.95rem
;
position
:
absolute
;
border-radius
:
18px
;
left
:
10px
;
border
:
none
;
top
:
50%
;
background-color
:
#3d3f56
;
transform
:
translateY
(
-50%
);
color
:
#eee
;
background
:
none
;
outline
:
none
;
border
:
none
;
transition
:
background-color
0.2s
ease
;
font-size
:
20px
;
}
cursor
:
pointer
;
.private-chat-input
::placeholder
{
z-index
:
10
;
color
:
#bbb
;
}
}
.chat-input
textarea
{
.private-chat-input
:focus
{
flex
:
1
;
background-color
:
#56577b
;
height
:
40px
;
}
padding
:
10px
40px
10px
60px
;
.private-send-btn
{
border
:
none
;
margin-left
:
10px
;
border-radius
:
20px
;
background
:
linear-gradient
(
135deg
,
#6c5ce7
,
#0984e3
);
background
:
#1e293b
;
border
:
none
;
color
:
#fff
;
padding
:
8px
20px
;
resize
:
none
;
color
:
white
;
font-size
:
14px
;
font-weight
:
700
;
}
font-size
:
0.95rem
;
.chat-input
button
{
border-radius
:
18px
;
height
:
40px
;
cursor
:
pointer
;
padding
:
10px
20px
;
user-select
:
none
;
background
:
#3b82f6
;
transition
:
background-color
0.3s
ease
;
border
:
none
;
}
border-radius
:
20px
;
.private-send-btn
:hover
{
color
:
#fff
;
background
:
linear-gradient
(
135deg
,
#5353c3
,
#0761c7
);
cursor
:
pointer
;
}
font-weight
:
500
;
}
@keyframes
fadeInUp
{
.emoji-picker
{
0
%
{
display
:
none
;
opacity
:
0
;
position
:
absolute
;
transform
:
translateY
(
20px
);
background
:
#2a2a2a
;
}
border
:
1px
solid
#3b82f6
;
100
%
{
padding
:
10px
;
opacity
:
1
;
z-index
:
100
;
transform
:
translateY
(
0
);
border-radius
:
8px
;
}
max-width
:
300px
;
}
flex-wrap
:
wrap
;
}
/* Context Menu Styling */
.emoji-picker
span
{
.context-menu
{
cursor
:
pointer
;
position
:
absolute
;
font-size
:
20px
;
background
:
#2a2c38
;
margin
:
5px
;
border-radius
:
8px
;
}
padding
:
8px
0
;
width
:
180px
;
/* User list */
box-shadow
:
0
6px
15px
rgba
(
0
,
0
,
0
,
0.4
);
.user-list-header
{
z-index
:
2000
;
display
:
flex
;
display
:
none
;
justify-content
:
space-between
;
user-select
:
none
;
padding
:
8px
;
}
background
:
#2a2a2a
;
.context-menu.visible
{
border-bottom
:
2px
solid
#111111
;
display
:
block
;
font-size
:
16px
;
}
font-weight
:
600
;
.context-menu-item
{
}
padding
:
10px
16px
;
.user-list
.platform
{
font-size
:
0.9rem
;
margin
:
10px
0
;
color
:
#e0e0e0
;
}
cursor
:
pointer
;
.platform-header
{
transition
:
background-color
0.25s
ease
;
cursor
:
pointer
;
}
padding
:
8px
;
.context-menu-item
:hover
,
background
:
#1e293b
;
.context-menu-item
:focus
{
border-radius
:
8px
;
background
:
#5353c3
;
display
:
flex
;
color
:
white
;
justify-content
:
space-between
;
outline
:
none
;
font-weight
:
500
;
}
}
.user
{
/* Responsive */
padding
:
8px
10px
;
@media
(
max-width
:
768px
)
{
display
:
flex
;
body
{
justify-content
:
space-between
;
display
:
block
;
align-items
:
center
;
height
:
auto
;
font-size
:
14px
;
}
}
.container
{
.status-dot
{
height
:
auto
;
width
:
10px
;
display
:
flex
;
height
:
10px
;
flex-direction
:
column
;
border-radius
:
50%
;
}
display
:
inline-block
;
.chat-panel
{
margin-right
:
5px
;
order
:
2
;
}
height
:
70vh
;
.status-dot.online
{
min-width
:
100%
;
background
:
#28a745
;
}
}
.user-list
{
.status-dot.offline
{
order
:
1
;
background
:
#dc3545
;
height
:
30vh
;
}
min-width
:
100%
;
.users
{
border-left
:
none
!important
;
display
:
none
;
border-top
:
1px
solid
#2e2e3a
;
}
}
.users.active
{
.chat-messages
{
display
:
block
;
height
:
100%
;
}
min-height
:
0
;
}
/* Context menu */
.chat-input-container
{
.context-menu
{
padding
:
8px
16px
;
position
:
absolute
;
}
background
:
#2a2a2a
;
.chat-input
{
border
:
1px
solid
#3b82f6
;
font-size
:
0.9rem
;
padding
:
10px
;
padding
:
8px
12px
;
z-index
:
100
;
}
display
:
none
;
.send-btn
{
border-radius
:
8px
;
padding
:
8px
12px
;
}
font-size
:
0.9rem
;
.context-menu
a
,
.context-menu
button
{
}
display
:
block
;
.user-list-header
,
.chat-header
{
padding
:
8px
;
padding
:
12px
16px
;
color
:
#fff
;
font-size
:
1.1rem
;
text-decoration
:
none
;
}
background
:
none
;
.user-item
{
border
:
none
;
padding
:
8px
10px
;
cursor
:
pointer
;
}
width
:
100%
;
.user-avatar
{
text-align
:
left
;
width
:
28px
;
border-radius
:
20px
;
height
:
28px
;
font-size
:
14px
;
font-size
:
0.9rem
;
}
}
.context-menu
a
:hover
,
.context-menu
button
:hover
{
.user-name
{
background
:
#3b82f6
;
font-size
:
0.9rem
;
}
}
.private-chat-popup
{
/* Earnings table */
width
:
90vw
;
.earnings-table
{
max-height
:
50vh
;
width
:
100%
;
bottom
:
8px
;
max-width
:
100%
;
right
:
5vw
;
margin
:
0
;
font-size
:
0.85rem
;
border-collapse
:
collapse
;
}
font-size
:
12px
;
.private-chat-messages
{
}
max-height
:
200px
;
.earnings-table
th
,
.earnings-table
td
{
}
padding
:
6px
;
}
border
:
1px
solid
#111111
;
</style>
text-align
:
left
;
white-space
:
nowrap
;
}
.earnings-table
tr
:nth-child
(
even
)
{
background
:
#1e293b
;
}
.earnings-table
th
{
background
:
#3b82f6
;
font-weight
:
600
;
}
.earnings-table
.totals-row
{
background
:
#4b5563
;
font-weight
:
600
;
}
#earnings
{
overflow-x
:
auto
;
margin-bottom
:
10px
;
}
.reset-session-button
{
width
:
100%
;
padding
:
8px
;
background
:
#dc3545
;
border
:
none
;
border-radius
:
20px
;
color
:
#fff
;
cursor
:
pointer
;
font-size
:
12px
;
text-align
:
center
;
}
.reset-session-button
:hover
{
background
:
#b91c1c
;
}
/* Mobile layout */
@media
(
max-width
:
768px
)
{
.container
{
flex-direction
:
column
;
height
:
auto
;
}
.left-column
,
.center-column
,
.right-column
{
width
:
100%
;
border
:
none
;
min-width
:
0
;
height
:
auto
;
display
:
block
;
}
.left-column
{
order
:
-1
;
}
.resize-handle
{
display
:
none
;
}
.chat-window
{
height
:
50vh
;
}
.earnings-table
{
width
:
100%
;
}
.tab-content
{
max-height
:
none
;
display
:
block
;
}
video
{
height
:
200px
;
}
.top-bar
{
padding
:
0
10px
;
}
.top-bar
img
{
height
:
30px
;
}
.top-bar
span
{
font-size
:
16px
;
}
.chat-input-container
{
flex-direction
:
column
;
align-items
:
stretch
;
}
.platform-selector
{
width
:
100%
;
margin-bottom
:
5px
;
}
.chat-input
{
width
:
100%
;
}
}
</style>
</head>
</head>
<body>
<body>
<div
class=
"container"
>
<div
class=
"top-bar"
>
<!-- Main Chat -->
<img
src=
"https://www.sexhack.me/content/uploads/2022/06/cropped-sexhack-300x99.png"
alt=
"SexHack Logo"
>
<section
class=
"chat-panel"
aria-label=
"Public chat messages"
>
<span>
SHM CamStudio by
<a
href=
"https://www.sexhack.me"
>
sexhack.me
</a></span>
<header
class=
"chat-header"
>
Global Chat
</header>
</div>
<div
class=
"chat-messages"
id=
"chatMessages"
role=
"log"
aria-live=
"polite"
aria-relevant=
"additions"
></div>
<div
class=
"container"
>
<form
id=
"chatForm"
class=
"chat-input-container"
aria-label=
"Send message"
>
<div
class=
"left-column"
>
<input
type=
"text"
id=
"chatInput"
class=
"chat-input"
placeholder=
"Enter message..."
autocomplete=
"off"
aria-required=
"true"
/>
<div
class=
"resize-handle"
></div>
<button
type=
"submit"
class=
"send-btn"
aria-label=
"Send message"
>
Send
</button>
<div
class=
"video-container"
>
</form>
<video
id=
"video"
autoplay
></video>
</section>
<div
class=
"video-controls"
>
<select
id=
"video-source"
></select>
<!-- User List -->
<button
id=
"mirror-video"
>
Mirror
</button>
<aside
class=
"user-list"
aria-label=
"User list"
>
</div>
<div
class=
"user-list-header"
>
Online Users
</div>
<div
class=
"video-stats"
>
<div
class=
"users"
id=
"userList"
role=
"list"
tabindex=
"0"
aria-live=
"polite"
></div>
Bitrate: 2 Mbps | Quality: HD | Audio:
<span
id=
"audio-bar"
>
████
</span>
</aside>
</div>
</div>
</div>
<button
class=
"status-button offline"
id=
"status-button"
>
GO ONLINE
</button>
<!-- Private Chat Popup Template -->
<div
class=
"status-menu"
id=
"status-menu"
>
<template
id=
"privateChatTemplate"
>
<button
data-platform=
"C4.sexhackme"
>
C4.sexhackme: Offline
</button>
<section
class=
"private-chat-popup"
role=
"dialog"
aria-modal=
"true"
aria-label=
"Private chat with user"
>
<button
data-platform=
"SC.spora"
>
SC.spora: Offline
</button>
<header
class=
"private-chat-header"
>
</div>
<span
class=
"private-chat-user"
></span>
<div
class=
"tabs"
>
<button
class=
"private-chat-close"
aria-label=
"Close private chat"
>
×
</button>
<div
class=
"tab active"
data-tab=
"panel"
>
Panel
</div>
</header>
<div
class=
"tab"
data-tab=
"earnings"
>
Earnings
</div>
<div
class=
"private-chat-messages"
role=
"log"
aria-live=
"polite"
aria-relevant=
"additions"
></div>
</div>
<form
class=
"private-chat-input-container"
aria-label=
"Send private message"
>
<div
class=
"tab-content active"
id=
"panel"
>
<input
type=
"text"
class=
"private-chat-input"
placeholder=
"Enter private message..."
autocomplete=
"off"
aria-required=
"true"
/>
<iframe
id=
"panel-iframe"
style=
"width:100%;height:300px;border:none;border-radius:8px;"
></iframe>
<button
type=
"submit"
class=
"private-send-btn"
aria-label=
"Send private message"
>
Send
</button>
</div>
</form>
<div
class=
"tab-content"
id=
"earnings"
>
</section>
<table
class=
"earnings-table"
>
</template>
<thead>
<tr><th>
Plat.
<br>
Acc.
</th><th>
Last
<br>
Sess
</th><th>
Today
</th><th>
Hour
</th><th>
Sess
</th></tr>
<!-- Context Menu -->
</thead>
<nav
id=
"contextMenu"
class=
"context-menu"
role=
"menu"
aria-hidden=
"true"
tabindex=
"-1"
aria-label=
"User actions"
>
<tbody></tbody>
<div
class=
"context-menu-item"
data-action=
"openProfile"
role=
"menuitem"
tabindex=
"0"
>
View Profile
</div>
</table>
<div
class=
"context-menu-item"
data-action=
"privateChat"
role=
"menuitem"
tabindex=
"0"
>
Open Private Chat
</div>
<button
class=
"reset-session-button"
id=
"reset-session"
>
Reset Session
</button>
<div
class=
"context-menu-item"
data-action=
"ban"
role=
"menuitem"
tabindex=
"0"
>
Ban User
</div>
</div>
<div
class=
"context-menu-item"
data-action=
"kick"
role=
"menuitem"
tabindex=
"0"
>
Kick User
</div>
</div>
</nav>
<div
class=
"center-column"
>
<div
class=
"chat-window"
id=
"chat-window"
></div>
<script>
<div
class=
"chat-input-container"
>
const
users
=
[
<select
class=
"platform-selector"
id=
"platform-selector"
>
{
id
:
'u1'
,
name
:
'Alice'
},
<option
value=
"all"
>
All
</option>
{
id
:
'u2'
,
name
:
'Bob'
},
<option
value=
"C4.sexhackme"
>
C4.sexhackme
</option>
{
id
:
'u3'
,
name
:
'Charlie'
},
<option
value=
"SC.spora"
>
SC.spora
</option>
{
id
:
'u4'
,
name
:
'David'
}
</select>
];
<div
class=
"chat-input"
>
<button
class=
"emoji-button"
>
😊
</button>
const
userListElem
=
document
.
getElementById
(
'userList'
);
<textarea
id=
"chat-input"
rows=
"3"
placeholder=
"Type a message..."
></textarea>
const
chatMessagesElem
=
document
.
getElementById
(
'chatMessages'
);
<button
id=
"send-message"
>
Send
</button>
const
chatForm
=
document
.
getElementById
(
'chatForm'
);
<div
class=
"emoji-picker"
id=
"emoji-picker"
>
const
chatInput
=
document
.
getElementById
(
'chatInput'
);
<span
data-emoji=
"😊"
>
😊
</span><span
data-emoji=
"👍"
>
👍
</span><span
data-emoji=
"❤️"
>
❤️
</span><span
data-emoji=
"😂"
>
😂
</span>
<span
data-emoji=
"😍"
>
😍
</span><span
data-emoji=
"😢"
>
😢
</span><span
data-emoji=
"😎"
>
😎
</span><span
data-emoji=
"😴"
>
😴
</span>
const
privateChats
=
new
Map
();
<span
data-emoji=
"😛"
>
😛
</span><span
data-emoji=
"😣"
>
😣
</span><span
data-emoji=
"😇"
>
😇
</span><span
data-emoji=
"😱"
>
😱
</span>
<span
data-emoji=
"😳"
>
😳
</span><span
data-emoji=
"😬"
>
😬
</span><span
data-emoji=
"🙂"
>
🙂
</span><span
data-emoji=
"🙃"
>
🙃
</span>
const
contextMenu
=
document
.
getElementById
(
'contextMenu'
);
<span
data-emoji=
"🙈"
>
🙈
</span><span
data-emoji=
"🙉"
>
🙉
</span><span
data-emoji=
"🙊"
>
🙊
</span><span
data-emoji=
"💪"
>
💪
</span>
let
contextMenuTargetUser
=
null
;
<span
data-emoji=
"👏"
>
👏
</span><span
data-emoji=
"👋"
>
👋
</span><span
data-emoji=
"👌"
>
👌
</span><span
data-emoji=
"👀"
>
👀
</span>
let
contextMenuTargetType
=
null
;
<span
data-emoji=
"💋"
>
💋
</span><span
data-emoji=
"💕"
>
💕
</span><span
data-emoji=
"💖"
>
💖
</span><span
data-emoji=
"💗"
>
💗
</span>
<span
data-emoji=
"💥"
>
💥
</span><span
data-emoji=
"💦"
>
💦
</span><span
data-emoji=
"🔥"
>
🔥
</span><span
data-emoji=
"⭐"
>
⭐
</span>
function
renderUserList
()
{
<span
data-emoji=
"🌈"
>
🌈
</span><span
data-emoji=
"🎉"
>
🎉
</span><span
data-emoji=
"🎈"
>
🎈
</span><span
data-emoji=
"🎁"
>
🎁
</span>
userListElem
.
innerHTML
=
''
;
<span
data-emoji=
"🍑"
>
🍑
</span><span
data-emoji=
"🍆"
>
🍆
</span><span
data-emoji=
"🍒"
>
🍒
</span><span
data-emoji=
"🍓"
>
🍓
</span>
users
.
forEach
(
user
=>
{
</div>
const
userItem
=
document
.
createElement
(
'div'
);
</div>
userItem
.
className
=
'user-item'
;
</div>
userItem
.
setAttribute
(
'role'
,
'listitem'
);
</div>
userItem
.
tabIndex
=
0
;
<div
class=
"right-column"
>
userItem
.
dataset
.
userid
=
user
.
id
;
<div
class=
"user-list-header"
>
<span>
Userlist
</span>
const
avatar
=
document
.
createElement
(
'div'
);
<span
id=
"total-users"
>
0
</span>
avatar
.
className
=
'user-avatar'
;
</div>
avatar
.
textContent
=
user
.
name
.
charAt
(
0
).
toUpperCase
();
<div
class=
"user-list"
id=
"user-list"
></div>
</div>
const
name
=
document
.
createElement
(
'div'
);
</div>
name
.
className
=
'user-name'
;
<div
class=
"context-menu"
id=
"context-menu"
>
name
.
textContent
=
user
.
name
;
<a
href=
"#"
id=
"context-profile"
>
View Profile
</a>
<button
id=
"context-tokens"
>
Tokens: 0
</button>
const
status
=
document
.
createElement
(
'div'
);
<button
id=
"context-ban"
>
Ban User
</button>
status
.
className
=
'user-status'
;
<button
id=
"context-kick"
>
Kick User
</button>
status
.
title
=
'Online'
;
<button
id=
"context-private"
>
Private Chat
</button>
</div>
userItem
.
appendChild
(
avatar
);
userItem
.
appendChild
(
name
);
<script>
userItem
.
appendChild
(
status
);
// Fake data for messages, users, earnings, status, and RTSP URLs
const
fakeMessages
=
[
userItem
.
addEventListener
(
'click'
,
()
=>
openPrivateChat
(
user
));
{
sender
:
"me"
,
content
:
"Hello! 😊"
,
timestamp
:
"2025-06-21 20:00"
},
userItem
.
addEventListener
(
'keydown'
,
e
=>
{
if
(
e
.
key
===
'Enter'
||
e
.
key
===
' '
)
{
e
.
preventDefault
();
openPrivateChat
(
user
);
}
});
{
sender
:
"alice@C4.sexhackme"
,
content
:
"Hi! Check this: https://example.com"
,
timestamp
:
"2025-06-21 20:01"
},
{
sender
:
"bob@SC.spora"
,
content
:
"<img src='https://pbs.twimg.com/media/FsypEu2X0AEtwK3?format=jpg&name=small' alt='Placeholder Image'>"
,
timestamp
:
"2025-06-21 20:02"
},
userItem
.
addEventListener
(
'contextmenu'
,
e
=>
{
{
sender
:
"system"
,
content
:
"system: *** Server maintenance scheduled at 1 AM"
,
type
:
"notify-system"
,
timestamp
:
"2025-06-21 20:03"
},
e
.
preventDefault
();
{
sender
:
"platform@SC.spora"
,
content
:
"platform: *** User joined the room"
,
type
:
"notify-platform"
,
timestamp
:
"2025-06-21 20:04"
},
showContextMenu
(
e
,
user
,
'userlist'
);
{
sender
:
"bob@SC.spora"
,
content
:
"<span class='sender' data-sender='bob@SC.spora' style='color: #10b981'>bob@SC.spora</span> TIPPED <b>50 TOKENS</b> ($5.00)<div style='text-align: center; color: #6ee7b7'>PM REQUEST</div>"
,
type
:
"tip"
,
timestamp
:
"2025-06-21 20:05"
}
];
const
fakeUsers
=
{
"C4.sexhackme"
:
[
{
username
:
"alice"
,
status
:
"online"
,
tokens
:
50
},
{
username
:
"charlie"
,
status
:
"offline"
,
tokens
:
20
}
],
"SC.spora"
:
[
{
username
:
"bob"
,
status
:
"online"
,
tokens
:
30
}
]
};
const
fakeEarnings
=
[
{
platform
:
"C4.sexhackme"
,
lastSession
:
100
,
today
:
250
,
lastHour
:
50
,
sess
:
100
},
{
platform
:
"SC.spora"
,
lastSession
:
80
,
today
:
200
,
lastHour
:
30
,
sess
:
80
}
];
const
fakeStatus
=
{
"C4.sexhackme"
:
"online"
,
"SC.spora"
:
"offline"
};
const
fakeRtspUrls
=
[
{
id
:
"rtsp1"
,
url
:
"rtsp://example.com/stream1"
},
{
id
:
"rtsp2"
,
url
:
"rtsp://example.com/stream2"
}
];
// Initialize video
const
video
=
document
.
getElementById
(
'video'
);
const
videoSource
=
document
.
getElementById
(
'video-source'
);
const
mirrorVideo
=
document
.
getElementById
(
'mirror-video'
);
let
isMirrored
=
false
;
async
function
populateVideoSources
()
{
videoSource
.
innerHTML
=
'<option value="none">Select Source</option>'
;
try
{
const
devices
=
await
navigator
.
mediaDevices
.
enumerateDevices
();
devices
.
filter
(
d
=>
d
.
kind
===
'videoinput'
).
forEach
((
device
,
index
)
=>
{
const
option
=
document
.
createElement
(
'option'
);
option
.
value
=
device
.
deviceId
;
option
.
text
=
device
.
label
||
`Webcam
${
index
+
1
}
`
;
videoSource
.
appendChild
(
option
);
});
}
catch
(
err
)
{
console
.
error
(
'Error enumerating devices:'
,
err
);
}
fakeRtspUrls
.
forEach
(
url
=>
{
const
option
=
document
.
createElement
(
'option'
);
option
.
value
=
url
.
id
;
option
.
text
=
url
.
url
;
videoSource
.
appendChild
(
option
);
});
navigator
.
mediaDevices
.
getUserMedia
({
video
:
true
})
.
then
(
stream
=>
{
video
.
srcObject
=
stream
;
videoSource
.
value
=
stream
.
getVideoTracks
()[
0
].
getSettings
().
deviceId
;
})
.
catch
(
err
=>
console
.
error
(
'Webcam access error:'
,
err
));
}
videoSource
.
addEventListener
(
'change'
,
()
=>
{
if
(
videoSource
.
value
===
'none'
)
{
video
.
srcObject
=
null
;
video
.
src
=
''
;
}
else
if
(
fakeRtspUrls
.
find
(
url
=>
url
.
id
===
videoSource
.
value
))
{
video
.
srcObject
=
null
;
video
.
src
=
fakeRtspUrls
.
find
(
url
=>
url
.
id
===
videoSource
.
value
).
url
;
}
else
{
navigator
.
mediaDevices
.
getUserMedia
({
video
:
{
deviceId
:
videoSource
.
value
}
})
.
then
(
stream
=>
video
.
srcObject
=
stream
)
.
catch
(
err
=>
console
.
error
(
'Webcam error:'
,
err
));
}
});
mirrorVideo
.
addEventListener
(
'click'
,
()
=>
{
isMirrored
=
!
isMirrored
;
video
.
style
.
transform
=
isMirrored
?
'scaleX(-1)'
:
'scaleX(1)'
;
});
// Resize handle
const
leftColumn
=
document
.
querySelector
(
'.left-column'
);
const
resizeHandle
=
document
.
querySelector
(
'.resize-handle'
);
let
isResizing
=
false
;
resizeHandle
.
addEventListener
(
'mousedown'
,
(
e
)
=>
{
isResizing
=
true
;
e
.
preventDefault
();
});
document
.
addEventListener
(
'mousemove'
,
(
e
)
=>
{
if
(
isResizing
)
{
const
newWidth
=
e
.
clientX
;
if
(
newWidth
>=
window
.
innerWidth
*
0.2
&&
newWidth
<=
window
.
innerWidth
*
0.45
)
{
leftColumn
.
style
.
width
=
`
${
newWidth
}
px`
;
}
}
});
document
.
addEventListener
(
'mouseup'
,
()
=>
{
isResizing
=
false
;
});
// Status button and menu
const
statusButton
=
document
.
getElementById
(
'status-button'
);
const
statusMenu
=
document
.
getElementById
(
'status-menu'
);
statusButton
.
addEventListener
(
'click'
,
(
e
)
=>
{
statusMenu
.
style
.
display
=
statusMenu
.
style
.
display
===
'block'
?
'none'
:
'block'
;
statusMenu
.
style
.
left
=
`
${
e
.
pageX
}
px`
;
statusMenu
.
style
.
top
=
`
${
e
.
pageY
}
px`
;
});
statusMenu
.
querySelectorAll
(
'button'
).
forEach
(
btn
=>
{
btn
.
addEventListener
(
'click'
,
()
=>
{
const
platform
=
btn
.
dataset
.
platform
;
fakeStatus
[
platform
]
=
fakeStatus
[
platform
]
===
'online'
?
'offline'
:
'online'
;
updateStatus
();
statusMenu
.
style
.
display
=
'none'
;
});
});
});
userListElem
.
appendChild
(
userItem
);
function
updateStatus
()
{
});
const
isOnline
=
Object
.
values
(
fakeStatus
).
some
(
status
=>
status
===
'online'
);
}
statusButton
.
className
=
`status-button
${
isOnline
?
'online'
:
'offline'
}
`
;
statusButton
.
textContent
=
isOnline
?
'GO OFFLINE'
:
'GO ONLINE'
;
function
addPublicMessage
(
text
,
senderId
=
null
,
senderName
=
null
)
{
statusMenu
.
querySelectorAll
(
'button'
).
forEach
(
btn
=>
{
const
msg
=
document
.
createElement
(
'div'
);
const
platform
=
btn
.
dataset
.
platform
;
msg
.
className
=
senderId
===
"me"
?
'message user'
:
'message other'
;
btn
.
textContent
=
`
${
platform
}
:
${
fakeStatus
[
platform
]}
`
;
});
const
usernameSpan
=
document
.
createElement
(
'span'
);
}
usernameSpan
.
className
=
'msg-username'
;
if
(
!
senderName
)
senderName
=
senderId
?
(
users
.
find
(
u
=>
u
.
id
===
senderId
)?.
name
||
'Someone'
)
:
'Someone'
;
// Load panel content
if
(
!
senderId
)
senderId
=
'someone'
;
const
panelIframe
=
document
.
getElementById
(
'panel-iframe'
);
panelIframe
.
srcdoc
=
'<p>Control panel content loaded here.</p>'
;
usernameSpan
.
textContent
=
senderName
;
usernameSpan
.
tabIndex
=
0
;
// Chat functionality
usernameSpan
.
dataset
.
userid
=
senderId
;
const
chatWindow
=
document
.
getElementById
(
'chat-window'
);
const
chatInput
=
document
.
getElementById
(
'chat-input'
);
// Important: Use mousedown event to open context menu instantly on right click on usernames in main chat
const
sendButton
=
document
.
getElementById
(
'send-message'
);
usernameSpan
.
addEventListener
(
'contextmenu'
,
e
=>
{
const
emojiButton
=
document
.
querySelector
(
'.emoji-button'
);
e
.
preventDefault
();
const
emojiPicker
=
document
.
getElementById
(
'emoji-picker'
);
const
user
=
users
.
find
(
u
=>
u
.
id
===
senderId
)
||
{
id
:
'someone'
,
name
:
senderName
};
const
platformSelector
=
document
.
getElementById
(
'platform-selector'
);
showContextMenu
(
e
,
user
,
'chat'
);
});
function
renderMessages
(
messages
)
{
usernameSpan
.
addEventListener
(
'keydown'
,
(
e
)
=>
{
chatWindow
.
innerHTML
=
''
;
if
((
e
.
shiftKey
&&
e
.
key
===
"F10"
)
||
e
.
key
===
"ContextMenu"
)
{
messages
.
forEach
(
msg
=>
{
e
.
preventDefault
();
const
div
=
document
.
createElement
(
'div'
);
const
user
=
users
.
find
(
u
=>
u
.
id
===
senderId
)
||
{
id
:
'someone'
,
name
:
senderName
};
div
.
className
=
`message
${
msg
.
sender
===
'me'
?
'me'
:
msg
.
type
===
'notify-system'
?
'notify-system'
:
msg
.
type
===
'notify-platform'
?
'notify-platform'
:
msg
.
type
===
'tip'
?
'tip'
:
'other'
}
`
;
showContextMenu
(
e
,
user
,
'chat'
);
div
.
dataset
.
sender
=
msg
.
sender
;
}
let
content
=
msg
.
content
;
});
if
(
!
msg
.
type
?.
startsWith
(
'notify'
)
&&
msg
.
type
!==
'tip'
&&
!
msg
.
content
.
includes
(
'<img'
))
{
content
=
msg
.
content
msg
.
appendChild
(
usernameSpan
);
.
replace
(
/
\n
/g
,
'<br>'
)
const
textNode
=
document
.
createTextNode
(
`:
${
text
}
`
);
.
replace
(
/
(
https
?
:
\/\/[^\s]
+
)
/g
,
'<a href="$1" target="_blank">$1</a>'
);
msg
.
appendChild
(
textNode
);
}
if
(
msg
.
sender
===
'me'
)
{
chatMessagesElem
.
appendChild
(
msg
);
const
senderSpan
=
document
.
createElement
(
'span'
);
chatMessagesElem
.
scrollTop
=
chatMessagesElem
.
scrollHeight
;
senderSpan
.
className
=
'sender'
;
}
senderSpan
.
textContent
=
'me:'
;
senderSpan
.
style
.
color
=
'#93c5fd'
;
chatForm
.
addEventListener
(
'submit'
,
e
=>
{
div
.
appendChild
(
senderSpan
);
e
.
preventDefault
();
div
.
appendChild
(
document
.
createTextNode
(
' '
));
const
msg
=
chatInput
.
value
.
trim
();
}
else
if
(
msg
.
sender
!==
'me'
&&
!
msg
.
type
?.
startsWith
(
'notify'
)
&&
msg
.
type
!==
'tip'
)
{
if
(
!
msg
)
return
;
const
senderSpan
=
document
.
createElement
(
'span'
);
addPublicMessage
(
msg
,
"me"
,
"You"
);
senderSpan
.
className
=
'sender'
;
chatInput
.
value
=
''
;
senderSpan
.
textContent
=
msg
.
sender
;
chatInput
.
focus
();
senderSpan
.
dataset
.
sender
=
msg
.
sender
;
});
senderSpan
.
addEventListener
(
'contextmenu'
,
(
e
)
=>
{
e
.
preventDefault
();
function
openPrivateChat
(
user
)
{
showContextMenu
(
e
,
msg
.
sender
);
if
(
privateChats
.
has
(
user
.
id
))
{
});
const
chatPopup
=
privateChats
.
get
(
user
.
id
);
div
.
appendChild
(
senderSpan
);
chatPopup
.
element
.
style
.
display
=
'flex'
;
div
.
appendChild
(
document
.
createTextNode
(
': '
));
chatPopup
.
input
.
focus
();
}
return
;
const
contentSpan
=
document
.
createElement
(
'span'
);
}
contentSpan
.
className
=
'content'
;
contentSpan
.
innerHTML
=
content
;
const
template
=
document
.
getElementById
(
'privateChatTemplate'
);
div
.
appendChild
(
contentSpan
);
const
clone
=
template
.
content
.
cloneNode
(
true
);
const
senderSpan
=
div
.
querySelector
(
'.sender'
);
if
(
senderSpan
&&
msg
.
sender
!==
'me'
&&
!
msg
.
type
?.
startsWith
(
'notify'
))
{
const
popup
=
clone
.
querySelector
(
'.private-chat-popup'
);
senderSpan
.
addEventListener
(
'contextmenu'
,
(
e
)
=>
{
const
headerUser
=
clone
.
querySelector
(
'.private-chat-user'
);
e
.
preventDefault
();
const
closeBtn
=
clone
.
querySelector
(
'.private-chat-close'
);
showContextMenu
(
e
,
senderSpan
.
dataset
.
sender
);
const
msgsElem
=
clone
.
querySelector
(
'.private-chat-messages'
);
});
const
form
=
clone
.
querySelector
(
'form'
);
}
const
input
=
clone
.
querySelector
(
'input'
);
chatWindow
.
appendChild
(
div
);
});
headerUser
.
textContent
=
user
.
name
;
chatWindow
.
scrollTop
=
chatWindow
.
scrollHeight
;
closeBtn
.
addEventListener
(
'click'
,
()
=>
{
}
popup
.
style
.
display
=
'none'
;
});
// Username autocompletion
let
completionState
=
{
index
:
-
1
,
prefix
:
''
,
original
:
''
,
matches
:
[]
};
function
addPrivateMessage
(
text
,
sender
=
'other'
)
{
const
msg
=
document
.
createElement
(
'div'
);
function
getUsernames
()
{
msg
.
className
=
`private-message
${
sender
}
`
;
const
users
=
new
Set
();
msg
.
textContent
=
text
;
for
(
const
platform
in
fakeUsers
)
{
msgsElem
.
appendChild
(
msg
);
fakeUsers
[
platform
].
forEach
(
user
=>
{
msgsElem
.
scrollTop
=
msgsElem
.
scrollHeight
;
users
.
add
(
`
${
user
.
username
}
@
${
platform
}
`
);
}
});
}
form
.
addEventListener
(
'submit'
,
e
=>
{
fakeMessages
.
forEach
(
msg
=>
{
e
.
preventDefault
();
if
(
msg
.
sender
!==
'me'
&&
!
msg
.
sender
.
startsWith
(
'system'
)
&&
!
msg
.
sender
.
startsWith
(
'platform@'
))
{
const
message
=
input
.
value
.
trim
();
users
.
add
(
msg
.
sender
);
if
(
!
message
)
return
;
}
addPrivateMessage
(
message
,
'user'
);
});
input
.
value
=
''
;
return
Array
.
from
(
users
);
input
.
focus
();
}
});
chatInput
.
addEventListener
(
'keydown'
,
(
e
)
=>
{
popup
.
style
.
display
=
'flex'
;
if
(
e
.
key
===
'Tab'
)
{
document
.
body
.
appendChild
(
popup
);
e
.
preventDefault
();
const
text
=
chatInput
.
value
;
privateChats
.
set
(
user
.
id
,
{
const
cursorPos
=
chatInput
.
selectionStart
;
element
:
popup
,
const
textBeforeCursor
=
text
.
slice
(
0
,
cursorPos
);
input
:
input
,
const
lastWordMatch
=
textBeforeCursor
.
match
(
/
(\S
+
)
$/
);
addMessage
:
addPrivateMessage
if
(
lastWordMatch
)
{
});
const
prefix
=
lastWordMatch
[
1
];
const
usernames
=
getUsernames
();
input
.
focus
();
let
matches
=
usernames
.
filter
(
u
=>
u
.
toLowerCase
().
startsWith
(
prefix
.
toLowerCase
()));
}
if
(
matches
.
length
===
0
)
return
;
function
showContextMenu
(
event
,
user
,
type
)
{
if
(
completionState
.
prefix
!==
prefix
)
{
contextMenuTargetUser
=
user
;
completionState
=
{
index
:
-
1
,
prefix
,
original
:
prefix
,
matches
};
contextMenuTargetType
=
type
;
}
const
menuWidth
=
180
;
completionState
.
index
=
(
completionState
.
index
+
1
)
%
matches
.
length
;
const
menuHeight
=
150
;
const
selectedUsername
=
matches
[
completionState
.
index
];
let
x
=
event
.
clientX
;
const
isFirstWord
=
textBeforeCursor
.
trim
().
length
===
prefix
.
length
&&
textBeforeCursor
.
match
(
/^
\s
*/
)[
0
].
length
+
prefix
.
length
===
textBeforeCursor
.
length
;
let
y
=
event
.
clientY
;
const
replacement
=
isFirstWord
?
`
${
selectedUsername
}
: `
:
selectedUsername
;
if
(
x
+
menuWidth
>
window
.
innerWidth
)
{
const
newText
=
text
.
slice
(
0
,
cursorPos
-
prefix
.
length
)
+
replacement
+
text
.
slice
(
cursorPos
);
x
=
window
.
innerWidth
-
menuWidth
-
8
;
chatInput
.
value
=
newText
;
}
chatInput
.
selectionStart
=
chatInput
.
selectionEnd
=
cursorPos
-
prefix
.
length
+
replacement
.
length
;
if
(
y
+
menuHeight
>
window
.
innerHeight
)
{
}
y
=
window
.
innerHeight
-
menuHeight
-
8
;
}
else
if
(
e
.
key
===
'Enter'
)
{
}
if
(
e
.
shiftKey
||
e
.
ctrlKey
)
{
e
.
preventDefault
();
contextMenu
.
style
.
left
=
x
+
'px'
;
chatInput
.
value
+=
'
\
n'
;
contextMenu
.
style
.
top
=
y
+
'px'
;
}
else
{
contextMenu
.
classList
.
add
(
'visible'
);
e
.
preventDefault
();
contextMenu
.
setAttribute
(
'aria-hidden'
,
'false'
);
sendMessage
();
}
const
firstItem
=
contextMenu
.
querySelector
(
'.context-menu-item'
);
}
else
{
if
(
firstItem
)
firstItem
.
focus
();
completionState
=
{
index
:
-
1
,
prefix
:
''
,
original
:
''
,
matches
:
[]
};
}
document
.
addEventListener
(
'click'
,
outsideClickHandler
);
});
document
.
addEventListener
(
'keydown'
,
keyboardHandler
);
}
sendButton
.
addEventListener
(
'click'
,
sendMessage
);
function
hideContextMenu
()
{
function
sendMessage
()
{
contextMenu
.
classList
.
remove
(
'visible'
);
const
content
=
chatInput
.
value
.
trim
();
contextMenu
.
setAttribute
(
'aria-hidden'
,
'true'
);
const
platform
=
platformSelector
.
value
;
contextMenuTargetUser
=
null
;
if
(
content
)
{
contextMenuTargetType
=
null
;
console
.
log
(
`Sending message to
${
platform
}
:
${
content
}
`
);
document
.
removeEventListener
(
'click'
,
outsideClickHandler
);
fakeMessages
.
push
({
sender
:
'me'
,
content
,
timestamp
:
new
Date
().
toISOString
()
});
document
.
removeEventListener
(
'keydown'
,
keyboardHandler
);
renderMessages
(
fakeMessages
);
}
chatInput
.
value
=
''
;
completionState
=
{
index
:
-
1
,
prefix
:
''
,
original
:
''
,
matches
:
[]
};
function
outsideClickHandler
(
event
)
{
}
if
(
!
contextMenu
.
contains
(
event
.
target
))
{
}
hideContextMenu
();
}
// Emoji picker
}
emojiButton
.
addEventListener
(
'mouseover'
,
()
=>
{
emojiPicker
.
style
.
display
=
'flex'
;
function
keyboardHandler
(
event
)
{
emojiPicker
.
style
.
left
=
`
${
emojiButton
.
offsetLeft
}
px`
;
if
(
!
contextMenu
.
classList
.
contains
(
'visible'
))
return
;
emojiPicker
.
style
.
top
=
`
${
emojiButton
.
offsetTop
-
emojiPicker
.
offsetHeight
}
px`
;
});
const
items
=
[...
contextMenu
.
querySelectorAll
(
'.context-menu-item'
)];
emojiPicker
.
addEventListener
(
'mouseleave'
,
()
=>
{
const
currentIndex
=
items
.
indexOf
(
document
.
activeElement
);
emojiPicker
.
style
.
display
=
'none'
;
});
switch
(
event
.
key
)
{
emojiPicker
.
querySelectorAll
(
'span'
).
forEach
(
span
=>
{
case
'Escape'
:
span
.
addEventListener
(
'click'
,
()
=>
{
hideContextMenu
();
chatInput
.
value
+=
span
.
dataset
.
emoji
;
break
;
emojiPicker
.
style
.
display
=
'none'
;
case
'ArrowDown'
:
chatInput
.
focus
();
event
.
preventDefault
();
});
const
nextIndex
=
(
currentIndex
+
1
)
%
items
.
length
;
});
items
[
nextIndex
].
focus
();
break
;
// Context menu
case
'ArrowUp'
:
const
contextMenu
=
document
.
getElementById
(
'context-menu'
);
event
.
preventDefault
();
function
showContextMenu
(
e
,
sender
)
{
const
prevIndex
=
(
currentIndex
-
1
+
items
.
length
)
%
items
.
length
;
e
.
preventDefault
();
items
[
prevIndex
].
focus
();
const
parts
=
sender
.
split
(
'@'
);
break
;
if
(
parts
.
length
!==
2
)
{
case
'Enter'
:
console
.
error
(
`Invalid sender format:
${
sender
}
`
);
event
.
preventDefault
();
return
;
if
(
document
.
activeElement
.
classList
.
contains
(
'context-menu-item'
))
{
}
document
.
activeElement
.
click
();
const
[
username
,
platformAccount
]
=
parts
;
}
const
[
platform
,
account
]
=
platformAccount
.
split
(
'.'
);
break
;
if
(
!
platform
||
!
account
)
{
}
console
.
error
(
`Invalid platform.account format:
${
platformAccount
}
`
);
}
return
;
}
contextMenu
.
addEventListener
(
'click'
,
e
=>
{
const
userList
=
fakeUsers
[
`
${
platform
}
.
${
account
}
`
];
if
(
!
e
.
target
.
classList
.
contains
(
'context-menu-item'
))
return
;
if
(
!
userList
)
{
console
.
error
(
`Platform not found in fakeUsers:
${
platform
}
.
${
account
}
`
);
const
action
=
e
.
target
.
dataset
.
action
;
return
;
if
(
!
contextMenuTargetUser
)
return
;
}
const
user
=
userList
.
find
(
u
=>
u
.
username
===
username
);
switch
(
action
)
{
if
(
!
user
)
{
case
'openProfile'
:
console
.
error
(
`User not found:
${
username
}
in
${
platform
}
.
${
account
}
`
);
alert
(
`Open profile for
${
contextMenuTargetUser
.
name
}
(ID:
${
contextMenuTargetUser
.
id
}
)`
);
return
;
break
;
}
case
'privateChat'
:
contextMenu
.
querySelector
(
'#context-profile'
).
href
=
`https://
${
platform
}
.com/profile/
${
username
}
`
;
openPrivateChat
(
contextMenuTargetUser
);
contextMenu
.
querySelector
(
'#context-tokens'
).
textContent
=
`Tokens:
${
user
.
tokens
}
`
;
break
;
contextMenu
.
querySelector
(
'#context-ban'
).
onclick
=
()
=>
alert
(
`Ban
${
sender
}
`
);
case
'ban'
:
contextMenu
.
querySelector
(
'#context-kick'
).
onclick
=
()
=>
alert
(
`Kick
${
sender
}
`
);
alert
(
`Ban user
${
contextMenuTargetUser
.
name
}
`
);
contextMenu
.
querySelector
(
'#context-private'
).
onclick
=
()
=>
alert
(
`Open private chat with
${
sender
}
`
);
break
;
contextMenu
.
style
.
display
=
'block'
;
case
'kick'
:
contextMenu
.
style
.
left
=
`
${
e
.
pageX
}
px`
;
alert
(
`Kick user
${
contextMenuTargetUser
.
name
}
`
);
contextMenu
.
style
.
top
=
`
${
e
.
pageY
}
px`
;
break
;
}
default
:
break
;
document
.
addEventListener
(
'click'
,
()
=>
{
}
contextMenu
.
style
.
display
=
'none'
;
hideContextMenu
();
statusMenu
.
style
.
display
=
'none'
;
});
});
renderUserList
();
// User list
addPublicMessage
(
'Welcome to the chat!'
,
'someone'
,
'Someone'
);
function
renderUserList
()
{
</script>
const
userList
=
document
.
getElementById
(
'user-list'
);
const
totalUsers
=
document
.
getElementById
(
'total-users'
);
let
total
=
0
;
userList
.
innerHTML
=
''
;
for
(
const
platform
in
fakeUsers
)
{
total
+=
fakeUsers
[
platform
].
length
;
const
platformDiv
=
document
.
createElement
(
'div'
);
platformDiv
.
className
=
'platform'
;
platformDiv
.
innerHTML
=
`<div class="platform-header">
${
platform
}
<span>(
${
fakeUsers
[
platform
].
length
}
)</span></div>`
;
const
usersDiv
=
document
.
createElement
(
'div'
);
usersDiv
.
className
=
'users'
;
fakeUsers
[
platform
].
forEach
(
user
=>
{
const
userDiv
=
document
.
createElement
(
'div'
);
userDiv
.
className
=
'user'
;
userDiv
.
dataset
.
sender
=
`
${
user
.
username
}
@
${
platform
}
`
;
userDiv
.
innerHTML
=
`
<span><span class="status-dot
${
user
.
status
}
"></span>
${
user
.
username
}
</span>
<span>
${
user
.
tokens
}
tokens</span>
`
;
userDiv
.
addEventListener
(
'contextmenu'
,
(
e
)
=>
{
e
.
preventDefault
();
showContextMenu
(
e
,
userDiv
.
dataset
.
sender
);
});
usersDiv
.
appendChild
(
userDiv
);
});
platformDiv
.
appendChild
(
usersDiv
);
userList
.
appendChild
(
platformDiv
);
}
totalUsers
.
textContent
=
total
;
document
.
querySelectorAll
(
'.platform-header'
).
forEach
(
header
=>
{
header
.
addEventListener
(
'click'
,
()
=>
{
const
users
=
header
.
nextElementSibling
;
users
.
classList
.
toggle
(
'active'
);
});
});
}
// Tabs
document
.
querySelectorAll
(
'.tab'
).
forEach
(
tab
=>
{
tab
.
addEventListener
(
'click'
,
()
=>
{
document
.
querySelectorAll
(
'.tab'
).
forEach
(
t
=>
t
.
classList
.
remove
(
'active'
));
document
.
querySelectorAll
(
'.tab-content'
).
forEach
(
c
=>
c
.
classList
.
remove
(
'active'
));
tab
.
classList
.
add
(
'active'
);
document
.
getElementById
(
tab
.
dataset
.
tab
).
classList
.
add
(
'active'
);
});
});
// Earnings
function
renderEarnings
()
{
const
tbody
=
document
.
querySelector
(
'.earnings-table tbody'
);
tbody
.
innerHTML
=
''
;
const
totals
=
fakeEarnings
.
reduce
((
acc
,
e
)
=>
({
lastSession
:
acc
.
lastSession
+
e
.
lastSession
,
today
:
acc
.
today
+
e
.
today
,
lastHour
:
acc
.
lastHour
+
e
.
lastHour
,
sess
:
acc
.
sess
+
e
.
sess
}),
{
lastSession
:
0
,
today
:
0
,
lastHour
:
0
,
sess
:
0
});
const
totalsRow
=
document
.
createElement
(
'tr'
);
totalsRow
.
className
=
'totals-row'
;
totalsRow
.
innerHTML
=
`
<td>Totals</td>
<td>$
${
totals
.
lastSession
}
</td>
<td>$
${
totals
.
today
}
</td>
<td>$
${
totals
.
lastHour
}
</td>
<td>$
${
totals
.
sess
}
</td>
`
;
tbody
.
appendChild
(
totalsRow
);
fakeEarnings
.
forEach
(
e
=>
{
const
[
platform
,
account
]
=
e
.
platform
.
split
(
'.'
);
const
row
=
document
.
createElement
(
'tr'
);
row
.
innerHTML
=
`
<td>
${
platform
}
<br>
${
account
}
</td>
<td>$
${
e
.
lastSession
}
</td>
<td>$
${
e
.
today
}
</td>
<td>$
${
e
.
lastHour
}
</td>
<td>$
${
e
.
sess
}
</td>
`
;
tbody
.
appendChild
(
row
);
});
}
// Reset session button
document
.
getElementById
(
'reset-session'
).
addEventListener
(
'click'
,
()
=>
{
fakeEarnings
.
forEach
(
e
=>
e
.
sess
=
0
);
renderEarnings
();
});
// Initialize
populateVideoSources
();
renderMessages
(
fakeMessages
);
renderUserList
();
renderEarnings
();
updateStatus
();
</script>
</body>
</body>
</html>
</html>
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment