mirror of https://github.com/JustKato/FreePad.git
				
				
				
			Compare commits
	
		
			25 Commits
		
	
	
		
			400fd23b3e
			...
			ca10033ecf
		
	
	| Author | SHA1 | Date | 
|---|---|---|
|  | ca10033ecf | |
|  | b68e5c8e88 | |
|  | 84ccd44fd7 | |
|  | 0a1bff5cd4 | |
|  | 7982d564e8 | |
|  | becbf30752 | |
|  | 796a3b07c4 | |
|  | 5518103575 | |
|  | 177ab62720 | |
|  | 5a8ccf20a8 | |
|  | a71be11135 | |
|  | 30dc23c847 | |
|  | 0a3b5d50f2 | |
|  | b4c47ded35 | |
|  | 6e401a416f | |
|  | 3b137c5ed6 | |
|  | c4f6496e0e | |
|  | ee9516a109 | |
|  | 7dcad9dc31 | |
|  | 1b1fe59877 | |
|  | 6cc1628e77 | |
|  | bf144c6ecb | |
|  | 1d3383c8c6 | |
|  | 4138386fb3 | |
|  | cfe2c06dac | 
							
								
								
									
										2
									
								
								go.mod
								
								
								
								
							
							
						
						
									
										2
									
								
								go.mod
								
								
								
								
							|  | @ -4,6 +4,8 @@ go 1.15 | ||||||
| 
 | 
 | ||||||
| require ( | require ( | ||||||
| 	github.com/gin-gonic/gin v1.7.7 | 	github.com/gin-gonic/gin v1.7.7 | ||||||
|  | 	github.com/google/uuid v1.3.0 // indirect | ||||||
|  | 	github.com/gorilla/websocket v1.5.0 // indirect | ||||||
| 	github.com/joho/godotenv v1.4.0 | 	github.com/joho/godotenv v1.4.0 | ||||||
| 	github.com/mrz1836/go-sanitize v1.1.5 | 	github.com/mrz1836/go-sanitize v1.1.5 | ||||||
| 	github.com/ulule/limiter/v3 v3.10.0 | 	github.com/ulule/limiter/v3 v3.10.0 | ||||||
|  |  | ||||||
							
								
								
									
										4
									
								
								go.sum
								
								
								
								
							
							
						
						
									
										4
									
								
								go.sum
								
								
								
								
							|  | @ -38,6 +38,10 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ | ||||||
| github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= | github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= | ||||||
| github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= | ||||||
| github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= | ||||||
|  | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= | ||||||
|  | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= | ||||||
|  | github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= | ||||||
|  | github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= | ||||||
| github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= | ||||||
| github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= | github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= | ||||||
| github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= | github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= | ||||||
|  |  | ||||||
|  | @ -0,0 +1,197 @@ | ||||||
|  | package socketmanager | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"errors" | ||||||
|  | 	"fmt" | ||||||
|  | 
 | ||||||
|  | 	"github.com/JustKato/FreePad/lib/objects" | ||||||
|  | 	"github.com/gin-gonic/gin" | ||||||
|  | 	"github.com/google/uuid" | ||||||
|  | 	"github.com/gorilla/websocket" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | var wsUpgrader = websocket.Upgrader{ | ||||||
|  | 	ReadBufferSize:  1024, // TODO: Make it configurable via the .env file
 | ||||||
|  | 	WriteBufferSize: 1024, // TODO: Make it configurable via the .env file
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // The pad socket map caches all of the existing sockets
 | ||||||
|  | var padSocketMap map[string]map[string]*websocket.Conn = make(map[string]map[string]*websocket.Conn) | ||||||
|  | 
 | ||||||
|  | // TODO: Use generics so that we can take string messages, that'd be nice!
 | ||||||
|  | type SocketMessage struct { | ||||||
|  | 	EventType string                 `json:"eventType"` | ||||||
|  | 	PadName   string                 `json:"padName"` | ||||||
|  | 	Message   map[string]interface{} `json:"message"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Bind the websockets to the gin router
 | ||||||
|  | func BindSocket(router *gin.RouterGroup) { | ||||||
|  | 
 | ||||||
|  | 	router.GET("/get/:pad", func(ctx *gin.Context) { | ||||||
|  | 		// Get the name of the pad to assign to this socket
 | ||||||
|  | 		padName := ctx.Param("pad") | ||||||
|  | 		// Upgrade the socket connection
 | ||||||
|  | 		webSocketUpgrade(ctx, padName) | ||||||
|  | 	}) | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func webSocketUpgrade(ctx *gin.Context, padName string) { | ||||||
|  | 
 | ||||||
|  | 	conn, err := wsUpgrader.Upgrade(ctx.Writer, ctx.Request, ctx.Request.Header) | ||||||
|  | 	if err != nil { | ||||||
|  | 		fmt.Printf("Failed to set websocket upgrade: %v\n", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Check if we have any sockets in this padName
 | ||||||
|  | 	if _, ok := padSocketMap[padName]; !ok { | ||||||
|  | 		// Initialize a new map of sockets
 | ||||||
|  | 		padSocketMap[padName] = make(map[string]*websocket.Conn) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Give this socket a token
 | ||||||
|  | 	socketToken := uuid.NewString() | ||||||
|  | 
 | ||||||
|  | 	// Set the current connection at the socket Token position
 | ||||||
|  | 	padSocketMap[padName][socketToken] = conn | ||||||
|  | 
 | ||||||
|  | 	// Somone just connected
 | ||||||
|  | 	UpdatePadStatus(padName) | ||||||
|  | 
 | ||||||
|  | 	// Start listening to this socket
 | ||||||
|  | 	for { | ||||||
|  | 		// Try Read the JSON input from the socket
 | ||||||
|  | 		_, msg, err := conn.ReadMessage() | ||||||
|  | 
 | ||||||
|  | 		// Check if anything but a read limit was created
 | ||||||
|  | 		if err != nil && !errors.Is(err, websocket.ErrReadLimit) { | ||||||
|  | 			// Remove self from the cache
 | ||||||
|  | 			delete(padSocketMap[padName], socketToken) | ||||||
|  | 			// Somone just disconnected
 | ||||||
|  | 			UpdatePadStatus(padName) | ||||||
|  | 			break | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		if err != nil { | ||||||
|  | 			// There has been an error reading the message
 | ||||||
|  | 			fmt.Println("Failed to read from the socket but probably still connected") | ||||||
|  | 			// Skip this cycle
 | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		// Init the variable
 | ||||||
|  | 		var p SocketMessage | ||||||
|  | 		// Try and parse the json
 | ||||||
|  | 		err = json.Unmarshal([]byte(msg), &p) | ||||||
|  | 		if err != nil { | ||||||
|  | 			// There has been an error reading the message
 | ||||||
|  | 			fmt.Println("Failed to parse the JSON", err) | ||||||
|  | 			// Skip this cycle
 | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		// Pass the message to the proper handlers
 | ||||||
|  | 		handleSocketMessage(p, socketToken, padName) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Handle the socket's message
 | ||||||
|  | func handleSocketMessage(msg SocketMessage, socketToken string, padName string) { | ||||||
|  | 
 | ||||||
|  | 	// Check if this is a pad Update
 | ||||||
|  | 	if msg.EventType == `padUpdate` { | ||||||
|  | 		handlePadUpdate(msg, socketToken, padName) | ||||||
|  | 
 | ||||||
|  | 		// Serialize the message
 | ||||||
|  | 		serialized, err := json.Marshal(msg) | ||||||
|  | 		// Check if there was an error
 | ||||||
|  | 		if err != nil { | ||||||
|  | 			fmt.Println(`Failed to broadcast the padUpdate`, err) | ||||||
|  | 			// Stop the execution
 | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		// Alert all the other pads other than this one.
 | ||||||
|  | 		for k, pad := range padSocketMap[padName] { | ||||||
|  | 			// Check if this is the same socket.
 | ||||||
|  | 			if k == socketToken { | ||||||
|  | 				// Skip self
 | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			// Send the message to the others.
 | ||||||
|  | 			pad.WriteMessage(websocket.TextMessage, serialized) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func handlePadUpdate(msg SocketMessage, socketToken string, padName string) { | ||||||
|  | 
 | ||||||
|  | 	// Check if the msg content is valid
 | ||||||
|  | 	if _, ok := msg.Message[`content`]; !ok { | ||||||
|  | 		fmt.Printf("Failed to update pad %s, invalid message\n", padName) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Check that the content is string
 | ||||||
|  | 	newPadContent, ok := msg.Message[`content`].(string) | ||||||
|  | 	if !ok { | ||||||
|  | 		fmt.Printf("Type assertion failed for %s, invalid message\n", padName) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Get the pad
 | ||||||
|  | 	pad := objects.GetPost(padName, false) | ||||||
|  | 	// Update the pad contents
 | ||||||
|  | 	pad.Content = newPadContent | ||||||
|  | 
 | ||||||
|  | 	// Save to file
 | ||||||
|  | 	objects.WritePost(pad) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Update the current users of the pad about the amount of live viewers.
 | ||||||
|  | func UpdatePadStatus(padName string) { | ||||||
|  | 
 | ||||||
|  | 	// Grab info about the map's key
 | ||||||
|  | 	sockets, ok := padSocketMap[padName] | ||||||
|  | 	// Check if the pad is set and has sockets connected.
 | ||||||
|  | 	if !ok || len(sockets) < 1 { | ||||||
|  | 		// Quit
 | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Generate the message
 | ||||||
|  | 	msg := SocketMessage{ | ||||||
|  | 		EventType: `statusUpdate`, | ||||||
|  | 		PadName:   padName, | ||||||
|  | 		Message: gin.H{ | ||||||
|  | 			// Send the current amount of live viewers
 | ||||||
|  | 			"currentViewers": len(sockets), | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	BroadcastMessage(padName, msg) | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func BroadcastMessage(padName string, msg SocketMessage) { | ||||||
|  | 
 | ||||||
|  | 	// Grab info about the map's key
 | ||||||
|  | 	sockets, ok := padSocketMap[padName] | ||||||
|  | 	// Check if the pad is set and has sockets connected.
 | ||||||
|  | 	if !ok || len(sockets) < 1 { | ||||||
|  | 		// Quit
 | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Get all the participants of the pad group
 | ||||||
|  | 	for _, s := range sockets { | ||||||
|  | 		// Send the message to the socket
 | ||||||
|  | 		s.WriteJSON(msg) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | } | ||||||
							
								
								
									
										4
									
								
								main.go
								
								
								
								
							
							
						
						
									
										4
									
								
								main.go
								
								
								
								
							|  | @ -7,6 +7,7 @@ import ( | ||||||
| 	"github.com/JustKato/FreePad/lib/controllers" | 	"github.com/JustKato/FreePad/lib/controllers" | ||||||
| 	"github.com/JustKato/FreePad/lib/objects" | 	"github.com/JustKato/FreePad/lib/objects" | ||||||
| 	"github.com/JustKato/FreePad/lib/routes" | 	"github.com/JustKato/FreePad/lib/routes" | ||||||
|  | 	"github.com/JustKato/FreePad/lib/socketmanager" | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/joho/godotenv" | 	"github.com/joho/godotenv" | ||||||
| ) | ) | ||||||
|  | @ -52,6 +53,9 @@ func main() { | ||||||
| 	// Add Routes
 | 	// Add Routes
 | ||||||
| 	routes.HomeRoutes(router) | 	routes.HomeRoutes(router) | ||||||
| 
 | 
 | ||||||
|  | 	// Bind the Web Sockets
 | ||||||
|  | 	socketmanager.BindSocket(router.Group("/ws")) | ||||||
|  | 
 | ||||||
| 	router.Run(":8080") | 	router.Run(":8080") | ||||||
| 
 | 
 | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -45,6 +45,24 @@ main#main-card { | ||||||
|     tab-size: 2; |     tab-size: 2; | ||||||
| 
 | 
 | ||||||
|     font-family: 'Roboto Mono', monospace !important; |     font-family: 'Roboto Mono', monospace !important; | ||||||
|  | 
 | ||||||
|  |     padding-top: 2rem; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #padTitle { | ||||||
|  |     padding: .3rem .75rem !important; | ||||||
|  |     border-radius: .25rem; | ||||||
|  |     display: inline-block; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .dark #padTitle { | ||||||
|  |     color: #db3384; | ||||||
|  |     background-color: rgba(0, 0, 0, 0.10); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .light #padTitle { | ||||||
|  |     color: #555273; | ||||||
|  |     border: 1px solid rgba(0, 0, 0, 0.15); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| #pad-content-area { | #pad-content-area { | ||||||
|  | @ -54,11 +72,16 @@ main#main-card { | ||||||
|     flex-flow: column; |     flex-flow: column; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .light .edit-content-text { | ||||||
|  |     color: white !important; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| .edit-content-text { | .edit-content-text { | ||||||
|     display: none; |     display: none; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .read-only-content .edit-content-text { | .read-only-content .edit-content-text { | ||||||
|  |     margin-top: 1rem; | ||||||
|     display: block !important; |     display: block !important; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -78,6 +101,9 @@ main#main-card { | ||||||
|     max-height: calc(17rem + 30vh); |     max-height: calc(17rem + 30vh); | ||||||
|     min-height: 17rem; |     min-height: 17rem; | ||||||
|     overflow: auto; |     overflow: auto; | ||||||
|  | 
 | ||||||
|  |     padding-top: 2rem; | ||||||
|  |     margin-top: 1rem; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| textarea:focus, | textarea:focus, | ||||||
|  |  | ||||||
|  | @ -138,7 +138,10 @@ function renderArchivesSelection() { | ||||||
|             let resp = confirm("Load contents of pad from memory? This will overwrite the current pad for everyone."); |             let resp = confirm("Load contents of pad from memory? This will overwrite the current pad for everyone."); | ||||||
| 
 | 
 | ||||||
|             if (!!resp) { |             if (!!resp) { | ||||||
|                 document.getElementById(`pad-content`).value = a.content; |                 // Update visually for the client
 | ||||||
|  |                 updatePadContent(a.content); | ||||||
|  |                 // Send the update
 | ||||||
|  |                 window.socket.sendPadUpdate(); | ||||||
|             } |             } | ||||||
|         }) |         }) | ||||||
| 
 | 
 | ||||||
|  | @ -210,6 +213,8 @@ function setTextareaPreview(t = true) { | ||||||
| 
 | 
 | ||||||
|         padContentArea.classList.add(`read-only-content`); |         padContentArea.classList.add(`read-only-content`); | ||||||
| 
 | 
 | ||||||
|  |         prev.scrollTop = prev.scrollHeight; | ||||||
|  | 
 | ||||||
|         textarea.classList.add(`hidden`); |         textarea.classList.add(`hidden`); | ||||||
|     } else { |     } else { | ||||||
|         // Toggle edit mode
 |         // Toggle edit mode
 | ||||||
|  |  | ||||||
|  | @ -0,0 +1,261 @@ | ||||||
|  | class PadSocket { | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * @type {WebSocket} | ||||||
|  |      */ | ||||||
|  |     ws      = null; | ||||||
|  |     /** | ||||||
|  |      * @type {String} | ||||||
|  |      */ | ||||||
|  |     padName = null; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * The actual textarea you write in | ||||||
|  |      * @type {HTMLTextAreaElement} | ||||||
|  |      */ | ||||||
|  |     padContents = null; | ||||||
|  |     /** | ||||||
|  |      * The <code> of the preview | ||||||
|  |      * @type {HTMLElement} | ||||||
|  |      */ | ||||||
|  |     padPreview = null; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Create a new PadSocket | ||||||
|  |      * @param {string} padName The name of the pad | ||||||
|  |      * @param {string} connUrl The URL to the websocket | ||||||
|  |      */ | ||||||
|  |     constructor(padName, connUrl = null) { | ||||||
|  |         // Assign the pad name
 | ||||||
|  |         this.padName = padName; | ||||||
|  | 
 | ||||||
|  |         // Check if a connection URL was mentioned
 | ||||||
|  |         if ( connUrl == null ) { | ||||||
|  | 
 | ||||||
|  |             let connProtocol = `ws://`; | ||||||
|  |             if ( window.location.protocol == `https:` ) { | ||||||
|  |                 connProtocol = `wss://`; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             // Try and connect to the local websocket
 | ||||||
|  |             connUrl = connProtocol + window.location.host + `/ws/get/${padName}`; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Connect to the websocket
 | ||||||
|  |         const ws = new WebSocket(connUrl); | ||||||
|  | 
 | ||||||
|  |         // Bind the onMessage function
 | ||||||
|  |         ws.onmessage = this.handleMessage; | ||||||
|  | 
 | ||||||
|  |         ws.onopen = () => { | ||||||
|  |             updateStatus(`Established`, `text-success`); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         function onFail() { | ||||||
|  |             updateStatus(`Connection Failed`, `text-dangerous`); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Try and reconnect on failure
 | ||||||
|  |         ws.onclose = onFail; | ||||||
|  |         ws.onerror = onFail; | ||||||
|  |          | ||||||
|  |         // Assign the websocket
 | ||||||
|  |         this.ws = ws; | ||||||
|  | 
 | ||||||
|  |         // Get all relevant references from the HTML
 | ||||||
|  |         this.padContents = document.getElementById(`pad-content`); | ||||||
|  |         this.padPreview  = document.getElementById(`textarea-preview`); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * @description Send a message to the server | ||||||
|  |      * @param {string} eventType The type of event, this can be anything really, it's just used for routing by the server | ||||||
|  |      * @param {Object} message The message to send out to the server, this can only be of format string but JSON is parsed. | ||||||
|  |      */ | ||||||
|  |     sendMessage = (eventType, message) => { | ||||||
|  | 
 | ||||||
|  |         if ( this.ws.readyState !== WebSocket.OPEN ) { | ||||||
|  |             throw new Error(`The websocket connection is not active`); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Check if the message is a string
 | ||||||
|  |         if ( typeof message !== 'object' ) { | ||||||
|  |             // Convert the message into a map[string]interface{}
 | ||||||
|  |             message = { | ||||||
|  |                 "message": message, | ||||||
|  |             }; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // TODO: Compress the message, usually we will be sending the whole body of the pad from the client to the server or vice-versa.
 | ||||||
|  |         this.ws.send( JSON.stringify({ | ||||||
|  |             eventType, | ||||||
|  |             padName: this.padName, | ||||||
|  |             message, | ||||||
|  |         })) | ||||||
|  | 
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Handle the message from the socket based on the message type | ||||||
|  |      * @param {MessageEvent} e The websocket message | ||||||
|  |      */ | ||||||
|  |     handleMessage = ev => { | ||||||
|  |         updateStatus(`Catching Message`, `text-white`); | ||||||
|  | 
 | ||||||
|  |         // Check if the message has valid data
 | ||||||
|  |         if ( !!ev.data ) { | ||||||
|  |             // Try and parse the data
 | ||||||
|  |             let parsedData = null; | ||||||
|  | 
 | ||||||
|  |             try {  | ||||||
|  |                 parsedData = JSON.parse(ev.data); | ||||||
|  |             } catch ( err ) { | ||||||
|  |                 console.error(`Failed to parse the WebSocket data`,err); | ||||||
|  |                 updateStatus(`Parse Fail`, `text-warning`); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if ( !!!parsedData['message'] ) { | ||||||
|  |                 console.error(`Failed to find the message`) | ||||||
|  |                 updateStatus(`Message Fail`, `text-warning`); | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             // Check if this is a pad Content Update
 | ||||||
|  |             if ( parsedData['eventType'] === `padUpdate`) { | ||||||
|  |                 // Pass on the parsed data
 | ||||||
|  |                 this.onPadUpdate(parsedData); | ||||||
|  |             } // Check if this is a pad  Status Update 
 | ||||||
|  |             else if ( parsedData['eventType'] === `statusUpdate`) { | ||||||
|  |                 // Pass on the parsed data
 | ||||||
|  |                 this.onStatusUpdate(parsedData); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             updateStatus(`Established`, `text-success`); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Whenever a pad update is trigered, run this function | ||||||
|  |      * @param {Object} The response from the server | ||||||
|  |      */ | ||||||
|  |     onPadUpdate = data => { | ||||||
|  |         // Check that the content is clear
 | ||||||
|  |         if ( !!data['message']['content'] ) { | ||||||
|  |             // Send over the new content to be updated.
 | ||||||
|  |             updatePadContent(data['message']['content']); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     onStatusUpdate = data => { | ||||||
|  |         // Check that the content is clear
 | ||||||
|  |         if ( !!data['message']['currentViewers'] ) { | ||||||
|  |             // Get the amount of viewers reported by the server
 | ||||||
|  |             const viewerCount = Number(data['message']['currentViewers']); | ||||||
|  |             // Check if this is a valid number
 | ||||||
|  |             if ( Number.isNaN(viewerCount) ) { | ||||||
|  |                 // Looks like this is a malformed message
 | ||||||
|  |                 return console.error(`Malformed Message`, data); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             // Send over the new content to be updated.
 | ||||||
|  |             updatePadViewers(viewerCount); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Sending a pad update for each keystroke to the server. | ||||||
|  |      * @param {String} msg The new contents of the pad | ||||||
|  |      */ | ||||||
|  |     sendPadUpdate = msg => { | ||||||
|  |         // Get the contents of the pad
 | ||||||
|  |         const padContents = this.padContents.value; | ||||||
|  | 
 | ||||||
|  |         // Send the data over the webSocket
 | ||||||
|  |         this.sendMessage(`padUpdate`, { | ||||||
|  |             "content": padContents, | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         updatePadContent(padContents, false); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Update the contents of the pad | ||||||
|  |  * @param {String} newContent  | ||||||
|  |  */ | ||||||
|  | function updatePadContent(newContent, textArea = true) { | ||||||
|  |     // Update the textarea
 | ||||||
|  |     if ( textArea ) { | ||||||
|  |         document.getElementById(`pad-content`).value = newContent; | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     // Update the preview
 | ||||||
|  |     const prev = document.getElementById(`textarea-preview`); | ||||||
|  |     const shouldScroll = prev.scrollTop >= (prev.scrollHeight - Number(getComputedStyle(prev).height.replace(/px/g, ''))) * 0.98; | ||||||
|  | 
 | ||||||
|  |     prev.innerHTML = escapeHtml(newContent); | ||||||
|  | 
 | ||||||
|  |     prev.classList.remove(`language-undefined`); | ||||||
|  | 
 | ||||||
|  |     prev.classList.forEach( c => { | ||||||
|  |         if ( c.indexOf(`language-`) != -1 ) { | ||||||
|  |             prev.classList.remove(c); | ||||||
|  |         } | ||||||
|  |     }) | ||||||
|  | 
 | ||||||
|  |     try { // highlights
 | ||||||
|  |         hljs.highlightElement(document.getElementById(`textarea-preview`)); | ||||||
|  |     } catch ( err ) { | ||||||
|  |         console.err(err); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // Check if we should follow the bottom scrolling
 | ||||||
|  |     if (shouldScroll) { | ||||||
|  |         prev.scrollTop = prev.scrollHeight; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function updatePadViewers(vc) { | ||||||
|  |     // Get the reference to the viewers count inputElement
 | ||||||
|  |     /** | ||||||
|  |      * @type {HTMLInputElement} | ||||||
|  |      */ | ||||||
|  |     const viewerCount = document.getElementById(`currentViewers`); | ||||||
|  | 
 | ||||||
|  |     // Get the amount of total viewers
 | ||||||
|  |     const totalViews = viewerCount.value.split("|")[1].trim(); | ||||||
|  | 
 | ||||||
|  |     // Set back the real value
 | ||||||
|  |     viewerCount.value = `${vc} | ${totalViews}`; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function connectSocket() { | ||||||
|  |     // Check if the socket is established
 | ||||||
|  |     if ( !!!window.socket || window.socket.readyState !== WebSocket.OPEN ) { | ||||||
|  |         updateStatus(`Connecting...`, `text-warning`); | ||||||
|  |         // Connect the socket
 | ||||||
|  |         window.socket = new PadSocket(padTitle); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // TODO: Test if this is actually necessary or the DOMContentLoaded event would suffice
 | ||||||
|  | // wait for the whole window to load
 | ||||||
|  | window.addEventListener(`load`, e => { | ||||||
|  |     connectSocket() | ||||||
|  | }) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | // lol
 | ||||||
|  | function escapeHtml(html){ | ||||||
|  |     const text = document.createTextNode(html); | ||||||
|  |     const p = document.createElement('p'); | ||||||
|  | 
 | ||||||
|  |     p.appendChild(text); | ||||||
|  |     const content = p.innerHTML; | ||||||
|  |     p.remove(); | ||||||
|  | 
 | ||||||
|  |     return content; | ||||||
|  |   } | ||||||
|  | @ -35,7 +35,9 @@ | ||||||
| 
 | 
 | ||||||
|         </div> |         </div> | ||||||
| 
 | 
 | ||||||
|         <h2 class="mb-4">{{.title}}</h2> |         <h2 class="mb-4" id="padTitle"> | ||||||
|  |             {{.title}} | ||||||
|  |         </h2> | ||||||
| 
 | 
 | ||||||
|         <div id="pad-content-area"> |         <div id="pad-content-area"> | ||||||
|             <div class="btn-sm btn" id="pad-content-toggler" onclick="toggleTextareaPreview()"> |             <div class="btn-sm btn" id="pad-content-toggler" onclick="toggleTextareaPreview()"> | ||||||
|  | @ -53,8 +55,7 @@ | ||||||
| 
 | 
 | ||||||
|             <pre><code id="textarea-preview" class="form-control hidden">{{.post_content}}</code></pre> |             <pre><code id="textarea-preview" class="form-control hidden">{{.post_content}}</code></pre> | ||||||
| 
 | 
 | ||||||
|             <textarea maxlength="{{.maximumPadSize}}" name="pad-content" id="pad-content" onchange="sendMyData(this)" |             <textarea maxlength="{{.maximumPadSize}}" name="pad-content" id="pad-content" onkeyup="window.socket.sendPadUpdate()" | ||||||
|                 onkeydown="updateStatus(`Not Saved`, `text-warning`); toggleWritingWatch(this)" |  | ||||||
|                 class="form-control hidden">{{.post_content}}</textarea> |                 class="form-control hidden">{{.post_content}}</textarea> | ||||||
|         </div> |         </div> | ||||||
| 
 | 
 | ||||||
|  | @ -73,7 +74,7 @@ | ||||||
|                 </div> |                 </div> | ||||||
|             </div> |             </div> | ||||||
| 
 | 
 | ||||||
|             <div class="col-md-12 col-lg-4 col-xl-4 mt-4 mt-lg-0 mt-xl-0" title="Current Viewers"> |             <div class="col-md-12 col-lg-4 col-xl-4 mt-4 mt-lg-0 mt-xl-0" title="Current Viewers | Total Views"> | ||||||
|                 <div class="input-group"> |                 <div class="input-group"> | ||||||
|                     <span class="input-group-text"> |                     <span class="input-group-text"> | ||||||
|                         <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" |                         <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" | ||||||
|  | @ -86,7 +87,7 @@ | ||||||
|                             </path> |                             </path> | ||||||
|                         </svg> |                         </svg> | ||||||
|                     </span> |                     </span> | ||||||
|                     <input type="text" class="form-control" readonly value="{{.views}}"> |                     <input type="text" class="form-control" readonly value="1 | {{.views}}" id="currentViewers"> | ||||||
|                 </div> |                 </div> | ||||||
|             </div> |             </div> | ||||||
| 
 | 
 | ||||||
|  | @ -209,6 +210,7 @@ | ||||||
|     {{ template "inc/theme-toggle.html" .}} |     {{ template "inc/theme-toggle.html" .}} | ||||||
| </body> | </body> | ||||||
| 
 | 
 | ||||||
|  | <script src="/static/js/ws.js"></script> | ||||||
| <script src="/static/js/fileSaver.js"></script> | <script src="/static/js/fileSaver.js"></script> | ||||||
| <script src="/static/js/pad.js"></script> | <script src="/static/js/pad.js"></script> | ||||||
| <script src="/static/js/pad-scripts.js"></script> | <script src="/static/js/pad-scripts.js"></script> | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue