Progress on Chat Server
* Reworked full screen CSS layout for the chat.html, still using Bulma components with some custom CSS Grid. * Duplicate username handling: server can push a new username to change the client's selection. * Who List sync between clients. * Local video casting works so far - plays back your camera in the local feed. Your video broadcasting boolean is synced to backend, which lights up the video button in the Who List.ipad-testing
parent
b627fe0ffa
commit
4f93c27651
|
@ -6,6 +6,7 @@ import (
|
|||
"time"
|
||||
|
||||
barertc "git.kirsle.net/apps/barertc/pkg"
|
||||
"git.kirsle.net/apps/barertc/pkg/log"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
@ -22,6 +23,10 @@ func main() {
|
|||
flag.StringVar(&address, "address", ":9000", "Address to listen on, like localhost:5000 or :8080")
|
||||
flag.Parse()
|
||||
|
||||
if debug {
|
||||
log.SetDebug(true)
|
||||
}
|
||||
|
||||
app := barertc.NewServer()
|
||||
app.Setup()
|
||||
panic(app.ListenAndServe(address))
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
package barertc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"git.kirsle.net/apps/barertc/pkg/log"
|
||||
)
|
||||
|
||||
// OnLogin handles "login" actions from the client.
|
||||
func (s *Server) OnLogin(sub *Subscriber, msg Message) {
|
||||
// Ensure the username is unique, or rename it.
|
||||
var duplicate bool
|
||||
for other := range s.IterSubscribers() {
|
||||
if other.ID != sub.ID && other.Username == msg.Username {
|
||||
duplicate = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if duplicate {
|
||||
// Give them one that is unique.
|
||||
msg.Username = fmt.Sprintf("%s %d",
|
||||
msg.Username,
|
||||
time.Now().Nanosecond(),
|
||||
)
|
||||
}
|
||||
|
||||
// Use their username.
|
||||
sub.Username = msg.Username
|
||||
log.Debug("OnLogin: %s joins the room", sub.Username)
|
||||
|
||||
// Tell everyone they joined.
|
||||
s.Broadcast(Message{
|
||||
Action: ActionPresence,
|
||||
Username: msg.Username,
|
||||
Message: "has joined the room!",
|
||||
})
|
||||
|
||||
// Send the user back their settings.
|
||||
sub.SendMe()
|
||||
|
||||
// Send the WhoList to everybody.
|
||||
s.SendWhoList()
|
||||
}
|
||||
|
||||
// OnMessage handles a chat message posted by the user.
|
||||
func (s *Server) OnMessage(sub *Subscriber, msg Message) {
|
||||
log.Info("[%s] %s", sub.Username, msg.Message)
|
||||
if sub.Username == "" {
|
||||
sub.SendJSON(Message{
|
||||
Action: ActionMessage,
|
||||
Username: "ChatServer",
|
||||
Message: "You must log in first.",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Broadcast a chat message to the room.
|
||||
s.Broadcast(Message{
|
||||
Action: ActionMessage,
|
||||
Username: sub.Username,
|
||||
Message: msg.Message,
|
||||
})
|
||||
}
|
||||
|
||||
// OnMe handles current user state updates.
|
||||
func (s *Server) OnMe(sub *Subscriber, msg Message) {
|
||||
if msg.VideoActive {
|
||||
log.Debug("User %s turns on their video feed", sub.Username)
|
||||
}
|
||||
|
||||
sub.VideoActive = msg.VideoActive
|
||||
|
||||
// Sync the WhoList to everybody.
|
||||
s.SendWhoList()
|
||||
}
|
|
@ -4,9 +4,29 @@ type Message struct {
|
|||
Action string `json:"action,omitempty"`
|
||||
Username string `json:"username"`
|
||||
Message string `json:"message"`
|
||||
|
||||
// WhoList for `who` actions
|
||||
WhoList []WhoList `json:"whoList"`
|
||||
|
||||
// Sent on `me` actions along with Username
|
||||
VideoActive bool `json:"videoActive"` // user tells us their cam status
|
||||
}
|
||||
|
||||
const (
|
||||
ActionLogin = "login" // post the username to backend
|
||||
// Actions sent by the client side only
|
||||
ActionLogin = "login" // post the username to backend
|
||||
|
||||
// Actions sent by server or client
|
||||
ActionMessage = "message" // post a message to the room
|
||||
ActionMe = "me" // user self-info sent by FE or BE
|
||||
|
||||
// Actions sent by server only
|
||||
ActionWhoList = "who" // server pushes the Who List
|
||||
ActionPresence = "presence" // a user joined or left the room
|
||||
)
|
||||
|
||||
// WhoList is a member entry in the chat room.
|
||||
type WhoList struct {
|
||||
Username string `json:"username"`
|
||||
VideoActive bool `json:"videoActive"`
|
||||
}
|
||||
|
|
117
pkg/websocket.go
117
pkg/websocket.go
|
@ -13,11 +13,14 @@ import (
|
|||
|
||||
// Subscriber represents a connected WebSocket session.
|
||||
type Subscriber struct {
|
||||
Username string
|
||||
conn *websocket.Conn
|
||||
ctx context.Context
|
||||
messages chan []byte
|
||||
closeSlow func()
|
||||
// User properties
|
||||
ID int // ID assigned by server
|
||||
Username string
|
||||
VideoActive bool
|
||||
conn *websocket.Conn
|
||||
ctx context.Context
|
||||
messages chan []byte
|
||||
closeSlow func()
|
||||
}
|
||||
|
||||
// ReadLoop spawns a goroutine that reads from the websocket connection.
|
||||
|
@ -27,6 +30,13 @@ func (sub *Subscriber) ReadLoop(s *Server) {
|
|||
msgType, data, err := sub.conn.Read(sub.ctx)
|
||||
if err != nil {
|
||||
log.Error("ReadLoop error: %+v", err)
|
||||
s.DeleteSubscriber(sub)
|
||||
s.Broadcast(Message{
|
||||
Action: ActionPresence,
|
||||
Username: sub.Username,
|
||||
Message: "has exited the room!",
|
||||
})
|
||||
s.SendWhoList()
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -37,6 +47,7 @@ func (sub *Subscriber) ReadLoop(s *Server) {
|
|||
|
||||
// Read the user's posted message.
|
||||
var msg Message
|
||||
log.Debug("Read(%s): %s", sub.Username, data)
|
||||
if err := json.Unmarshal(data, &msg); err != nil {
|
||||
log.Error("Message error: %s", err)
|
||||
continue
|
||||
|
@ -45,28 +56,14 @@ func (sub *Subscriber) ReadLoop(s *Server) {
|
|||
// What action are they performing?
|
||||
switch msg.Action {
|
||||
case ActionLogin:
|
||||
// TODO: ensure unique?
|
||||
sub.Username = msg.Username
|
||||
s.Broadcast(Message{
|
||||
Username: msg.Username,
|
||||
Message: "has joined the room!",
|
||||
})
|
||||
s.OnLogin(sub, msg)
|
||||
case ActionMessage:
|
||||
if sub.Username == "" {
|
||||
sub.SendJSON(Message{
|
||||
Username: "ChatServer",
|
||||
Message: "You must log in first.",
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
// Broadcast a chat message to the room.
|
||||
s.Broadcast(Message{
|
||||
Username: sub.Username,
|
||||
Message: msg.Message,
|
||||
})
|
||||
s.OnMessage(sub, msg)
|
||||
case ActionMe:
|
||||
s.OnMe(sub, msg)
|
||||
default:
|
||||
sub.SendJSON(Message{
|
||||
Action: ActionMessage,
|
||||
Username: "ChatServer",
|
||||
Message: "Unsupported message type.",
|
||||
})
|
||||
|
@ -81,9 +78,19 @@ func (sub *Subscriber) SendJSON(v interface{}) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Debug("SendJSON(%s): %s", sub.Username, data)
|
||||
return sub.conn.Write(sub.ctx, websocket.MessageText, data)
|
||||
}
|
||||
|
||||
// SendMe sends the current user state to the client.
|
||||
func (sub *Subscriber) SendMe() {
|
||||
sub.SendJSON(Message{
|
||||
Action: ActionMe,
|
||||
Username: sub.Username,
|
||||
VideoActive: sub.VideoActive,
|
||||
})
|
||||
}
|
||||
|
||||
// WebSocket handles the /ws websocket connection.
|
||||
func (s *Server) WebSocket() http.HandlerFunc {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
@ -112,7 +119,7 @@ func (s *Server) WebSocket() http.HandlerFunc {
|
|||
}
|
||||
|
||||
s.AddSubscriber(sub)
|
||||
defer s.DeleteSubscriber(sub)
|
||||
// defer s.DeleteSubscriber(sub)
|
||||
|
||||
go sub.ReadLoop(s)
|
||||
for {
|
||||
|
@ -130,8 +137,16 @@ func (s *Server) WebSocket() http.HandlerFunc {
|
|||
})
|
||||
}
|
||||
|
||||
// Auto incrementing Subscriber ID, assigned in AddSubscriber.
|
||||
var SubscriberID int
|
||||
|
||||
// AddSubscriber adds a WebSocket subscriber to the server.
|
||||
func (s *Server) AddSubscriber(sub *Subscriber) {
|
||||
// Assign a unique ID.
|
||||
SubscriberID++
|
||||
sub.ID = SubscriberID
|
||||
log.Debug("AddSubscriber: %s", sub.ID)
|
||||
|
||||
s.subscribersMu.Lock()
|
||||
s.subscribers[sub] = struct{}{}
|
||||
s.subscribersMu.Unlock()
|
||||
|
@ -139,23 +154,73 @@ func (s *Server) AddSubscriber(sub *Subscriber) {
|
|||
|
||||
// DeleteSubscriber removes a subscriber from the server.
|
||||
func (s *Server) DeleteSubscriber(sub *Subscriber) {
|
||||
log.Error("DeleteSubscriber: %s", sub.Username)
|
||||
s.subscribersMu.Lock()
|
||||
delete(s.subscribers, sub)
|
||||
s.subscribersMu.Unlock()
|
||||
}
|
||||
|
||||
// IterSubscribers loops over the subscriber list with a read lock. If the
|
||||
// caller already holds a lock, pass the optional `true` parameter for isLocked.
|
||||
func (s *Server) IterSubscribers(isLocked ...bool) chan *Subscriber {
|
||||
var out = make(chan *Subscriber)
|
||||
go func() {
|
||||
log.Debug("IterSubscribers START..")
|
||||
|
||||
var result = []*Subscriber{}
|
||||
|
||||
// Has the caller already taken the read lock or do we get it?
|
||||
if locked := len(isLocked) > 0 && isLocked[0]; !locked {
|
||||
log.Debug("Taking the lock")
|
||||
s.subscribersMu.RLock()
|
||||
defer s.subscribersMu.RUnlock()
|
||||
}
|
||||
|
||||
for sub := range s.subscribers {
|
||||
result = append(result, sub)
|
||||
}
|
||||
|
||||
for _, r := range result {
|
||||
out <- r
|
||||
}
|
||||
close(out)
|
||||
log.Debug("IterSubscribers STOP!")
|
||||
}()
|
||||
return out
|
||||
}
|
||||
|
||||
// Broadcast a message to the chat room.
|
||||
func (s *Server) Broadcast(msg Message) {
|
||||
log.Debug("Broadcast: %+v", msg)
|
||||
s.subscribersMu.RLock()
|
||||
defer s.subscribersMu.RUnlock()
|
||||
for sub := range s.subscribers {
|
||||
for sub := range s.IterSubscribers(true) {
|
||||
sub.SendJSON(Message{
|
||||
Action: msg.Action,
|
||||
Username: msg.Username,
|
||||
Message: msg.Message,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// SendWhoList broadcasts the connected members to everybody in the room.
|
||||
func (s *Server) SendWhoList() {
|
||||
var users = []WhoList{}
|
||||
for sub := range s.IterSubscribers() {
|
||||
users = append(users, WhoList{
|
||||
Username: sub.Username,
|
||||
VideoActive: sub.VideoActive,
|
||||
})
|
||||
}
|
||||
|
||||
for sub := range s.IterSubscribers() {
|
||||
sub.SendJSON(Message{
|
||||
Action: ActionWhoList,
|
||||
WhoList: users,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func writeTimeout(ctx context.Context, timeout time.Duration, c *websocket.Conn, msg []byte) error {
|
||||
ctx, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
|
|
|
@ -0,0 +1,102 @@
|
|||
html {
|
||||
height: 100vh;
|
||||
}
|
||||
body {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/************************
|
||||
* Main CSS Grid Layout *
|
||||
************************/
|
||||
|
||||
.chat-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(255, 0, 0, 0.2);
|
||||
padding: 10px;
|
||||
|
||||
display: grid;
|
||||
column-gap: 10px;
|
||||
row-gap: 10px;
|
||||
grid-template-columns: 260px 1fr 260px;
|
||||
grid-template-rows: 1fr auto;
|
||||
}
|
||||
|
||||
/* Left column: DMs and channels */
|
||||
.chat-container > .left-column {
|
||||
grid-column: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Main column: chat history */
|
||||
.chat-container > .chat-column {
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
background-color: yellow;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Footer row: message entry box */
|
||||
.chat-container > .chat-footer {
|
||||
grid-column: 1 / 4;
|
||||
grid-row: 2 / 2;
|
||||
}
|
||||
|
||||
/* Right column: Who List */
|
||||
.chat-container > .right-column {
|
||||
grid-column: 3;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/***********************************************
|
||||
* Reusable CSS Grid-based Bulma Card layouts *
|
||||
* with a fixed header, full size scrollable *
|
||||
* content, and (optionally) video-feeds under *
|
||||
* the header (main chat card only) *
|
||||
***********************************************/
|
||||
|
||||
.grid-card {
|
||||
height: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: auto auto 1fr;
|
||||
}
|
||||
|
||||
.grid-card > .card-header {
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
.grid-card > .video-feeds {
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
.grid-card > .card-content {
|
||||
grid-row: 3;
|
||||
/* background-color: magenta; */
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
/*******************
|
||||
* Video Feeds CSS *
|
||||
*******************/
|
||||
|
||||
.video-feeds {
|
||||
background-color: yellow;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
overflow-x: scroll;
|
||||
|
||||
display: flex;
|
||||
align-items: left;
|
||||
}
|
||||
|
||||
.video-feeds > .feed {
|
||||
flex: 10 0 auto;
|
||||
width: 120px;
|
||||
height: 80px;
|
||||
background-color: black;
|
||||
margin: 5px;
|
||||
}
|
|
@ -6,7 +6,8 @@ const app = Vue.createApp({
|
|||
return {
|
||||
busy: false,
|
||||
|
||||
username: "",
|
||||
channel: "lobby",
|
||||
username: "", //"test",
|
||||
message: "",
|
||||
|
||||
// WebSocket connection.
|
||||
|
@ -15,8 +16,20 @@ const app = Vue.createApp({
|
|||
connected: false,
|
||||
},
|
||||
|
||||
// Who List for the room.
|
||||
whoList: [],
|
||||
|
||||
// My video feed.
|
||||
webcam: {
|
||||
busy: false,
|
||||
active: false,
|
||||
elem: null, // <video id="localVideo"> element
|
||||
stream: null, // MediaStream object
|
||||
},
|
||||
|
||||
// Chat history.
|
||||
history: [],
|
||||
historyScrollbox: null,
|
||||
DMs: {},
|
||||
|
||||
loginModal: {
|
||||
|
@ -25,13 +38,15 @@ const app = Vue.createApp({
|
|||
}
|
||||
},
|
||||
mounted() {
|
||||
this.pushHistory({
|
||||
username: "ChatServer",
|
||||
message: "Welcome to BareRTC!",
|
||||
});
|
||||
this.webcam.elem = document.querySelector("#localVideo");
|
||||
this.historyScrollbox = document.querySelector("#chatHistory");
|
||||
|
||||
this.ChatServer("Welcome to BareRTC!")
|
||||
|
||||
if (!this.username) {
|
||||
this.loginModal.visible = true;
|
||||
} else {
|
||||
this.signIn();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
@ -46,10 +61,7 @@ const app = Vue.createApp({
|
|||
}
|
||||
|
||||
if (!this.ws.connected) {
|
||||
this.pushHistory({
|
||||
username: "ChatClient",
|
||||
message: "You are not connected to the server.",
|
||||
});
|
||||
this.ChatClient("You are not connected to the server.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -62,32 +74,49 @@ const app = Vue.createApp({
|
|||
this.message = "";
|
||||
},
|
||||
|
||||
// Sync the current user state (such as video broadcasting status) to
|
||||
// the backend, which will reload everybody's Who List.
|
||||
sendMe() {
|
||||
this.ws.conn.send(JSON.stringify({
|
||||
action: "me",
|
||||
videoActive: this.webcam.active,
|
||||
}));
|
||||
},
|
||||
onMe(msg) {
|
||||
// We have had settings pushed to us by the server, such as a change
|
||||
// in our choice of username.
|
||||
if (this.username != msg.username) {
|
||||
this.ChatServer(`Your username has been changed to ${msg.username}.`);
|
||||
}
|
||||
|
||||
this.ChatClient(`User sync from backend: ${JSON.stringify(msg)}`);
|
||||
},
|
||||
|
||||
// Handle messages sent in chat.
|
||||
onMessage(msg) {
|
||||
this.pushHistory({
|
||||
username: msg.username,
|
||||
message: msg.message,
|
||||
});
|
||||
},
|
||||
|
||||
// Dial the WebSocket connection.
|
||||
dial() {
|
||||
const conn = new WebSocket(`ws://${location.host}/ws`);
|
||||
|
||||
conn.addEventListener("close", ev => {
|
||||
this.ws.connected = false;
|
||||
this.pushHistory({
|
||||
username: "ChatClient",
|
||||
message: `WebSocket Disconnected code: ${ev.code}, reason: ${ev.reason}`,
|
||||
});
|
||||
this.ChatClient(`WebSocket Disconnected code: ${ev.code}, reason: ${ev.reason}`);
|
||||
|
||||
if (ev.code !== 1001) {
|
||||
this.pushHistory({
|
||||
username: "ChatClient",
|
||||
message: "Reconnecting in 1s",
|
||||
});
|
||||
this.ChatClient("Reconnecting in 1s");
|
||||
setTimeout(this.dial, 1000);
|
||||
}
|
||||
});
|
||||
|
||||
conn.addEventListener("open", ev => {
|
||||
this.ws.connected = true;
|
||||
this.pushHistory({
|
||||
username: "ChatClient",
|
||||
message: "Websocket connected!",
|
||||
});
|
||||
this.ChatClient("Websocket connected!");
|
||||
|
||||
// Tell the server our username.
|
||||
this.ws.conn.send(JSON.stringify({
|
||||
|
@ -104,22 +133,101 @@ const app = Vue.createApp({
|
|||
}
|
||||
|
||||
let msg = JSON.parse(ev.data);
|
||||
|
||||
this.pushHistory({
|
||||
username: msg.username,
|
||||
message: msg.message,
|
||||
});
|
||||
switch (msg.action) {
|
||||
case "who":
|
||||
console.log("Got the Who List: %s", msg);
|
||||
this.whoList = msg.whoList;
|
||||
break;
|
||||
case "me":
|
||||
console.log("Got a self-update: %s", msg);
|
||||
this.onMe(msg);
|
||||
break;
|
||||
case "message":
|
||||
this.onMessage(msg);
|
||||
break;
|
||||
case "presence":
|
||||
this.pushHistory({
|
||||
action: msg.action,
|
||||
username: msg.username,
|
||||
message: msg.message,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
console.error("Unexpected action: %s", JSON.stringify(msg));
|
||||
}
|
||||
});
|
||||
|
||||
this.ws.conn = conn;
|
||||
},
|
||||
|
||||
pushHistory({username, message}) {
|
||||
// Start broadcasting my webcam.
|
||||
startVideo() {
|
||||
if (this.webcam.busy) return;
|
||||
this.webcam.busy = true;
|
||||
|
||||
navigator.mediaDevices.getUserMedia({
|
||||
audio: true,
|
||||
video: true,
|
||||
}).then(stream => {
|
||||
this.webcam.active = true;
|
||||
this.webcam.elem.srcObject = stream;
|
||||
this.webcam.stream = stream;
|
||||
|
||||
// Tell backend the camera is ready.
|
||||
this.sendMe();
|
||||
}).catch(err => {
|
||||
this.ChatClient(`Webcam error: ${err}`);
|
||||
}).finally(() => {
|
||||
this.webcam.busy = false;
|
||||
})
|
||||
},
|
||||
|
||||
// Stop broadcasting.
|
||||
stopVideo() {
|
||||
this.webcam.elem.srcObject = null;
|
||||
this.webcam.stream = null;
|
||||
this.webcam.active = false;
|
||||
|
||||
// Tell backend our camera state.
|
||||
this.sendMe();
|
||||
},
|
||||
|
||||
pushHistory({username, message, action="message", isChatServer, isChatClient}) {
|
||||
this.history.push({
|
||||
action: action,
|
||||
username: username,
|
||||
message: message,
|
||||
isChatServer,
|
||||
isChatClient,
|
||||
});
|
||||
}
|
||||
this.scrollHistory();
|
||||
},
|
||||
|
||||
scrollHistory() {
|
||||
window.requestAnimationFrame(() => {
|
||||
this.historyScrollbox.scroll({
|
||||
top: this.historyScrollbox.scrollHeight,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
// Send a chat message as ChatServer
|
||||
ChatServer(message) {
|
||||
this.pushHistory({
|
||||
username: "ChatServer",
|
||||
message: message,
|
||||
isChatServer: true,
|
||||
});
|
||||
},
|
||||
ChatClient(message) {
|
||||
this.pushHistory({
|
||||
username: "ChatClient",
|
||||
message: message,
|
||||
isChatClient: true,
|
||||
});
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -0,0 +1,186 @@
|
|||
{{define "index"}}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" type="text/css" href="/static/css/bulma.min.css">
|
||||
<link rel="stylesheet" href="/static/fontawesome-free-6.1.2-web/css/all.css">
|
||||
<link rel="stylesheet" type="text/css" href="/static/css/BareRTC.css?{{CacheHash}}">
|
||||
<title>BareRTC</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="BareRTC-App">
|
||||
<nav class="navbar" role="navigation" aria-label="main navigation">
|
||||
<div class="navbar-brand">
|
||||
<a class="navbar-item" href="/">
|
||||
BareRTC
|
||||
</a>
|
||||
|
||||
<a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false" data-target="navbarBasicExample">
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div id="navbarBasicExample" class="navbar-menu">
|
||||
<div class="navbar-start">
|
||||
<a class="navbar-item" href="/">
|
||||
<span class="icon"><i class="fa fa-home"></i></span>
|
||||
<span>Home</span>
|
||||
</a>
|
||||
|
||||
<a class="navbar-item" href="/about">
|
||||
About
|
||||
</a>
|
||||
|
||||
<a class="navbar-item" href="/faq">
|
||||
FAQ
|
||||
</a>
|
||||
|
||||
<div id="navbar-more" class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link">
|
||||
More
|
||||
</a>
|
||||
|
||||
<div class="navbar-dropdown is-active">
|
||||
<a class="navbar-item" href="/about">
|
||||
<span class="icon"><i class="fa fa-circle-info"></i></span>
|
||||
<span>About</span>
|
||||
</a>
|
||||
<a class="navbar-item" href="/faq">
|
||||
<span class="icon"><i class="fa fa-circle-question"></i></span>
|
||||
<span>FAQ</span>
|
||||
</a>
|
||||
<a class="navbar-item" href="/tos">
|
||||
<span class="icon"><i class="fa fa-list"></i></span>
|
||||
<span>Terms of Service</span>
|
||||
</a>
|
||||
<a class="navbar-item" href="/privacy">
|
||||
<span class="icon"><i class="fa fa-file-shield"></i></span>
|
||||
<span>Privacy Policy</span>
|
||||
</a>
|
||||
<a class="navbar-item" href="/contact">
|
||||
<span class="icon"><i class="fa fa-message"></i></span>
|
||||
<span>Contact</span>
|
||||
</a>
|
||||
<hr class="navbar-divider">
|
||||
<a class="navbar-item" href="/contact?intent=report">
|
||||
<span class="icon"><i class="fa fa-triangle-exclamation"></i></span>
|
||||
<span>Report an issue</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Sign In modal -->
|
||||
<div class="modal" :class="{'is-active': loginModal.visible}">
|
||||
<div class="modal-background"></div>
|
||||
<div class="modal-content">
|
||||
<div class="card">
|
||||
<header class="card-header has-background-info">
|
||||
<p class="card-header-title has-text-light">Sign In</p>
|
||||
</header>
|
||||
<div class="card-content">
|
||||
<form @submit.prevent="signIn()">
|
||||
|
||||
<div class="field">
|
||||
<label class="label">Username</label>
|
||||
<input class="input"
|
||||
v-model="username"
|
||||
placeholder="Username"
|
||||
autocomplete="off"
|
||||
autofocus
|
||||
required>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<button class="button is-link">Submit</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="block p-4">
|
||||
<div class="columns">
|
||||
<div class="column is-one-fifth">
|
||||
<div class="card">
|
||||
<header class="card-header has-background-success-dark">
|
||||
<p class="card-header-title has-text-light">
|
||||
Channels
|
||||
</p>
|
||||
</header>
|
||||
<div class="card-content">
|
||||
<aside class="menu">
|
||||
<p class="menu-label">
|
||||
Chat Rooms
|
||||
</p>
|
||||
|
||||
<ul class="menu-list">
|
||||
<li><a href="#" class="is-active">Chat Room</a></li>
|
||||
</ul>
|
||||
|
||||
<p class="menu-label">
|
||||
Private Messages
|
||||
</p>
|
||||
|
||||
<ul class="menu-list">
|
||||
<li><a href="#">Chat Room</a></li>
|
||||
<li><a href="#">DMs</a></li>
|
||||
</ul>
|
||||
</aside>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column">
|
||||
<div class="card">
|
||||
<header class="card-header has-background-link">
|
||||
<p class="card-header-title has-text-light">
|
||||
Chat Room
|
||||
</p>
|
||||
</header>
|
||||
<div class="card-content">
|
||||
|
||||
<div v-for="(msg, i) in history" v-bind:key="i">
|
||||
<div>
|
||||
<label class="label">[[msg.username]]</label>
|
||||
</div>
|
||||
[[msg.message]]
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-content">
|
||||
|
||||
<form @submit.prevent="sendMessage()">
|
||||
<input type="text" class="input"
|
||||
v-model="message"
|
||||
placeholder="Message">
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h1>Hello</h1>
|
||||
</div>
|
||||
|
||||
</div><!-- /app -->
|
||||
<script src="/static/js/vue-3.2.45.js"></script>
|
||||
<script src="/static/js/BareRTC.js?{{CacheHash}}"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
{{end}}
|
|
@ -6,76 +6,11 @@
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" type="text/css" href="/static/css/bulma.min.css">
|
||||
<link rel="stylesheet" href="/static/fontawesome-free-6.1.2-web/css/all.css">
|
||||
<link rel="stylesheet" type="text/css" href="/static/css/BareRTC.css?{{CacheHash}}">
|
||||
<link rel="stylesheet" type="text/css" href="/static/css/chat.css?{{CacheHash}}">
|
||||
<title>BareRTC</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="BareRTC-App">
|
||||
<nav class="navbar" role="navigation" aria-label="main navigation">
|
||||
<div class="navbar-brand">
|
||||
<a class="navbar-item" href="/">
|
||||
BareRTC
|
||||
</a>
|
||||
|
||||
<a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false" data-target="navbarBasicExample">
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div id="navbarBasicExample" class="navbar-menu">
|
||||
<div class="navbar-start">
|
||||
<a class="navbar-item" href="/">
|
||||
<span class="icon"><i class="fa fa-home"></i></span>
|
||||
<span>Home</span>
|
||||
</a>
|
||||
|
||||
<a class="navbar-item" href="/about">
|
||||
About
|
||||
</a>
|
||||
|
||||
<a class="navbar-item" href="/faq">
|
||||
FAQ
|
||||
</a>
|
||||
|
||||
<div id="navbar-more" class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link">
|
||||
More
|
||||
</a>
|
||||
|
||||
<div class="navbar-dropdown is-active">
|
||||
<a class="navbar-item" href="/about">
|
||||
<span class="icon"><i class="fa fa-circle-info"></i></span>
|
||||
<span>About</span>
|
||||
</a>
|
||||
<a class="navbar-item" href="/faq">
|
||||
<span class="icon"><i class="fa fa-circle-question"></i></span>
|
||||
<span>FAQ</span>
|
||||
</a>
|
||||
<a class="navbar-item" href="/tos">
|
||||
<span class="icon"><i class="fa fa-list"></i></span>
|
||||
<span>Terms of Service</span>
|
||||
</a>
|
||||
<a class="navbar-item" href="/privacy">
|
||||
<span class="icon"><i class="fa fa-file-shield"></i></span>
|
||||
<span>Privacy Policy</span>
|
||||
</a>
|
||||
<a class="navbar-item" href="/contact">
|
||||
<span class="icon"><i class="fa fa-message"></i></span>
|
||||
<span>Contact</span>
|
||||
</a>
|
||||
<hr class="navbar-divider">
|
||||
<a class="navbar-item" href="/contact?intent=report">
|
||||
<span class="icon"><i class="fa fa-triangle-exclamation"></i></span>
|
||||
<span>Report an issue</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Sign In modal -->
|
||||
<div class="modal" :class="{'is-active': loginModal.visible}">
|
||||
<div class="modal-background"></div>
|
||||
|
@ -109,72 +44,196 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="block p-4">
|
||||
<div class="columns">
|
||||
<div class="column is-one-fifth">
|
||||
<div class="card">
|
||||
<header class="card-header has-background-success-dark">
|
||||
<p class="card-header-title has-text-light">
|
||||
Channels
|
||||
<div class="chat-container">
|
||||
<div class="left-column">
|
||||
<div class="card grid-card">
|
||||
<header class="card-header has-background-success-dark">
|
||||
<p class="card-header-title has-text-light">
|
||||
Channels
|
||||
</p>
|
||||
</header>
|
||||
<div class="card-content">
|
||||
<aside class="menu">
|
||||
<p class="menu-label">
|
||||
Chat Rooms
|
||||
</p>
|
||||
</header>
|
||||
<div class="card-content">
|
||||
<aside class="menu">
|
||||
<p class="menu-label">
|
||||
Chat Rooms
|
||||
</p>
|
||||
|
||||
<ul class="menu-list">
|
||||
<li><a href="#" class="is-active">Chat Room</a></li>
|
||||
</ul>
|
||||
<ul class="menu-list">
|
||||
<li><a href="#" class="is-active">Chat Room</a></li>
|
||||
</ul>
|
||||
|
||||
<p class="menu-label">
|
||||
Private Messages
|
||||
</p>
|
||||
|
||||
<ul class="menu-list">
|
||||
<li><a href="#">Chat Room</a></li>
|
||||
<li><a href="#">DMs</a></li>
|
||||
</ul>
|
||||
</aside>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column">
|
||||
<div class="card">
|
||||
<header class="card-header has-background-link">
|
||||
<p class="card-header-title has-text-light">
|
||||
Chat Room
|
||||
<p class="menu-label">
|
||||
Private Messages
|
||||
</p>
|
||||
</header>
|
||||
<div class="card-content">
|
||||
|
||||
<div v-for="(msg, i) in history" v-bind:key="i">
|
||||
<div>
|
||||
<label class="label">[[msg.username]]</label>
|
||||
</div>
|
||||
[[msg.message]]
|
||||
</div>
|
||||
<ul class="menu-list">
|
||||
<li><a href="#">Chat Room</a></li>
|
||||
<li><a href="#">DMs</a></li>
|
||||
<li><a href="#">DMs</a></li>
|
||||
<li><a href="#">DMs</a></li>
|
||||
<li><a href="#">DMs</a></li>
|
||||
<li><a href="#">DMs</a></li>
|
||||
<li><a href="#">DMs</a></li>
|
||||
<li><a href="#">DMs</a></li>
|
||||
<li><a href="#">DMs</a></li>
|
||||
<li><a href="#">DMs</a></li>
|
||||
<li><a href="#">DMs</a></li>
|
||||
<li><a href="#">DMs</a></li>
|
||||
<li><a href="#">DMs</a></li>
|
||||
<li><a href="#">DMs</a></li>
|
||||
<li><a href="#">DMs</a></li>
|
||||
<li><a href="#">DMs</a></li>
|
||||
<li><a href="#">DMs</a></li>
|
||||
<li><a href="#">DMs</a></li>
|
||||
<li><a href="#">DMs</a></li>
|
||||
<li><a href="#">DMs</a></li>
|
||||
</ul>
|
||||
</aside>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chat-column">
|
||||
<div class="card grid-card">
|
||||
<header class="card-header has-background-link">
|
||||
<p class="card-header-title has-text-light">
|
||||
Chat Room
|
||||
</p>
|
||||
</header>
|
||||
<div class="video-feeds">
|
||||
<video class="feed"
|
||||
v-show="webcam.active"
|
||||
id="localVideo"
|
||||
autoplay muted>
|
||||
x
|
||||
</video>
|
||||
<div class="feed">
|
||||
y
|
||||
</div>
|
||||
<div class="feed">
|
||||
y
|
||||
</div>
|
||||
<div class="feed">
|
||||
y
|
||||
</div>
|
||||
<div class="feed">
|
||||
y
|
||||
</div>
|
||||
<div class="feed">
|
||||
y
|
||||
</div>
|
||||
<div class="feed">
|
||||
y
|
||||
</div>
|
||||
<div class="feed">
|
||||
y
|
||||
</div>
|
||||
<div class="feed">
|
||||
y
|
||||
</div>
|
||||
<div class="feed">
|
||||
y
|
||||
</div>
|
||||
<div class="feed">
|
||||
y
|
||||
</div>
|
||||
<div class="feed">
|
||||
y
|
||||
</div>
|
||||
<div class="feed">
|
||||
y
|
||||
</div>
|
||||
<div class="feed">
|
||||
y
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-content" id="chatHistory">
|
||||
|
||||
<div v-for="(msg, i) in history" v-bind:key="i">
|
||||
<div>
|
||||
<label class="label"
|
||||
:class="{'has-text-success is-dark': msg.isChatServer,
|
||||
'has-text-warning is-dark': msg.isAdmin,
|
||||
'has-text-danger': msg.isChatClient}">
|
||||
[[msg.username]]
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div v-if="msg.action === 'presence'">
|
||||
<em>[[msg.message]]</em>
|
||||
</div>
|
||||
<div v-else>
|
||||
[[msg.message]]
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chat-footer">
|
||||
<div class="card">
|
||||
<div class="card-content p-2">
|
||||
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<form @submit.prevent="sendMessage()">
|
||||
<input type="text" class="input"
|
||||
v-model="message"
|
||||
placeholder="Message">
|
||||
</form>
|
||||
</div>
|
||||
<div class="column is-narrow">
|
||||
<button type="button"
|
||||
v-if="webcam.active"
|
||||
class="button is-danger"
|
||||
@click="stopVideo()">
|
||||
<i class="fa fa-camera mr-2"></i>
|
||||
Stop
|
||||
</button>
|
||||
|
||||
<button type="button"
|
||||
v-else
|
||||
class="button is-success"
|
||||
@click="startVideo()"
|
||||
:disabled="webcam.busy">
|
||||
<i class="fa fa-camera mr-2"></i>
|
||||
Start
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="right-column">
|
||||
<div class="card grid-card">
|
||||
<header class="card-header has-background-success-dark">
|
||||
<p class="card-header-title has-text-light">
|
||||
Who Is Online
|
||||
</p>
|
||||
</header>
|
||||
<div class="card-content p-2">
|
||||
|
||||
<ul class="menu-list">
|
||||
<li v-for="(u, i) in whoList" v-bind:key="i">
|
||||
<div class="columns is-mobile">
|
||||
<div class="column">[[ u.username ]]</div>
|
||||
<div class="column is-narrow">
|
||||
<button type="button" class="button is-small"
|
||||
:disabled="!u.videoActive">
|
||||
<i class="fa fa-video"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-content">
|
||||
|
||||
<form @submit.prevent="sendMessage()">
|
||||
<input type="text" class="input"
|
||||
v-model="message"
|
||||
placeholder="Message">
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h1>Hello</h1>
|
||||
</div>
|
||||
|
||||
</div><!-- /app -->
|
||||
|
|
Loading…
Reference in New Issue