├── README.md
├── conn.go
├── hub.go
├── LICENSE.md
├── main.go
├── gotail.go
├── home.html
├── home.html.go
└── websocket.article
/README.md:
--------------------------------------------------------------------------------
1 | This demo is based on [fsnotify](https://github.com/howeyc/fsnotify)
2 | and [Implementing Chat with WebSockets and Go](http://gary.beagledreams.com/page/go-websocket-chat.html).
3 | It's the web version of my [gotail](https://github.com/spin6lock/gotail).
4 | The home.html.go is packed by [go-bindata](https://github.com/jteeuwen/go-bindata).
5 | I really enjoy log.io but it's a little bit heavy for me.
6 | So I write my own version with golang. Hope you like it.
7 |
8 | Usage:
9 | ======
10 | $gowebtail -addr=":8081" test.log
11 |
12 | Then you can read it in your browser through http://localhost:8081
13 |
--------------------------------------------------------------------------------
/conn.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "code.google.com/p/go.net/websocket"
5 | "fmt"
6 | "time"
7 | )
8 |
9 | type connection struct {
10 | // The websocket connection.
11 | ws *websocket.Conn
12 |
13 | // Buffered channel of outbound messages.
14 | send chan string
15 | }
16 |
17 | func (c *connection) writer() {
18 | for {
19 | select {
20 | case message := <-c.send:
21 | err := websocket.Message.Send(c.ws, message)
22 | if err != nil {
23 | break
24 | }
25 | }
26 | }
27 | c.ws.Close()
28 | }
29 |
30 | func (c *connection) timeTeller() {
31 | t := time.Tick(2 * time.Second)
32 | for now := range t {
33 | fmt.Println(now)
34 | h.broadcast <- now.String()
35 | }
36 | }
37 |
38 | func wsHandler(ws *websocket.Conn) {
39 | c := &connection{send: make(chan string, 256), ws: ws}
40 | h.register <- c
41 | defer func() { h.unregister <- c }()
42 | c.writer()
43 | }
44 |
--------------------------------------------------------------------------------
/hub.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | type hub struct {
4 | // Registered connections.
5 | connections map[*connection]bool
6 |
7 | // Inbound messages from the connections.
8 | broadcast chan string
9 |
10 | // Register requests from the connections.
11 | register chan *connection
12 |
13 | // Unregister requests from connections.
14 | unregister chan *connection
15 | }
16 |
17 | var h = hub{
18 | broadcast: make(chan string),
19 | register: make(chan *connection),
20 | unregister: make(chan *connection),
21 | connections: make(map[*connection]bool),
22 | }
23 |
24 | func (h *hub) run() {
25 | for {
26 | select {
27 | case c := <-h.register:
28 | h.connections[c] = true
29 | case c := <-h.unregister:
30 | delete(h.connections, c)
31 | close(c.send)
32 | case m := <-h.broadcast:
33 | for c := range h.connections {
34 | select {
35 | case c.send <- m:
36 | default:
37 | delete(h.connections, c)
38 | close(c.send)
39 | go c.ws.Close()
40 | }
41 | }
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2016 John Luk
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "code.google.com/p/go.net/websocket"
5 | "flag"
6 | "fmt"
7 | "github.com/howeyc/fsnotify"
8 | "log"
9 | "net/http"
10 | "os"
11 | "strings"
12 | "text/template"
13 | )
14 |
15 | var homeHtml = string(home_html())
16 | var addr = flag.String("addr", ":8080", "http service address")
17 | var homeTempl = template.Must(template.New("homeHtml").Parse(homeHtml))
18 |
19 | func homeHandler(c http.ResponseWriter, req *http.Request) {
20 | homeTempl.Execute(c, req.Host)
21 | }
22 |
23 | func MonitorMultiFiles(names []string, out chan []string,
24 | h hub) {
25 | watcher, err := fsnotify.NewWatcher()
26 | if err != nil {
27 | log.Fatal(err)
28 | }
29 | for _, name := range names {
30 | result, _ := ReadLastNLines(name, 10)
31 | PrintMultiLines(result)
32 | MonitorFile(name, out, watcher)
33 | }
34 | for {
35 | select {
36 | case lines := <-out:
37 | content := strings.Join(lines, "\n")
38 | fmt.Print(content)
39 | h.broadcast <- content
40 | }
41 | }
42 | watcher.Close()
43 | }
44 |
45 | var usage = func() {
46 | fmt.Fprintf(os.Stderr,
47 | "Usage: gowebtail [FILE]...\n")
48 | flag.PrintDefaults()
49 | os.Exit(2)
50 | }
51 |
52 | func main() {
53 | flag.Usage = usage
54 | flag.Parse()
55 | if len(flag.Args()) < 1 {
56 | usage()
57 | }
58 | out := make(chan []string)
59 | go h.run()
60 | go MonitorMultiFiles(flag.Args(), out, h)
61 | http.HandleFunc("/", homeHandler)
62 | http.Handle("/ws", websocket.Handler(wsHandler))
63 | if err := http.ListenAndServe(*addr, nil); err != nil {
64 | log.Fatal("ListenAndServe:", err)
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/gotail.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "github.com/howeyc/fsnotify"
7 | "log"
8 | "os"
9 | "strings"
10 | )
11 |
12 | func LineCount(lines []byte) int {
13 | return bytes.Count(lines, []byte("\n"))
14 | }
15 |
16 | func GetFileSize(filename string) int {
17 | fileInfo, err := os.Stat(filename)
18 | if err != nil {
19 | log.Fatal(err)
20 | }
21 | return int(fileInfo.Size())
22 | }
23 |
24 | func ByteArrayToMultiLines(bytes []byte) []string {
25 | lines := string(bytes)
26 | return strings.Split(lines, "\n")
27 | }
28 |
29 | func ReadNBytes(filename string, start int, end int) []byte {
30 | fh, _ := os.Open(filename)
31 | defer fh.Close()
32 | fh.Seek(int64(start), 0)
33 | size := end - start + 1
34 | buff := make([]byte, size)
35 | fh.Read(buff)
36 | return buff
37 | }
38 |
39 | func ReadLastNLines(name string, n int) ([]string, error) {
40 | curr := GetFileSize(name)
41 | var end int
42 | count := n
43 | result := make([]byte, n)
44 | for count > 0 && curr != 0 {
45 | curr -= n
46 | end = curr + n - 1
47 | if curr < 0 {
48 | curr = 0
49 | }
50 | buff := ReadNBytes(name, curr, end)
51 | result = append(buff, result...)
52 | count -= LineCount(buff)
53 | }
54 | return ByteArrayToMultiLines(result), nil
55 | }
56 |
57 | func MonitorFile(filename string, out chan []string,
58 | watcher *fsnotify.Watcher) {
59 | size := GetFileSize(filename)
60 | go func() {
61 | for {
62 | select {
63 | case ev := <-watcher.Event:
64 | if ev.IsModify() {
65 | NewSize := GetFileSize(ev.Name)
66 | if NewSize <= size {
67 | MonitorFile(ev.Name, out, watcher)
68 | return
69 | }
70 | content := ReadNBytes(ev.Name, size,
71 | NewSize-1)
72 | size = NewSize
73 | out <- ByteArrayToMultiLines(content)
74 | }
75 | case err := <-watcher.Error:
76 | log.Println("error:", err)
77 | }
78 | }
79 | }()
80 | err := watcher.Watch(filename)
81 | if err != nil {
82 | log.Fatal(err)
83 | }
84 | }
85 |
86 | func PrintMultiLines(lines []string) {
87 | for _, line := range lines {
88 | fmt.Println(line)
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/home.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | gowebtail
4 |
5 |
46 |
83 |
84 |
85 |
86 |
87 |
88 |
--------------------------------------------------------------------------------
/home.html.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "compress/gzip"
6 | "io"
7 | )
8 |
9 | // home_html returns raw, uncompressed file data.
10 | func home_html() []byte {
11 | gz, err := gzip.NewReader(bytes.NewBuffer([]byte{
12 | 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x00, 0xff, 0x94, 0x55,
13 | 0x4d, 0x6f, 0xdb, 0x38, 0x10, 0xbd, 0xfb, 0x57, 0x70, 0x99, 0x2c, 0x20,
14 | 0x1f, 0x56, 0x72, 0xb6, 0xed, 0x45, 0x91, 0x7d, 0xe9, 0xa5, 0x87, 0xde,
15 | 0x12, 0xa0, 0x28, 0x82, 0x1c, 0x28, 0x91, 0x96, 0xe8, 0x50, 0x1c, 0x95,
16 | 0xa4, 0xac, 0x18, 0x81, 0xff, 0x7b, 0x87, 0xfa, 0xb6, 0x63, 0xa3, 0x8d,
17 | 0x0e, 0xb6, 0xf8, 0x86, 0x33, 0xef, 0xcd, 0x70, 0x86, 0x4a, 0x0a, 0x57,
18 | 0xaa, 0xcd, 0x22, 0x29, 0x04, 0xe3, 0xf8, 0xe7, 0xa4, 0x53, 0x62, 0x93,
19 | 0x43, 0x23, 0x52, 0xc7, 0xa4, 0x4a, 0xa2, 0x0e, 0x58, 0x24, 0x36, 0x33,
20 | 0xb2, 0x72, 0xc4, 0x1d, 0x2a, 0xb1, 0xa6, 0x4e, 0xbc, 0xba, 0x68, 0xc7,
21 | 0xf6, 0xac, 0x43, 0x29, 0xb1, 0x26, 0x5b, 0xd3, 0xc2, 0xb9, 0x2a, 0x8e,
22 | 0x22, 0xb6, 0x63, 0xaf, 0x61, 0x0e, 0x90, 0x2b, 0xc1, 0x2a, 0x69, 0xc3,
23 | 0x0c, 0xca, 0x16, 0x8b, 0x94, 0x4c, 0x6d, 0xb4, 0xfb, 0x55, 0x0b, 0x73,
24 | 0x88, 0xee, 0xc2, 0xcf, 0xe1, 0xff, 0xfd, 0x22, 0x2c, 0xa5, 0x0e, 0x77,
25 | 0x96, 0x6e, 0x92, 0xa8, 0x8b, 0xf7, 0x27, 0xba, 0xcd, 0x82, 0xe0, 0x73,
26 | 0x1b, 0x6c, 0x6b, 0x9d, 0x39, 0x09, 0x3a, 0x58, 0x92, 0xb7, 0x45, 0x8b,
27 | 0xed, 0x99, 0x21, 0x19, 0x68, 0x7d, 0x3f, 0xae, 0x4a, 0x9b, 0x93, 0x35,
28 | 0xee, 0xa5, 0x37, 0xf8, 0x46, 0x97, 0x93, 0x41, 0xc1, 0x60, 0xc0, 0x37,
29 | 0x6f, 0x68, 0x2d, 0x43, 0x48, 0xc2, 0xaa, 0x4a, 0x68, 0xfe, 0x1d, 0xf2,
30 | 0x00, 0xfd, 0x7c, 0x7c, 0xd2, 0x3f, 0xde, 0x97, 0xa3, 0x27, 0x7a, 0x3d,
31 | 0xad, 0x9e, 0x4f, 0x61, 0x78, 0xc8, 0x0c, 0x28, 0x85, 0x56, 0x1e, 0xda,
32 | 0xf6, 0xf5, 0x11, 0x2a, 0xb2, 0x9e, 0x96, 0xdf, 0x84, 0xcc, 0x0b, 0x47,
33 | 0xfe, 0x43, 0x20, 0x53, 0x52, 0x68, 0xd7, 0x01, 0xf7, 0x63, 0x18, 0x24,
34 | 0x0b, 0x3b, 0xea, 0x47, 0x08, 0x90, 0x62, 0x39, 0x5a, 0xe4, 0x96, 0x04,
35 | 0x03, 0xc1, 0x5c, 0x8f, 0x7f, 0x4e, 0xd8, 0xfe, 0x9e, 0xec, 0xb8, 0xe8,
36 | 0x7e, 0xfb, 0x72, 0xd2, 0x9b, 0x2d, 0x98, 0x92, 0x2e, 0x43, 0x5b, 0xa7,
37 | 0xa5, 0x74, 0xa7, 0xe5, 0x9d, 0xcb, 0xf8, 0xc7, 0x97, 0xf8, 0x5c, 0x83,
38 | 0x11, 0xae, 0x36, 0x9a, 0x6c, 0x99, 0xb2, 0xe2, 0x9c, 0x62, 0x74, 0xf4,
39 | 0xe9, 0xed, 0x99, 0x0a, 0x96, 0x1f, 0xf5, 0xf6, 0x8c, 0xa1, 0xc5, 0xb2,
40 | 0x04, 0x53, 0x88, 0xd3, 0xaa, 0x79, 0x8c, 0xd2, 0x19, 0x38, 0x0f, 0xd9,
41 | 0x25, 0x3a, 0x1c, 0xb2, 0x97, 0xd2, 0x48, 0xcd, 0xa1, 0x79, 0xa2, 0x3f,
42 | 0x44, 0xfa, 0x00, 0xd9, 0x8b, 0x70, 0xf4, 0x79, 0xae, 0xc9, 0xf3, 0x61,
43 | 0x29, 0xb5, 0x68, 0xc8, 0xb8, 0x23, 0xa0, 0x8d, 0xc5, 0xfe, 0x7e, 0x7b,
44 | 0xbb, 0x3d, 0x1e, 0xa3, 0xc6, 0xce, 0xb9, 0x5a, 0x79, 0xa0, 0x33, 0x05,
45 | 0x56, 0xa0, 0xdb, 0x58, 0x39, 0xb1, 0x77, 0xe7, 0x99, 0x4e, 0x8d, 0x85,
46 | 0x15, 0x4f, 0xb8, 0xdc, 0x6f, 0x92, 0x74, 0xf3, 0x15, 0xfd, 0x45, 0xd7,
47 | 0x78, 0x6d, 0x0c, 0x1e, 0x26, 0x51, 0x8a, 0xf3, 0xe0, 0xcd, 0x74, 0xb9,
48 | 0xbc, 0x56, 0x10, 0xd0, 0xa5, 0xb0, 0x96, 0xe5, 0x1f, 0xe6, 0x8c, 0x30,
49 | 0x6a, 0xe8, 0xc7, 0xca, 0xef, 0x0e, 0x39, 0x73, 0xec, 0x1d, 0xc9, 0x91,
50 | 0x08, 0x2c, 0xdc, 0x2c, 0xd0, 0x45, 0xe1, 0x3f, 0xa1, 0x36, 0x24, 0x35,
51 | 0xd0, 0x58, 0xe1, 0xfb, 0x5f, 0x58, 0xa2, 0xc1, 0x11, 0x5b, 0x57, 0x15,
52 | 0x18, 0x37, 0x95, 0xce, 0x5e, 0xc8, 0xe7, 0x38, 0x1e, 0xca, 0x7c, 0xee,
53 | 0xdd, 0x41, 0x89, 0xf9, 0xd8, 0x67, 0x16, 0x2f, 0x86, 0x85, 0xbf, 0xa4,
54 | 0x7a, 0x29, 0xb0, 0x17, 0x66, 0xab, 0xa0, 0x89, 0x49, 0x21, 0x39, 0x17,
55 | 0x38, 0xea, 0xd8, 0xc0, 0x29, 0xf0, 0xc3, 0x55, 0xbb, 0x47, 0x2b, 0xc6,
56 | 0xb9, 0xd4, 0x79, 0x4c, 0x56, 0xdd, 0xba, 0x64, 0x26, 0x97, 0x7a, 0x5c,
57 | 0x36, 0x92, 0xbb, 0x22, 0x26, 0x77, 0xab, 0xd5, 0xbf, 0x1d, 0x50, 0xb4,
58 | 0xa3, 0x32, 0x47, 0x52, 0x96, 0xbd, 0xe4, 0x06, 0x6a, 0xcd, 0x63, 0x92,
59 | 0x1b, 0x76, 0x68, 0x79, 0xfd, 0xdd, 0xd1, 0xf3, 0xce, 0xed, 0x4d, 0x21,
60 | 0x9d, 0xb8, 0x48, 0x34, 0xe9, 0x08, 0xbf, 0x88, 0xf2, 0xfd, 0x6f, 0xbf,
61 | 0x0b, 0xac, 0xf4, 0x67, 0x19, 0x13, 0x96, 0x5a, 0x50, 0xf5, 0x10, 0xcc,
62 | 0x41, 0x15, 0xcf, 0xf7, 0x29, 0xb1, 0x75, 0x27, 0x80, 0xe9, 0x54, 0xcf,
63 | 0x90, 0x14, 0x9c, 0x83, 0x32, 0x26, 0x9f, 0x06, 0x60, 0xaa, 0x0f, 0xab,
64 | 0x1d, 0x74, 0x59, 0xf8, 0xb9, 0xef, 0xd3, 0x98, 0x04, 0x0e, 0xb2, 0xe6,
65 | 0xd1, 0xce, 0xb3, 0xb9, 0xa2, 0x73, 0x20, 0xbd, 0x3b, 0x13, 0x5a, 0xbd,
66 | 0x5e, 0x29, 0xf7, 0xc5, 0x43, 0xc5, 0xb6, 0xf0, 0xdd, 0x80, 0x5d, 0x11,
67 | 0xf5, 0x5f, 0x27, 0x7f, 0xcc, 0xf8, 0x87, 0x5d, 0x44, 0x24, 0x5f, 0x53,
68 | 0x7f, 0x71, 0xf7, 0x4d, 0x85, 0x7b, 0x7a, 0x63, 0xd4, 0x7d, 0xd0, 0x7e,
69 | 0x07, 0x00, 0x00, 0xff, 0xff, 0xf8, 0x9e, 0xa3, 0x2f, 0xd8, 0x06, 0x00,
70 | 0x00,
71 | }))
72 |
73 | if err != nil {
74 | panic("Decompression failed: " + err.Error())
75 | }
76 |
77 | var b bytes.Buffer
78 | io.Copy(&b, gz)
79 | gz.Close()
80 |
81 | return b.Bytes()
82 | }
83 |
--------------------------------------------------------------------------------
/websocket.article:
--------------------------------------------------------------------------------
1 | Implementing Chat with WebSockets and Go
2 |
3 | Gary Burd
4 | @gburd
5 |
6 | * Introduction
7 |
8 | This example application shows how to use
9 | [[http://www.websockets.org/][WebSockets]], the
10 | [[http://golang.org/][Go programming language]] and
11 | [[http://jquery.com/][jQuery]] to create a simple
12 | web chat application.
13 |
14 | * Running the example
15 |
16 | The example requires a working Go development environment. The
17 | [[http://golang.org/doc/install.html][Getting Started]] page describes
18 | how to install the development environment.
19 |
20 | Once you have Go up and running, you can download, build and run the example
21 | using the following commands.
22 |
23 | go get code.google.com/p/go.net/websocket
24 | git clone git://gist.github.com/1316852.git websocket-example
25 | cd websocket-example
26 | go run *.go
27 |
28 | Open http://127.0.0.1:8080/ in a websocket capable browser to try the application.
29 |
30 | * Server
31 |
32 | The server application is implemented using the
33 | [[http://golang.org/pkg/net/http/][http]] package included with the Go
34 | distribution and the Go Project's
35 | [[http://go.pkgdoc.org/code.google.com/p/go.net/websocket][websocket]] package.
36 |
37 | The application defines two types, `connection` and
38 | `hub`. The server creates an instance of the `connection`
39 | type for each webscocket connection. Connections act as an intermediary
40 | between the websocket and a single instance of the `hub` type. The
41 | hub maintains a set of registered connections and broadcasts messages to the
42 | connections.
43 |
44 | The application runs one goroutine for the hub and two goroutines for each
45 | connection. The goroutines communicate with each other using channels. The hub
46 | has channels for registering connections, unregistering connections and
47 | broadcasting messages. A connection has a buffered channel of outbound messages.
48 | One of the connection's goroutines reads messages from this channel and writes
49 | the messages to the webscoket. The other connection goroutine reads messages
50 | from the websocket and sends them to the hub.
51 |
52 | Here's the code for the `hub` type. A description of the code follows.
53 |
54 | .code hub.go
55 |
56 | The application's `main` function starts the hub `run`
57 | method as a goroutine. Connections send requests to the hub using the
58 | `register`, `unregister` and `broadcast`
59 | channels.
60 |
61 | The hub registers connections by adding the connection pointer as a key in
62 | the `connections` map. The map value is always `true`.
63 |
64 | The unregister code is a little more complicated. In addition to deleting
65 | the connection pointer from the `connections` map, the hub closes
66 | the connection's `send` channel to signal the connection that no
67 | more messages will be sent to the connection.
68 |
69 | The hub handles messages by looping over the registered connections and
70 | sending the message to the connection's `send` channel. If the
71 | connection's `send` buffer is full, then the hub assumes that
72 | the client is dead or stuck. In this case, the hub unregisters the connection
73 | and closes the websocket.
74 |
75 | Here's the code related to the `connection` type.
76 |
77 | .code conn.go
78 |
79 | The `wsHandler` function is registered by the application's
80 | `main` function as a
81 | [[http://golang.org/pkg/websocket/#Handler][websocket handler]]. The
82 | function creates a connection object, registers the connection with the hub and
83 | schedules the connection to be unregistered using a
84 | [[http://weekly.golang.org/doc/effective_go.html#defer][defer]] statement.
85 |
86 | Next, the `wsHandler` function starts the connection's
87 | `writer` method as a goroutine. The `writer` method
88 | transfers messages from the connection's `send` channel to the
89 | websocket. The `writer` method exits when the channel is closed by
90 | the hub or there's an error writing to the websocket.
91 |
92 | Finally, the `wsHandler` function calls the connection's
93 | `reader` method. The `reader` method transfers inbound
94 | messages from the websocket to the hub.
95 |
96 | The remainder of the server code follows:
97 |
98 | .code main.go
99 |
100 | The application's `main` function starts the hub goroutine. Next,
101 | the main function registers handlers for the home page and websocket
102 | connections. Finally, the main function starts the HTTP server.
103 |
104 | * Client
105 |
106 | The client is implemented in a single HTML file.
107 |
108 | .code home.html
109 |
110 | The client uses [[http://jquery.com/][jQuery]] to manipulate
111 | objects in the browser.
112 |
113 | On document load, the script checks for websocket functionality in the
114 | browser. If websocket functionality is available, then the script opens a
115 | connection to the server and registers a callback to handle messages from the
116 | server. The callback appends the message to the chat log using the
117 | `appendLog` function.
118 |
119 | To allow the user to manually scroll through the chat log without interruption
120 | from new messages, the `appendLog` function checks the scroll position before
121 | adding new content. If the chat log is scrolled to the bottom, then the
122 | function scrolls new content into view after adding the content. Otherwise,
123 | the scroll position is not changed.
124 |
125 | The form handler writes the user input to the websocket and clears the input
126 | field.
127 |
--------------------------------------------------------------------------------