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>
<html
lang=
"en"
>
<head>
<meta
charset=
"UTF-8"
/>
<meta
name=
"viewport"
content=
"width=device-width, initial-scale=1"
/>
<title>
Responsive Chat with Context Menus and Fixed Private Chat
</title>
<style>
@import
url('https://fonts.googleapis.com/css2?family=Poppins&display=swap')
;
/* Reset and base */
*
{
box-sizing
:
border-box
;
}
body
{
margin
:
0
;
font-family
:
'Poppins'
,
sans-serif
;
background
:
#121212
;
color
:
#e0e0e0
;
display
:
flex
;
min-height
:
100vh
;
user-select
:
none
;
-webkit-font-smoothing
:
antialiased
;
-moz-osx-font-smoothing
:
grayscale
;
}
/* Container Layout */
.container
{
display
:
grid
;
grid-template-columns
:
1
fr
280px
;
grid-template-rows
:
1
fr
;
gap
:
0
;
width
:
100%
;
height
:
100vh
;
background
:
#1f1f1f
;
overflow
:
hidden
;
}
/* User List Panel */
.user-list
{
background
:
#20232a
;
border-left
:
1px
solid
#2e2e3a
;
display
:
flex
;
flex-direction
:
column
;
user-select
:
none
;
min-width
:
280px
;
}
.user-list-header
{
padding
:
16px
;
font-weight
:
700
;
font-size
:
1.2rem
;
border-bottom
:
1px
solid
#2e2e3a
;
flex-shrink
:
0
;
}
.users
{
flex-grow
:
1
;
overflow-y
:
auto
;
padding
:
12px
8px
;
-webkit-overflow-scrolling
:
touch
;
}
.user-item
{
padding
:
10px
12px
;
margin-bottom
:
8px
;
border-radius
:
8px
;
cursor
:
pointer
;
display
:
flex
;
align-items
:
center
;
gap
:
12px
;
transition
:
background-color
0.3s
ease
;
user-select
:
none
;
}
.user-item
:hover
,
.user-item
:focus
{
background-color
:
#393d4d
;
outline
:
none
;
}
.user-avatar
{
flex-shrink
:
0
;
width
:
32px
;
height
:
32px
;
border-radius
:
50%
;
background
:
linear-gradient
(
135deg
,
#6c5ce7
,
#0984e3
);
color
:
white
;
font-weight
:
700
;
font-size
:
1rem
;
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
user-select
:
none
;
}
.user-name
{
flex-grow
:
1
;
font-size
:
0.95rem
;
color
:
#dcdde1
;
white-space
:
nowrap
;
overflow
:
hidden
;
text-overflow
:
ellipsis
;
user-select
:
text
;
}
.user-status
{
width
:
10px
;
height
:
10px
;
border-radius
:
50%
;
background-color
:
#44bd32
;
box-shadow
:
0
0
4px
#44bd32
aa
;
flex-shrink
:
0
;
}
/* Main Chat Panel */
.chat-panel
{
display
:
flex
;
flex-direction
:
column
;
background
:
#181a20
;
user-select
:
none
;
min-width
:
0
;
}
.chat-header
{
padding
:
16px
24px
;
border-bottom
:
1px
solid
#2e2e3a
;
font-weight
:
700
;
font-size
:
1.3rem
;
background
:
linear-gradient
(
135deg
,
#6c5ce7
,
#0984e3
);
color
:
white
;
user-select
:
none
;
flex-shrink
:
0
;
}
.chat-messages
{
flex-grow
:
1
;
overflow-y
:
auto
;
padding
:
20px
24px
;
scrollbar-width
:
thin
;
scrollbar-color
:
#394264
transparent
;
background
:
#121217
;
user-select
:
text
;
-webkit-overflow-scrolling
:
touch
;
min-height
:
0
;
}
.chat-messages
::-webkit-scrollbar
{
width
:
8px
;
}
.chat-messages
::-webkit-scrollbar-thumb
{
background-color
:
#394264
;
border-radius
:
4px
;
}
.message
{
max-width
:
60%
;
margin-bottom
:
16px
;
padding
:
12px
16px
;
border-radius
:
12px
;
line-height
:
1.4
;
font-size
:
0.95rem
;
word-wrap
:
break-word
;
box-shadow
:
0
2px
5px
rgb
(
0
0
0
/
0.3
);
user-select
:
text
;
display
:
flex
;
align-items
:
center
;
gap
:
8px
;
}
.message.user
{
background
:
linear-gradient
(
135deg
,
#0984e3
,
#6c5ce7
);
color
:
white
;
align-self
:
flex-end
;
border-bottom-right-radius
:
2px
;
justify-content
:
flex-end
;
}
.message.other
{
background
:
#2d2f3a
;
color
:
#ddd
;
align-self
:
flex-start
;
border-bottom-left-radius
:
2px
;
}
.msg-username
{
font-weight
:
700
;
cursor
:
pointer
;
user-select
:
text
;
color
:
#a2d2ff
;
}
.chat-input-container
{
display
:
flex
;
padding
:
12px
24px
;
border-top
:
1px
solid
#2e2e3a
;
background
:
#20232a
;
align-items
:
center
;
user-select
:
none
;
flex-shrink
:
0
;
}
.chat-input
{
flex-grow
:
1
;
padding
:
10px
16px
;
font-size
:
1rem
;
border-radius
:
24px
;
border
:
none
;
background-color
:
#2d2f3a
;
color
:
#eee
;
outline
:
none
;
transition
:
background-color
0.2s
ease
;
}
.chat-input
::placeholder
{
color
:
#888
;
}
.chat-input
:focus
{
background-color
:
#424559
;
}
.send-btn
{
margin-left
:
12px
;
background
:
linear-gradient
(
135deg
,
#6c5ce7
,
#0984e3
);
border
:
none
;
padding
:
10px
18px
;
color
:
white
;
font-weight
:
700
;
font-size
:
1rem
;
border-radius
:
24px
;
cursor
:
pointer
;
user-select
:
none
;
transition
:
background-color
0.3s
ease
;
}
.send-btn
:hover
{
background
:
linear-gradient
(
135deg
,
#5353c3
,
#0761c7
);
}
/* Private Chat Popup */
.private-chat-popup
{
position
:
fixed
;
bottom
:
16px
;
right
:
16px
;
width
:
320px
;
max-height
:
480px
;
background
:
#292b37
;
border-radius
:
12px
;
box-shadow
:
0
8px
24px
rgba
(
0
,
0
,
0
,
0.7
);
display
:
flex
;
flex-direction
:
column
;
overflow
:
hidden
;
z-index
:
1000
;
font-size
:
0.9rem
;
animation
:
fadeInUp
0.3s
ease
forwards
;
user-select
:
none
;
resize
:
both
;
overflow
:
auto
;
}
.private-chat-header
{
background
:
linear-gradient
(
135deg
,
#6c5ce7
,
#0984e3
);
padding
:
12px
16px
;
font-weight
:
700
;
color
:
white
;
display
:
flex
;
justify-content
:
space-between
;
align-items
:
center
;
flex-shrink
:
0
;
cursor
:
move
;
}
.private-chat-close
{
cursor
:
pointer
;
font-weight
:
700
;
font-size
:
1.2rem
;
line-height
:
0
;
user-select
:
none
;
transition
:
color
0.3s
ease
;
background
:
none
;
border
:
none
;
color
:
white
;
}
.private-chat-close
:hover
{
color
:
#ffd32a
;
}
.private-chat-messages
{
flex-grow
:
1
;
padding
:
12px
16px
;
overflow-y
:
auto
;
background
:
#20212a
;
scrollbar-width
:
thin
;
scrollbar-color
:
#52526a
transparent
;
-webkit-overflow-scrolling
:
touch
;
user-select
:
text
;
min-height
:
120px
;
max-height
:
320px
;
}
.private-chat-messages
::-webkit-scrollbar
{
width
:
6px
;
}
.private-chat-messages
::-webkit-scrollbar-thumb
{
background-color
:
#52526a
;
border-radius
:
4px
;
}
.private-message
{
margin-bottom
:
12px
;
background
:
#3a3c4d
;
padding
:
10px
14px
;
border-radius
:
10px
;
color
:
#e4e6f0
;
max-width
:
90%
;
word-wrap
:
break-word
;
}
.private-message.user
{
background
:
linear-gradient
(
135deg
,
#0984e3
,
#6c5ce7
);
color
:
white
;
align-self
:
flex-end
;
border-bottom-right-radius
:
2px
;
}
.private-message.other
{
background
:
#3a3c4d
;
color
:
#ddd
;
align-self
:
flex-start
;
border-bottom-left-radius
:
2px
;
}
.private-chat-input-container
{
display
:
flex
;
padding
:
10px
14px
;
border-top
:
1px
solid
#44475a
;
background
:
#2b2e42
;
align-items
:
center
;
flex-shrink
:
0
;
}
.private-chat-input
{
flex-grow
:
1
;
padding
:
8px
14px
;
font-size
:
0.95rem
;
border-radius
:
18px
;
border
:
none
;
background-color
:
#3d3f56
;
color
:
#eee
;
outline
:
none
;
transition
:
background-color
0.2s
ease
;
}
.private-chat-input
::placeholder
{
color
:
#bbb
;
}
.private-chat-input
:focus
{
background-color
:
#56577b
;
}
.private-send-btn
{
margin-left
:
10px
;
background
:
linear-gradient
(
135deg
,
#6c5ce7
,
#0984e3
);
border
:
none
;
padding
:
8px
20px
;
color
:
white
;
font-weight
:
700
;
font-size
:
0.95rem
;
border-radius
:
18px
;
cursor
:
pointer
;
user-select
:
none
;
transition
:
background-color
0.3s
ease
;
}
.private-send-btn
:hover
{
background
:
linear-gradient
(
135deg
,
#5353c3
,
#0761c7
);
}
@keyframes
fadeInUp
{
0
%
{
opacity
:
0
;
transform
:
translateY
(
20px
);
}
100
%
{
opacity
:
1
;
transform
:
translateY
(
0
);
}
}
/* Context Menu Styling */
.context-menu
{
position
:
absolute
;
background
:
#2a2c38
;
border-radius
:
8px
;
padding
:
8px
0
;
width
:
180px
;
box-shadow
:
0
6px
15px
rgba
(
0
,
0
,
0
,
0.4
);
z-index
:
2000
;
display
:
none
;
user-select
:
none
;
}
.context-menu.visible
{
display
:
block
;
}
.context-menu-item
{
padding
:
10px
16px
;
font-size
:
0.9rem
;
color
:
#e0e0e0
;
cursor
:
pointer
;
transition
:
background-color
0.25s
ease
;
}
.context-menu-item
:hover
,
.context-menu-item
:focus
{
background
:
#5353c3
;
color
:
white
;
outline
:
none
;
}
/* Responsive */
@media
(
max-width
:
768px
)
{
body
{
display
:
block
;
height
:
auto
;
}
.container
{
height
:
auto
;
display
:
flex
;
flex-direction
:
column
;
}
.chat-panel
{
order
:
2
;
height
:
70vh
;
min-width
:
100%
;
}
.user-list
{
order
:
1
;
height
:
30vh
;
min-width
:
100%
;
border-left
:
none
!important
;
border-top
:
1px
solid
#2e2e3a
;
}
.chat-messages
{
height
:
100%
;
min-height
:
0
;
}
.chat-input-container
{
padding
:
8px
16px
;
}
.chat-input
{
font-size
:
0.9rem
;
padding
:
8px
12px
;
}
.send-btn
{
padding
:
8px
12px
;
font-size
:
0.9rem
;
}
.user-list-header
,
.chat-header
{
padding
:
12px
16px
;
font-size
:
1.1rem
;
}
.user-item
{
padding
:
8px
10px
;
}
.user-avatar
{
width
:
28px
;
height
:
28px
;
font-size
:
0.9rem
;
}
.user-name
{
font-size
:
0.9rem
;
}
.private-chat-popup
{
width
:
90vw
;
max-height
:
50vh
;
bottom
:
8px
;
right
:
5vw
;
font-size
:
0.85rem
;
}
.private-chat-messages
{
max-height
:
200px
;
}
}
</style>
<meta
charset=
"UTF-8"
>
<meta
name=
"viewport"
content=
"width=device-width, initial-scale=1.0"
>
<title>
SHMCamStudio
</title>
<link
href=
"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap"
rel=
"stylesheet"
>
<style>
/* Reset and base styles */
*
{
margin
:
0
;
padding
:
0
;
box-sizing
:
border-box
;
font-family
:
'Inter'
,
Arial
,
sans-serif
;
}
body
{
background
:
#0a1122
;
color
:
#ffffff
;
display
:
flex
;
flex-direction
:
column
;
min-height
:
100vh
;
overflow
:
hidden
;
}
/* Top bar */
.top-bar
{
height
:
60px
;
background
:
linear-gradient
(
to
right
,
#3b4d66
,
#6a1b9a
);
display
:
flex
;
align-items
:
center
;
padding
:
0
20px
;
gap
:
10px
;
border-bottom
:
2px
solid
#3b82f6
;
}
.top-bar
img
{
height
:
40px
;
width
:
auto
;
margin-right
:
10px
;
}
.top-bar
span
{
font-size
:
20px
;
font-weight
:
600
;
}
.top-bar
a
{
color
:
#3b82f6
;
text-decoration
:
none
;
}
.top-bar
a
:hover
{
text-decoration
:
underline
;
}
/* Desktop layout: 3 columns */
.container
{
display
:
flex
;
height
:
calc
(
100vh
-
60px
);
width
:
100%
;
}
.left-column
{
width
:
25%
;
min-width
:
20%
;
max-width
:
45%
;
background
:
linear-gradient
(
to
bottom
,
#2a2a2a
,
#1f1f1f
);
padding
:
10px
;
border-right
:
2px
solid
#111111
;
height
:
100%
;
display
:
flex
;
flex-direction
:
column
;
overflow-x
:
hidden
;
overflow-y
:
auto
;
position
:
relative
;
}
.resize-handle
{
position
:
absolute
;
right
:
-2px
;
top
:
0
;
width
:
4px
;
height
:
100%
;
background
:
#111111
;
cursor
:
col-resize
;
z-index
:
10
;
}
.center-column
{
flex
:
1
;
padding
:
10px
;
background
:
linear-gradient
(
to
bottom
,
#1c1c1c
,
#121212
);
display
:
flex
;
flex-direction
:
column
;
}
.right-column
{
flex
:
0
0
20%
;
background
:
linear-gradient
(
to
bottom
,
#2a2a2a
,
#1f1f1f
);
padding
:
10px
;
border-left
:
2px
solid
#111111
;
overflow-y
:
auto
;
}
/* Video window */
.video-container
{
position
:
relative
;
background
:
#000
;
border-radius
:
8px
;
overflow
:
hidden
;
flex-shrink
:
0
;
width
:
100%
;
}
video
{
width
:
100%
;
height
:
auto
;
aspect-ratio
:
16
/
9
;
display
:
block
;
}
.video-controls
{
position
:
absolute
;
top
:
10px
;
left
:
10px
;
display
:
flex
;
gap
:
10px
;
opacity
:
0
;
transition
:
opacity
0.3s
;
}
.video-container
:hover
.video-controls
{
opacity
:
1
;
}
.video-controls
select
,
.video-controls
button
{
padding
:
5px
;
border
:
none
;
border-radius
:
20px
;
background
:
#3b82f6
;
color
:
#fff
;
cursor
:
pointer
;
}
.video-controls
select
{
flex
:
1
;
min-width
:
100px
;
max-width
:
150px
;
}
.video-controls
button
{
min-width
:
60px
;
}
.video-stats
{
position
:
absolute
;
bottom
:
0
;
width
:
100%
;
background
:
rgba
(
0
,
0
,
0
,
0.7
);
padding
:
5px
;
font-size
:
12px
;
border-radius
:
0
0
8px
8px
;
}
/* Online/offline button */
.status-button
{
width
:
100%
;
padding
:
12px
;
margin
:
10px
0
;
font-size
:
18px
;
font-weight
:
600
;
border
:
none
;
border-radius
:
20px
;
cursor
:
pointer
;
text-align
:
center
;
flex-shrink
:
0
;
}
.status-button.online
{
background
:
#dc3545
;
}
.status-button.offline
{
background
:
#28a745
;
}
.status-menu
{
display
:
none
;
position
:
absolute
;
background
:
#2a2a2a
;
border
:
1px
solid
#3b82f6
;
padding
:
10px
;
z-index
:
100
;
border-radius
:
8px
;
}
.status-menu
button
{
display
:
block
;
width
:
100%
;
padding
:
8px
;
background
:
none
;
border
:
none
;
color
:
#fff
;
text-align
:
left
;
cursor
:
pointer
;
border-radius
:
20px
;
}
.status-menu
button
:hover
{
background
:
#3b82f6
;
}
/* Tabs */
.tabs
{
display
:
flex
;
border-bottom
:
2px
solid
#111111
;
margin-top
:
10px
;
flex-shrink
:
0
;
overflow-x
:
hidden
;
}
.tab
{
padding
:
8px
12px
;
cursor
:
pointer
;
background
:
#2a2a2a
;
border-radius
:
8px
8px
0
0
;
margin-right
:
5px
;
font-size
:
14px
;
white-space
:
nowrap
;
}
.tab.active
{
background
:
#3b82f6
;
}
.tab-content
{
display
:
none
;
padding
:
15px
;
background
:
#2a2a2a
;
border-radius
:
0
0
8px
8px
;
overflow-y
:
auto
;
flex-grow
:
1
;
max-height
:
100%
;
overflow-x
:
hidden
;
}
.tab-content.active
{
display
:
block
;
}
/* Chat window */
.chat-window
{
flex
:
1
;
background
:
#121212
;
border-radius
:
8px
;
padding
:
15px
;
overflow-y
:
auto
;
margin-bottom
:
10px
;
}
.message
{
margin
:
8px
0
;
padding
:
10px
;
font-size
:
16px
;
}
.message.me
{
background
:
linear-gradient
(
to
right
,
#2563eb
,
#4b0082
);
margin-left
:
20%
;
text-align
:
right
;
border-radius
:
10px
10px
0px
10px
;
}
.message.me
.sender
{
color
:
#93c5fd
;
font-weight
:
600
;
}
.message.me
.content
{
color
:
#f0f9ff
;
}
.message.other
{
background
:
linear-gradient
(
to
right
,
#1e293b
,
#2a1b3d
);
margin-right
:
20%
;
border-radius
:
10px
10px
10px
0px
;
}
.message.notify-system
{
background
:
linear-gradient
(
to
right
,
#1f2937
,
#2a1b3d
);
margin
:
8px
10%
;
text-align
:
center
;
border-radius
:
8px
;
}
.message.notify-platform
{
background
:
linear-gradient
(
to
right
,
#1f2937
,
#2a1b3d
);
margin
:
8px
10%
;
text-align
:
center
;
border-radius
:
8px
;
}
.message.tip
{
background
:
linear-gradient
(
to
right
,
#064e3b
,
#1b3d2a
);
margin
:
8px
10%
;
text-align
:
center
;
border-radius
:
8px
8px
0
8px
;
}
.message.tip
.content
{
color
:
#d1fae5
;
}
.message
.sender
{
color
:
#60a5fa
;
font-weight
:
600
;
cursor
:
pointer
;
}
.message
.content
{
color
:
#a3bffa
;
}
.message
img
{
max-width
:
100px
;
border-radius
:
8px
;
}
.message
a
{
color
:
#00b7ff
;
text-decoration
:
underline
;
}
.chat-input-container
{
display
:
flex
;
gap
:
10px
;
align-items
:
stretch
;
}
.chat-input
{
display
:
flex
;
gap
:
10px
;
position
:
relative
;
flex
:
1
;
}
.platform-selector
{
height
:
40px
;
padding
:
8px
;
border
:
none
;
border-radius
:
20px
;
background
:
#3b82f6
;
color
:
#fff
;
cursor
:
pointer
;
font-size
:
14px
;
width
:
120px
;
}
.chat-input
.emoji-button
{
position
:
absolute
;
left
:
10px
;
top
:
50%
;
transform
:
translateY
(
-50%
);
background
:
none
;
border
:
none
;
font-size
:
20px
;
cursor
:
pointer
;
z-index
:
10
;
}
.chat-input
textarea
{
flex
:
1
;
height
:
40px
;
padding
:
10px
40px
10px
60px
;
border
:
none
;
border-radius
:
20px
;
background
:
#1e293b
;
color
:
#fff
;
resize
:
none
;
font-size
:
14px
;
}
.chat-input
button
{
height
:
40px
;
padding
:
10px
20px
;
background
:
#3b82f6
;
border
:
none
;
border-radius
:
20px
;
color
:
#fff
;
cursor
:
pointer
;
font-weight
:
500
;
}
.emoji-picker
{
display
:
none
;
position
:
absolute
;
background
:
#2a2a2a
;
border
:
1px
solid
#3b82f6
;
padding
:
10px
;
z-index
:
100
;
border-radius
:
8px
;
max-width
:
300px
;
flex-wrap
:
wrap
;
}
.emoji-picker
span
{
cursor
:
pointer
;
font-size
:
20px
;
margin
:
5px
;
}
/* User list */
.user-list-header
{
display
:
flex
;
justify-content
:
space-between
;
padding
:
8px
;
background
:
#2a2a2a
;
border-bottom
:
2px
solid
#111111
;
font-size
:
16px
;
font-weight
:
600
;
}
.user-list
.platform
{
margin
:
10px
0
;
}
.platform-header
{
cursor
:
pointer
;
padding
:
8px
;
background
:
#1e293b
;
border-radius
:
8px
;
display
:
flex
;
justify-content
:
space-between
;
font-weight
:
500
;
}
.user
{
padding
:
8px
10px
;
display
:
flex
;
justify-content
:
space-between
;
align-items
:
center
;
font-size
:
14px
;
}
.status-dot
{
width
:
10px
;
height
:
10px
;
border-radius
:
50%
;
display
:
inline-block
;
margin-right
:
5px
;
}
.status-dot.online
{
background
:
#28a745
;
}
.status-dot.offline
{
background
:
#dc3545
;
}
.users
{
display
:
none
;
}
.users.active
{
display
:
block
;
}
/* Context menu */
.context-menu
{
position
:
absolute
;
background
:
#2a2a2a
;
border
:
1px
solid
#3b82f6
;
padding
:
10px
;
z-index
:
100
;
display
:
none
;
border-radius
:
8px
;
}
.context-menu
a
,
.context-menu
button
{
display
:
block
;
padding
:
8px
;
color
:
#fff
;
text-decoration
:
none
;
background
:
none
;
border
:
none
;
cursor
:
pointer
;
width
:
100%
;
text-align
:
left
;
border-radius
:
20px
;
font-size
:
14px
;
}
.context-menu
a
:hover
,
.context-menu
button
:hover
{
background
:
#3b82f6
;
}
/* Earnings table */
.earnings-table
{
width
:
100%
;
max-width
:
100%
;
margin
:
0
;
border-collapse
:
collapse
;
font-size
:
12px
;
}
.earnings-table
th
,
.earnings-table
td
{
padding
:
6px
;
border
:
1px
solid
#111111
;
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>
<body>
<div
class=
"container"
>
<!-- Main Chat -->
<section
class=
"chat-panel"
aria-label=
"Public chat messages"
>
<header
class=
"chat-header"
>
Global Chat
</header>
<div
class=
"chat-messages"
id=
"chatMessages"
role=
"log"
aria-live=
"polite"
aria-relevant=
"additions"
></div>
<form
id=
"chatForm"
class=
"chat-input-container"
aria-label=
"Send message"
>
<input
type=
"text"
id=
"chatInput"
class=
"chat-input"
placeholder=
"Enter message..."
autocomplete=
"off"
aria-required=
"true"
/>
<button
type=
"submit"
class=
"send-btn"
aria-label=
"Send message"
>
Send
</button>
</form>
</section>
<!-- User List -->
<aside
class=
"user-list"
aria-label=
"User list"
>
<div
class=
"user-list-header"
>
Online Users
</div>
<div
class=
"users"
id=
"userList"
role=
"list"
tabindex=
"0"
aria-live=
"polite"
></div>
</aside>
</div>
<!-- Private Chat Popup Template -->
<template
id=
"privateChatTemplate"
>
<section
class=
"private-chat-popup"
role=
"dialog"
aria-modal=
"true"
aria-label=
"Private chat with user"
>
<header
class=
"private-chat-header"
>
<span
class=
"private-chat-user"
></span>
<button
class=
"private-chat-close"
aria-label=
"Close private chat"
>
×
</button>
</header>
<div
class=
"private-chat-messages"
role=
"log"
aria-live=
"polite"
aria-relevant=
"additions"
></div>
<form
class=
"private-chat-input-container"
aria-label=
"Send private message"
>
<input
type=
"text"
class=
"private-chat-input"
placeholder=
"Enter private message..."
autocomplete=
"off"
aria-required=
"true"
/>
<button
type=
"submit"
class=
"private-send-btn"
aria-label=
"Send private message"
>
Send
</button>
</form>
</section>
</template>
<!-- Context Menu -->
<nav
id=
"contextMenu"
class=
"context-menu"
role=
"menu"
aria-hidden=
"true"
tabindex=
"-1"
aria-label=
"User actions"
>
<div
class=
"context-menu-item"
data-action=
"openProfile"
role=
"menuitem"
tabindex=
"0"
>
View Profile
</div>
<div
class=
"context-menu-item"
data-action=
"privateChat"
role=
"menuitem"
tabindex=
"0"
>
Open Private Chat
</div>
<div
class=
"context-menu-item"
data-action=
"ban"
role=
"menuitem"
tabindex=
"0"
>
Ban User
</div>
<div
class=
"context-menu-item"
data-action=
"kick"
role=
"menuitem"
tabindex=
"0"
>
Kick User
</div>
</nav>
<script>
const
users
=
[
{
id
:
'u1'
,
name
:
'Alice'
},
{
id
:
'u2'
,
name
:
'Bob'
},
{
id
:
'u3'
,
name
:
'Charlie'
},
{
id
:
'u4'
,
name
:
'David'
}
];
const
userListElem
=
document
.
getElementById
(
'userList'
);
const
chatMessagesElem
=
document
.
getElementById
(
'chatMessages'
);
const
chatForm
=
document
.
getElementById
(
'chatForm'
);
const
chatInput
=
document
.
getElementById
(
'chatInput'
);
const
privateChats
=
new
Map
();
const
contextMenu
=
document
.
getElementById
(
'contextMenu'
);
let
contextMenuTargetUser
=
null
;
let
contextMenuTargetType
=
null
;
function
renderUserList
()
{
userListElem
.
innerHTML
=
''
;
users
.
forEach
(
user
=>
{
const
userItem
=
document
.
createElement
(
'div'
);
userItem
.
className
=
'user-item'
;
userItem
.
setAttribute
(
'role'
,
'listitem'
);
userItem
.
tabIndex
=
0
;
userItem
.
dataset
.
userid
=
user
.
id
;
const
avatar
=
document
.
createElement
(
'div'
);
avatar
.
className
=
'user-avatar'
;
avatar
.
textContent
=
user
.
name
.
charAt
(
0
).
toUpperCase
();
const
name
=
document
.
createElement
(
'div'
);
name
.
className
=
'user-name'
;
name
.
textContent
=
user
.
name
;
const
status
=
document
.
createElement
(
'div'
);
status
.
className
=
'user-status'
;
status
.
title
=
'Online'
;
userItem
.
appendChild
(
avatar
);
userItem
.
appendChild
(
name
);
userItem
.
appendChild
(
status
);
userItem
.
addEventListener
(
'click'
,
()
=>
openPrivateChat
(
user
));
userItem
.
addEventListener
(
'keydown'
,
e
=>
{
if
(
e
.
key
===
'Enter'
||
e
.
key
===
' '
)
{
e
.
preventDefault
();
openPrivateChat
(
user
);
}
});
userItem
.
addEventListener
(
'contextmenu'
,
e
=>
{
e
.
preventDefault
();
showContextMenu
(
e
,
user
,
'userlist'
);
<div
class=
"top-bar"
>
<img
src=
"https://www.sexhack.me/content/uploads/2022/06/cropped-sexhack-300x99.png"
alt=
"SexHack Logo"
>
<span>
SHM CamStudio by
<a
href=
"https://www.sexhack.me"
>
sexhack.me
</a></span>
</div>
<div
class=
"container"
>
<div
class=
"left-column"
>
<div
class=
"resize-handle"
></div>
<div
class=
"video-container"
>
<video
id=
"video"
autoplay
></video>
<div
class=
"video-controls"
>
<select
id=
"video-source"
></select>
<button
id=
"mirror-video"
>
Mirror
</button>
</div>
<div
class=
"video-stats"
>
Bitrate: 2 Mbps | Quality: HD | Audio:
<span
id=
"audio-bar"
>
████
</span>
</div>
</div>
<button
class=
"status-button offline"
id=
"status-button"
>
GO ONLINE
</button>
<div
class=
"status-menu"
id=
"status-menu"
>
<button
data-platform=
"C4.sexhackme"
>
C4.sexhackme: Offline
</button>
<button
data-platform=
"SC.spora"
>
SC.spora: Offline
</button>
</div>
<div
class=
"tabs"
>
<div
class=
"tab active"
data-tab=
"panel"
>
Panel
</div>
<div
class=
"tab"
data-tab=
"earnings"
>
Earnings
</div>
</div>
<div
class=
"tab-content active"
id=
"panel"
>
<iframe
id=
"panel-iframe"
style=
"width:100%;height:300px;border:none;border-radius:8px;"
></iframe>
</div>
<div
class=
"tab-content"
id=
"earnings"
>
<table
class=
"earnings-table"
>
<thead>
<tr><th>
Plat.
<br>
Acc.
</th><th>
Last
<br>
Sess
</th><th>
Today
</th><th>
Hour
</th><th>
Sess
</th></tr>
</thead>
<tbody></tbody>
</table>
<button
class=
"reset-session-button"
id=
"reset-session"
>
Reset Session
</button>
</div>
</div>
<div
class=
"center-column"
>
<div
class=
"chat-window"
id=
"chat-window"
></div>
<div
class=
"chat-input-container"
>
<select
class=
"platform-selector"
id=
"platform-selector"
>
<option
value=
"all"
>
All
</option>
<option
value=
"C4.sexhackme"
>
C4.sexhackme
</option>
<option
value=
"SC.spora"
>
SC.spora
</option>
</select>
<div
class=
"chat-input"
>
<button
class=
"emoji-button"
>
😊
</button>
<textarea
id=
"chat-input"
rows=
"3"
placeholder=
"Type a message..."
></textarea>
<button
id=
"send-message"
>
Send
</button>
<div
class=
"emoji-picker"
id=
"emoji-picker"
>
<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>
<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>
<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>
<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>
<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>
</div>
</div>
</div>
</div>
<div
class=
"right-column"
>
<div
class=
"user-list-header"
>
<span>
Userlist
</span>
<span
id=
"total-users"
>
0
</span>
</div>
<div
class=
"user-list"
id=
"user-list"
></div>
</div>
</div>
<div
class=
"context-menu"
id=
"context-menu"
>
<a
href=
"#"
id=
"context-profile"
>
View Profile
</a>
<button
id=
"context-tokens"
>
Tokens: 0
</button>
<button
id=
"context-ban"
>
Ban User
</button>
<button
id=
"context-kick"
>
Kick User
</button>
<button
id=
"context-private"
>
Private Chat
</button>
</div>
<script>
// Fake data for messages, users, earnings, status, and RTSP URLs
const
fakeMessages
=
[
{
sender
:
"me"
,
content
:
"Hello! 😊"
,
timestamp
:
"2025-06-21 20:00"
},
{
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"
},
{
sender
:
"system"
,
content
:
"system: *** Server maintenance scheduled at 1 AM"
,
type
:
"notify-system"
,
timestamp
:
"2025-06-21 20:03"
},
{
sender
:
"platform@SC.spora"
,
content
:
"platform: *** User joined the room"
,
type
:
"notify-platform"
,
timestamp
:
"2025-06-21 20:04"
},
{
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
addPublicMessage
(
text
,
senderId
=
null
,
senderName
=
null
)
{
const
msg
=
document
.
createElement
(
'div'
);
msg
.
className
=
senderId
===
"me"
?
'message user'
:
'message other'
;
const
usernameSpan
=
document
.
createElement
(
'span'
);
usernameSpan
.
className
=
'msg-username'
;
if
(
!
senderName
)
senderName
=
senderId
?
(
users
.
find
(
u
=>
u
.
id
===
senderId
)?.
name
||
'Someone'
)
:
'Someone'
;
if
(
!
senderId
)
senderId
=
'someone'
;
usernameSpan
.
textContent
=
senderName
;
usernameSpan
.
tabIndex
=
0
;
usernameSpan
.
dataset
.
userid
=
senderId
;
// Important: Use mousedown event to open context menu instantly on right click on usernames in main chat
usernameSpan
.
addEventListener
(
'contextmenu'
,
e
=>
{
e
.
preventDefault
();
const
user
=
users
.
find
(
u
=>
u
.
id
===
senderId
)
||
{
id
:
'someone'
,
name
:
senderName
};
showContextMenu
(
e
,
user
,
'chat'
);
});
usernameSpan
.
addEventListener
(
'keydown'
,
(
e
)
=>
{
if
((
e
.
shiftKey
&&
e
.
key
===
"F10"
)
||
e
.
key
===
"ContextMenu"
)
{
e
.
preventDefault
();
const
user
=
users
.
find
(
u
=>
u
.
id
===
senderId
)
||
{
id
:
'someone'
,
name
:
senderName
};
showContextMenu
(
e
,
user
,
'chat'
);
}
});
msg
.
appendChild
(
usernameSpan
);
const
textNode
=
document
.
createTextNode
(
`:
${
text
}
`
);
msg
.
appendChild
(
textNode
);
chatMessagesElem
.
appendChild
(
msg
);
chatMessagesElem
.
scrollTop
=
chatMessagesElem
.
scrollHeight
;
}
chatForm
.
addEventListener
(
'submit'
,
e
=>
{
e
.
preventDefault
();
const
msg
=
chatInput
.
value
.
trim
();
if
(
!
msg
)
return
;
addPublicMessage
(
msg
,
"me"
,
"You"
);
chatInput
.
value
=
''
;
chatInput
.
focus
();
});
function
openPrivateChat
(
user
)
{
if
(
privateChats
.
has
(
user
.
id
))
{
const
chatPopup
=
privateChats
.
get
(
user
.
id
);
chatPopup
.
element
.
style
.
display
=
'flex'
;
chatPopup
.
input
.
focus
();
return
;
}
const
template
=
document
.
getElementById
(
'privateChatTemplate'
);
const
clone
=
template
.
content
.
cloneNode
(
true
);
const
popup
=
clone
.
querySelector
(
'.private-chat-popup'
);
const
headerUser
=
clone
.
querySelector
(
'.private-chat-user'
);
const
closeBtn
=
clone
.
querySelector
(
'.private-chat-close'
);
const
msgsElem
=
clone
.
querySelector
(
'.private-chat-messages'
);
const
form
=
clone
.
querySelector
(
'form'
);
const
input
=
clone
.
querySelector
(
'input'
);
headerUser
.
textContent
=
user
.
name
;
closeBtn
.
addEventListener
(
'click'
,
()
=>
{
popup
.
style
.
display
=
'none'
;
});
function
addPrivateMessage
(
text
,
sender
=
'other'
)
{
const
msg
=
document
.
createElement
(
'div'
);
msg
.
className
=
`private-message
${
sender
}
`
;
msg
.
textContent
=
text
;
msgsElem
.
appendChild
(
msg
);
msgsElem
.
scrollTop
=
msgsElem
.
scrollHeight
;
}
form
.
addEventListener
(
'submit'
,
e
=>
{
e
.
preventDefault
();
const
message
=
input
.
value
.
trim
();
if
(
!
message
)
return
;
addPrivateMessage
(
message
,
'user'
);
input
.
value
=
''
;
input
.
focus
();
});
popup
.
style
.
display
=
'flex'
;
document
.
body
.
appendChild
(
popup
);
privateChats
.
set
(
user
.
id
,
{
element
:
popup
,
input
:
input
,
addMessage
:
addPrivateMessage
});
input
.
focus
();
}
function
showContextMenu
(
event
,
user
,
type
)
{
contextMenuTargetUser
=
user
;
contextMenuTargetType
=
type
;
const
menuWidth
=
180
;
const
menuHeight
=
150
;
let
x
=
event
.
clientX
;
let
y
=
event
.
clientY
;
if
(
x
+
menuWidth
>
window
.
innerWidth
)
{
x
=
window
.
innerWidth
-
menuWidth
-
8
;
}
if
(
y
+
menuHeight
>
window
.
innerHeight
)
{
y
=
window
.
innerHeight
-
menuHeight
-
8
;
}
contextMenu
.
style
.
left
=
x
+
'px'
;
contextMenu
.
style
.
top
=
y
+
'px'
;
contextMenu
.
classList
.
add
(
'visible'
);
contextMenu
.
setAttribute
(
'aria-hidden'
,
'false'
);
const
firstItem
=
contextMenu
.
querySelector
(
'.context-menu-item'
);
if
(
firstItem
)
firstItem
.
focus
();
document
.
addEventListener
(
'click'
,
outsideClickHandler
);
document
.
addEventListener
(
'keydown'
,
keyboardHandler
);
}
function
hideContextMenu
()
{
contextMenu
.
classList
.
remove
(
'visible'
);
contextMenu
.
setAttribute
(
'aria-hidden'
,
'true'
);
contextMenuTargetUser
=
null
;
contextMenuTargetType
=
null
;
document
.
removeEventListener
(
'click'
,
outsideClickHandler
);
document
.
removeEventListener
(
'keydown'
,
keyboardHandler
);
}
function
outsideClickHandler
(
event
)
{
if
(
!
contextMenu
.
contains
(
event
.
target
))
{
hideContextMenu
();
}
}
function
keyboardHandler
(
event
)
{
if
(
!
contextMenu
.
classList
.
contains
(
'visible'
))
return
;
const
items
=
[...
contextMenu
.
querySelectorAll
(
'.context-menu-item'
)];
const
currentIndex
=
items
.
indexOf
(
document
.
activeElement
);
switch
(
event
.
key
)
{
case
'Escape'
:
hideContextMenu
();
break
;
case
'ArrowDown'
:
event
.
preventDefault
();
const
nextIndex
=
(
currentIndex
+
1
)
%
items
.
length
;
items
[
nextIndex
].
focus
();
break
;
case
'ArrowUp'
:
event
.
preventDefault
();
const
prevIndex
=
(
currentIndex
-
1
+
items
.
length
)
%
items
.
length
;
items
[
prevIndex
].
focus
();
break
;
case
'Enter'
:
event
.
preventDefault
();
if
(
document
.
activeElement
.
classList
.
contains
(
'context-menu-item'
))
{
document
.
activeElement
.
click
();
}
break
;
}
}
contextMenu
.
addEventListener
(
'click'
,
e
=>
{
if
(
!
e
.
target
.
classList
.
contains
(
'context-menu-item'
))
return
;
const
action
=
e
.
target
.
dataset
.
action
;
if
(
!
contextMenuTargetUser
)
return
;
switch
(
action
)
{
case
'openProfile'
:
alert
(
`Open profile for
${
contextMenuTargetUser
.
name
}
(ID:
${
contextMenuTargetUser
.
id
}
)`
);
break
;
case
'privateChat'
:
openPrivateChat
(
contextMenuTargetUser
);
break
;
case
'ban'
:
alert
(
`Ban user
${
contextMenuTargetUser
.
name
}
`
);
break
;
case
'kick'
:
alert
(
`Kick user
${
contextMenuTargetUser
.
name
}
`
);
break
;
default
:
break
;
}
hideContextMenu
();
});
renderUserList
();
addPublicMessage
(
'Welcome to the chat!'
,
'someone'
,
'Someone'
);
</script>
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'
;
statusMenu
.
querySelectorAll
(
'button'
).
forEach
(
btn
=>
{
const
platform
=
btn
.
dataset
.
platform
;
btn
.
textContent
=
`
${
platform
}
:
${
fakeStatus
[
platform
]}
`
;
});
}
// Load panel content
const
panelIframe
=
document
.
getElementById
(
'panel-iframe'
);
panelIframe
.
srcdoc
=
'<p>Control panel content loaded here.</p>'
;
// Chat functionality
const
chatWindow
=
document
.
getElementById
(
'chat-window'
);
const
chatInput
=
document
.
getElementById
(
'chat-input'
);
const
sendButton
=
document
.
getElementById
(
'send-message'
);
const
emojiButton
=
document
.
querySelector
(
'.emoji-button'
);
const
emojiPicker
=
document
.
getElementById
(
'emoji-picker'
);
const
platformSelector
=
document
.
getElementById
(
'platform-selector'
);
function
renderMessages
(
messages
)
{
chatWindow
.
innerHTML
=
''
;
messages
.
forEach
(
msg
=>
{
const
div
=
document
.
createElement
(
'div'
);
div
.
className
=
`message
${
msg
.
sender
===
'me'
?
'me'
:
msg
.
type
===
'notify-system'
?
'notify-system'
:
msg
.
type
===
'notify-platform'
?
'notify-platform'
:
msg
.
type
===
'tip'
?
'tip'
:
'other'
}
`
;
div
.
dataset
.
sender
=
msg
.
sender
;
let
content
=
msg
.
content
;
if
(
!
msg
.
type
?.
startsWith
(
'notify'
)
&&
msg
.
type
!==
'tip'
&&
!
msg
.
content
.
includes
(
'<img'
))
{
content
=
msg
.
content
.
replace
(
/
\n
/g
,
'<br>'
)
.
replace
(
/
(
https
?
:
\/\/[^\s]
+
)
/g
,
'<a href="$1" target="_blank">$1</a>'
);
}
if
(
msg
.
sender
===
'me'
)
{
const
senderSpan
=
document
.
createElement
(
'span'
);
senderSpan
.
className
=
'sender'
;
senderSpan
.
textContent
=
'me:'
;
senderSpan
.
style
.
color
=
'#93c5fd'
;
div
.
appendChild
(
senderSpan
);
div
.
appendChild
(
document
.
createTextNode
(
' '
));
}
else
if
(
msg
.
sender
!==
'me'
&&
!
msg
.
type
?.
startsWith
(
'notify'
)
&&
msg
.
type
!==
'tip'
)
{
const
senderSpan
=
document
.
createElement
(
'span'
);
senderSpan
.
className
=
'sender'
;
senderSpan
.
textContent
=
msg
.
sender
;
senderSpan
.
dataset
.
sender
=
msg
.
sender
;
senderSpan
.
addEventListener
(
'contextmenu'
,
(
e
)
=>
{
e
.
preventDefault
();
showContextMenu
(
e
,
msg
.
sender
);
});
div
.
appendChild
(
senderSpan
);
div
.
appendChild
(
document
.
createTextNode
(
': '
));
}
const
contentSpan
=
document
.
createElement
(
'span'
);
contentSpan
.
className
=
'content'
;
contentSpan
.
innerHTML
=
content
;
div
.
appendChild
(
contentSpan
);
const
senderSpan
=
div
.
querySelector
(
'.sender'
);
if
(
senderSpan
&&
msg
.
sender
!==
'me'
&&
!
msg
.
type
?.
startsWith
(
'notify'
))
{
senderSpan
.
addEventListener
(
'contextmenu'
,
(
e
)
=>
{
e
.
preventDefault
();
showContextMenu
(
e
,
senderSpan
.
dataset
.
sender
);
});
}
chatWindow
.
appendChild
(
div
);
});
chatWindow
.
scrollTop
=
chatWindow
.
scrollHeight
;
}
// Username autocompletion
let
completionState
=
{
index
:
-
1
,
prefix
:
''
,
original
:
''
,
matches
:
[]
};
function
getUsernames
()
{
const
users
=
new
Set
();
for
(
const
platform
in
fakeUsers
)
{
fakeUsers
[
platform
].
forEach
(
user
=>
{
users
.
add
(
`
${
user
.
username
}
@
${
platform
}
`
);
});
}
fakeMessages
.
forEach
(
msg
=>
{
if
(
msg
.
sender
!==
'me'
&&
!
msg
.
sender
.
startsWith
(
'system'
)
&&
!
msg
.
sender
.
startsWith
(
'platform@'
))
{
users
.
add
(
msg
.
sender
);
}
});
return
Array
.
from
(
users
);
}
chatInput
.
addEventListener
(
'keydown'
,
(
e
)
=>
{
if
(
e
.
key
===
'Tab'
)
{
e
.
preventDefault
();
const
text
=
chatInput
.
value
;
const
cursorPos
=
chatInput
.
selectionStart
;
const
textBeforeCursor
=
text
.
slice
(
0
,
cursorPos
);
const
lastWordMatch
=
textBeforeCursor
.
match
(
/
(\S
+
)
$/
);
if
(
lastWordMatch
)
{
const
prefix
=
lastWordMatch
[
1
];
const
usernames
=
getUsernames
();
let
matches
=
usernames
.
filter
(
u
=>
u
.
toLowerCase
().
startsWith
(
prefix
.
toLowerCase
()));
if
(
matches
.
length
===
0
)
return
;
if
(
completionState
.
prefix
!==
prefix
)
{
completionState
=
{
index
:
-
1
,
prefix
,
original
:
prefix
,
matches
};
}
completionState
.
index
=
(
completionState
.
index
+
1
)
%
matches
.
length
;
const
selectedUsername
=
matches
[
completionState
.
index
];
const
isFirstWord
=
textBeforeCursor
.
trim
().
length
===
prefix
.
length
&&
textBeforeCursor
.
match
(
/^
\s
*/
)[
0
].
length
+
prefix
.
length
===
textBeforeCursor
.
length
;
const
replacement
=
isFirstWord
?
`
${
selectedUsername
}
: `
:
selectedUsername
;
const
newText
=
text
.
slice
(
0
,
cursorPos
-
prefix
.
length
)
+
replacement
+
text
.
slice
(
cursorPos
);
chatInput
.
value
=
newText
;
chatInput
.
selectionStart
=
chatInput
.
selectionEnd
=
cursorPos
-
prefix
.
length
+
replacement
.
length
;
}
}
else
if
(
e
.
key
===
'Enter'
)
{
if
(
e
.
shiftKey
||
e
.
ctrlKey
)
{
e
.
preventDefault
();
chatInput
.
value
+=
'
\
n'
;
}
else
{
e
.
preventDefault
();
sendMessage
();
}
}
else
{
completionState
=
{
index
:
-
1
,
prefix
:
''
,
original
:
''
,
matches
:
[]
};
}
});
sendButton
.
addEventListener
(
'click'
,
sendMessage
);
function
sendMessage
()
{
const
content
=
chatInput
.
value
.
trim
();
const
platform
=
platformSelector
.
value
;
if
(
content
)
{
console
.
log
(
`Sending message to
${
platform
}
:
${
content
}
`
);
fakeMessages
.
push
({
sender
:
'me'
,
content
,
timestamp
:
new
Date
().
toISOString
()
});
renderMessages
(
fakeMessages
);
chatInput
.
value
=
''
;
completionState
=
{
index
:
-
1
,
prefix
:
''
,
original
:
''
,
matches
:
[]
};
}
}
// Emoji picker
emojiButton
.
addEventListener
(
'mouseover'
,
()
=>
{
emojiPicker
.
style
.
display
=
'flex'
;
emojiPicker
.
style
.
left
=
`
${
emojiButton
.
offsetLeft
}
px`
;
emojiPicker
.
style
.
top
=
`
${
emojiButton
.
offsetTop
-
emojiPicker
.
offsetHeight
}
px`
;
});
emojiPicker
.
addEventListener
(
'mouseleave'
,
()
=>
{
emojiPicker
.
style
.
display
=
'none'
;
});
emojiPicker
.
querySelectorAll
(
'span'
).
forEach
(
span
=>
{
span
.
addEventListener
(
'click'
,
()
=>
{
chatInput
.
value
+=
span
.
dataset
.
emoji
;
emojiPicker
.
style
.
display
=
'none'
;
chatInput
.
focus
();
});
});
// Context menu
const
contextMenu
=
document
.
getElementById
(
'context-menu'
);
function
showContextMenu
(
e
,
sender
)
{
e
.
preventDefault
();
const
parts
=
sender
.
split
(
'@'
);
if
(
parts
.
length
!==
2
)
{
console
.
error
(
`Invalid sender format:
${
sender
}
`
);
return
;
}
const
[
username
,
platformAccount
]
=
parts
;
const
[
platform
,
account
]
=
platformAccount
.
split
(
'.'
);
if
(
!
platform
||
!
account
)
{
console
.
error
(
`Invalid platform.account format:
${
platformAccount
}
`
);
return
;
}
const
userList
=
fakeUsers
[
`
${
platform
}
.
${
account
}
`
];
if
(
!
userList
)
{
console
.
error
(
`Platform not found in fakeUsers:
${
platform
}
.
${
account
}
`
);
return
;
}
const
user
=
userList
.
find
(
u
=>
u
.
username
===
username
);
if
(
!
user
)
{
console
.
error
(
`User not found:
${
username
}
in
${
platform
}
.
${
account
}
`
);
return
;
}
contextMenu
.
querySelector
(
'#context-profile'
).
href
=
`https://
${
platform
}
.com/profile/
${
username
}
`
;
contextMenu
.
querySelector
(
'#context-tokens'
).
textContent
=
`Tokens:
${
user
.
tokens
}
`
;
contextMenu
.
querySelector
(
'#context-ban'
).
onclick
=
()
=>
alert
(
`Ban
${
sender
}
`
);
contextMenu
.
querySelector
(
'#context-kick'
).
onclick
=
()
=>
alert
(
`Kick
${
sender
}
`
);
contextMenu
.
querySelector
(
'#context-private'
).
onclick
=
()
=>
alert
(
`Open private chat with
${
sender
}
`
);
contextMenu
.
style
.
display
=
'block'
;
contextMenu
.
style
.
left
=
`
${
e
.
pageX
}
px`
;
contextMenu
.
style
.
top
=
`
${
e
.
pageY
}
px`
;
}
document
.
addEventListener
(
'click'
,
()
=>
{
contextMenu
.
style
.
display
=
'none'
;
statusMenu
.
style
.
display
=
'none'
;
});
// User list
function
renderUserList
()
{
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>
</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