From cfe2c06daccdc8b66a6a631b9386036e542feb3f Mon Sep 17 00:00:00 2001 From: Kato Twofold Date: Sat, 4 Jun 2022 16:32:53 +0300 Subject: [PATCH 01/14] WebSockets Work started + Implemented Gorilla Sockets + Implemented a javascript class + Golang Struct introduced + JSON parsing from the websocket + Fail handlers --- go.mod | 1 + go.sum | 2 + lib/socketmanager/socketmanager.go | 84 ++++++++++++++++++++++++++++++ main.go | 4 ++ static/js/ws.js | 71 +++++++++++++++++++++++++ templates/pages/page.html | 1 + 6 files changed, 163 insertions(+) create mode 100644 lib/socketmanager/socketmanager.go create mode 100644 static/js/ws.js diff --git a/go.mod b/go.mod index 9f6cd63..43ef10f 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.15 require ( github.com/gin-gonic/gin v1.7.7 + github.com/gorilla/websocket v1.5.0 // indirect github.com/joho/godotenv v1.4.0 github.com/mrz1836/go-sanitize v1.1.5 github.com/ulule/limiter/v3 v3.10.0 diff --git a/go.sum b/go.sum index 8f1cb05..38f7d7f 100644 --- a/go.sum +++ b/go.sum @@ -38,6 +38,8 @@ 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/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +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/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= diff --git a/lib/socketmanager/socketmanager.go b/lib/socketmanager/socketmanager.go new file mode 100644 index 0000000..219ad37 --- /dev/null +++ b/lib/socketmanager/socketmanager.go @@ -0,0 +1,84 @@ +package socketmanager + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/gorilla/websocket" +) + +var wsUpgrader = websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, +} + +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", func(ctx *gin.Context) { + webSocketUpgrade(ctx.Writer, ctx.Request) + }) + +} + +func webSocketUpgrade(w http.ResponseWriter, r *http.Request) { + conn, err := wsUpgrader.Upgrade(w, r, nil) + if err != nil { + fmt.Printf("Failed to set websocket upgrade: %v\n", err) + return + } + + // Start listening to this socket + for { + // Try Read the JSON input from the socket + _, msg, err := conn.ReadMessage() + + // Check if a close request was sent + if errors.Is(err, websocket.ErrCloseSent) { + break + } + + if err != nil { + // There has been an error reading the message + fmt.Println("Failed to read from the socket") + // 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) + } +} + +// Handle the socket's message +func handleSocketMessage(msg SocketMessage) { + + // Check the type of message + fmt.Println(msg.EventType) + +} + +func BroadcastMessage(padName string, message string) { + +} diff --git a/main.go b/main.go index 6549a86..45a1e75 100644 --- a/main.go +++ b/main.go @@ -7,6 +7,7 @@ import ( "github.com/JustKato/FreePad/lib/controllers" "github.com/JustKato/FreePad/lib/objects" "github.com/JustKato/FreePad/lib/routes" + "github.com/JustKato/FreePad/lib/socketmanager" "github.com/gin-gonic/gin" "github.com/joho/godotenv" ) @@ -52,6 +53,9 @@ func main() { // Add Routes routes.HomeRoutes(router) + // Bind the Web Sockets + socketmanager.BindSocket(router.Group("/ws")) + router.Run(":8080") } diff --git a/static/js/ws.js b/static/js/ws.js new file mode 100644 index 0000000..f33ced3 --- /dev/null +++ b/static/js/ws.js @@ -0,0 +1,71 @@ +class PadSocket { + + ws = null; + padName = null; + state = 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) { + + // Check if a connection URL was mentioned + if ( connUrl == null ) { + // Try and connect to the local websocket + connUrl = `ws://` + window.location.host + "/ws/get"; + } + + // Connect to the websocket + const ws = new WebSocket(connUrl); + ws.onopen = () => { + this.state = 'active'; + } + + // Bind the onMessage function + ws.onmessage = this.handleMessage; + + // Assign the websocket + this.ws = ws; + // Assign the pad name + this.padName = padName; + } + + /** + * @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.state != 'active' ) { + throw new Error(`The websocket connection is not active`); + } + + // Check if the message is a string + if ( typeof message == 'string' ) { + // Convert the message into a map[string]interface{} + message = { + "message": message, + }; + } + + this.ws.send( JSON.stringify({ + eventType, + padName: this.padName, + message, + })) + + } + + handleMessage = ev => { + console.log(ev); + } + +} + +// wait for the whole window to load +window.addEventListener(`load`, e => { + window.socket = new PadSocket(padTitle); +}) \ No newline at end of file diff --git a/templates/pages/page.html b/templates/pages/page.html index a0b282d..7870dcf 100644 --- a/templates/pages/page.html +++ b/templates/pages/page.html @@ -209,6 +209,7 @@ {{ template "inc/theme-toggle.html" .}} + From 4138386fb355acb2d623e5a3544c528f929509c6 Mon Sep 17 00:00:00 2001 From: Kato Twofold Date: Sat, 4 Jun 2022 16:36:30 +0300 Subject: [PATCH 02/14] * Some code notes --- lib/socketmanager/socketmanager.go | 5 +++-- static/js/ws.js | 6 ++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/socketmanager/socketmanager.go b/lib/socketmanager/socketmanager.go index 219ad37..f836ad2 100644 --- a/lib/socketmanager/socketmanager.go +++ b/lib/socketmanager/socketmanager.go @@ -11,10 +11,11 @@ import ( ) var wsUpgrader = websocket.Upgrader{ - ReadBufferSize: 1024, - WriteBufferSize: 1024, + ReadBufferSize: 1024, // TODO: Make it configurable via the .env file + WriteBufferSize: 1024, // TODO: Make it configurable via the .env file } +// 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"` diff --git a/static/js/ws.js b/static/js/ws.js index f33ced3..cc9f75c 100644 --- a/static/js/ws.js +++ b/static/js/ws.js @@ -2,6 +2,9 @@ class PadSocket { ws = null; padName = null; + /** + * @deprecated + */ state = null; /** @@ -20,6 +23,7 @@ class PadSocket { // Connect to the websocket const ws = new WebSocket(connUrl); ws.onopen = () => { + // TODO: This is redundant, we could check the websocket status: ws.readyState == WebSocket.OPEN this.state = 'active'; } @@ -51,6 +55,7 @@ class PadSocket { }; } + // 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, @@ -65,6 +70,7 @@ class PadSocket { } +// TODO: Test if this is actually necessary or the DOMContentLoaded event would suffice // wait for the whole window to load window.addEventListener(`load`, e => { window.socket = new PadSocket(padTitle); From 1d3383c8c622ed1d5ee5492b4c469bb02042b754 Mon Sep 17 00:00:00 2001 From: Kato Twofold Date: Mon, 6 Jun 2022 22:49:55 +0300 Subject: [PATCH 03/14] * Removed deprecated variable --- static/js/ws.js | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/static/js/ws.js b/static/js/ws.js index cc9f75c..8fe8e7b 100644 --- a/static/js/ws.js +++ b/static/js/ws.js @@ -2,10 +2,6 @@ class PadSocket { ws = null; padName = null; - /** - * @deprecated - */ - state = null; /** * Create a new PadSocket @@ -22,10 +18,6 @@ class PadSocket { // Connect to the websocket const ws = new WebSocket(connUrl); - ws.onopen = () => { - // TODO: This is redundant, we could check the websocket status: ws.readyState == WebSocket.OPEN - this.state = 'active'; - } // Bind the onMessage function ws.onmessage = this.handleMessage; @@ -43,7 +35,7 @@ class PadSocket { */ sendMessage = (eventType, message) => { - if ( this.state != 'active' ) { + if ( this.ws.readyState !== WebSocket.OPEN ) { throw new Error(`The websocket connection is not active`); } From bf144c6ecb0f7f8368431bd38f911efbb5e35ea1 Mon Sep 17 00:00:00 2001 From: Kato Twofold Date: Mon, 6 Jun 2022 22:50:20 +0300 Subject: [PATCH 04/14] * Fixed possible bug --- static/js/ws.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/js/ws.js b/static/js/ws.js index 8fe8e7b..803cd37 100644 --- a/static/js/ws.js +++ b/static/js/ws.js @@ -40,7 +40,7 @@ class PadSocket { } // Check if the message is a string - if ( typeof message == 'string' ) { + if ( typeof message !== 'object' ) { // Convert the message into a map[string]interface{} message = { "message": message, From 6cc1628e77e669b7b1a2e896017c355ca8b4492c Mon Sep 17 00:00:00 2001 From: Kato Twofold Date: Tue, 7 Jun 2022 00:02:31 +0300 Subject: [PATCH 05/14] Real time WebSocket pasting + Pad update moved to sockets + Pad status updates instantly update TODO: Fix live text highlight --- go.mod | 1 + go.sum | 2 + lib/socketmanager/socketmanager.go | 136 ++++++++++++++++++++++--- static/js/ws.js | 153 ++++++++++++++++++++++++++++- templates/pages/page.html | 7 +- 5 files changed, 278 insertions(+), 21 deletions(-) diff --git a/go.mod b/go.mod index 43ef10f..28bb13b 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.15 require ( 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/mrz1836/go-sanitize v1.1.5 diff --git a/go.sum b/go.sum index 38f7d7f..149ae52 100644 --- a/go.sum +++ b/go.sum @@ -38,6 +38,8 @@ 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/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 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= diff --git a/lib/socketmanager/socketmanager.go b/lib/socketmanager/socketmanager.go index f836ad2..b7d0b98 100644 --- a/lib/socketmanager/socketmanager.go +++ b/lib/socketmanager/socketmanager.go @@ -6,7 +6,9 @@ import ( "fmt" "net/http" + "github.com/JustKato/FreePad/lib/objects" "github.com/gin-gonic/gin" + "github.com/google/uuid" "github.com/gorilla/websocket" ) @@ -15,6 +17,9 @@ var wsUpgrader = websocket.Upgrader{ 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"` @@ -25,32 +30,54 @@ type SocketMessage struct { // Bind the websockets to the gin router func BindSocket(router *gin.RouterGroup) { - router.GET("/get", func(ctx *gin.Context) { - webSocketUpgrade(ctx.Writer, ctx.Request) + 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.Writer, ctx.Request, padName) }) } -func webSocketUpgrade(w http.ResponseWriter, r *http.Request) { +func webSocketUpgrade(w http.ResponseWriter, r *http.Request, padName string) { conn, err := wsUpgrader.Upgrade(w, r, nil) 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 a close request was sent - if errors.Is(err, websocket.ErrCloseSent) { + // 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") + fmt.Println("Failed to read from the socket but probably still connected") // Skip this cycle continue } @@ -67,19 +94,104 @@ func webSocketUpgrade(w http.ResponseWriter, r *http.Request) { } // Pass the message to the proper handlers - - handleSocketMessage(p) + handleSocketMessage(p, socketToken, padName) } } // Handle the socket's message -func handleSocketMessage(msg SocketMessage) { +func handleSocketMessage(msg SocketMessage, socketToken string, padName string) { - // Check the type of message - fmt.Println(msg.EventType) + // 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 BroadcastMessage(padName string, message string) { +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) + } } diff --git a/static/js/ws.js b/static/js/ws.js index 803cd37..c920897 100644 --- a/static/js/ws.js +++ b/static/js/ws.js @@ -1,19 +1,38 @@ class PadSocket { + /** + * @type {WebSocket} + */ ws = null; + /** + * @type {String} + */ padName = null; + /** + * The actual textarea you write in + * @type {HTMLTextAreaElement} + */ + padContents = null; + /** + * The 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 ) { // Try and connect to the local websocket - connUrl = `ws://` + window.location.host + "/ws/get"; + connUrl = `ws://` + window.location.host + `/ws/get/${padName}`; } // Connect to the websocket @@ -22,10 +41,20 @@ class PadSocket { // Bind the onMessage function ws.onmessage = this.handleMessage; + ws.onopen = () => { + updateStatus(`Established`, `text-success`); + } + + // Try and reconnect on failure + ws.onclose = connectSocket; + ws.onerror = connectSocket; + // Assign the websocket this.ws = ws; - // Assign the pad name - this.padName = padName; + + // Get all relevant references from the HTML + this.padContents = document.getElementById(`pad-content`); + this.padPreview = document.getElementById(`textarea-preview`); } /** @@ -56,8 +85,122 @@ class PadSocket { } + /** + * Handle the message from the socket based on the message type + * @param {MessageEvent} e The websocket message + */ handleMessage = ev => { - console.log(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, + }); + } + +} + +/** + * Update the contents of the pad + * @param {String} newContent + */ +function updatePadContent(newContent) { + // Update the textarea + document.getElementById(`pad-content`).value = newContent; + // Update the preview + document.getElementById(`textarea-preview`).innerHTML = newContent; + // TODO: Re-run the syntax highlight + +} + +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); } } @@ -65,5 +208,5 @@ class PadSocket { // TODO: Test if this is actually necessary or the DOMContentLoaded event would suffice // wait for the whole window to load window.addEventListener(`load`, e => { - window.socket = new PadSocket(padTitle); + connectSocket() }) \ No newline at end of file diff --git a/templates/pages/page.html b/templates/pages/page.html index 7870dcf..a92f21d 100644 --- a/templates/pages/page.html +++ b/templates/pages/page.html @@ -53,8 +53,7 @@
- @@ -73,7 +72,7 @@ -
+
- +
From 1b1fe5987719be906a5bfe87790e4c79f9cb68ca Mon Sep 17 00:00:00 2001 From: Kato Twofold Date: Tue, 7 Jun 2022 00:04:49 +0300 Subject: [PATCH 06/14] * Fixed preview not updating --- static/js/ws.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/static/js/ws.js b/static/js/ws.js index c920897..5f8675f 100644 --- a/static/js/ws.js +++ b/static/js/ws.js @@ -164,6 +164,8 @@ class PadSocket { this.sendMessage(`padUpdate`, { "content": padContents, }); + + updatePadContent(padContents, false); } } @@ -172,9 +174,9 @@ class PadSocket { * Update the contents of the pad * @param {String} newContent */ -function updatePadContent(newContent) { +function updatePadContent(newContent, textArea = true) { // Update the textarea - document.getElementById(`pad-content`).value = newContent; + if ( textArea ) document.getElementById(`pad-content`).value = newContent; // Update the preview document.getElementById(`textarea-preview`).innerHTML = newContent; // TODO: Re-run the syntax highlight From 7dcad9dc316d98b7552987d91e616aee40226b56 Mon Sep 17 00:00:00 2001 From: Kato Twofold Date: Tue, 7 Jun 2022 00:16:27 +0300 Subject: [PATCH 07/14] * Archive Fix --- static/js/pad-scripts.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/static/js/pad-scripts.js b/static/js/pad-scripts.js index 3190d98..cb355e9 100644 --- a/static/js/pad-scripts.js +++ b/static/js/pad-scripts.js @@ -138,7 +138,10 @@ function renderArchivesSelection() { let resp = confirm("Load contents of pad from memory? This will overwrite the current pad for everyone."); if (!!resp) { - document.getElementById(`pad-content`).value = a.content; + // Update visually for the client + updatePadContent(a.content); + // Send the update + window.socket.sendPadUpdate(); } }) From ee9516a109a098216d8a5b98a9c25a9ce28d9968 Mon Sep 17 00:00:00 2001 From: Kato Twofold Date: Tue, 7 Jun 2022 00:20:26 +0300 Subject: [PATCH 08/14] * Fixed annoying alignment of edit/view --- static/css/main.css | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/static/css/main.css b/static/css/main.css index 9940596..d15f254 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -45,6 +45,8 @@ main#main-card { tab-size: 2; font-family: 'Roboto Mono', monospace !important; + + padding-top: 2rem; } #pad-content-area { @@ -59,6 +61,7 @@ main#main-card { } .read-only-content .edit-content-text { + margin-top: 1rem; display: block !important; } @@ -78,6 +81,9 @@ main#main-card { max-height: calc(17rem + 30vh); min-height: 17rem; overflow: auto; + + padding-top: 2rem; + margin-top: 1rem; } textarea:focus, From c4f6496e0e4eddcf581b053ed675860619b42317 Mon Sep 17 00:00:00 2001 From: Kato Twofold Date: Tue, 7 Jun 2022 00:27:51 +0300 Subject: [PATCH 09/14] * Restyled the Pad title --- static/css/main.css | 16 ++++++++++++++++ templates/pages/page.html | 4 +++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/static/css/main.css b/static/css/main.css index d15f254..7d74945 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -49,6 +49,22 @@ main#main-card { 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: #fe79b9; + background-color: rgba(0, 0, 0, 0.15); +} + #pad-content-area { position: relative; diff --git a/templates/pages/page.html b/templates/pages/page.html index a92f21d..81e762a 100644 --- a/templates/pages/page.html +++ b/templates/pages/page.html @@ -35,7 +35,9 @@
-

{{.title}}

+

+ {{.title}} +

From 3b137c5ed648fdbd27927b3783d4c21ef81b1338 Mon Sep 17 00:00:00 2001 From: Kato Twofold Date: Tue, 7 Jun 2022 00:28:53 +0300 Subject: [PATCH 10/14] * Fixed invisible edit button on Light Mode --- static/css/main.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/static/css/main.css b/static/css/main.css index 7d74945..3f5145a 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -72,6 +72,10 @@ main#main-card { flex-flow: column; } +.light .edit-content-text { + color: white !important; +} + .edit-content-text { display: none; } From 6e401a416f7d0774d24bceeffee7d98c48bbde3f Mon Sep 17 00:00:00 2001 From: Kato Twofold Date: Tue, 7 Jun 2022 00:29:30 +0300 Subject: [PATCH 11/14] * Fixed light pad title style --- static/css/main.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/static/css/main.css b/static/css/main.css index 3f5145a..81e62ee 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -61,8 +61,8 @@ main#main-card { } .light #padTitle { - color: #fe79b9; - background-color: rgba(0, 0, 0, 0.15); + color: #555273; + border: 1px solid rgba(0, 0, 0, 0.15); } #pad-content-area { From b4c47ded350857cd0a6b86641f5ef9bd5e778897 Mon Sep 17 00:00:00 2001 From: Kato Twofold Date: Tue, 7 Jun 2022 00:37:35 +0300 Subject: [PATCH 12/14] * Better text update follow --- static/js/ws.js | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/static/js/ws.js b/static/js/ws.js index 5f8675f..75a95f5 100644 --- a/static/js/ws.js +++ b/static/js/ws.js @@ -176,9 +176,21 @@ class PadSocket { */ function updatePadContent(newContent, textArea = true) { // Update the textarea - if ( textArea ) document.getElementById(`pad-content`).value = newContent; + if ( textArea ) { + document.getElementById(`pad-content`).value = newContent; + } + // Update the preview - document.getElementById(`textarea-preview`).innerHTML = newContent; + const prev = document.getElementById(`textarea-preview`); + const shouldScroll = prev.scrollTop >= (prev.scrollHeight - Number(getComputedStyle(prev).height.replace(/px/g, ''))) * 0.98; + + prev.innerHTML = newContent; + + // Check if we should follow the bottom scrolling + if (shouldScroll) { + prev.scrollTop = prev.scrollHeight; + } + // TODO: Re-run the syntax highlight } From 0a3b5d50f2aae4f2a90e79bb09a12f30a399ba94 Mon Sep 17 00:00:00 2001 From: Kato Twofold Date: Tue, 7 Jun 2022 00:39:05 +0300 Subject: [PATCH 13/14] * Default to scroll at the bottom of content --- static/js/pad-scripts.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/static/js/pad-scripts.js b/static/js/pad-scripts.js index cb355e9..83b945d 100644 --- a/static/js/pad-scripts.js +++ b/static/js/pad-scripts.js @@ -213,6 +213,8 @@ function setTextareaPreview(t = true) { padContentArea.classList.add(`read-only-content`); + prev.scrollTop = prev.scrollHeight; + textarea.classList.add(`hidden`); } else { // Toggle edit mode From 30dc23c847a3271d8b20d8aa213f33da67c9879f Mon Sep 17 00:00:00 2001 From: Kato Twofold Date: Tue, 7 Jun 2022 00:53:25 +0300 Subject: [PATCH 14/14] * Dynamic updates for highlights --- static/js/ws.js | 33 +++++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/static/js/ws.js b/static/js/ws.js index 75a95f5..3076608 100644 --- a/static/js/ws.js +++ b/static/js/ws.js @@ -184,15 +184,27 @@ function updatePadContent(newContent, textArea = true) { const prev = document.getElementById(`textarea-preview`); const shouldScroll = prev.scrollTop >= (prev.scrollHeight - Number(getComputedStyle(prev).height.replace(/px/g, ''))) * 0.98; - prev.innerHTML = newContent; + 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; } - // TODO: Re-run the syntax highlight - } function updatePadViewers(vc) { @@ -223,4 +235,17 @@ function connectSocket() { // wait for the whole window to load window.addEventListener(`load`, e => { connectSocket() -}) \ No newline at end of file +}) + + +// 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; + } \ No newline at end of file