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
Show 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>
<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>
@import
url('https://fonts.googleapis.com/css2?family=Poppins&display=swap')
;
/* Reset and base */
/* Reset and base styles */
*
{
margin
:
0
;
padding
:
0
;
box-sizing
:
border-box
;
font-family
:
'Inter'
,
Arial
,
sans-serif
;
}
body
{
margin
:
0
;
font-family
:
'Poppins'
,
sans-serif
;
background
:
#121212
;
color
:
#e0e0e0
;
background
:
#0a1122
;
color
:
#ffffff
;
display
:
flex
;
flex-direction
:
column
;
min-height
:
100vh
;
user-select
:
none
;
-webkit-font-smoothing
:
antialiased
;
-moz-osx-font-smoothing
:
grayscale
;
overflow
:
hidden
;
}
/* Container Layout */
/* 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
:
grid
;
grid-template-columns
:
1
fr
280px
;
grid-template-rows
:
1
fr
;
gap
:
0
;
display
:
flex
;
height
:
calc
(
100vh
-
60px
);
width
:
100%
;
height
:
100vh
;
background
:
#1f1f1f
;
overflow
:
hidden
;
}
/* User List Panel */
.user-list
{
background
:
#20232a
;
border-left
:
1px
solid
#2e2e3a
;
.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
;
user-select
:
none
;
min-width
:
280px
;
overflow-x
:
hidden
;
overflow-y
:
auto
;
position
:
relative
;
}
.user-list-header
{
padding
:
16px
;
font-weight
:
700
;
font-size
:
1.2rem
;
border-bottom
:
1px
solid
#2e2e3a
;
flex-shrink
:
0
;
.resize-handle
{
position
:
absolute
;
right
:
-2px
;
top
:
0
;
width
:
4px
;
height
:
100%
;
background
:
#111111
;
cursor
:
col-resize
;
z-index
:
10
;
}
.users
{
flex-grow
:
1
;
.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
;
padding
:
12px
8px
;
-webkit-overflow-scrolling
:
touch
;
}
.user-item
{
padding
:
10px
12px
;
margin-bottom
:
8px
;
/* Video window */
.video-container
{
position
:
relative
;
background
:
#000
;
border-radius
:
8px
;
cursor
:
pointer
;
display
:
flex
;
align-items
:
center
;
gap
:
12px
;
transition
:
background-color
0.3s
ease
;
user-select
:
none
;
overflow
:
hidden
;
flex-shrink
:
0
;
width
:
100%
;
}
.user-item
:hover
,
.user-item
:focus
{
background-color
:
#393d4d
;
outline
:
none
;
video
{
width
:
100%
;
height
:
auto
;
aspect-ratio
:
16
/
9
;
display
:
block
;
}
.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
;
.video-controls
{
position
:
absolute
;
top
:
10px
;
left
:
10px
;
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
user-select
:
none
;
gap
:
10px
;
opacity
:
0
;
transition
:
opacity
0.3s
;
}
.user-name
{
flex-grow
:
1
;
font-size
:
0.95rem
;
color
:
#dcdde1
;
white-space
:
nowrap
;
overflow
:
hidden
;
text-overflow
:
ellipsis
;
user-select
:
text
;
.video-container
:hover
.video-controls
{
opacity
:
1
;
}
.user-status
{
width
:
10
px
;
height
:
10px
;
border-radius
:
50%
;
background-color
:
#44bd32
;
box-shadow
:
0
0
4px
#44bd32
aa
;
flex-shrink
:
0
;
.video-controls
select
,
.video-controls
button
{
padding
:
5
px
;
border
:
none
;
border-radius
:
20px
;
background
:
#3b82f6
;
color
:
#fff
;
cursor
:
pointer
;
}
/* Main Chat Panel */
.chat-panel
{
display
:
flex
;
flex-direction
:
column
;
background
:
#181a20
;
user-select
:
none
;
min-width
:
0
;
.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
;
}
.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
;
/* 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
;
}
.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
;
/* Tabs */
.tabs
{
display
:
flex
;
border-bottom
:
2px
solid
#111111
;
margin-top
:
10px
;
flex-shrink
:
0
;
overflow-x
:
hidden
;
}
.chat-messages
::-webkit-scrollbar
{
width
:
8px
;
.tab
{
padding
:
8px
12px
;
cursor
:
pointer
;
background
:
#2a2a2a
;
border-radius
:
8px
8px
0
0
;
margin-right
:
5px
;
font-size
:
14px
;
white-space
:
nowrap
;
}
.chat-messages
::-webkit-scrollbar-thumb
{
background-color
:
#394264
;
border-radius
:
4px
;
.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
{
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
;
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.user
{
background
:
linear-gradient
(
135deg
,
#0984e3
,
#6c5ce7
);
color
:
white
;
align-self
:
flex-end
;
border-bottom-right-radius
:
2px
;
justify-content
:
flex-end
;
.message.me
.content
{
color
:
#f0f9ff
;
}
.message.other
{
background
:
#2d2f3a
;
color
:
#ddd
;
align-self
:
flex-start
;
border-bottom-left-radius
:
2px
;
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
;
}
.msg-username
{
font-weight
:
700
;
.message.tip
.content
{
color
:
#d1fae5
;
}
.message
.sender
{
color
:
#60a5fa
;
font-weight
:
600
;
cursor
:
pointer
;
user-select
:
text
;
color
:
#a2d2ff
;
}
.message
.content
{
color
:
#a3bffa
;
}
.message
img
{
max-width
:
100px
;
border-radius
:
8px
;
}
.message
a
{
color
:
#00b7ff
;
text-decoration
:
underline
;
}
.chat-input-container
{
display
:
flex
;
padding
:
12px
24px
;
border-top
:
1px
solid
#2e2e3a
;
background
:
#20232a
;
align-items
:
center
;
user-select
:
none
;
flex-shrink
:
0
;
gap
:
10px
;
align-items
:
stretch
;
}
.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
;
display
:
flex
;
gap
:
10px
;
position
:
relative
;
flex
:
1
;
}
.chat-input
::placeholder
{
color
:
#888
;
.platform-selector
{
height
:
40px
;
padding
:
8px
;
border
:
none
;
border-radius
:
20px
;
background
:
#3b82f6
;
color
:
#fff
;
cursor
:
pointer
;
font-size
:
14px
;
width
:
120px
;
}
.chat-input
:focus
{
background-color
:
#424559
;
.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
;
}
.send-btn
{
margin-left
:
12px
;
background
:
linear-gradient
(
135deg
,
#6c5ce7
,
#0984e3
);
.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
;
padding
:
10px
18px
;
color
:
white
;
font-weight
:
700
;
font-size
:
1rem
;
border-radius
:
24px
;
border-radius
:
20px
;
color
:
#fff
;
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
;
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
;
align-items
:
center
;
flex-shrink
:
0
;
cursor
:
move
;
padding
:
8px
;
background
:
#2a2a2a
;
border-bottom
:
2px
solid
#111111
;
font-size
:
16px
;
font-weight
:
600
;
}
.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
;
.user-list
.platform
{
margin
:
10px
0
;
}
.private-chat-close
:hover
{
color
:
#ffd32a
;
.platform-header
{
cursor
:
pointer
;
padding
:
8px
;
background
:
#1e293b
;
border-radius
:
8px
;
display
:
flex
;
justify-content
:
space-between
;
font-weight
:
500
;
}
.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
{
.user
{
padding
:
8px
10px
;
display
:
flex
;
padding
:
10px
14px
;
border-top
:
1px
solid
#44475a
;
background
:
#2b2e42
;
justify-content
:
space-between
;
align-items
:
center
;
flex-shrink
:
0
;
font-size
:
14px
;
}
.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
;
.status-dot
{
width
:
10px
;
height
:
10px
;
border-radius
:
50%
;
display
:
inline-block
;
margin-right
:
5px
;
}
.status-dot.online
{
background
:
#28a745
;
}
.private-chat-input
::placeholder
{
color
:
#bbb
;
.status-dot.offline
{
background
:
#dc3545
;
}
.private-chat-input
:focu
s
{
background-color
:
#56577b
;
.user
s
{
display
:
none
;
}
.private-send-btn
{
margin-left
:
10px
;
background
:
linear-gradient
(
135deg
,
#6c5ce7
,
#0984e3
);
.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
;
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
;
width
:
100%
;
text-align
:
left
;
border-radius
:
20px
;
font-size
:
14px
;
}
.private-send-bt
n
:hover
{
background
:
linear-gradient
(
135deg
,
#5353c3
,
#0761c7
)
;
.context-menu
a
:hover
,
.context-menu
butto
n
:hover
{
background
:
#3b82f6
;
}
@keyframes
fadeInUp
{
0
%
{
opacity
:
0
;
transform
:
translateY
(
20px
);
/* Earnings table */
.earnings-table
{
width
:
100%
;
max-width
:
100%
;
margin
:
0
;
border-collapse
:
collapse
;
font-size
:
12px
;
}
100
%
{
opacity
:
1
;
transform
:
translateY
(
0
);
.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
;
}
/* 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
;
.earnings-table
th
{
background
:
#3b82f6
;
font-weight
:
600
;
}
.context-menu.visible
{
display
:
block
;
.earnings-table
.totals-row
{
background
:
#4b5563
;
font-weight
:
600
;
}
#earnings
{
overflow-x
:
auto
;
margin-bottom
:
10px
;
}
.context-menu-item
{
padding
:
10px
16px
;
font-size
:
0.9rem
;
color
:
#e0e0e0
;
.reset-session-button
{
width
:
100%
;
padding
:
8px
;
background
:
#dc3545
;
border
:
none
;
border-radius
:
20px
;
color
:
#fff
;
cursor
:
pointer
;
transition
:
background-color
0.25s
ease
;
font-size
:
12px
;
text-align
:
center
;
}
.context-menu-item
:hover
,
.context-menu-item
:focus
{
background
:
#5353c3
;
color
:
white
;
outline
:
none
;
.reset-session-button
:hover
{
background
:
#b91c1c
;
}
/* Responsive
*/
/* Mobile layout
*/
@media
(
max-width
:
768px
)
{
body
{
display
:
block
;
.container
{
flex-direction
:
column
;
height
:
auto
;
}
.container
{
.left-column
,
.center-column
,
.right-column
{
width
:
100%
;
border
:
none
;
min-width
:
0
;
height
:
auto
;
display
:
flex
;
flex-direction
:
column
;
display
:
block
;
}
.chat-panel
{
order
:
2
;
height
:
70vh
;
min-width
:
100%
;
.left-column
{
order
:
-1
;
}
.user-list
{
order
:
1
;
height
:
30vh
;
min-width
:
100%
;
border-left
:
none
!important
;
border-top
:
1px
solid
#2e2e3a
;
.resize-handle
{
display
:
none
;
}
.chat-messages
{
height
:
100%
;
min-height
:
0
;
.chat-window
{
height
:
50vh
;
}
.chat-input-container
{
padding
:
8px
16px
;
.earnings-table
{
width
:
100%
;
}
.chat-inpu
t
{
font-size
:
0.9rem
;
padding
:
8px
12px
;
.tab-conten
t
{
max-height
:
none
;
display
:
block
;
}
.send-btn
{
padding
:
8px
12px
;
font-size
:
0.9rem
;
video
{
height
:
200px
;
}
.user-list-header
,
.chat-header
{
padding
:
12px
16px
;
font-size
:
1.1rem
;
.top-bar
{
padding
:
0
10px
;
}
.user-item
{
padding
:
8px
1
0px
;
.top-bar
img
{
height
:
3
0px
;
}
.user-avatar
{
width
:
28px
;
height
:
28px
;
font-size
:
0.9rem
;
.top-bar
span
{
font-size
:
16px
;
}
.user-name
{
font-size
:
0.9rem
;
.chat-input-container
{
flex-direction
:
column
;
align-items
:
stretch
;
}
.private-chat-popup
{
width
:
90vw
;
max-height
:
50vh
;
bottom
:
8px
;
right
:
5vw
;
font-size
:
0.85rem
;
.platform-selector
{
width
:
100%
;
margin-bottom
:
5px
;
}
.private-chat-messages
{
max-height
:
200px
;
.chat-input
{
width
:
100%
;
}
}
</style>
</head>
<body>
<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"
>
<!-- 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
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>
<!-- 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'
}
// 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"
}
];
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
;
// Initialize video
const
video
=
document
.
getElementById
(
'video'
);
const
videoSource
=
document
.
getElementById
(
'video-source'
);
const
mirrorVideo
=
document
.
getElementById
(
'mirror-video'
);
let
isMirrored
=
false
;
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'
);
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
);
});
userListElem
.
appendChild
(
userItem
);
}
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
));
}
function
addPublicMessage
(
text
,
senderId
=
null
,
senderName
=
null
)
{
const
msg
=
document
.
createElement
(
'div'
);
msg
.
className
=
senderId
===
"me"
?
'message user'
:
'message other'
;
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
));
}
});
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'
;
mirrorVideo
.
addEventListener
(
'click'
,
()
=>
{
isMirrored
=
!
isMirrored
;
video
.
style
.
transform
=
isMirrored
?
'scaleX(-1)'
:
'scaleX(1)
'
;
})
;
usernameSpan
.
textContent
=
senderName
;
usernameSpan
.
tabIndex
=
0
;
usernameSpan
.
dataset
.
userid
=
senderId
;
// Resize handle
const
leftColumn
=
document
.
querySelector
(
'.left-column'
);
const
resizeHandle
=
document
.
querySelector
(
'.resize-handle'
);
let
isResizing
=
false
;
// Important: Use mousedown event to open context menu instantly on right click on usernames in main chat
usernameSpan
.
addEventListener
(
'contextmenu'
,
e
=>
{
resizeHandle
.
addEventListener
(
'mousedown'
,
(
e
)
=>
{
isResizing
=
true
;
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'
);
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`
;
}
}
});
msg
.
appendChild
(
usernameSpan
);
const
textNode
=
document
.
createTextNode
(
`:
${
text
}
`
);
msg
.
appendChild
(
textNode
);
chatMessagesElem
.
appendChild
(
msg
);
chatMessagesElem
.
scrollTop
=
chatMessagesElem
.
scrollHeight
;
}
document
.
addEventListener
(
'mouseup'
,
()
=>
{
isResizing
=
false
;
});
chatForm
.
addEventListener
(
'submit'
,
e
=>
{
e
.
preventDefault
();
const
msg
=
chatInput
.
value
.
trim
();
if
(
!
msg
)
return
;
addPublicMessage
(
msg
,
"me"
,
"You"
);
chatInput
.
value
=
''
;
chatInput
.
focus
();
// 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'
;
});
});
function
openPrivateChat
(
user
)
{
if
(
privateChats
.
has
(
user
.
id
))
{
const
chatPopup
=
privateChats
.
get
(
user
.
id
);
chatPopup
.
element
.
style
.
display
=
'flex'
;
chatPopup
.
input
.
focus
();
return
;
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
]}
`
;
});
}
const
template
=
document
.
getElementById
(
'privateChatTemplate'
);
const
clone
=
template
.
content
.
cloneNode
(
true
);
// Load panel content
const
panelIframe
=
document
.
getElementById
(
'panel-iframe'
);
panelIframe
.
srcdoc
=
'<p>Control panel content loaded here.</p>'
;
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'
);
// 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'
);
headerUser
.
textContent
=
user
.
name
;
closeBtn
.
addEventListener
(
'click'
,
()
=>
{
popup
.
style
.
display
=
'none'
;
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
);
});
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
=>
{
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
();
const
message
=
input
.
value
.
trim
();
if
(
!
message
)
return
;
addPrivateMessage
(
message
,
'user'
);
input
.
value
=
''
;
input
.
focus
();
showContextMenu
(
e
,
senderSpan
.
dataset
.
sender
);
});
popup
.
style
.
display
=
'flex'
;
document
.
body
.
appendChild
(
popup
);
privateChats
.
set
(
user
.
id
,
{
element
:
popup
,
input
:
input
,
addMessage
:
addPrivateMessage
}
chatWindow
.
appendChild
(
div
);
});
input
.
focus
();
chatWindow
.
scrollTop
=
chatWindow
.
scrollHeight
;
}
function
showContextMenu
(
event
,
user
,
type
)
{
contextMenuTargetUser
=
user
;
contextMenuTargetType
=
type
;
// Username autocompletion
let
completionState
=
{
index
:
-
1
,
prefix
:
''
,
original
:
''
,
matches
:
[]
};
const
menuWidth
=
180
;
const
menuHeight
=
150
;
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
);
}
let
x
=
event
.
clientX
;
let
y
=
event
.
clientY
;
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
(
x
+
menuWidth
>
window
.
innerWidth
)
{
x
=
window
.
innerWidth
-
menuWidth
-
8
;
}
if
(
y
+
menuHeight
>
window
.
innerHeight
)
{
y
=
window
.
innerHeight
-
menuHeight
-
8
;
if
(
completionState
.
prefix
!==
prefix
)
{
completionState
=
{
index
:
-
1
,
prefix
,
original
:
prefix
,
matches
};
}
contextMenu
.
style
.
left
=
x
+
'px'
;
contextMenu
.
style
.
top
=
y
+
'px'
;
contextMenu
.
classList
.
add
(
'visible'
);
contextMenu
.
setAttribute
(
'aria-hidden'
,
'false'
);
completionState
.
index
=
(
completionState
.
index
+
1
)
%
matches
.
length
;
const
selectedUsername
=
matches
[
completionState
.
index
];
const
firstItem
=
contextMenu
.
querySelector
(
'.context-menu-item'
)
;
if
(
firstItem
)
firstItem
.
focus
()
;
const
isFirstWord
=
textBeforeCursor
.
trim
().
length
===
prefix
.
length
&&
textBeforeCursor
.
match
(
/^
\s
*/
)[
0
].
length
+
prefix
.
length
===
textBeforeCursor
.
length
;
const
replacement
=
isFirstWord
?
`
${
selectedUsername
}
: `
:
selectedUsername
;
document
.
addEventListener
(
'click'
,
outsideClickHandler
);
document
.
addEventListener
(
'keydown'
,
keyboardHandler
);
const
newText
=
text
.
slice
(
0
,
cursorPos
-
prefix
.
length
)
+
replacement
+
text
.
slice
(
cursorPos
);
chatInput
.
value
=
newText
;
chatInput
.
selectionStart
=
chatInput
.
selectionEnd
=
cursorPos
-
prefix
.
length
+
replacement
.
length
;
}
function
hideContextMenu
()
{
contextMenu
.
classList
.
remove
(
'visible'
);
contextMenu
.
setAttribute
(
'aria-hidden'
,
'true'
);
contextMenuTargetUser
=
null
;
contextMenuTargetType
=
null
;
document
.
removeEventListener
(
'click'
,
outsideClickHandler
);
document
.
removeEventListener
(
'keydown'
,
keyboardHandler
);
}
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
:
[]
};
}
});
function
outsideClickHandler
(
event
)
{
if
(
!
contextMenu
.
contains
(
event
.
target
))
{
hideContextMenu
();
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
:
[]
};
}
}
function
keyboardHandler
(
event
)
{
if
(
!
contextMenu
.
classList
.
contains
(
'visible'
))
return
;
const
items
=
[...
contextMenu
.
querySelectorAll
(
'.context-menu-item'
)];
const
currentIndex
=
items
.
indexOf
(
document
.
activeElement
);
// 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
();
});
});
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
();
// 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
;
}
break
;
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`
;
}
contextMenu
.
addEventListener
(
'click'
,
e
=>
{
if
(
!
e
.
target
.
classList
.
contains
(
'context-menu-item'
))
return
;
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'
);
});
});
}
const
action
=
e
.
target
.
dataset
.
action
;
if
(
!
contextMenuTargetUser
)
return
;
// 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'
);
});
});
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
;
// 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
);
});
}
hideContextMenu
();
// Reset session button
document
.
getElementById
(
'reset-session'
).
addEventListener
(
'click'
,
()
=>
{
fakeEarnings
.
forEach
(
e
=>
e
.
sess
=
0
);
renderEarnings
();
});
// Initialize
populateVideoSources
();
renderMessages
(
fakeMessages
);
renderUserList
();
addPublicMessage
(
'Welcome to the chat!'
,
'someone'
,
'Someone'
);
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