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.
This commit is contained in:
parent
b627fe0ffa
commit
4f93c27651
|
@ -6,6 +6,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
barertc "git.kirsle.net/apps/barertc/pkg"
|
barertc "git.kirsle.net/apps/barertc/pkg"
|
||||||
|
"git.kirsle.net/apps/barertc/pkg/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
@ -22,6 +23,10 @@ func main() {
|
||||||
flag.StringVar(&address, "address", ":9000", "Address to listen on, like localhost:5000 or :8080")
|
flag.StringVar(&address, "address", ":9000", "Address to listen on, like localhost:5000 or :8080")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
|
if debug {
|
||||||
|
log.SetDebug(true)
|
||||||
|
}
|
||||||
|
|
||||||
app := barertc.NewServer()
|
app := barertc.NewServer()
|
||||||
app.Setup()
|
app.Setup()
|
||||||
panic(app.ListenAndServe(address))
|
panic(app.ListenAndServe(address))
|
||||||
|
|
77
pkg/handlers.go
Normal file
77
pkg/handlers.go
Normal file
|
@ -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"`
|
Action string `json:"action,omitempty"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
Message string `json:"message"`
|
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 (
|
const (
|
||||||
|
// Actions sent by the client side only
|
||||||
ActionLogin = "login" // post the username to backend
|
ActionLogin = "login" // post the username to backend
|
||||||
|
|
||||||
|
// Actions sent by server or client
|
||||||
ActionMessage = "message" // post a message to the room
|
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"`
|
||||||
|
}
|
||||||
|
|
109
pkg/websocket.go
109
pkg/websocket.go
|
@ -13,7 +13,10 @@ import (
|
||||||
|
|
||||||
// Subscriber represents a connected WebSocket session.
|
// Subscriber represents a connected WebSocket session.
|
||||||
type Subscriber struct {
|
type Subscriber struct {
|
||||||
|
// User properties
|
||||||
|
ID int // ID assigned by server
|
||||||
Username string
|
Username string
|
||||||
|
VideoActive bool
|
||||||
conn *websocket.Conn
|
conn *websocket.Conn
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
messages chan []byte
|
messages chan []byte
|
||||||
|
@ -27,6 +30,13 @@ func (sub *Subscriber) ReadLoop(s *Server) {
|
||||||
msgType, data, err := sub.conn.Read(sub.ctx)
|
msgType, data, err := sub.conn.Read(sub.ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("ReadLoop error: %+v", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,6 +47,7 @@ func (sub *Subscriber) ReadLoop(s *Server) {
|
||||||
|
|
||||||
// Read the user's posted message.
|
// Read the user's posted message.
|
||||||
var msg Message
|
var msg Message
|
||||||
|
log.Debug("Read(%s): %s", sub.Username, data)
|
||||||
if err := json.Unmarshal(data, &msg); err != nil {
|
if err := json.Unmarshal(data, &msg); err != nil {
|
||||||
log.Error("Message error: %s", err)
|
log.Error("Message error: %s", err)
|
||||||
continue
|
continue
|
||||||
|
@ -45,28 +56,14 @@ func (sub *Subscriber) ReadLoop(s *Server) {
|
||||||
// What action are they performing?
|
// What action are they performing?
|
||||||
switch msg.Action {
|
switch msg.Action {
|
||||||
case ActionLogin:
|
case ActionLogin:
|
||||||
// TODO: ensure unique?
|
s.OnLogin(sub, msg)
|
||||||
sub.Username = msg.Username
|
|
||||||
s.Broadcast(Message{
|
|
||||||
Username: msg.Username,
|
|
||||||
Message: "has joined the room!",
|
|
||||||
})
|
|
||||||
case ActionMessage:
|
case ActionMessage:
|
||||||
if sub.Username == "" {
|
s.OnMessage(sub, msg)
|
||||||
sub.SendJSON(Message{
|
case ActionMe:
|
||||||
Username: "ChatServer",
|
s.OnMe(sub, msg)
|
||||||
Message: "You must log in first.",
|
|
||||||
})
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Broadcast a chat message to the room.
|
|
||||||
s.Broadcast(Message{
|
|
||||||
Username: sub.Username,
|
|
||||||
Message: msg.Message,
|
|
||||||
})
|
|
||||||
default:
|
default:
|
||||||
sub.SendJSON(Message{
|
sub.SendJSON(Message{
|
||||||
|
Action: ActionMessage,
|
||||||
Username: "ChatServer",
|
Username: "ChatServer",
|
||||||
Message: "Unsupported message type.",
|
Message: "Unsupported message type.",
|
||||||
})
|
})
|
||||||
|
@ -81,9 +78,19 @@ func (sub *Subscriber) SendJSON(v interface{}) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
log.Debug("SendJSON(%s): %s", sub.Username, data)
|
||||||
return sub.conn.Write(sub.ctx, websocket.MessageText, 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.
|
// WebSocket handles the /ws websocket connection.
|
||||||
func (s *Server) WebSocket() http.HandlerFunc {
|
func (s *Server) WebSocket() http.HandlerFunc {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
@ -112,7 +119,7 @@ func (s *Server) WebSocket() http.HandlerFunc {
|
||||||
}
|
}
|
||||||
|
|
||||||
s.AddSubscriber(sub)
|
s.AddSubscriber(sub)
|
||||||
defer s.DeleteSubscriber(sub)
|
// defer s.DeleteSubscriber(sub)
|
||||||
|
|
||||||
go sub.ReadLoop(s)
|
go sub.ReadLoop(s)
|
||||||
for {
|
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.
|
// AddSubscriber adds a WebSocket subscriber to the server.
|
||||||
func (s *Server) AddSubscriber(sub *Subscriber) {
|
func (s *Server) AddSubscriber(sub *Subscriber) {
|
||||||
|
// Assign a unique ID.
|
||||||
|
SubscriberID++
|
||||||
|
sub.ID = SubscriberID
|
||||||
|
log.Debug("AddSubscriber: %s", sub.ID)
|
||||||
|
|
||||||
s.subscribersMu.Lock()
|
s.subscribersMu.Lock()
|
||||||
s.subscribers[sub] = struct{}{}
|
s.subscribers[sub] = struct{}{}
|
||||||
s.subscribersMu.Unlock()
|
s.subscribersMu.Unlock()
|
||||||
|
@ -139,23 +154,73 @@ func (s *Server) AddSubscriber(sub *Subscriber) {
|
||||||
|
|
||||||
// DeleteSubscriber removes a subscriber from the server.
|
// DeleteSubscriber removes a subscriber from the server.
|
||||||
func (s *Server) DeleteSubscriber(sub *Subscriber) {
|
func (s *Server) DeleteSubscriber(sub *Subscriber) {
|
||||||
|
log.Error("DeleteSubscriber: %s", sub.Username)
|
||||||
s.subscribersMu.Lock()
|
s.subscribersMu.Lock()
|
||||||
delete(s.subscribers, sub)
|
delete(s.subscribers, sub)
|
||||||
s.subscribersMu.Unlock()
|
s.subscribersMu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Broadcast a message to the chat room.
|
// IterSubscribers loops over the subscriber list with a read lock. If the
|
||||||
func (s *Server) Broadcast(msg Message) {
|
// 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()
|
s.subscribersMu.RLock()
|
||||||
defer s.subscribersMu.RUnlock()
|
defer s.subscribersMu.RUnlock()
|
||||||
|
}
|
||||||
|
|
||||||
for sub := range s.subscribers {
|
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.IterSubscribers(true) {
|
||||||
sub.SendJSON(Message{
|
sub.SendJSON(Message{
|
||||||
|
Action: msg.Action,
|
||||||
Username: msg.Username,
|
Username: msg.Username,
|
||||||
Message: msg.Message,
|
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 {
|
func writeTimeout(ctx context.Context, timeout time.Duration, c *websocket.Conn, msg []byte) error {
|
||||||
ctx, cancel := context.WithTimeout(ctx, timeout)
|
ctx, cancel := context.WithTimeout(ctx, timeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
102
web/static/css/chat.css
Normal file
102
web/static/css/chat.css
Normal file
|
@ -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 {
|
return {
|
||||||
busy: false,
|
busy: false,
|
||||||
|
|
||||||
username: "",
|
channel: "lobby",
|
||||||
|
username: "", //"test",
|
||||||
message: "",
|
message: "",
|
||||||
|
|
||||||
// WebSocket connection.
|
// WebSocket connection.
|
||||||
|
@ -15,8 +16,20 @@ const app = Vue.createApp({
|
||||||
connected: false,
|
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.
|
// Chat history.
|
||||||
history: [],
|
history: [],
|
||||||
|
historyScrollbox: null,
|
||||||
DMs: {},
|
DMs: {},
|
||||||
|
|
||||||
loginModal: {
|
loginModal: {
|
||||||
|
@ -25,13 +38,15 @@ const app = Vue.createApp({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.pushHistory({
|
this.webcam.elem = document.querySelector("#localVideo");
|
||||||
username: "ChatServer",
|
this.historyScrollbox = document.querySelector("#chatHistory");
|
||||||
message: "Welcome to BareRTC!",
|
|
||||||
});
|
this.ChatServer("Welcome to BareRTC!")
|
||||||
|
|
||||||
if (!this.username) {
|
if (!this.username) {
|
||||||
this.loginModal.visible = true;
|
this.loginModal.visible = true;
|
||||||
|
} else {
|
||||||
|
this.signIn();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -46,10 +61,7 @@ const app = Vue.createApp({
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.ws.connected) {
|
if (!this.ws.connected) {
|
||||||
this.pushHistory({
|
this.ChatClient("You are not connected to the server.");
|
||||||
username: "ChatClient",
|
|
||||||
message: "You are not connected to the server.",
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,32 +74,49 @@ const app = Vue.createApp({
|
||||||
this.message = "";
|
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 the WebSocket connection.
|
||||||
dial() {
|
dial() {
|
||||||
const conn = new WebSocket(`ws://${location.host}/ws`);
|
const conn = new WebSocket(`ws://${location.host}/ws`);
|
||||||
|
|
||||||
conn.addEventListener("close", ev => {
|
conn.addEventListener("close", ev => {
|
||||||
this.ws.connected = false;
|
this.ws.connected = false;
|
||||||
this.pushHistory({
|
this.ChatClient(`WebSocket Disconnected code: ${ev.code}, reason: ${ev.reason}`);
|
||||||
username: "ChatClient",
|
|
||||||
message: `WebSocket Disconnected code: ${ev.code}, reason: ${ev.reason}`,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (ev.code !== 1001) {
|
if (ev.code !== 1001) {
|
||||||
this.pushHistory({
|
this.ChatClient("Reconnecting in 1s");
|
||||||
username: "ChatClient",
|
|
||||||
message: "Reconnecting in 1s",
|
|
||||||
});
|
|
||||||
setTimeout(this.dial, 1000);
|
setTimeout(this.dial, 1000);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
conn.addEventListener("open", ev => {
|
conn.addEventListener("open", ev => {
|
||||||
this.ws.connected = true;
|
this.ws.connected = true;
|
||||||
this.pushHistory({
|
this.ChatClient("Websocket connected!");
|
||||||
username: "ChatClient",
|
|
||||||
message: "Websocket connected!",
|
|
||||||
});
|
|
||||||
|
|
||||||
// Tell the server our username.
|
// Tell the server our username.
|
||||||
this.ws.conn.send(JSON.stringify({
|
this.ws.conn.send(JSON.stringify({
|
||||||
|
@ -104,22 +133,101 @@ const app = Vue.createApp({
|
||||||
}
|
}
|
||||||
|
|
||||||
let msg = JSON.parse(ev.data);
|
let msg = JSON.parse(ev.data);
|
||||||
|
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({
|
this.pushHistory({
|
||||||
|
action: msg.action,
|
||||||
username: msg.username,
|
username: msg.username,
|
||||||
message: msg.message,
|
message: msg.message,
|
||||||
});
|
});
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.error("Unexpected action: %s", JSON.stringify(msg));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.ws.conn = conn;
|
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({
|
this.history.push({
|
||||||
|
action: action,
|
||||||
username: username,
|
username: username,
|
||||||
message: message,
|
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,
|
||||||
|
});
|
||||||
|
},
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
186
web/templates/chat-backup.html
Normal file
186
web/templates/chat-backup.html
Normal file
|
@ -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">
|
<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" 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" 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>
|
<title>BareRTC</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="BareRTC-App">
|
<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 -->
|
<!-- Sign In modal -->
|
||||||
<div class="modal" :class="{'is-active': loginModal.visible}">
|
<div class="modal" :class="{'is-active': loginModal.visible}">
|
||||||
<div class="modal-background"></div>
|
<div class="modal-background"></div>
|
||||||
|
@ -109,10 +44,9 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="block p-4">
|
<div class="chat-container">
|
||||||
<div class="columns">
|
<div class="left-column">
|
||||||
<div class="column is-one-fifth">
|
<div class="card grid-card">
|
||||||
<div class="card">
|
|
||||||
<header class="card-header has-background-success-dark">
|
<header class="card-header has-background-success-dark">
|
||||||
<p class="card-header-title has-text-light">
|
<p class="card-header-title has-text-light">
|
||||||
Channels
|
Channels
|
||||||
|
@ -135,6 +69,24 @@
|
||||||
<ul class="menu-list">
|
<ul class="menu-list">
|
||||||
<li><a href="#">Chat Room</a></li>
|
<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>
|
||||||
|
<li><a href="#">DMs</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
|
@ -142,39 +94,146 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="column">
|
<div class="chat-column">
|
||||||
<div class="card">
|
<div class="card grid-card">
|
||||||
<header class="card-header has-background-link">
|
<header class="card-header has-background-link">
|
||||||
<p class="card-header-title has-text-light">
|
<p class="card-header-title has-text-light">
|
||||||
Chat Room
|
Chat Room
|
||||||
</p>
|
</p>
|
||||||
</header>
|
</header>
|
||||||
<div class="card-content">
|
<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 v-for="(msg, i) in history" v-bind:key="i">
|
||||||
<div>
|
<div>
|
||||||
<label class="label">[[msg.username]]</label>
|
<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>
|
||||||
|
|
||||||
|
<div v-if="msg.action === 'presence'">
|
||||||
|
<em>[[msg.message]]</em>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
[[msg.message]]
|
[[msg.message]]
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="chat-footer">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-content">
|
<div class="card-content p-2">
|
||||||
|
|
||||||
|
<div class="columns">
|
||||||
|
<div class="column">
|
||||||
<form @submit.prevent="sendMessage()">
|
<form @submit.prevent="sendMessage()">
|
||||||
<input type="text" class="input"
|
<input type="text" class="input"
|
||||||
v-model="message"
|
v-model="message"
|
||||||
placeholder="Message">
|
placeholder="Message">
|
||||||
</form>
|
</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>
|
||||||
</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>
|
</div>
|
||||||
<h1>Hello</h1>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div><!-- /app -->
|
</div><!-- /app -->
|
||||||
|
|
Loading…
Reference in New Issue
Block a user