├── static ├── favicon.ico ├── images │ ├── rooms │ │ ├── 1.jpg │ │ ├── 2.jpg │ │ ├── 3.jpg │ │ ├── 4.jpg │ │ ├── 5.jpg │ │ └── 6.jpg │ ├── user │ │ ├── 1.png │ │ ├── 10.png │ │ ├── 11.png │ │ ├── 12.png │ │ ├── 2.png │ │ ├── 3.png │ │ ├── 4.png │ │ ├── 5.png │ │ ├── 6.png │ │ ├── 7.png │ │ ├── 8.png │ │ └── 9.png │ ├── header_bg.png │ ├── login_bg.jpg │ ├── theme │ │ ├── 10_bg.jpg │ │ ├── 10_mh.jpg │ │ ├── 10_xs.jpg │ │ ├── 11_bg.jpg │ │ ├── 11_mh.jpg │ │ ├── 11_xs.jpg │ │ ├── 12_bg.jpg │ │ ├── 12_mh.jpg │ │ ├── 12_xs.jpg │ │ ├── 1_bg.jpg │ │ ├── 1_mh.jpg │ │ ├── 1_xs.jpg │ │ ├── 2_bg.jpg │ │ ├── 2_mh.jpg │ │ ├── 2_xs.jpg │ │ ├── 3_bg.jpg │ │ ├── 3_mh.jpg │ │ ├── 3_xs.jpg │ │ ├── 4_bg.jpg │ │ ├── 4_mh.jpg │ │ ├── 4_xs.jpg │ │ ├── 5_bg.jpg │ │ ├── 5_mh.jpg │ │ ├── 5_xs.jpg │ │ ├── 6_bg.jpg │ │ ├── 6_mh.jpg │ │ ├── 6_xs.jpg │ │ ├── 7_bg.jpg │ │ ├── 7_mh.jpg │ │ ├── 7_xs.jpg │ │ ├── 8_bg.jpg │ │ ├── 8_mh.jpg │ │ ├── 8_xs.jpg │ │ ├── 9_bg.jpg │ │ ├── 9_mh.jpg │ │ └── 9_xs.jpg │ └── room_input_bg.png ├── static_global_val.go ├── javascripts │ ├── load-msg-more.js │ ├── websocket-heartbeat.js │ └── Public.js ├── rolling │ ├── css │ │ └── rolling.css │ └── js │ │ └── rolling.js ├── stylesheets │ └── style.css └── emoji │ └── emojionearea.min.css ├── .gitattributes ├── .gitignore ├── services ├── img_upload_interface.go ├── validator │ └── validator.go ├── safe │ └── safe.go ├── message_service │ └── message.go ├── img_upload_connector │ └── img_factory.go ├── helper │ └── helper.go ├── user_service │ └── user.go ├── session │ └── session.go ├── img_bb │ └── imgBBImage.go ├── img_freeimage │ └── imgFreeImage.go └── sm_app │ └── smApp.go ├── ws ├── ServeInterface.go ├── go_ws │ ├── time_task.go │ └── serve.go ├── primary │ └── start.go ├── ws_test │ ├── exec.go │ └── mock_ws_client_coon.go └── serve.go ├── models ├── mysql.go ├── user.go └── message.go ├── views ├── temp_global_var.go ├── index.html ├── login.html ├── private_chat.html └── room.html ├── conf └── config.go.env ├── main.go ├── LICENSE.txt ├── routes └── route.go ├── controller ├── ImageController.go └── IndexController.go ├── sql └── go_gin_chat.sql ├── go.mod └── readme.md /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hezhizheng/go-gin-chat/HEAD/static/favicon.ico -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.js linguist-language=Go 2 | *.css linguist-language=Go 3 | *.html linguist-language=Go -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | /conf/config.go 3 | /tmp 4 | /tmp_images 5 | /bindata 6 | *.exe 7 | *.exe~ 8 | .air.toml -------------------------------------------------------------------------------- /static/images/rooms/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hezhizheng/go-gin-chat/HEAD/static/images/rooms/1.jpg -------------------------------------------------------------------------------- /static/images/rooms/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hezhizheng/go-gin-chat/HEAD/static/images/rooms/2.jpg -------------------------------------------------------------------------------- /static/images/rooms/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hezhizheng/go-gin-chat/HEAD/static/images/rooms/3.jpg -------------------------------------------------------------------------------- /static/images/rooms/4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hezhizheng/go-gin-chat/HEAD/static/images/rooms/4.jpg -------------------------------------------------------------------------------- /static/images/rooms/5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hezhizheng/go-gin-chat/HEAD/static/images/rooms/5.jpg -------------------------------------------------------------------------------- /static/images/rooms/6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hezhizheng/go-gin-chat/HEAD/static/images/rooms/6.jpg -------------------------------------------------------------------------------- /static/images/user/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hezhizheng/go-gin-chat/HEAD/static/images/user/1.png -------------------------------------------------------------------------------- /static/images/user/10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hezhizheng/go-gin-chat/HEAD/static/images/user/10.png -------------------------------------------------------------------------------- /static/images/user/11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hezhizheng/go-gin-chat/HEAD/static/images/user/11.png -------------------------------------------------------------------------------- /static/images/user/12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hezhizheng/go-gin-chat/HEAD/static/images/user/12.png -------------------------------------------------------------------------------- /static/images/user/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hezhizheng/go-gin-chat/HEAD/static/images/user/2.png -------------------------------------------------------------------------------- /static/images/user/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hezhizheng/go-gin-chat/HEAD/static/images/user/3.png -------------------------------------------------------------------------------- /static/images/user/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hezhizheng/go-gin-chat/HEAD/static/images/user/4.png -------------------------------------------------------------------------------- /static/images/user/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hezhizheng/go-gin-chat/HEAD/static/images/user/5.png -------------------------------------------------------------------------------- /static/images/user/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hezhizheng/go-gin-chat/HEAD/static/images/user/6.png -------------------------------------------------------------------------------- /static/images/user/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hezhizheng/go-gin-chat/HEAD/static/images/user/7.png -------------------------------------------------------------------------------- /static/images/user/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hezhizheng/go-gin-chat/HEAD/static/images/user/8.png -------------------------------------------------------------------------------- /static/images/user/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hezhizheng/go-gin-chat/HEAD/static/images/user/9.png -------------------------------------------------------------------------------- /static/images/header_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hezhizheng/go-gin-chat/HEAD/static/images/header_bg.png -------------------------------------------------------------------------------- /static/images/login_bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hezhizheng/go-gin-chat/HEAD/static/images/login_bg.jpg -------------------------------------------------------------------------------- /static/images/theme/10_bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hezhizheng/go-gin-chat/HEAD/static/images/theme/10_bg.jpg -------------------------------------------------------------------------------- /static/images/theme/10_mh.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hezhizheng/go-gin-chat/HEAD/static/images/theme/10_mh.jpg -------------------------------------------------------------------------------- /static/images/theme/10_xs.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hezhizheng/go-gin-chat/HEAD/static/images/theme/10_xs.jpg -------------------------------------------------------------------------------- /static/images/theme/11_bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hezhizheng/go-gin-chat/HEAD/static/images/theme/11_bg.jpg -------------------------------------------------------------------------------- /static/images/theme/11_mh.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hezhizheng/go-gin-chat/HEAD/static/images/theme/11_mh.jpg -------------------------------------------------------------------------------- /static/images/theme/11_xs.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hezhizheng/go-gin-chat/HEAD/static/images/theme/11_xs.jpg -------------------------------------------------------------------------------- /static/images/theme/12_bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hezhizheng/go-gin-chat/HEAD/static/images/theme/12_bg.jpg -------------------------------------------------------------------------------- /static/images/theme/12_mh.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hezhizheng/go-gin-chat/HEAD/static/images/theme/12_mh.jpg -------------------------------------------------------------------------------- /static/images/theme/12_xs.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hezhizheng/go-gin-chat/HEAD/static/images/theme/12_xs.jpg -------------------------------------------------------------------------------- /static/images/theme/1_bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hezhizheng/go-gin-chat/HEAD/static/images/theme/1_bg.jpg -------------------------------------------------------------------------------- /static/images/theme/1_mh.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hezhizheng/go-gin-chat/HEAD/static/images/theme/1_mh.jpg -------------------------------------------------------------------------------- /static/images/theme/1_xs.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hezhizheng/go-gin-chat/HEAD/static/images/theme/1_xs.jpg -------------------------------------------------------------------------------- /static/images/theme/2_bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hezhizheng/go-gin-chat/HEAD/static/images/theme/2_bg.jpg -------------------------------------------------------------------------------- /static/images/theme/2_mh.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hezhizheng/go-gin-chat/HEAD/static/images/theme/2_mh.jpg -------------------------------------------------------------------------------- /static/images/theme/2_xs.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hezhizheng/go-gin-chat/HEAD/static/images/theme/2_xs.jpg -------------------------------------------------------------------------------- /static/images/theme/3_bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hezhizheng/go-gin-chat/HEAD/static/images/theme/3_bg.jpg -------------------------------------------------------------------------------- /static/images/theme/3_mh.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hezhizheng/go-gin-chat/HEAD/static/images/theme/3_mh.jpg -------------------------------------------------------------------------------- /static/images/theme/3_xs.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hezhizheng/go-gin-chat/HEAD/static/images/theme/3_xs.jpg -------------------------------------------------------------------------------- /static/images/theme/4_bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hezhizheng/go-gin-chat/HEAD/static/images/theme/4_bg.jpg -------------------------------------------------------------------------------- /static/images/theme/4_mh.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hezhizheng/go-gin-chat/HEAD/static/images/theme/4_mh.jpg -------------------------------------------------------------------------------- /static/images/theme/4_xs.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hezhizheng/go-gin-chat/HEAD/static/images/theme/4_xs.jpg -------------------------------------------------------------------------------- /static/images/theme/5_bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hezhizheng/go-gin-chat/HEAD/static/images/theme/5_bg.jpg -------------------------------------------------------------------------------- /static/images/theme/5_mh.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hezhizheng/go-gin-chat/HEAD/static/images/theme/5_mh.jpg -------------------------------------------------------------------------------- /static/images/theme/5_xs.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hezhizheng/go-gin-chat/HEAD/static/images/theme/5_xs.jpg -------------------------------------------------------------------------------- /static/images/theme/6_bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hezhizheng/go-gin-chat/HEAD/static/images/theme/6_bg.jpg -------------------------------------------------------------------------------- /static/images/theme/6_mh.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hezhizheng/go-gin-chat/HEAD/static/images/theme/6_mh.jpg -------------------------------------------------------------------------------- /static/images/theme/6_xs.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hezhizheng/go-gin-chat/HEAD/static/images/theme/6_xs.jpg -------------------------------------------------------------------------------- /static/images/theme/7_bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hezhizheng/go-gin-chat/HEAD/static/images/theme/7_bg.jpg -------------------------------------------------------------------------------- /static/images/theme/7_mh.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hezhizheng/go-gin-chat/HEAD/static/images/theme/7_mh.jpg -------------------------------------------------------------------------------- /static/images/theme/7_xs.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hezhizheng/go-gin-chat/HEAD/static/images/theme/7_xs.jpg -------------------------------------------------------------------------------- /static/images/theme/8_bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hezhizheng/go-gin-chat/HEAD/static/images/theme/8_bg.jpg -------------------------------------------------------------------------------- /static/images/theme/8_mh.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hezhizheng/go-gin-chat/HEAD/static/images/theme/8_mh.jpg -------------------------------------------------------------------------------- /static/images/theme/8_xs.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hezhizheng/go-gin-chat/HEAD/static/images/theme/8_xs.jpg -------------------------------------------------------------------------------- /static/images/theme/9_bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hezhizheng/go-gin-chat/HEAD/static/images/theme/9_bg.jpg -------------------------------------------------------------------------------- /static/images/theme/9_mh.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hezhizheng/go-gin-chat/HEAD/static/images/theme/9_mh.jpg -------------------------------------------------------------------------------- /static/images/theme/9_xs.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hezhizheng/go-gin-chat/HEAD/static/images/theme/9_xs.jpg -------------------------------------------------------------------------------- /static/static_global_val.go: -------------------------------------------------------------------------------- 1 | package static 2 | 3 | import "embed" 4 | 5 | //go:embed * 6 | var EmbedStatic embed.FS -------------------------------------------------------------------------------- /static/images/room_input_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hezhizheng/go-gin-chat/HEAD/static/images/room_input_bg.png -------------------------------------------------------------------------------- /services/img_upload_interface.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | type ImgUploadInterface interface { 4 | Upload(filename string) string 5 | } 6 | -------------------------------------------------------------------------------- /ws/ServeInterface.go: -------------------------------------------------------------------------------- 1 | package ws 2 | 3 | import "github.com/gin-gonic/gin" 4 | 5 | type ServeInterface interface { 6 | RunWs(gin *gin.Context) 7 | GetOnlineUserCount() int 8 | GetOnlineRoomUserCount(roomId int) int 9 | } 10 | -------------------------------------------------------------------------------- /services/validator/validator.go: -------------------------------------------------------------------------------- 1 | package validator 2 | 3 | type User struct { 4 | Username string `binding:"required,max=16,min=2"` 5 | Password string `binding:"required,max=32,min=6"` 6 | AvatarId string `binding:"required,numeric"` 7 | } 8 | -------------------------------------------------------------------------------- /services/safe/safe.go: -------------------------------------------------------------------------------- 1 | package safe 2 | 3 | import "sync" 4 | 5 | var Safety ThreadSafety 6 | 7 | type ThreadSafety struct { 8 | mu sync.Mutex 9 | } 10 | 11 | func (receiver *ThreadSafety) Do(x func() interface{}) interface{} { 12 | receiver.mu.Lock() 13 | defer receiver.mu.Unlock() 14 | return x() 15 | } 16 | -------------------------------------------------------------------------------- /ws/go_ws/time_task.go: -------------------------------------------------------------------------------- 1 | package go_ws 2 | 3 | import ( 4 | "github.com/robfig/cron/v3" 5 | ) 6 | 7 | func CleanOfflineConn() { 8 | 9 | c := cron.New() 10 | 11 | // 每天定时执行的条件 12 | spec := `* * * * *` 13 | 14 | c.AddFunc(spec, func() { 15 | // fmt.Println("CleanOfflineConn") 16 | HandelOfflineCoon() 17 | }) 18 | 19 | go c.Start() 20 | } 21 | -------------------------------------------------------------------------------- /services/message_service/message.go: -------------------------------------------------------------------------------- 1 | package message_service 2 | 3 | import "go-gin-chat/models" 4 | 5 | func GetLimitMsg(roomId string, offset int) []map[string]interface{} { 6 | return models.GetLimitMsg(roomId,offset) 7 | } 8 | 9 | func GetLimitPrivateMsg(uid, toUId string , offset int) []map[string]interface{} { 10 | return models.GetLimitPrivateMsg(uid, toUId,offset) 11 | } -------------------------------------------------------------------------------- /models/mysql.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/spf13/viper" 5 | "gorm.io/driver/mysql" 6 | "gorm.io/gorm" 7 | ) 8 | 9 | var ChatDB *gorm.DB 10 | 11 | func InitDB() { 12 | // 参考 https://github.com/go-sql-driver/mysql#dsn-data-source-name 获取详情 13 | dsn := viper.GetString(`mysql.dsn`) 14 | ChatDB, _ = gorm.Open(mysql.Open(dsn), &gorm.Config{}) 15 | return 16 | } 17 | -------------------------------------------------------------------------------- /views/temp_global_var.go: -------------------------------------------------------------------------------- 1 | package views 2 | 3 | import ( 4 | "embed" 5 | "html/template" 6 | ) 7 | 8 | // main 函数中定义的全局变量 在其他包中无法调用 ,重新定义新包实现全局变量 9 | // go:embed 不支持相对路径,只能获取当前目录下的目录或文件 10 | var ( 11 | //go:embed *.html 12 | embedTmpl embed.FS 13 | 14 | // 自定义的函数必须在调用ParseFiles() ParseFS()之前创建。 15 | funcMap = template.FuncMap{} 16 | GoTpl = template.Must( 17 | template.New(""). 18 | Funcs(funcMap). 19 | ParseFS(embedTmpl, "*.html")) 20 | ) 21 | -------------------------------------------------------------------------------- /services/img_upload_connector/img_factory.go: -------------------------------------------------------------------------------- 1 | package img_upload_connector 2 | 3 | import ( 4 | "go-gin-chat/services" 5 | "go-gin-chat/services/img_bb" 6 | "go-gin-chat/services/img_freeimage" 7 | "go-gin-chat/services/sm_app" 8 | ) 9 | 10 | // 定义 serve 的映射关系 11 | var serveMap = map[string]services.ImgUploadInterface{ 12 | "img_bb": &img_bb.ImgBBImageService{}, 13 | "fi": &img_freeimage.ImgFreeImageService{}, 14 | "sm": &sm_app.SmAppService{}, 15 | } 16 | 17 | func ImgCreate(imgType string) services.ImgUploadInterface { 18 | typeMap := "img_bb" 19 | if imgType != "" { 20 | typeMap = imgType 21 | } 22 | return serveMap[typeMap] 23 | } 24 | -------------------------------------------------------------------------------- /ws/primary/start.go: -------------------------------------------------------------------------------- 1 | package primary 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/spf13/viper" 6 | "go-gin-chat/ws" 7 | "go-gin-chat/ws/go_ws" 8 | ) 9 | 10 | // 定义 serve 的映射关系 11 | var serveMap = map[string]ws.ServeInterface{ 12 | "Serve": &ws.Serve{}, 13 | "GoServe": &go_ws.GoServe{}, 14 | } 15 | 16 | func Create() ws.ServeInterface { 17 | // GoServe or Serve 18 | _type := viper.GetString("app.serve_type") 19 | return serveMap[_type] 20 | } 21 | 22 | func Start(gin *gin.Context) { 23 | Create().RunWs(gin) 24 | } 25 | 26 | func OnlineUserCount() int { 27 | return Create().GetOnlineUserCount() 28 | } 29 | 30 | func OnlineRoomUserCount(roomId int) int { 31 | return Create().GetOnlineRoomUserCount(roomId) 32 | } -------------------------------------------------------------------------------- /conf/config.go.env: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | /*参数说明 4 | app.port // 应用端口 5 | app.upload_file_path // 图片上传的临时文件夹目录,绝对路径! 6 | app.cookie_key // 生成加密session 7 | app.serve_type // 默认请使用GoServe 8 | app.sm_token // smms.app 图床token 9 | mysql.dsn // mysql 连接地址dsn 10 | app.debug_mod // 开发模式建议设置为`true` 避免修改静态资源需要重启服务 11 | */ 12 | 13 | var AppJsonConfig = []byte(` 14 | { 15 | "app": { 16 | "port": "8322", 17 | "upload_file_path": "e:\\golang\\www\\go-gin-chat\\tmp_images\\", 18 | "cookie_key": "4238uihfieh49r3453kjdfg", 19 | "serve_type": "GoServe", 20 | "sm_token": "xxxxxxxxxxx", 21 | "debug_mod": "true" 22 | }, 23 | "mysql": { 24 | "dsn": "admin:admin@tcp(127.0.0.1:3306)/go_gin_chat?charset=utf8mb4&parseTime=True&loc=Local" 25 | } 26 | } 27 | `) -------------------------------------------------------------------------------- /services/helper/helper.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import ( 4 | "crypto/md5" 5 | "encoding/hex" 6 | "unicode/utf8" 7 | ) 8 | 9 | func InArray(needle interface{}, hystack interface{}) bool { 10 | switch key := needle.(type) { 11 | case string: 12 | for _, item := range hystack.([]string) { 13 | if key == item { 14 | return true 15 | } 16 | } 17 | case int: 18 | for _, item := range hystack.([]int) { 19 | if key == item { 20 | return true 21 | } 22 | } 23 | case int64: 24 | for _, item := range hystack.([]int64) { 25 | if key == item { 26 | return true 27 | } 28 | } 29 | default: 30 | return false 31 | } 32 | return false 33 | } 34 | 35 | func Md5Encrypt(s string) string { 36 | m := md5.New() 37 | m.Write([]byte (s)) 38 | return hex.EncodeToString(m.Sum(nil)) 39 | } 40 | 41 | func MbStrLen(str string) int { 42 | return utf8.RuneCountInString(str) 43 | } 44 | -------------------------------------------------------------------------------- /ws/ws_test/exec.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os/exec" 6 | "strconv" 7 | "sync" 8 | ) 9 | 10 | var wg = sync.WaitGroup{} 11 | var ch = make(chan int, 20) 12 | 13 | func main() { 14 | 15 | for i := 500; i <= 600; i++ { 16 | wg.Add(1) 17 | go execCommand(i) 18 | } 19 | 20 | wg.Wait() 21 | 22 | log.Println("okkkkkkkkkkkkkkkkkk") 23 | } 24 | 25 | func execCommand(i int) { 26 | 27 | defer func() { 28 | //捕获read抛出的panic 29 | if err := recover();err!=nil{ 30 | log.Println("execCommand",err) 31 | } 32 | }() 33 | 34 | ch <- i 35 | strI := strconv.Itoa(i) 36 | //cmd := exec.Command("./mock_ws_client_coon.exe", strI) 37 | cmd := exec.Command("E:\\go1.15.2.windows-386\\go\\bin\\go.exe", 38 | "run", 39 | "D:\\phpstudy_pro\\WWW\\org\\public-go-gin-chat\\ws\\ws_test\\mock_ws_client_coon.go", 40 | strI) 41 | 42 | err := cmd.Start() 43 | 44 | if err != nil { 45 | log.Println(err) 46 | } 47 | 48 | log.Println(i) 49 | 50 | //time.Sleep(time.Second * 1) 51 | <-ch 52 | wg.Done() 53 | } 54 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "github.com/gin-gonic/gin" 6 | "github.com/spf13/viper" 7 | "go-gin-chat/conf" 8 | "go-gin-chat/models" 9 | "go-gin-chat/routes" 10 | "go-gin-chat/views" 11 | "log" 12 | "net/http" 13 | ) 14 | 15 | func init() { 16 | 17 | viper.SetConfigType("json") // 设置配置文件的类型 18 | 19 | if err := viper.ReadConfig(bytes.NewBuffer(conf.AppJsonConfig)); err != nil { 20 | if _, ok := err.(viper.ConfigFileNotFoundError); ok { 21 | // Config file not found; ignore error if desired 22 | log.Println("no such config file") 23 | } else { 24 | // Config file was found but another error was produced 25 | log.Println("read config error") 26 | } 27 | log.Fatal(err) // 读取配置文件失败致命错误 28 | } 29 | 30 | models.InitDB() 31 | } 32 | 33 | func main() { 34 | // 关闭debug模式 35 | gin.SetMode(gin.ReleaseMode) 36 | 37 | port := viper.GetString(`app.port`) 38 | router := routes.InitRoute() 39 | 40 | //加载模板文件 41 | router.SetHTMLTemplate(views.GoTpl) 42 | 43 | //go_ws.CleanOfflineConn() 44 | 45 | log.Println("监听端口", "http://127.0.0.1:"+port) 46 | 47 | http.ListenAndServe(":"+port, router) 48 | } 49 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) DexterHo(HeZhiZheng) 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. -------------------------------------------------------------------------------- /ws/ws_test/mock_ws_client_coon.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "github.com/gorilla/websocket" 6 | "log" 7 | "net/url" 8 | "os" 9 | "strconv" 10 | ) 11 | 12 | 13 | func start() { 14 | var addr = flag.String("addr", "localhost:8322", "http service address") 15 | 16 | flag.Parse() 17 | log.SetFlags(0) 18 | 19 | u := url.URL{Scheme: "ws", Host: *addr, Path: "/ws"} 20 | log.Printf("connecting to %s", u.String()) 21 | 22 | c, _, err := websocket.DefaultDialer.Dial(u.String(), nil) 23 | 24 | if err != nil { 25 | log.Fatal("dial:", err) 26 | } 27 | defer c.Close() 28 | 29 | p := os.Args 30 | 31 | log.Println("Args",p) 32 | 33 | d := make(map[string]interface{}) 34 | d["status"] = 1 35 | 36 | // string转成int64: 37 | uid, _ := strconv.ParseFloat(p[1], 64) 38 | 39 | d["data"] = map[string]interface{}{ 40 | "uid": uid, 41 | "room_id": "1", 42 | "avatar_id": "4", 43 | "username": "suiji"+p[1], 44 | } 45 | 46 | c.WriteJSON(d) 47 | 48 | for { 49 | _, message, err := c.ReadMessage() 50 | if err != nil { 51 | log.Println("read:", err) 52 | break 53 | } 54 | log.Printf("recv: %s", message) 55 | } 56 | 57 | } 58 | 59 | func main() { 60 | start() 61 | } -------------------------------------------------------------------------------- /models/user.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "gorm.io/gorm" 5 | "time" 6 | ) 7 | 8 | type User struct { 9 | gorm.Model 10 | ID uint 11 | Username string `json:"username"` 12 | Password string `json:"password"` 13 | AvatarId string `json:"avatar_id"` 14 | CreatedAt time.Time `time_format:"2006-01-02 15:04:05"` 15 | UpdatedAt time.Time `time_format:"2006-01-02 15:04:05"` 16 | } 17 | 18 | func AddUser(value interface{}) User { 19 | var u User 20 | u.Username = value.(map[string]interface{})["username"].(string) 21 | u.Password = value.(map[string]interface{})["password"].(string) 22 | u.AvatarId = value.(map[string]interface{})["avatar_id"].(string) 23 | ChatDB.Create(&u) 24 | return u 25 | } 26 | 27 | func SaveAvatarId(AvatarId string, u User) User { 28 | u.AvatarId = AvatarId 29 | ChatDB.Save(&u) 30 | return u 31 | } 32 | 33 | func FindUserByField(field, value string) User { 34 | var u User 35 | 36 | if field == "id" || field == "username" { 37 | ChatDB.Where(field+" = ?", value).First(&u) 38 | } 39 | 40 | return u 41 | } 42 | 43 | func GetOnlineUserList(uids []float64 ) []map[string]interface{} { 44 | var results []map[string]interface{} 45 | ChatDB.Where("id IN ?", uids).Find(&results) 46 | 47 | return results 48 | } 49 | -------------------------------------------------------------------------------- /routes/route.go: -------------------------------------------------------------------------------- 1 | package routes 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/spf13/viper" 6 | "go-gin-chat/controller" 7 | "go-gin-chat/services/session" 8 | "go-gin-chat/static" 9 | "go-gin-chat/ws/primary" 10 | "net/http" 11 | ) 12 | 13 | func InitRoute() *gin.Engine { 14 | //router := gin.Default() 15 | router := gin.New() 16 | 17 | if viper.GetString(`app.debug_mod`) == "false" { 18 | // live 模式 打包用 19 | router.StaticFS("/static", http.FS(static.EmbedStatic)) 20 | }else{ 21 | // dev 开发用 避免修改静态资源需要重启服务 22 | router.StaticFS("/static", http.Dir("static")) 23 | } 24 | 25 | sr := router.Group("/", session.EnableCookieSession()) 26 | { 27 | sr.GET("/", controller.Index) 28 | 29 | sr.POST("/login", controller.Login) 30 | sr.GET("/logout", controller.Logout) 31 | sr.GET("/ws", primary.Start) 32 | 33 | authorized := sr.Group("/", session.AuthSessionMiddle()) 34 | { 35 | //authorized.GET("/ws", ws.Run) 36 | authorized.GET("/home", controller.Home) 37 | authorized.GET("/room/:room_id", controller.Room) 38 | authorized.GET("/private-chat", controller.PrivateChat) 39 | authorized.POST("/img-kr-upload", controller.ImgKrUpload) 40 | authorized.GET("/pagination", controller.Pagination) 41 | } 42 | 43 | } 44 | 45 | return router 46 | } 47 | -------------------------------------------------------------------------------- /controller/ImageController.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "fmt" 5 | "github.com/gin-gonic/gin" 6 | "github.com/spf13/viper" 7 | "go-gin-chat/services/img_upload_connector" 8 | "net/http" 9 | "os" 10 | ) 11 | 12 | func ImgKrUpload(c *gin.Context) { 13 | file, err := c.FormFile("file") 14 | if err != nil { 15 | c.String(http.StatusBadRequest, fmt.Sprintf("get form err: %s", err.Error())) 16 | return 17 | } 18 | 19 | filepath := viper.GetString(`app.upload_file_path`) 20 | 21 | if _, err := os.Stat(filepath); err != nil { 22 | if !os.IsExist(err) { 23 | os.MkdirAll(filepath, os.ModePerm) 24 | } 25 | } 26 | 27 | filename := filepath + file.Filename 28 | 29 | if err := c.SaveUploadedFile(file, filename); err != nil { 30 | c.String(http.StatusBadRequest, fmt.Sprintf("upload file err: %s", err.Error())) 31 | return 32 | } 33 | 34 | krUpload := img_upload_connector.ImgCreate("").Upload(filename) 35 | if krUpload == "" { 36 | krUpload = img_upload_connector.ImgCreate("fi").Upload(filename) 37 | } 38 | if krUpload == "" { 39 | krUpload = img_upload_connector.ImgCreate("sm").Upload(filename) 40 | } 41 | 42 | // 删除临时图片 43 | os.Remove(filename) 44 | 45 | c.JSON(http.StatusOK, gin.H{ 46 | "code": 0, 47 | "data": map[string]interface{}{ 48 | "url": krUpload, 49 | }, 50 | }) 51 | } 52 | -------------------------------------------------------------------------------- /services/user_service/user.go: -------------------------------------------------------------------------------- 1 | package user_service 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "go-gin-chat/models" 6 | "go-gin-chat/services/helper" 7 | "go-gin-chat/services/session" 8 | "go-gin-chat/services/validator" 9 | "net/http" 10 | "strconv" 11 | ) 12 | 13 | func Login(c *gin.Context) { 14 | 15 | username := c.PostForm("username") 16 | pwd := c.PostForm("password") 17 | avatarId := c.PostForm("avatar_id") 18 | 19 | var u validator.User 20 | 21 | u.Username = username 22 | u.Password = pwd 23 | u.AvatarId = avatarId 24 | 25 | if err := c.ShouldBind(&u); err != nil { 26 | c.JSON(http.StatusOK, gin.H{"code": 5000,"msg": err.Error()}) 27 | return 28 | } 29 | 30 | user := models.FindUserByField("username", username) 31 | userInfo := user 32 | md5Pwd := helper.Md5Encrypt(pwd) 33 | 34 | if userInfo.ID > 0 { 35 | // json 用户存在 36 | // 验证密码 37 | if userInfo.Password != md5Pwd { 38 | c.JSON(http.StatusOK, gin.H{ 39 | "code": 5000, 40 | "msg": "密码错误", 41 | }) 42 | return 43 | } 44 | 45 | models.SaveAvatarId(avatarId,user) 46 | 47 | } else { 48 | // 新用户 49 | userInfo = models.AddUser(map[string]interface{}{ 50 | "username": username, 51 | "password": md5Pwd, 52 | "avatar_id": avatarId, 53 | }) 54 | } 55 | 56 | if userInfo.ID > 0 { 57 | session.SaveAuthSession(c, string(strconv.Itoa(int(userInfo.ID)))) 58 | c.JSON(http.StatusOK, gin.H{ 59 | "code": 0, 60 | }) 61 | return 62 | } else { 63 | c.JSON(http.StatusOK, gin.H{ 64 | "code": 5001, 65 | "msg": "系统错误", 66 | }) 67 | return 68 | } 69 | } 70 | 71 | func GetUserInfo(c *gin.Context) map[string]interface{} { 72 | return session.GetSessionUserInfo(c) 73 | } 74 | 75 | func Logout(c *gin.Context) { 76 | session.ClearAuthSession(c) 77 | c.Redirect(http.StatusFound,"/") 78 | return 79 | } -------------------------------------------------------------------------------- /views/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | go-gin-chat 聊天室 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 |
19 | 25 | 31 | 32 |
33 |
34 |
35 | 36 | {{range .rooms}} 37 | 38 | 44 | 45 | {{ end }} 46 |
47 |
48 |
49 | 50 | -------------------------------------------------------------------------------- /sql/go_gin_chat.sql: -------------------------------------------------------------------------------- 1 | 2 | 3 | SET NAMES utf8mb4; 4 | SET FOREIGN_KEY_CHECKS = 0; 5 | 6 | -- ---------------------------- 7 | -- Table structure for messages 8 | -- ---------------------------- 9 | DROP TABLE IF EXISTS `messages`; 10 | CREATE TABLE `messages` ( 11 | `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT, 12 | `user_id` int(11) NOT NULL COMMENT '用户ID', 13 | `room_id` int(11) NOT NULL COMMENT '房间ID', 14 | `to_user_id` int(11) NULL DEFAULT 0 COMMENT '私聊用户ID', 15 | `content` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '聊天内容', 16 | `image_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '图片URL', 17 | `created_at` datetime(0) NULL DEFAULT NULL, 18 | `updated_at` datetime(0) NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP(0), 19 | `deleted_at` datetime(0) NULL DEFAULT NULL, 20 | PRIMARY KEY (`id`) USING BTREE, 21 | INDEX `idx_user_id`(`user_id`) USING BTREE 22 | ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; 23 | 24 | -- ---------------------------- 25 | -- Table structure for users 26 | -- ---------------------------- 27 | DROP TABLE IF EXISTS `users`; 28 | CREATE TABLE `users` ( 29 | `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT, 30 | `username` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '昵称', 31 | `password` varchar(125) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '密码', 32 | `avatar_id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '1' COMMENT '头像ID', 33 | `created_at` datetime(0) NULL DEFAULT NULL, 34 | `updated_at` datetime(0) NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP(0), 35 | `deleted_at` datetime(0) NULL DEFAULT NULL, 36 | PRIMARY KEY (`id`) USING BTREE, 37 | UNIQUE INDEX `username`(`username`) USING BTREE 38 | ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; 39 | 40 | SET FOREIGN_KEY_CHECKS = 1; 41 | -------------------------------------------------------------------------------- /services/session/session.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import ( 4 | "github.com/gin-contrib/sessions" 5 | "github.com/gin-contrib/sessions/cookie" 6 | "github.com/gin-gonic/gin" 7 | "github.com/spf13/viper" 8 | "go-gin-chat/models" 9 | "net/http" 10 | "strconv" 11 | ) 12 | 13 | func EnableCookieSession() gin.HandlerFunc { 14 | store := cookie.NewStore([]byte(viper.GetString(`app.cookie_key`))) 15 | return sessions.Sessions("go-gin-chat", store) 16 | } 17 | 18 | // 注册和登陆时都需要保存seesion信息 19 | func SaveAuthSession(c *gin.Context, info interface{}) { 20 | session := sessions.Default(c) 21 | session.Set("uid", info) 22 | // c.SetCookie("user_id",string(info.(map[string]interface{})["b"].(uint)), 1000, "/", "localhost", false, true) 23 | session.Save() 24 | } 25 | 26 | func GetSessionUserInfo(c *gin.Context) map[string]interface{} { 27 | session := sessions.Default(c) 28 | 29 | uid := session.Get("uid") 30 | 31 | data := make(map[string]interface{}) 32 | if uid != nil { 33 | user := models.FindUserByField("id", uid.(string)) 34 | data["uid"] = user.ID 35 | data["username"] = user.Username 36 | data["avatar_id"] = user.AvatarId 37 | } 38 | return data 39 | } 40 | 41 | // 退出时清除session 42 | func ClearAuthSession(c *gin.Context) { 43 | session := sessions.Default(c) 44 | session.Clear() 45 | session.Save() 46 | } 47 | 48 | func HasSession(c *gin.Context) bool { 49 | session := sessions.Default(c) 50 | if sessionValue := session.Get("uid"); sessionValue == nil { 51 | return false 52 | } 53 | return true 54 | } 55 | 56 | func AuthSessionMiddle() gin.HandlerFunc { 57 | return func(c *gin.Context) { 58 | session := sessions.Default(c) 59 | sessionValue := session.Get("uid") 60 | if sessionValue == nil { 61 | c.Redirect(http.StatusFound, "/") 62 | return 63 | } 64 | 65 | uidInt, _ := strconv.Atoi(sessionValue.(string)) 66 | 67 | if uidInt <= 0 { 68 | c.Redirect(http.StatusFound, "/") 69 | return 70 | } 71 | 72 | // 设置简单的变量 73 | c.Set("uid", sessionValue) 74 | 75 | c.Next() 76 | return 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /services/img_bb/imgBBImage.go: -------------------------------------------------------------------------------- 1 | package img_bb 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "github.com/valyala/fasthttp" 7 | "go-gin-chat/services" 8 | "io" 9 | "log" 10 | "mime/multipart" 11 | "os" 12 | "path" 13 | ) 14 | 15 | type ImgBBImageService struct { 16 | services.ImgUploadInterface 17 | } 18 | 19 | func (serve *ImgBBImageService) Upload(filename string) string { 20 | return Upload(filename) 21 | } 22 | 23 | func Upload(uploadFile string) string { 24 | 25 | bodyBufer := &bytes.Buffer{} 26 | //创建一个multipart文件写入器,方便按照http规定格式写入内容 27 | bodyWriter := multipart.NewWriter(bodyBufer) 28 | bodyWriter.WriteField("type", "file") 29 | bodyWriter.WriteField("action", "upload") 30 | //从bodyWriter生成fileWriter,并将文件内容写入fileWriter,多个文件可进行多次 31 | fileWriter, err := bodyWriter.CreateFormFile("source", path.Base(uploadFile)) 32 | 33 | if err != nil { 34 | log.Println(err) 35 | return "" 36 | } 37 | 38 | file, err2 := os.Open(uploadFile) 39 | if err2 != nil { 40 | log.Println(err2) 41 | return "" 42 | } 43 | //不要忘记关闭打开的文件 44 | defer file.Close() 45 | _, err3 := io.Copy(fileWriter, file) 46 | if err3 != nil { 47 | log.Println(err3) 48 | return "" 49 | } 50 | 51 | //关闭bodyWriter停止写入数据 52 | bodyWriter.Close() 53 | 54 | contentType := bodyWriter.FormDataContentType() 55 | //构建request,发送请求 56 | request := fasthttp.AcquireRequest() 57 | response := fasthttp.AcquireResponse() 58 | 59 | defer func() { 60 | // 用完需要释放资源 61 | fasthttp.ReleaseResponse(response) 62 | fasthttp.ReleaseRequest(request) 63 | }() 64 | 65 | request.Header.SetContentType(contentType) 66 | //直接将构建好的数据放入post的body中 67 | request.SetBody(bodyBufer.Bytes()) 68 | request.Header.SetMethod("POST") 69 | 70 | request.SetRequestURI("https://imgbb.com/json") 71 | err4 := fasthttp.Do(request, response) 72 | if err4 != nil { 73 | log.Println(err4) 74 | return "" 75 | } 76 | 77 | var res map[string]interface{} 78 | e := json.Unmarshal(response.Body(), &res) 79 | if e != nil { 80 | log.Println(e, string(response.Body())) 81 | return "" 82 | } 83 | 84 | if _, ok := res["image"]; ok { 85 | // process q 86 | if _, set := res["image"].(map[string]interface{})["display_url"]; set { 87 | return res["image"].(map[string]interface{})["display_url"].(string) 88 | } 89 | } else { 90 | log.Println(res) 91 | } 92 | 93 | return "" 94 | } 95 | -------------------------------------------------------------------------------- /services/img_freeimage/imgFreeImage.go: -------------------------------------------------------------------------------- 1 | package img_freeimage 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "github.com/valyala/fasthttp" 7 | "go-gin-chat/services" 8 | "io" 9 | "log" 10 | "mime/multipart" 11 | "os" 12 | "path" 13 | ) 14 | 15 | type ImgFreeImageService struct { 16 | services.ImgUploadInterface 17 | } 18 | 19 | func (serve *ImgFreeImageService) Upload(filename string) string { 20 | return Upload(filename) 21 | } 22 | 23 | func Upload(uploadFile string) string { 24 | 25 | bodyBufer := &bytes.Buffer{} 26 | //创建一个multipart文件写入器,方便按照http规定格式写入内容 27 | bodyWriter := multipart.NewWriter(bodyBufer) 28 | bodyWriter.WriteField("type", "file") 29 | bodyWriter.WriteField("action", "upload") 30 | //从bodyWriter生成fileWriter,并将文件内容写入fileWriter,多个文件可进行多次 31 | fileWriter, err := bodyWriter.CreateFormFile("source", path.Base(uploadFile)) 32 | 33 | if err != nil { 34 | log.Println(err) 35 | return "" 36 | } 37 | 38 | file, err2 := os.Open(uploadFile) 39 | if err2 != nil { 40 | log.Println(err2) 41 | return "" 42 | } 43 | //不要忘记关闭打开的文件 44 | defer file.Close() 45 | _, err3 := io.Copy(fileWriter, file) 46 | if err3 != nil { 47 | log.Println(err3) 48 | return "" 49 | } 50 | 51 | //关闭bodyWriter停止写入数据 52 | bodyWriter.Close() 53 | 54 | contentType := bodyWriter.FormDataContentType() 55 | //构建request,发送请求 56 | request := fasthttp.AcquireRequest() 57 | response := fasthttp.AcquireResponse() 58 | 59 | defer func() { 60 | // 用完需要释放资源 61 | fasthttp.ReleaseResponse(response) 62 | fasthttp.ReleaseRequest(request) 63 | }() 64 | 65 | request.Header.SetContentType(contentType) 66 | //直接将构建好的数据放入post的body中 67 | request.SetBody(bodyBufer.Bytes()) 68 | request.Header.SetMethod("POST") 69 | 70 | request.SetRequestURI("https://freeimage.host/json") 71 | err4 := fasthttp.Do(request, response) 72 | if err4 != nil { 73 | log.Println(err4) 74 | return "" 75 | } 76 | 77 | var res map[string]interface{} 78 | e := json.Unmarshal(response.Body(), &res) 79 | if e != nil { 80 | log.Println(e, string(response.Body())) 81 | return "" 82 | } 83 | 84 | if _, ok := res["image"]; ok { 85 | // process q 86 | if _, set := res["image"].(map[string]interface{})["display_url"]; set { 87 | return res["image"].(map[string]interface{})["display_url"].(string) 88 | } 89 | } else { 90 | log.Println(res) 91 | } 92 | 93 | return "" 94 | } 95 | -------------------------------------------------------------------------------- /models/message.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "gorm.io/gorm" 5 | "sort" 6 | "strconv" 7 | "time" 8 | ) 9 | 10 | type Message struct { 11 | gorm.Model 12 | ID uint 13 | UserId int 14 | ToUserId int 15 | RoomId int 16 | Content string 17 | ImageUrl string 18 | CreatedAt time.Time 19 | UpdatedAt time.Time 20 | } 21 | 22 | func SaveContent(value interface{}) Message { 23 | var m Message 24 | m.UserId = value.(map[string]interface{})["user_id"].(int) 25 | m.ToUserId = value.(map[string]interface{})["to_user_id"].(int) 26 | m.Content = value.(map[string]interface{})["content"].(string) 27 | 28 | roomIdStr := value.(map[string]interface{})["room_id"].(string) 29 | 30 | roomIdInt, _ := strconv.Atoi(roomIdStr) 31 | 32 | m.RoomId = roomIdInt 33 | 34 | if _, ok := value.(map[string]interface{})["image_url"]; ok { 35 | m.ImageUrl = value.(map[string]interface{})["image_url"].(string) 36 | } 37 | 38 | ChatDB.Create(&m) 39 | return m 40 | } 41 | 42 | func GetLimitMsg(roomId string,offset int) []map[string]interface{} { 43 | 44 | var results []map[string]interface{} 45 | ChatDB.Model(&Message{}). 46 | Select("messages.*, users.username ,users.avatar_id"). 47 | Joins("INNER Join users on users.id = messages.user_id"). 48 | Where("messages.room_id = " + roomId). 49 | Where("messages.to_user_id = 0"). 50 | Order("messages.id desc"). 51 | Offset(offset). 52 | Limit(100). 53 | Scan(&results) 54 | 55 | if offset == 0{ 56 | sort.Slice(results, func(i, j int) bool { 57 | return results[i]["id"].(uint32) < results[j]["id"].(uint32) 58 | }) 59 | } 60 | 61 | return results 62 | } 63 | 64 | func GetLimitPrivateMsg(uid, toUId string,offset int) []map[string]interface{} { 65 | 66 | var results []map[string]interface{} 67 | ChatDB.Model(&Message{}). 68 | Select("messages.*, users.username ,users.avatar_id"). 69 | Joins("INNER Join users on users.id = messages.user_id"). 70 | Where("(" + 71 | "(" + "messages.user_id = " + uid + " and messages.to_user_id=" + toUId + ")" + 72 | " or " + 73 | "(" + "messages.user_id = " + toUId + " and messages.to_user_id=" + uid + ")" + 74 | ")"). 75 | Order("messages.id desc"). 76 | Offset(offset). 77 | Limit(100). 78 | Scan(&results) 79 | 80 | if offset == 0{ 81 | sort.Slice(results, func(i, j int) bool { 82 | return results[i]["id"].(uint32) < results[j]["id"].(uint32) 83 | }) 84 | } 85 | 86 | return results 87 | } 88 | -------------------------------------------------------------------------------- /services/sm_app/smApp.go: -------------------------------------------------------------------------------- 1 | package sm_app 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "github.com/spf13/viper" 7 | "github.com/valyala/fasthttp" 8 | "go-gin-chat/services" 9 | "io" 10 | "log" 11 | "mime/multipart" 12 | "os" 13 | "path" 14 | ) 15 | 16 | type SmAppService struct { 17 | services.ImgUploadInterface 18 | } 19 | 20 | func (serve *SmAppService) Upload(filename string) string { 21 | return Upload(filename) 22 | } 23 | 24 | func Upload(uploadFile string) string { 25 | 26 | bodyBufer := &bytes.Buffer{} 27 | //创建一个multipart文件写入器,方便按照http规定格式写入内容 28 | bodyWriter := multipart.NewWriter(bodyBufer) 29 | //bodyWriter.WriteField("type", "file") 30 | bodyWriter.WriteField("format", "json") 31 | //从bodyWriter生成fileWriter,并将文件内容写入fileWriter,多个文件可进行多次 32 | fileWriter, err := bodyWriter.CreateFormFile("smfile", path.Base(uploadFile)) 33 | 34 | if err != nil { 35 | log.Println(err) 36 | return "" 37 | } 38 | 39 | file, err2 := os.Open(uploadFile) 40 | if err2 != nil { 41 | log.Println(err2) 42 | return "" 43 | } 44 | //不要忘记关闭打开的文件 45 | defer file.Close() 46 | _, err3 := io.Copy(fileWriter, file) 47 | if err3 != nil { 48 | log.Println(err3) 49 | return "" 50 | } 51 | 52 | //关闭bodyWriter停止写入数据 53 | bodyWriter.Close() 54 | 55 | contentType := bodyWriter.FormDataContentType() 56 | //构建request,发送请求 57 | request := fasthttp.AcquireRequest() 58 | response := fasthttp.AcquireResponse() 59 | 60 | defer func() { 61 | // 用完需要释放资源 62 | fasthttp.ReleaseResponse(response) 63 | fasthttp.ReleaseRequest(request) 64 | }() 65 | 66 | request.Header.SetContentType(contentType) 67 | smToken := viper.GetString(`app.sm_token`) 68 | request.Header.Set("Authorization", smToken) 69 | //直接将构建好的数据放入post的body中 70 | request.SetBody(bodyBufer.Bytes()) 71 | request.Header.SetMethod("POST") 72 | 73 | request.SetRequestURI("https://smms.app/api/v2/upload") 74 | err4 := fasthttp.Do(request, response) 75 | if err4 != nil { 76 | log.Println(err4) 77 | return "" 78 | } 79 | 80 | var res map[string]interface{} 81 | e := json.Unmarshal(response.Body(), &res) 82 | if e != nil { 83 | log.Println(e, string(response.Body())) 84 | return "" 85 | } 86 | 87 | if _, ok := res["data"]; ok { 88 | // process q 89 | if _, set := res["data"].(map[string]interface{})["url"]; set { 90 | return res["data"].(map[string]interface{})["url"].(string) 91 | } 92 | } else if _, set := res["images"]; set { // 图片已存在 93 | return res["images"].(string) 94 | } else { 95 | log.Println(res) 96 | } 97 | 98 | return "" 99 | } 100 | -------------------------------------------------------------------------------- /static/javascripts/load-msg-more.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function(){ 2 | 3 | $(document).on('click', '#chat-list-li-top', function() { 4 | // $("#chat-list-li-top").click(function (){ 5 | let offset = $("#hidden-chat-list-li-top").attr("data-offset") 6 | let room_id = $('.room').attr('data-room_id') 7 | let uid = getURLParam('uid') 8 | $.ajax({ 9 | url: '/pagination?room_id='+room_id+'&offset='+offset+'&uid='+uid, 10 | success: function(data) { 11 | //设置数据 12 | var item = data.data.list 13 | if ( item == null ) 14 | { 15 | layer.msg('没有更多了!') 16 | $("#hidden-chat-list-li-top").attr("data-offset",offset) 17 | $("#chat-list-li-top").hide() 18 | return false 19 | } 20 | 21 | 22 | $.each(item,function (index, value) { 23 | if ( value.user_id == $("#body-room").attr("data-uid") ) 24 | { 25 | $('#chat-list-li-top').after( 26 | '
  • ' + 29 | value.username + 30 | '' + 31 | value.created_at + 32 | '
    ' + 33 | value.content+ 34 | '
  • '); 35 | }else{ 36 | $('#chat-list-li-top').after( 37 | '
  • ' + 40 | value.username + 41 | '' + 42 | value.created_at + 43 | '
    ' + 44 | value.content+ 45 | '
  • '); 46 | } 47 | 48 | }) 49 | $("#hidden-chat-list-li-top").attr("data-offset",parseInt(offset)+100) 50 | 51 | }, 52 | error: function(data) { 53 | 54 | } 55 | }); 56 | }) 57 | }) 58 | 59 | 60 | var getURLParam = function(name) { 61 | return decodeURIComponent((new RegExp('[?|&]' + name + '=' + '([^&;]+?)(&|#|;|$)', "ig").exec(location.search) || [, ""])[1].replace(/\+/g, '%20')) || ''; 62 | }; -------------------------------------------------------------------------------- /static/javascripts/websocket-heartbeat.js: -------------------------------------------------------------------------------- 1 | !function(e,t){if("object"==typeof exports&&"object"==typeof module)module.exports=t();else if("function"==typeof define&&define.amd)define([],t);else{var o=t();for(var n in o)("object"==typeof exports?exports:e)[n]=o[n]}}(window,function(){return function(e){var t={};function o(n){if(t[n])return t[n].exports;var i=t[n]={i:n,l:!1,exports:{}};return e[n].call(i.exports,i,i.exports,o),i.l=!0,i.exports}return o.m=e,o.c=t,o.d=function(e,t,n){o.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},o.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},o.t=function(e,t){if(1&t&&(e=o(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(o.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var i in e)o.d(n,i,function(t){return e[t]}.bind(null,i));return n},o.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return o.d(t,"a",t),t},o.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},o.p="",o(o.s=0)}([function(e,t,o){"use strict";function n(e){var t=e.url,o=e.pingTimeout,n=void 0===o?15e3:o,i=e.pongTimeout,r=void 0===i?1e4:i,c=e.reconnectTimeout,s=void 0===c?2e3:c,u=e.pingMsg,p=void 0===u?"heartbeat":u,f=e.repeatLimit,a=void 0===f?null:f;this.opts={url:t,pingTimeout:n,pongTimeout:r,reconnectTimeout:s,pingMsg:p,repeatLimit:a},this.ws=null,this.repeat=0,this.onclose=function(){},this.onerror=function(){},this.onopen=function(){},this.onmessage=function(){},this.onreconnect=function(){},this.createWebSocket()}Object.defineProperty(t,"__esModule",{value:!0}),n.prototype.createWebSocket=function(){try{this.ws=new WebSocket(this.opts.url),this.initEventHandle()}catch(e){throw this.reconnect(),e}},n.prototype.initEventHandle=function(){var e=this;this.ws.onclose=function(){e.onclose(),e.reconnect()},this.ws.onerror=function(){e.onerror(),e.reconnect()},this.ws.onopen=function(){e.repeat=0,e.onopen(),e.heartCheck()},this.ws.onmessage=function(t){e.onmessage(t),e.heartCheck()}},n.prototype.reconnect=function(){var e=this;this.opts.repeatLimit>0&&this.opts.repeatLimit<=this.repeat||this.lockReconnect||this.forbidReconnect||(this.lockReconnect=!0,this.repeat++,this.onreconnect(),setTimeout(function(){e.createWebSocket(),e.lockReconnect=!1},this.opts.reconnectTimeout))},n.prototype.send=function(e){this.ws.send(e)},n.prototype.heartCheck=function(){this.heartReset(),this.heartStart()},n.prototype.heartStart=function(){var e=this;this.forbidReconnect||(this.pingTimeoutId=setTimeout(function(){e.ws.send(e.opts.pingMsg),e.pongTimeoutId=setTimeout(function(){e.ws.close()},e.opts.pongTimeout)},this.opts.pingTimeout))},n.prototype.heartReset=function(){clearTimeout(this.pingTimeoutId),clearTimeout(this.pongTimeoutId)},n.prototype.close=function(){this.forbidReconnect=!0,this.heartReset(),this.ws.close()},"undefined"!=typeof window&&(window.WebsocketHeartbeatJs=n),t.default=n}])}); -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module go-gin-chat 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/gin-contrib/sessions v0.0.5 7 | github.com/gin-gonic/gin v1.8.1 8 | github.com/gorilla/websocket v1.5.0 9 | github.com/jianfengye/collection v1.4.1 10 | github.com/robfig/cron/v3 v3.0.0 11 | github.com/spf13/viper v1.12.0 12 | github.com/valyala/fasthttp v1.37.0 13 | gorm.io/driver/mysql v1.3.4 14 | gorm.io/gorm v1.23.6 15 | ) 16 | 17 | require ( 18 | github.com/andybalholm/brotli v1.0.4 // indirect 19 | github.com/fsnotify/fsnotify v1.5.4 // indirect 20 | github.com/gin-contrib/sse v0.1.0 // indirect 21 | github.com/go-playground/locales v0.14.0 // indirect 22 | github.com/go-playground/universal-translator v0.18.0 // indirect 23 | github.com/go-playground/validator/v10 v10.11.0 // indirect 24 | github.com/go-sql-driver/mysql v1.6.0 // indirect 25 | github.com/goccy/go-json v0.9.7 // indirect 26 | github.com/golang/protobuf v1.5.2 // indirect 27 | github.com/gorilla/context v1.1.1 // indirect 28 | github.com/gorilla/securecookie v1.1.1 // indirect 29 | github.com/gorilla/sessions v1.2.1 // indirect 30 | github.com/hashicorp/hcl v1.0.0 // indirect 31 | github.com/howeyc/fsnotify v0.9.0 // indirect 32 | github.com/jessevdk/go-assets v0.0.0-20160921144138-4f4301a06e15 // indirect 33 | github.com/jessevdk/go-assets-builder v0.0.0-20130903091706-b8483521738f // indirect 34 | github.com/jessevdk/go-flags v1.4.0 // indirect 35 | github.com/jinzhu/inflection v1.0.0 // indirect 36 | github.com/jinzhu/now v1.1.5 // indirect 37 | github.com/json-iterator/go v1.1.12 // indirect 38 | github.com/klauspost/compress v1.15.6 // indirect 39 | github.com/leodido/go-urn v1.2.1 // indirect 40 | github.com/magiconair/properties v1.8.6 // indirect 41 | github.com/mattn/go-colorable v0.1.12 // indirect 42 | github.com/mattn/go-isatty v0.0.14 // indirect 43 | github.com/mitchellh/gox v1.0.1 // indirect 44 | github.com/mitchellh/mapstructure v1.5.0 // indirect 45 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 46 | github.com/modern-go/reflect2 v1.0.2 // indirect 47 | github.com/pelletier/go-toml v1.9.5 // indirect 48 | github.com/pelletier/go-toml/v2 v2.0.2 // indirect 49 | github.com/pilu/config v0.0.0-20131214182432-3eb99e6c0b9a // indirect 50 | github.com/pilu/fresh v0.0.0-20190826141211-0fa698148017 // indirect 51 | github.com/pkg/errors v0.9.1 // indirect 52 | github.com/spf13/afero v1.8.2 // indirect 53 | github.com/spf13/cast v1.5.0 // indirect 54 | github.com/spf13/jwalterweatherman v1.1.0 // indirect 55 | github.com/spf13/pflag v1.0.5 // indirect 56 | github.com/subosito/gotenv v1.4.0 // indirect 57 | github.com/ugorji/go v1.2.7 // indirect 58 | github.com/ugorji/go/codec v1.2.7 // indirect 59 | github.com/valyala/bytebufferpool v1.0.0 // indirect 60 | golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e // indirect 61 | golang.org/x/net v0.0.0-20220615171555-694bf12d69de // indirect 62 | golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c // indirect 63 | golang.org/x/text v0.3.7 // indirect 64 | google.golang.org/protobuf v1.28.0 // indirect 65 | gopkg.in/ini.v1 v1.66.6 // indirect 66 | gopkg.in/yaml.v2 v2.4.0 // indirect 67 | gopkg.in/yaml.v3 v3.0.1 // indirect 68 | gorm.io/driver/sqlite v1.1.4 // indirect 69 | ) 70 | -------------------------------------------------------------------------------- /controller/IndexController.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "go-gin-chat/services/helper" 6 | "go-gin-chat/services/message_service" 7 | "go-gin-chat/services/user_service" 8 | "go-gin-chat/ws/primary" 9 | "net/http" 10 | "strconv" 11 | ) 12 | 13 | func Index(c *gin.Context) { 14 | // 已登录跳转room界面,多页面应该考虑放在中间件实现 15 | userInfo := user_service.GetUserInfo(c) 16 | if len(userInfo) > 0 { 17 | c.Redirect(http.StatusFound, "/home") 18 | return 19 | } 20 | 21 | OnlineUserCount := primary.OnlineUserCount() 22 | 23 | c.HTML(http.StatusOK, "login.html", gin.H{ 24 | "OnlineUserCount": OnlineUserCount, 25 | }) 26 | } 27 | 28 | func Login(c *gin.Context) { 29 | user_service.Login(c) 30 | } 31 | 32 | func Logout(c *gin.Context) { 33 | user_service.Logout(c) 34 | } 35 | 36 | func Home(c *gin.Context) { 37 | userInfo := user_service.GetUserInfo(c) 38 | rooms := []map[string]interface{}{ 39 | {"id": 1, "num": primary.OnlineRoomUserCount(1)}, 40 | {"id": 2, "num": primary.OnlineRoomUserCount(2)}, 41 | {"id": 3, "num": primary.OnlineRoomUserCount(3)}, 42 | {"id": 4, "num": primary.OnlineRoomUserCount(4)}, 43 | {"id": 5, "num": primary.OnlineRoomUserCount(5)}, 44 | {"id": 6, "num": primary.OnlineRoomUserCount(6)}, 45 | } 46 | 47 | c.HTML(http.StatusOK, "index.html", gin.H{ 48 | "rooms": rooms, 49 | "user_info": userInfo, 50 | }) 51 | } 52 | 53 | func Room(c *gin.Context) { 54 | roomId := c.Param("room_id") 55 | 56 | rooms := []string{"1", "2", "3", "4", "5", "6"} 57 | 58 | if !helper.InArray(roomId, rooms) { 59 | c.Redirect(http.StatusFound, "/room/1") 60 | return 61 | } 62 | 63 | userInfo := user_service.GetUserInfo(c) 64 | msgList := message_service.GetLimitMsg(roomId, 0) 65 | 66 | c.HTML(http.StatusOK, "room.html", gin.H{ 67 | "user_info": userInfo, 68 | "msg_list": msgList, 69 | "msg_list_count": len(msgList), 70 | "room_id": roomId, 71 | }) 72 | } 73 | 74 | func PrivateChat(c *gin.Context) { 75 | 76 | roomId := c.Query("room_id") 77 | toUid := c.Query("uid") 78 | 79 | userInfo := user_service.GetUserInfo(c) 80 | 81 | uid := strconv.Itoa(int(userInfo["uid"].(uint))) 82 | 83 | msgList := message_service.GetLimitPrivateMsg(uid, toUid, 0) 84 | 85 | c.HTML(http.StatusOK, "private_chat.html", gin.H{ 86 | "user_info": userInfo, 87 | "msg_list": msgList, 88 | "room_id": roomId, 89 | }) 90 | } 91 | 92 | func Pagination(c *gin.Context) { 93 | roomId := c.Query("room_id") 94 | toUid := c.Query("uid") 95 | offset := c.Query("offset") 96 | offsetInt, e := strconv.Atoi(offset) 97 | if e != nil || offsetInt <= 0 { 98 | offsetInt = 0 99 | } 100 | 101 | rooms := []string{"1", "2", "3", "4", "5", "6"} 102 | 103 | if !helper.InArray(roomId, rooms) { 104 | c.JSON(http.StatusOK, gin.H{ 105 | "code": 0, 106 | "data": map[string]interface{}{ 107 | "list": nil, 108 | }, 109 | }) 110 | return 111 | } 112 | 113 | msgList := []map[string]interface{}{} 114 | if toUid != "" { 115 | userInfo := user_service.GetUserInfo(c) 116 | 117 | uid := strconv.Itoa(int(userInfo["uid"].(uint))) 118 | 119 | msgList = message_service.GetLimitPrivateMsg(uid, toUid, offsetInt) 120 | } else { 121 | msgList = message_service.GetLimitMsg(roomId, offsetInt) 122 | } 123 | 124 | c.JSON(http.StatusOK, gin.H{ 125 | "code": 0, 126 | "data": map[string]interface{}{ 127 | "list": msgList, 128 | }, 129 | }) 130 | } 131 | -------------------------------------------------------------------------------- /views/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | go-gin-chat 聊天室 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
    15 |
    16 |
    17 |

    欢迎来到 go-gin-chat 聊天室,请先登录

    18 |

    19 | portrait_1 20 |

    21 |

    22 | portrait_1 23 | portrait_1 24 | portrait_1 25 | portrait_1 26 | portrait_1 27 | portrait_1 28 | portrait_1 29 | portrait_1 30 | portrait_1 31 | portrait_1 32 | portrait_1 33 | portrait_1 34 |

    35 | 36 |
    37 | 38 | 39 | 40 | 41 | 44 |
    45 |
    46 | 49 |
    50 |
    51 | 52 | 99 | -------------------------------------------------------------------------------- /static/rolling/css/rolling.css: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: sublime text 3 | * @Date: 2015-09-12 17:15:12 4 | * @Last Modified by: sublime text 5 | * @Last Modified time: 2015-09-13 10:36:24 6 | */ 7 | 8 | 9 | /*************** SCROLLBAR BASE CSS ***************/ 10 | 11 | .scroll-wrapper { 12 | overflow: hidden !important; 13 | padding: 0 !important; 14 | position: relative; 15 | } 16 | 17 | .scroll-wrapper > .scroll-content { 18 | border: none !important; 19 | box-sizing: content-box !important; 20 | height: auto; 21 | left: 0; 22 | margin: 0; 23 | max-height: none; 24 | max-width: none !important; 25 | overflow: scroll !important; 26 | padding: 0; 27 | position: relative !important; 28 | top: 0; 29 | width: auto !important; 30 | } 31 | 32 | .scroll-wrapper > .scroll-content::-webkit-scrollbar { 33 | height: 0; 34 | width: 0; 35 | } 36 | 37 | .scroll-element { 38 | display: none; 39 | } 40 | .scroll-element, .scroll-element div { 41 | box-sizing: content-box; 42 | } 43 | 44 | .scroll-element.scroll-x.scroll-scrollx_visible, 45 | .scroll-element.scroll-y.scroll-scrolly_visible { 46 | display: block; 47 | } 48 | 49 | .scroll-element .scroll-bar, 50 | .scroll-element .scroll-arrow { 51 | cursor: default; 52 | } 53 | 54 | .scroll-textarea { 55 | border: 1px solid #cccccc; 56 | border-top-color: #999999; 57 | } 58 | .scroll-textarea > .scroll-content { 59 | overflow: hidden !important; 60 | } 61 | .scroll-textarea > .scroll-content > textarea { 62 | border: none !important; 63 | box-sizing: border-box; 64 | height: 100% !important; 65 | margin: 0; 66 | max-height: none !important; 67 | max-width: none !important; 68 | overflow: scroll !important; 69 | outline: none; 70 | padding: 2px; 71 | position: relative !important; 72 | top: 0; 73 | width: 100% !important; 74 | } 75 | .scroll-textarea > .scroll-content > textarea::-webkit-scrollbar { 76 | height: 0; 77 | width: 0; 78 | } 79 | 80 | /*************** SCROLLBAR MAC OS X ***************/ 81 | 82 | .scrollbar-macosx > .scroll-element, 83 | .scrollbar-macosx > .scroll-element div{ 84 | background: none; 85 | border: none; 86 | margin: 0; 87 | padding: 0; 88 | position: absolute; 89 | z-index: 10; 90 | } 91 | 92 | .scrollbar-macosx > .scroll-element div { 93 | display: block; 94 | height: 100%; 95 | left: 0; 96 | top: 0; 97 | width: 100%; 98 | } 99 | 100 | .scrollbar-macosx > .scroll-element .scroll-element_track { display: none; } 101 | .scrollbar-macosx > .scroll-element .scroll-bar { 102 | background-color: #6C6E71; 103 | display: block; 104 | 105 | -ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; 106 | filter: alpha(opacity=0); 107 | opacity: 0.2; 108 | 109 | -webkit-border-radius: 7px; 110 | -moz-border-radius: 7px; 111 | border-radius: 7px; 112 | 113 | -webkit-transition: opacity 0.2s linear; 114 | -moz-transition: opacity 0.2s linear; 115 | -o-transition: opacity 0.2s linear; 116 | -ms-transition: opacity 0.2s linear; 117 | transition: opacity 0.2s linear; 118 | } 119 | .scrollbar-macosx:hover > .scroll-element .scroll-bar, 120 | .scrollbar-macosx > .scroll-element.scroll-draggable .scroll-bar { 121 | -ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=70)"; 122 | filter: alpha(opacity=70); 123 | opacity: 1; 124 | } 125 | 126 | 127 | .scrollbar-macosx > .scroll-element.scroll-x { 128 | bottom: 0px; 129 | height: 0px; 130 | left: 0; 131 | min-width: 100%; 132 | overflow: visible; 133 | width: 100%; 134 | } 135 | 136 | .scrollbar-macosx > .scroll-element.scroll-y { 137 | height: 100%; 138 | min-height: 100%; 139 | right: 0px; 140 | top: 0; 141 | width: 0px; 142 | } 143 | 144 | /* scrollbar height/width & offset from container borders */ 145 | .scrollbar-macosx > .scroll-element.scroll-x .scroll-bar { height: 7px; min-width: 10px; top: -9px; } 146 | .scrollbar-macosx > .scroll-element.scroll-y .scroll-bar { left: -9px; min-height: 10px; width: 7px; } 147 | 148 | .scrollbar-macosx > .scroll-element.scroll-x .scroll-element_outer { left: 2px; } 149 | .scrollbar-macosx > .scroll-element.scroll-x .scroll-element_size { left: -4px; } 150 | 151 | .scrollbar-macosx > .scroll-element.scroll-y .scroll-element_outer { top: 2px; } 152 | .scrollbar-macosx > .scroll-element.scroll-y .scroll-element_size { top: -4px; } 153 | 154 | /* update scrollbar offset if both scrolls are visible */ 155 | .scrollbar-macosx > .scroll-element.scroll-x.scroll-scrolly_visible .scroll-element_size { left: -11px; } 156 | .scrollbar-macosx > .scroll-element.scroll-y.scroll-scrollx_visible .scroll-element_size { top: -11px; } 157 | 158 | 159 | 160 | 161 | -------------------------------------------------------------------------------- /views/private_chat.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | go-gin-chat 聊天室 7 | 8 | 9 | 10 | 11 | 12 | 13 | 15 | 16 |
    17 |
    18 |
    19 |
      20 | 21 |
    • 22 | 23 | 提示:点击加载更多消息 24 | 25 |
    • 26 | 27 | 28 | 29 | 30 | {{$uid:= .user_info.uid}} 31 | {{$nullSrl:= ""}} 32 | 33 | {{range .msg_list}} 34 | 35 | {{if eq $uid .user_id}} 36 | 37 |
    • 38 | 39 | {{ .username }} 40 | {{ .created_at }} 41 | 42 | {{ if eq .image_url $nullSrl }} 43 | 44 |
      {{ .content }}
      45 | {{else}} 46 |
      47 | {{end}} 48 |
    • 49 | 50 | {{else}} 51 | 52 |
    • 53 | 54 | {{ .username }} 55 | {{ .created_at }} 56 | {{ if eq .image_url $nullSrl }} 57 | 58 |
      {{ .content }}
      59 | {{else}} 60 |
      61 | {{end}} 62 |
    • 63 | 64 | {{end}} 65 | {{end}} 66 | 67 | 68 |
    69 |
    70 |
    71 |
    72 |
    73 |
    74 | 75 | 76 | 77 | 78 | 79 |
    80 |
    81 |
    82 | 83 |
    84 |
    85 |
    86 |
    87 | 88 |
    89 |
    90 |
    91 |
    92 |
    93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 110 | 111 | 124 | 125 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # go-gin-chat(Gin+websocket 的多人聊天室) 2 | 3 | > 练手小项目,为熟悉Gin框架跟websocket使用 💛💛💛💛💛💛 4 | 5 | > [在线demo](https://chat.hzz.cool/) (PS: 请尽量使用Chrome游览器,开启多个不同用户游览器即可体验效果) 6 | 7 | > [github地址](https://github.com/hezhizheng/go-gin-chat) 8 | 9 | ## Feature 10 | - 登录/注册(防止重复登录) 11 | - 群聊(多房间、支持文字、emoji、文件(图片)上传,使用 ~~[freeimage.host](https://freeimage.host/)~~ 做图床 ) 12 | - 图片使用多个图床依次上传,防止被墙,sm.ms做兜底。 13 | - 私聊(消息提醒) 14 | - 历史消息查看(点击加载更多) 15 | - 心跳检测及自动重连机制,来自 https://github.com/zimv/websocket-heartbeat-js 16 | - go mod 包管理 17 | - 使用 Golang 1.16 embed 内嵌静态资源(html、js、css等),运行只依赖编译好的可执行文件与mysql 18 | - 支持 http/ws 、 https/wss 19 | 20 | ## 结构 21 | ``` 22 | . 23 | |-- LICENSE.txt 24 | |-- conf #配置文件 25 | | |-- config.go 26 | | `-- config.go.env 27 | |-- controller 28 | | |-- ImageController.go 29 | | `-- IndexController.go 30 | |-- main.go 31 | |-- models 32 | | |-- message.go 33 | | |-- mysql.go 34 | | `-- user.go 35 | |-- routes 36 | | `-- route.go 37 | |-- services # 简单逻辑处理服务层 38 | | |-- helper 39 | | | `-- helper.go 40 | | |-- img_kr 41 | | | `-- imgKr.go 42 | | |-- message_service 43 | | | `-- message.go 44 | | |-- session 45 | | | `-- session.go 46 | | |-- user_service 47 | | | `-- user.go 48 | | `-- validator 49 | | `-- validator.go 50 | |-- sql 51 | | `-- go_gin_chat.sql 52 | |-- static #静态文件 js 、css 、image 目录 53 | |-- views 54 | | |-- index.html 55 | | |-- login.html 56 | | |-- private_chat.html 57 | | `-- room.html 58 | `-- ws websocket 服务端主要逻辑 59 | |-- ServeInterface.go 60 | |-- go_ws 61 | | `-- serve.go # websocket服务端处理代码 62 | |-- primary 63 | | `-- start.go # 为了兼容新旧版 websocket服务端 的调用策略 64 | |-- serve.go # 初版websocket服务端逻辑代码,可以忽略 65 | `-- ws_test #本地测试代码 66 | |-- exec.go 67 | `-- mock_ws_client_coon.go 68 | ``` 69 | 70 | ## 伪代码,详情可参考 [serve.go](./ws/go_ws/serve.go) 71 | - 定义客户端信息的结构体 72 | ```go 73 | type wsClients struct { 74 | Conn *websocket.Conn `json:"conn"` 75 | 76 | RemoteAddr string `json:"remote_addr"` 77 | 78 | Uid float64 `json:"uid"` 79 | 80 | Username string `json:"username"` 81 | 82 | RoomId string `json:"room_id"` 83 | 84 | AvatarId string `json:"avatar_id"` 85 | } 86 | 87 | // 88 | ``` 89 | - 定义全局变量 90 | ```go 91 | 92 | // client & serve 的消息体 93 | type msg struct { 94 | Status int `json:"status"` 95 | Data interface{} `json:"data"` 96 | Conn *websocket.Conn `json:"conn"` 97 | } 98 | 99 | // 上线、离线、消息发送事件 的 无缓冲区的 channel 100 | var ( 101 | clientMsg = msg{} 102 | 103 | enterRooms = make(chan wsClients) 104 | 105 | sMsg = make(chan msg) 106 | 107 | offline = make(chan *websocket.Conn) 108 | 109 | chNotify = make(chan int ,1) 110 | ) 111 | ``` 112 | - 使用 make 创建一个全局的 `map slice` 用于存放房间与用户的信息,用户上线、离线实际上是对map的 append 跟 remove 113 | ```go 114 | var ( 115 | rooms = make(map[int][]wsClients) 116 | ) 117 | ``` 118 | - 开启`goroutine`处理用户的连接、离线、消息发送等各个事件 119 | ```go 120 | go read(c) 121 | go write() 122 | select {} 123 | ``` 124 | 125 | 126 | 127 | ## 界面 128 | ![](https://static01.imgkr.com/temp/5c3c9096ef9f4796b404dd2f3e23c36d.png) 129 | ![](https://static01.imgkr.com/temp/cd66af62792f4d2e8c2fa974e82d0526.png) 130 | ![](https://static01.imgkr.com/temp/099bf697686445d79407962cdfb11e56.png) 131 | ![](https://static01.imgkr.com/temp/1e89fdd024de47fa862143fba246d632.png) 132 | 133 | ## database 134 | #### mysql 135 | ``` 136 | CREATE TABLE `messages` ( 137 | `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT, 138 | `user_id` int(11) NOT NULL COMMENT '用户ID', 139 | `room_id` int(11) NOT NULL COMMENT '房间ID', 140 | `to_user_id` int(11) NULL DEFAULT 0 COMMENT '私聊用户ID', 141 | `content` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '聊天内容', 142 | `image_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '图片URL', 143 | `created_at` datetime(0) NULL DEFAULT NULL, 144 | `updated_at` datetime(0) NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP(0), 145 | `deleted_at` datetime(0) NULL DEFAULT NULL, 146 | PRIMARY KEY (`id`) USING BTREE, 147 | INDEX `idx_user_id`(`user_id`) USING BTREE 148 | ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; 149 | 150 | CREATE TABLE `users` ( 151 | `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT, 152 | `username` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '昵称', 153 | `password` varchar(125) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '密码', 154 | `avatar_id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '1' COMMENT '头像ID', 155 | `created_at` datetime(0) NULL DEFAULT NULL, 156 | `updated_at` datetime(0) NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP(0), 157 | `deleted_at` datetime(0) NULL DEFAULT NULL, 158 | PRIMARY KEY (`id`) USING BTREE, 159 | UNIQUE INDEX `username`(`username`) USING BTREE 160 | ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; 161 | 162 | ``` 163 | 164 | ## Tools 165 | - [模板提供](https://github.com/zfowed/charooms-html) 166 | - github.com/gin-gonic/gin 167 | - gorm.io/driver/mysql 168 | - gorm.io/gorm 169 | - github.com/gravityblast/fresh 170 | - github.com/valyala/fasthttp 171 | - github.com/spf13/viper 172 | 173 | ## 使用 (go version >= 1.16) 174 | 175 | ``` 176 | # 自行导入数据库文件 sql/go_gin_chat.sql 177 | git clone github.com/hezhizheng/go-gin-chat 178 | cd go-gin-chat 179 | cp conf/config.go.env conf/config.go // 根据实际情况修改配置 180 | go run main.go 181 | ``` 182 | 183 | ## nginx 部署 184 | 185 | ``` 186 | server { 187 | listen 80; 188 | listen 443 ssl http2; 189 | server_name go-gin-chat.hzz.cool; 190 | 191 | #ssl on; 192 | ssl_certificate xxxpath\cert.pem; 193 | ssl_certificate_key xxxpath\key.pem; 194 | ssl_session_timeout 5m; 195 | ssl_protocols TLSv1 TLSv1.1 TLSv1.2; 196 | ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE; 197 | ssl_prefer_server_ciphers on; 198 | 199 | location ~ .*\.(gif|jpg|png|css|js)(.*) { 200 | proxy_pass http://127.0.0.1:8322; 201 | proxy_redirect off; 202 | proxy_set_header Host $host; 203 | proxy_cache cache_one; 204 | proxy_cache_valid 200 302 24h; 205 | proxy_cache_valid 301 30d; 206 | proxy_cache_valid any 5m; 207 | expires 90d; 208 | add_header wall "Big brother is watching you"; 209 | } 210 | 211 | 212 | location / { 213 | try_files /_not_exists_ @backend; 214 | } 215 | 216 | location @backend { 217 | proxy_set_header X-Forwarded-For $remote_addr; 218 | proxy_set_header Host $http_host; 219 | 220 | proxy_pass http://127.0.0.1:8322; 221 | } 222 | 223 | location /ws { 224 | proxy_pass http://127.0.0.1:8322; 225 | proxy_redirect off; 226 | proxy_http_version 1.1; 227 | 228 | proxy_set_header Upgrade $http_upgrade; 229 | proxy_set_header Connection "upgrade"; 230 | 231 | proxy_set_header Host $host:$server_port; 232 | proxy_set_header X-Real-IP $remote_addr; 233 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 234 | proxy_read_timeout 6000s; 235 | } 236 | ``` 237 | ## 编译可执行文件(跨平台) 238 | 239 | ``` 240 | # 用法参考 https://github.com/mitchellh/gox 241 | # go install github.com/mitchellh/gox@latest (go 1.18) 242 | # 生成文件可直接执行 Linux 243 | gox -osarch="linux/amd64" -ldflags "-s -w" -gcflags="all=-trimpath=${PWD}" -asmflags="all=-trimpath=${PWD}" 244 | ...... 245 | ``` 246 | 247 | 248 | ## TODO 249 | - [x] 心跳机制 250 | - [x] 多频道聊天 251 | - [x] 私聊 252 | - [x] 在线用户列表 253 | - [x] https支持 254 | 255 | ## License 256 | [MIT](./LICENSE.txt) 257 | -------------------------------------------------------------------------------- /static/stylesheets/style.css: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: sublime text 3 | * @Date: 2015-09-30 12:12:19 4 | * @Last Modified by: sublime text 5 | * @Last Modified time: 2015-10-05 21:07:47 6 | */ 7 | 8 | /* 全局 */ 9 | 10 | .login{ 11 | background: url('../images/login_bg.jpg') no-repeat fixed top; 12 | background-size: cover; 13 | } 14 | .rooms{ 15 | 16 | background: url('../images/theme/1_bg.jpg') no-repeat fixed top; 17 | background-size: cover; 18 | } 19 | .room{ 20 | background: url('../images/theme/12_mh.jpg') no-repeat fixed top; 21 | background-size: cover 22 | } 23 | 24 | 25 | 26 | 27 | body>.scrollbar-macosx{ 28 | position: absolute; 29 | width: 100%; 30 | height: 100%; 31 | top: 0; 32 | left: 0; 33 | overflow: hidden; 34 | } 35 | a{ 36 | cursor: pointer; 37 | color: #eee; 38 | text-decoration: none; 39 | } 40 | a:hover{ 41 | color: #fff; 42 | text-decoration: none; 43 | } 44 | ul{ 45 | list-style: none; 46 | padding: 0; 47 | margin: 0; 48 | } 49 | .container{ 50 | margin-top: 35px; 51 | max-width: 992px; 52 | } 53 | 54 | .header{ 55 | position: fixed; 56 | z-index: 7778; 57 | width: 100%; 58 | height: 35px; 59 | background: url('../images/header_bg.png') repeat-x; 60 | color: #eee; 61 | } 62 | .header .toptext{ 63 | line-height: 35px; 64 | padding: 0 15px; 65 | float: left; 66 | } 67 | .header .topnavlist{ 68 | float: right; 69 | } 70 | .header .topnavlist>li{ 71 | /*width: 75px;*/ 72 | height: 35px; 73 | line-height: 35px; 74 | text-align: center; 75 | float: left; 76 | } 77 | .header .topnavlist li a{ 78 | display: block; 79 | position: relative; 80 | z-index: 8889; 81 | /*width: 75px;*/ 82 | margin-right: 15px; 83 | height: 35px; 84 | } 85 | .header .popover{ 86 | position: relative; 87 | z-index: 8889; 88 | left: -235px; 89 | color: #000; 90 | max-width: 300px; 91 | width: 300px; 92 | } 93 | .header .popover-content{ 94 | text-align: center; 95 | padding: 0; 96 | } 97 | .header .userinfo .popover{ 98 | left: -160px; 99 | } 100 | .header .userinfo .popover .arrow{ 101 | margin-left: 45px; 102 | } 103 | .header .userinfo .user_portrait{ 104 | padding-top: 10px; 105 | } 106 | .header .userinfo .user_portrait img{ 107 | width: 100px; 108 | height: 100px; 109 | } 110 | .login .select_portrait img, 111 | .header .userinfo .select_portrait img{ 112 | width: 18px; 113 | height: 18px; 114 | opacity: .5; 115 | cursor: pointer; 116 | } 117 | .login .select_portrait .t, 118 | .header .userinfo .select_portrait .t{ 119 | opacity: 1; 120 | } 121 | .login .select_portrait img:hover, 122 | .header .userinfo .select_portrait img:hover{ 123 | opacity: 1; 124 | } 125 | .header .userinfo .user_name{ 126 | padding: 0 5px; 127 | } 128 | .header .userinfo .user_name input{ 129 | margin-bottom: 10px; 130 | } 131 | .header .userinfo .user_name input, 132 | .header .userinfo .user_name button{ 133 | border-radius: 0; 134 | } 135 | .header .theme .popover{ 136 | left: -235px; 137 | } 138 | .header .theme .popover .arrow{ 139 | margin-left: 115px; 140 | } 141 | .header .theme .popover img{ 142 | margin: 5px; 143 | width: 80px; 144 | height: 65px; 145 | } 146 | .header .theme .popover img:hover{ 147 | position: relative; 148 | top: -3px; 149 | box-shadow: 0 3px 15px #000; 150 | } 151 | 152 | 153 | 154 | 155 | 156 | .header li.userlist{ 157 | width: 100px; 158 | } 159 | .header .userlist .popover{ 160 | left: -150px; 161 | width: 300px; 162 | } 163 | .header .userlist .popover-content{ 164 | max-height: 400px; 165 | } 166 | .header .userlist .popover .arrow{ 167 | margin-left: 20px; 168 | } 169 | .header .userlist .popover li{ 170 | text-align: left; 171 | height: 35px; 172 | line-height: 35px; 173 | padding: 0 5px; 174 | } 175 | .header .userlist .popover li:hover{ 176 | background-color: #bbb; 177 | cursor: pointer; 178 | } 179 | .header .userlist .popover li img{ 180 | width: 25px; 181 | height: 25px; 182 | } 183 | .header .clapboard{ 184 | position: fixed; 185 | z-index: 8888; 186 | width: 100%; 187 | height: 100%; 188 | top: 0; 189 | left: 0; 190 | background-color: rgba(0,0,0,0.5); 191 | } 192 | 193 | /* -------------------------------------------------------login------------------------------- */ 194 | 195 | .login .jumbotron{ 196 | background: none; 197 | text-align: center; 198 | } 199 | 200 | 201 | 202 | .login .input-group{ 203 | margin: 30px auto; 204 | max-width: 300px; 205 | } 206 | 207 | .login .input-group input{ 208 | margin: 10px auto; 209 | } 210 | 211 | .login input:hover{ 212 | background-color: #fff; 213 | box-shadow: none; 214 | } 215 | .login .input-group .btn{ 216 | border-left: none; 217 | } 218 | .login .input-group .btn:hover{ 219 | background-color: #fff; 220 | box-shadow: none; 221 | border-color: #ccc; 222 | } 223 | .login .footer{ 224 | font-size: 20px; 225 | position: fixed; 226 | z-index: 8888; 227 | left: 0; 228 | bottom: 0; 229 | width: 100%; 230 | height: 35px; 231 | line-height: 35px; 232 | text-align: center; 233 | color: #eee; 234 | background-color: rgba(0,0,0,.3); 235 | } 236 | 237 | 238 | .login .footer span{ 239 | color: #f00; 240 | } 241 | 242 | 243 | /* -------------------------------------------------------rooms------------------------------- */ 244 | 245 | .rooms .room_list{ 246 | margin-top: 35px; 247 | } 248 | .rooms .room_list a>span { 249 | position: absolute; 250 | bottom: 30px; 251 | right: 30px; 252 | color: #272; 253 | } 254 | 255 | 256 | /* -------------------------------------------------------room------------------------------- */ 257 | 258 | .room .chat_info{ 259 | margin-bottom: 115px; 260 | } 261 | .room .chat_info li{ 262 | margin-top: 15px; 263 | overflow: hidden; 264 | } 265 | .room .chat_info li>img{ 266 | width: 50px; 267 | height: 50px; 268 | margin: 0 5px; 269 | } 270 | .room .chat_info i{ 271 | display: block; 272 | margin-bottom: 10px; 273 | } 274 | .room .chat_info div{ 275 | text-align: left; 276 | padding: 5px; 277 | background-color: #D0D7DF; 278 | margin: 0 60px; 279 | word-break: break-all; 280 | } 281 | .room .chat_info div>img{ 282 | max-width: 100%; 283 | max-height: 250px; 284 | } 285 | .room .chat_info .left{ 286 | text-align: left; 287 | } 288 | .room .chat_info .right{ 289 | text-align: right; 290 | } 291 | .room .chat_info .left img, 292 | .room .chat_info .left div{ 293 | float: left; 294 | } 295 | .room .chat_info .right>img, 296 | .room .chat_info .right>div{ 297 | float: right; 298 | } 299 | .room .chat_info .systeminfo span{ 300 | display: block; 301 | width: 200px; 302 | margin: 0 auto; 303 | text-align: center; 304 | border-radius: 20px; 305 | font-size: 12px; 306 | } 307 | .room .input{ 308 | /*position: fixed;*/ 309 | /*bottom: 0;*/ 310 | z-index: 7777; 311 | width: 100%; 312 | /*pointer-events: none;*/ 313 | } 314 | .room .input .center{ 315 | max-width: 910px; 316 | width: 100%; 317 | height: 110px; 318 | margin: 0 auto; 319 | background: url('../images/room_input_bg.png'); 320 | position: fixed; 321 | bottom: 0; 322 | left: 50%; 323 | transform: translateX(-50%); 324 | } 325 | .room .input .tools{ 326 | height: 10px; 327 | line-height: 10px; 328 | margin-bottom: 10px; 329 | margin-left: 15px; 330 | } 331 | .room .input .tools span{ 332 | padding: 15px; 333 | cursor: pointer; 334 | color: #eee; 335 | font-size: 20px; 336 | } 337 | .room .input .tools span:hover{ 338 | color: #58f; 339 | } 340 | 341 | 342 | .room .input .faces{ 343 | max-width: 288px; 344 | position: relative; 345 | top: -265px; 346 | left: -1px; 347 | z-index: 8889; 348 | } 349 | .room .input .faces.popover .arrow{ 350 | margin-left: -127px; 351 | } 352 | .room .input .faces img:hover{ 353 | box-shadow: 0 0 5px #000; 354 | } 355 | 356 | 357 | 358 | .room .input .text{ 359 | margin: 30px 15px 0; 360 | /*overflow: hidden;*/ 361 | height: 34px; 362 | line-height: 34px; 363 | } 364 | .room .input .text>div{ 365 | padding: 0 15px; 366 | /*text-align: center;*/ 367 | } 368 | 369 | .room .input ._submit{ 370 | margin: 0 15px 0; 371 | height: 34px; 372 | line-height: 34px; 373 | } 374 | 375 | .room .input ._submit>div{ 376 | padding: 0 15px; 377 | } 378 | 379 | .room .input .input .text input{ 380 | border-radius: 0; 381 | 382 | } 383 | .room .input .text a{ 384 | font-size: 25px; 385 | color: #eee; 386 | } 387 | .room .input .text a:visited{ 388 | color: #eee; 389 | } 390 | .room .input .text a:hover{ 391 | color: #afa; 392 | } 393 | 394 | 395 | -------------------------------------------------------------------------------- /ws/serve.go: -------------------------------------------------------------------------------- 1 | package ws 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/gin-gonic/gin" 6 | "github.com/gorilla/websocket" 7 | "go-gin-chat/models" 8 | "log" 9 | "net/http" 10 | "strconv" 11 | "sync" 12 | "time" 13 | ) 14 | 15 | type Serve struct { 16 | ServeInterface 17 | } 18 | 19 | func (serve *Serve) RunWs(gin *gin.Context) { 20 | Run(gin) 21 | } 22 | 23 | func (serve *Serve) GetOnlineUserCount() int { 24 | return GetOnlineUserCount() 25 | } 26 | 27 | func (serve *Serve) GetOnlineRoomUserCount(roomId int) int { 28 | return GetOnlineRoomUserCount(roomId) 29 | } 30 | 31 | // 客户端连接详情 32 | type wsClients struct { 33 | Conn *websocket.Conn `json:"conn"` 34 | 35 | RemoteAddr string `json:"remote_addr"` 36 | 37 | Uid float64 `json:"uid"` 38 | 39 | Username string `json:"username"` 40 | 41 | RoomId string `json:"room_id"` 42 | 43 | AvatarId string `json:"avatar_id"` 44 | 45 | ToUser interface{} `json:"to_user"` 46 | } 47 | 48 | // client & serve 的消息体 49 | type msg struct { 50 | Status int `json:"status"` 51 | Data interface{} `json:"data"` 52 | } 53 | 54 | // 变量定义初始化 55 | var ( 56 | wsUpgrader = websocket.Upgrader{} 57 | 58 | clientMsg = msg{} 59 | 60 | mutex = sync.Mutex{} 61 | 62 | rooms = [roomCount + 1][]wsClients{} 63 | 64 | privateChat = []wsClients{} 65 | ) 66 | 67 | // 定义消息类型 68 | const msgTypeOnline = 1 // 上线 69 | const msgTypeOffline = 2 // 离线 70 | const msgTypeSend = 3 // 消息发送 71 | const msgTypeGetOnlineUser = 4 // 获取用户列表 72 | const msgTypePrivateChat = 5 // 私聊 73 | 74 | const roomCount = 6 // 房间总数 75 | 76 | func Run(gin *gin.Context) { 77 | 78 | // @see https://github.com/gorilla/websocket/issues/523 79 | wsUpgrader.CheckOrigin = func(r *http.Request) bool { return true } 80 | 81 | c, _ := wsUpgrader.Upgrade(gin.Writer, gin.Request, nil) 82 | 83 | defer c.Close() 84 | 85 | mainProcess(c) 86 | } 87 | 88 | // 主程序,负责循环读取客户端消息跟消息的发送 89 | func mainProcess(c *websocket.Conn) { 90 | for { 91 | _, message, err := c.ReadMessage() 92 | serveMsgStr := message 93 | 94 | // 处理心跳响应 , heartbeat为与客户端约定的值 95 | if string(serveMsgStr) == `heartbeat` { 96 | c.WriteMessage(websocket.TextMessage, []byte(`{"status":0,"data":"heartbeat ok"}`)) 97 | continue 98 | } 99 | 100 | json.Unmarshal(message, &clientMsg) 101 | // log.Println("来自客户端的消息", clientMsg,c.RemoteAddr()) 102 | if clientMsg.Data == nil { 103 | return 104 | //mainProcess(c) 105 | } 106 | 107 | if err != nil { // 离线通知 108 | log.Println("ReadMessage error1", err) 109 | disconnect(c) 110 | c.Close() 111 | return 112 | } 113 | 114 | if clientMsg.Status == msgTypeOnline { // 进入房间,建立连接 115 | handleConnClients(c) 116 | serveMsgStr = formatServeMsgStr(msgTypeOnline) 117 | } 118 | 119 | if clientMsg.Status == msgTypePrivateChat { 120 | // 处理私聊 121 | serveMsgStr = formatServeMsgStr(msgTypePrivateChat) 122 | toC := findToUserCoonClient() 123 | if toC != nil { 124 | toC.(wsClients).Conn.WriteMessage(websocket.TextMessage, serveMsgStr) 125 | } 126 | } 127 | 128 | if clientMsg.Status == msgTypeSend { // 消息发送 129 | serveMsgStr = formatServeMsgStr(msgTypeSend) 130 | } 131 | 132 | if clientMsg.Status == msgTypeGetOnlineUser { 133 | serveMsgStr = formatServeMsgStr(msgTypeGetOnlineUser) 134 | c.WriteMessage(websocket.TextMessage, serveMsgStr) 135 | continue 136 | } 137 | 138 | //log.Println("serveMsgStr", string(serveMsgStr)) 139 | if clientMsg.Status == msgTypeSend || clientMsg.Status == msgTypeOnline { 140 | notify(c, string(serveMsgStr)) 141 | } 142 | } 143 | } 144 | 145 | // 获取私聊的用户连接 146 | func findToUserCoonClient() interface{} { 147 | _, roomIdInt := getRoomId() 148 | 149 | toUserUid := clientMsg.Data.(map[string]interface{})["to_uid"].(string) 150 | 151 | for _, c := range rooms[roomIdInt] { 152 | stringUid := strconv.FormatFloat(c.Uid, 'f', -1, 64) 153 | if stringUid == toUserUid { 154 | return c 155 | } 156 | } 157 | 158 | return nil 159 | } 160 | 161 | // 处理建立连接的用户 162 | func handleConnClients(c *websocket.Conn) { 163 | roomId, roomIdInt := getRoomId() 164 | 165 | for cKey, wcl := range rooms[roomIdInt] { 166 | if wcl.Uid == clientMsg.Data.(map[string]interface{})["uid"].(float64) { 167 | mutex.Lock() 168 | // 通知当前用户下线 169 | wcl.Conn.WriteMessage(websocket.TextMessage, []byte(`{"status":-1,"data":[]}`)) 170 | rooms[roomIdInt] = append(rooms[roomIdInt][:cKey], rooms[roomIdInt][cKey+1:]...) 171 | wcl.Conn.Close() 172 | mutex.Unlock() 173 | } 174 | } 175 | 176 | mutex.Lock() 177 | rooms[roomIdInt] = append(rooms[roomIdInt], wsClients{ 178 | Conn: c, 179 | RemoteAddr: c.RemoteAddr().String(), 180 | Uid: clientMsg.Data.(map[string]interface{})["uid"].(float64), 181 | Username: clientMsg.Data.(map[string]interface{})["username"].(string), 182 | RoomId: roomId, 183 | AvatarId: clientMsg.Data.(map[string]interface{})["avatar_id"].(string), 184 | }) 185 | mutex.Unlock() 186 | } 187 | 188 | // 统一消息发放 189 | func notify(conn *websocket.Conn, msg string) { 190 | _, roomIdInt := getRoomId() 191 | for _, con := range rooms[roomIdInt] { 192 | if con.RemoteAddr != conn.RemoteAddr().String() { 193 | con.Conn.WriteMessage(websocket.TextMessage, []byte(msg)) 194 | } 195 | } 196 | } 197 | 198 | // 离线通知 199 | func disconnect(conn *websocket.Conn) { 200 | _, roomIdInt := getRoomId() 201 | for index, con := range rooms[roomIdInt] { 202 | if con.RemoteAddr == conn.RemoteAddr().String() { 203 | data := map[string]interface{}{ 204 | "username": con.Username, 205 | "uid": con.Uid, 206 | "time": time.Now().UnixNano() / 1e6, // 13位 10位 => now.Unix() 207 | } 208 | 209 | jsonStrServeMsg := msg{ 210 | Status: msgTypeOffline, 211 | Data: data, 212 | } 213 | serveMsgStr, _ := json.Marshal(jsonStrServeMsg) 214 | 215 | disMsg := string(serveMsgStr) 216 | 217 | mutex.Lock() 218 | rooms[roomIdInt] = append(rooms[roomIdInt][:index], rooms[roomIdInt][index+1:]...) 219 | con.Conn.Close() 220 | mutex.Unlock() 221 | notify(conn, disMsg) 222 | } 223 | } 224 | } 225 | 226 | // 格式化传送给客户端的消息数据 227 | func formatServeMsgStr(status int) []byte { 228 | 229 | roomId, roomIdInt := getRoomId() 230 | 231 | data := map[string]interface{}{ 232 | "username": clientMsg.Data.(map[string]interface{})["username"].(string), 233 | "uid": clientMsg.Data.(map[string]interface{})["uid"].(float64), 234 | "room_id": roomId, 235 | "time": time.Now().UnixNano() / 1e6, // 13位 10位 => now.Unix() 236 | } 237 | 238 | if status == msgTypeSend || status == msgTypePrivateChat { 239 | data["avatar_id"] = clientMsg.Data.(map[string]interface{})["avatar_id"].(string) 240 | data["content"] = clientMsg.Data.(map[string]interface{})["content"].(string) 241 | 242 | toUidStr := clientMsg.Data.(map[string]interface{})["to_uid"].(string) 243 | toUid, _ := strconv.Atoi(toUidStr) 244 | 245 | // 保存消息 246 | stringUid := strconv.FormatFloat(data["uid"].(float64), 'f', -1, 64) 247 | intUid, _ := strconv.Atoi(stringUid) 248 | 249 | if _, ok := clientMsg.Data.(map[string]interface{})["image_url"]; ok { 250 | // 存在图片 251 | models.SaveContent(map[string]interface{}{ 252 | "user_id": intUid, 253 | "to_user_id": toUid, 254 | "content": data["content"], 255 | "room_id": data["room_id"], 256 | "image_url": clientMsg.Data.(map[string]interface{})["image_url"].(string), 257 | }) 258 | } else { 259 | models.SaveContent(map[string]interface{}{ 260 | "user_id": intUid, 261 | "to_user_id": toUid, 262 | "room_id": data["room_id"], 263 | "content": data["content"], 264 | }) 265 | } 266 | 267 | } 268 | 269 | if status == msgTypeGetOnlineUser { 270 | data["count"] = GetOnlineRoomUserCount(roomIdInt) 271 | data["list"] = onLineUserList(roomIdInt) 272 | } 273 | 274 | jsonStrServeMsg := msg{ 275 | Status: status, 276 | Data: data, 277 | } 278 | serveMsgStr, _ := json.Marshal(jsonStrServeMsg) 279 | 280 | return serveMsgStr 281 | } 282 | 283 | func getRoomId() (string, int) { 284 | roomId := clientMsg.Data.(map[string]interface{})["room_id"].(string) 285 | 286 | roomIdInt, _ := strconv.Atoi(roomId) 287 | return roomId, roomIdInt 288 | } 289 | 290 | // 获取在线用户列表 291 | func onLineUserList(roomId int) []wsClients { 292 | return rooms[roomId] 293 | } 294 | 295 | // =======================对外方法===================================== 296 | 297 | func GetOnlineUserCount() int { 298 | num := 0 299 | for i := 1; i <= roomCount; i++ { 300 | num = num + GetOnlineRoomUserCount(i) 301 | } 302 | return num 303 | } 304 | 305 | func GetOnlineRoomUserCount(roomId int) int { 306 | return len(rooms[roomId]) 307 | } 308 | -------------------------------------------------------------------------------- /views/room.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | go-gin-chat 聊天室 7 | 8 | 9 | 10 | 11 | 12 | 13 | 15 | 16 |
    17 |
    18 | 23 | 24 | 46 | 47 |
    48 |
    49 |
    50 |
      51 | 52 |
    • 53 | 54 | 提示:点击加载更多消息 55 | 56 |
    • 57 | 58 | 59 | 60 | 61 | {{$uid:= .user_info.uid}} 62 | {{$nullSrl:= ""}} 63 | 64 | {{range .msg_list}} 65 | 66 | {{if eq $uid .user_id}} 67 | 68 |
    • 69 | 70 | {{ .username }} 71 | {{ .created_at }} 72 | 73 | {{ if eq .image_url $nullSrl }} 74 | 75 |
      {{ .content }}
      76 | {{else}} 77 |
      78 | {{end}} 79 |
    • 80 | 81 | {{else}} 82 | 83 |
    • 84 | 85 | {{ .username }} 86 | {{ .created_at }} 87 | {{ if eq .image_url $nullSrl }} 88 | 89 |
      {{ .content }}
      90 | {{else}} 91 |
      92 | {{end}} 93 |
    • 94 | 95 | {{end}} 96 | {{end}} 97 | 98 | 99 |
    100 |
    101 |
    102 |
    103 |
    104 |
    105 | 106 | 107 | 108 | 109 | 110 |
    111 |
    112 |
    113 | 114 |
    115 |
    116 |
    117 |
    118 | 119 |
    120 |
    121 |
    122 |
    123 |
    124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 153 | 154 | 212 | 213 | 226 | 227 | 287 | 288 | -------------------------------------------------------------------------------- /ws/go_ws/serve.go: -------------------------------------------------------------------------------- 1 | package go_ws 2 | 3 | import ( 4 | "encoding/json" 5 | "go-gin-chat/models" 6 | "go-gin-chat/services/helper" 7 | "go-gin-chat/services/safe" 8 | "go-gin-chat/ws" 9 | "log" 10 | "net/http" 11 | "strconv" 12 | "sync" 13 | "time" 14 | 15 | "github.com/gin-gonic/gin" 16 | "github.com/gorilla/websocket" 17 | "github.com/jianfengye/collection" 18 | ) 19 | 20 | // 客户端连接详情 21 | type wsClients struct { 22 | Conn *websocket.Conn `json:"conn"` 23 | 24 | RemoteAddr string `json:"remote_addr"` 25 | 26 | Uid string `json:"uid"` 27 | 28 | Username string `json:"username"` 29 | 30 | RoomId string `json:"room_id"` 31 | 32 | AvatarId string `json:"avatar_id"` 33 | } 34 | 35 | type msgData struct { 36 | Uid string `json:"uid"` 37 | Username string `json:"username"` 38 | AvatarId string `json:"avatar_id"` 39 | ToUid string `json:"to_uid"` 40 | Content string `json:"content"` 41 | ImageUrl string `json:"image_url"` 42 | RoomId string `json:"room_id"` 43 | Count int `json:"count"` 44 | List []interface{} `json:"list"` 45 | Time int64 `json:"time"` 46 | } 47 | 48 | // client & serve 的消息体 49 | type msg struct { 50 | Status int `json:"status"` 51 | Data msgData `json:"data"` 52 | Conn *websocket.Conn `json:"conn"` 53 | } 54 | 55 | type pingStorage struct { 56 | Conn *websocket.Conn `json:"conn"` 57 | RemoteAddr string `json:"remote_addr"` 58 | Time int64 `json:"time"` 59 | } 60 | 61 | // 变量定义初始化 62 | var ( 63 | wsUpgrader = websocket.Upgrader{} 64 | 65 | clientMsg = msg{} 66 | 67 | mutex = sync.Mutex{} 68 | 69 | //rooms = [roomCount + 1][]wsClients{} 70 | rooms = make(map[int][]interface{}) 71 | 72 | enterRooms = make(chan wsClients) 73 | 74 | sMsg = make(chan msg) 75 | 76 | offline = make(chan *websocket.Conn) 77 | 78 | chNotify = make(chan int, 1) 79 | 80 | pingMap []interface{} 81 | 82 | clientMsgLock = sync.Mutex{} 83 | clientMsgData = clientMsg // 临时存储 clientMsg 数据 84 | ) 85 | 86 | // 定义消息类型 87 | const msgTypeOnline = 1 // 上线 88 | const msgTypeOffline = 2 // 离线 89 | const msgTypeSend = 3 // 消息发送 90 | const msgTypeGetOnlineUser = 4 // 获取用户列表 91 | const msgTypePrivateChat = 5 // 私聊 92 | 93 | const roomCount = 6 // 房间总数 94 | 95 | type GoServe struct { 96 | ws.ServeInterface 97 | } 98 | 99 | func (goServe *GoServe) RunWs(gin *gin.Context) { 100 | // 使用 channel goroutine 101 | Run(gin) 102 | } 103 | 104 | func (goServe *GoServe) GetOnlineUserCount() int { 105 | return GetOnlineUserCount() 106 | } 107 | 108 | func (goServe *GoServe) GetOnlineRoomUserCount(roomId int) int { 109 | return GetOnlineRoomUserCount(roomId) 110 | } 111 | 112 | func Run(gin *gin.Context) { 113 | 114 | // @see https://github.com/gorilla/websocket/issues/523 115 | wsUpgrader.CheckOrigin = func(r *http.Request) bool { return true } 116 | 117 | c, err := wsUpgrader.Upgrade(gin.Writer, gin.Request, nil) 118 | if err != nil { 119 | log.Println("WebSocket upgrade error:", err) 120 | return 121 | } 122 | 123 | defer c.Close() 124 | done := make(chan struct{}) 125 | 126 | go read(c, done) 127 | go write(done) 128 | 129 | // 等待 done 信号,确保连接断开后 goroutine 能正确退出 130 | <-done 131 | 132 | } 133 | 134 | // HandelOfflineCoon 定时任务清理没有心跳的连接 135 | func HandelOfflineCoon() { 136 | 137 | objColl := collection.NewObjCollection(pingMap) 138 | retColl := safe.Safety.Do(func() interface{} { 139 | return objColl.Reject(func(obj interface{}, index int) bool { 140 | nowTime := time.Now().Unix() 141 | timeDiff := nowTime - obj.(pingStorage).Time 142 | // log.Println("timeDiff", nowTime, obj.(pingStorage).Time, timeDiff) 143 | if timeDiff > 60 { // 超过 60s 没有心跳 主动断开连接 144 | offline <- obj.(pingStorage).Conn 145 | return true 146 | } 147 | return false 148 | }) 149 | }).(collection.ICollection) 150 | 151 | interfaces, _ := retColl.ToInterfaces() 152 | 153 | pingMap = interfaces 154 | } 155 | 156 | func appendPing(c *websocket.Conn) { 157 | objColl := collection.NewObjCollection(pingMap) 158 | 159 | // 先删除相同的 160 | retColl := safe.Safety.Do(func() interface{} { 161 | return objColl.Reject(func(obj interface{}, index int) bool { 162 | if obj.(pingStorage).RemoteAddr == c.RemoteAddr().String() { 163 | return true 164 | } 165 | return false 166 | }) 167 | }).(collection.ICollection) 168 | 169 | // 再追加 170 | retColl = safe.Safety.Do(func() interface{} { 171 | return retColl.Append(pingStorage{ 172 | Conn: c, 173 | RemoteAddr: c.RemoteAddr().String(), 174 | Time: time.Now().Unix(), 175 | }) 176 | }).(collection.ICollection) 177 | 178 | interfaces, _ := retColl.ToInterfaces() 179 | 180 | pingMap = interfaces 181 | 182 | } 183 | 184 | func read(c *websocket.Conn, done chan<- struct{}) { 185 | 186 | defer func() { 187 | //捕获read抛出的panic 188 | if err := recover(); err != nil { 189 | log.Println("read发生错误", err) 190 | //panic(nil) 191 | } 192 | }() 193 | 194 | for { 195 | _, message, err := c.ReadMessage() 196 | //log.Println("client message", string(message), c.RemoteAddr()) 197 | if err != nil { // 离线通知 198 | offline <- c 199 | log.Println("ReadMessage error1", err) 200 | c.Close() 201 | close(done) 202 | return 203 | } 204 | 205 | // 处理心跳响应 , heartbeat为与客户端约定的值 206 | if string(message) == `heartbeat` { 207 | appendPing(c) 208 | chNotify <- 1 209 | // log.Println("heartbeat pingMap:", pingMap) 210 | c.WriteMessage(websocket.TextMessage, []byte(`{"status":0,"data":"heartbeat ok"}`)) 211 | <-chNotify 212 | continue 213 | } 214 | 215 | json.Unmarshal(message, &clientMsgData) 216 | 217 | clientMsgLock.Lock() 218 | clientMsg = clientMsgData 219 | // 保存需要使用的字段到局部变量 220 | dataUid := clientMsg.Data.Uid 221 | status := clientMsg.Status 222 | dataUsername := clientMsg.Data.Username 223 | dataAvatarId := clientMsg.Data.AvatarId 224 | dataRoomId := clientMsg.Data.RoomId 225 | clientMsgLock.Unlock() 226 | 227 | //fmt.Println("来自客户端的消息", clientMsg, c.RemoteAddr()) 228 | if dataUid != "" { 229 | if status == msgTypeOnline { // 进入房间,建立连接 230 | enterRooms <- wsClients{ 231 | Conn: c, 232 | RemoteAddr: c.RemoteAddr().String(), 233 | Uid: dataUid, 234 | Username: dataUsername, 235 | RoomId: dataRoomId, 236 | AvatarId: dataAvatarId, 237 | } 238 | } 239 | 240 | _, serveMsg := formatServeMsgStr(status, c) 241 | sMsg <- serveMsg 242 | } 243 | } 244 | } 245 | 246 | func write(done <-chan struct{}) { 247 | 248 | defer func() { 249 | //捕获write抛出的panic 250 | if err := recover(); err != nil { 251 | log.Println("write发生错误", err) 252 | //panic(err) 253 | } 254 | }() 255 | 256 | for { 257 | select { 258 | case <-done: // 当 done 通道关闭时,退出 write 函数 259 | return 260 | case r := <-enterRooms: 261 | handleConnClients(r.Conn) 262 | case cl := <-sMsg: 263 | serveMsgStr, _ := json.Marshal(cl) 264 | switch cl.Status { 265 | case msgTypeOnline, msgTypeSend: 266 | notify(cl.Conn, string(serveMsgStr)) 267 | case msgTypeGetOnlineUser: 268 | chNotify <- 1 269 | cl.Conn.WriteMessage(websocket.TextMessage, serveMsgStr) 270 | <-chNotify 271 | case msgTypePrivateChat: 272 | chNotify <- 1 273 | toC := findToUserCoonClient() 274 | if toC != nil { 275 | toC.(wsClients).Conn.WriteMessage(websocket.TextMessage, serveMsgStr) 276 | } 277 | <-chNotify 278 | } 279 | case o := <-offline: 280 | disconnect(o) 281 | } 282 | } 283 | } 284 | 285 | func handleConnClients(c *websocket.Conn) { 286 | // 读取clientMsg需要加锁 287 | clientMsgLock.Lock() 288 | uid := clientMsg.Data.Uid 289 | username := clientMsg.Data.Username 290 | avatarId := clientMsg.Data.AvatarId 291 | roomId := clientMsg.Data.RoomId 292 | clientMsgLock.Unlock() 293 | 294 | roomIdInt, _ := strconv.Atoi(roomId) 295 | 296 | objColl := collection.NewObjCollection(rooms[roomIdInt]) 297 | 298 | retColl := safe.Safety.Do(func() interface{} { 299 | return objColl.Reject(func(item interface{}, key int) bool { 300 | if item.(wsClients).Uid == uid { 301 | chNotify <- 1 302 | item.(wsClients).Conn.WriteMessage(websocket.TextMessage, []byte(`{"status":-1,"data":[]}`)) 303 | <-chNotify 304 | return true 305 | } 306 | return false 307 | }) 308 | }).(collection.ICollection) 309 | 310 | retColl = safe.Safety.Do(func() interface{} { 311 | return retColl.Append(wsClients{ 312 | Conn: c, 313 | RemoteAddr: c.RemoteAddr().String(), 314 | Uid: uid, 315 | Username: username, 316 | RoomId: roomId, 317 | AvatarId: avatarId, 318 | }) 319 | }).(collection.ICollection) 320 | 321 | interfaces, _ := retColl.ToInterfaces() 322 | 323 | rooms[roomIdInt] = interfaces 324 | } 325 | 326 | // 获取私聊的用户连接 327 | func findToUserCoonClient() interface{} { 328 | // 读取clientMsg需要加锁 329 | clientMsgLock.Lock() 330 | toUserUid := clientMsg.Data.ToUid 331 | roomId := clientMsg.Data.RoomId 332 | clientMsgLock.Unlock() 333 | 334 | roomIdInt, _ := strconv.Atoi(roomId) 335 | 336 | assignRoom := rooms[roomIdInt] 337 | for _, c := range assignRoom { 338 | stringUid := c.(wsClients).Uid 339 | if stringUid == toUserUid { 340 | return c 341 | } 342 | } 343 | 344 | return nil 345 | } 346 | 347 | // 统一消息发放 348 | func notify(conn *websocket.Conn, msg string) { 349 | chNotify <- 1 // 利用channel阻塞 避免并发去对同一个连接发送消息出现panic: concurrent write to websocket connection这样的异常 350 | // 读取clientMsg需要加锁 351 | clientMsgLock.Lock() 352 | roomId := clientMsg.Data.RoomId 353 | clientMsgLock.Unlock() 354 | 355 | roomIdInt, _ := strconv.Atoi(roomId) 356 | assignRoom := rooms[roomIdInt] 357 | for _, con := range assignRoom { 358 | if con.(wsClients).RemoteAddr != conn.RemoteAddr().String() { 359 | con.(wsClients).Conn.WriteMessage(websocket.TextMessage, []byte(msg)) 360 | } 361 | } 362 | <-chNotify 363 | } 364 | 365 | // 离线通知 366 | func disconnect(conn *websocket.Conn) { 367 | // 从 rooms 中移除客户端 368 | _, roomIdInt := getRoomId() 369 | 370 | objColl := collection.NewObjCollection(rooms[roomIdInt]) 371 | 372 | retColl := safe.Safety.Do(func() interface{} { 373 | return objColl.Reject(func(item interface{}, key int) bool { 374 | if item.(wsClients).RemoteAddr == conn.RemoteAddr().String() { 375 | 376 | data := msgData{ 377 | Username: item.(wsClients).Username, 378 | Uid: item.(wsClients).Uid, 379 | Time: time.Now().UnixNano() / 1e6, // 13位 10位 => now.Unix() 380 | } 381 | 382 | jsonStrServeMsg := msg{ 383 | Status: msgTypeOffline, 384 | Data: data, 385 | } 386 | serveMsgStr, _ := json.Marshal(jsonStrServeMsg) 387 | 388 | disMsg := string(serveMsgStr) 389 | 390 | item.(wsClients).Conn.Close() 391 | 392 | notify(conn, disMsg) 393 | 394 | return true 395 | } 396 | return false 397 | }) 398 | }).(collection.ICollection) 399 | 400 | interfaces, _ := retColl.ToInterfaces() 401 | rooms[roomIdInt] = interfaces 402 | 403 | // 从 pingMap 中移除客户端,避免内存泄漏 404 | objCollPing := collection.NewObjCollection(pingMap) 405 | retCollPing := safe.Safety.Do(func() interface{} { 406 | return objCollPing.Reject(func(obj interface{}, index int) bool { 407 | if obj.(pingStorage).RemoteAddr == conn.RemoteAddr().String() { 408 | return true 409 | } 410 | return false 411 | }) 412 | }).(collection.ICollection) 413 | 414 | pingInterfaces, _ := retCollPing.ToInterfaces() 415 | pingMap = pingInterfaces 416 | } 417 | 418 | // 格式化传送给客户端的消息数据 419 | func formatServeMsgStr(status int, conn *websocket.Conn) ([]byte, msg) { 420 | // 读取clientMsg需要加锁 421 | clientMsgLock.Lock() 422 | // 将需要使用的字段复制到局部变量 423 | username := clientMsg.Data.Username 424 | uid := clientMsg.Data.Uid 425 | roomId := clientMsg.Data.RoomId 426 | avatarId := clientMsg.Data.AvatarId 427 | content := clientMsg.Data.Content 428 | toUidStr := clientMsg.Data.ToUid 429 | imageUrl := clientMsg.Data.ImageUrl 430 | clientMsgLock.Unlock() 431 | 432 | roomIdInt, _ := strconv.Atoi(roomId) 433 | 434 | //log.Println(reflect.TypeOf(var)) 435 | 436 | data := msgData{ 437 | Username: username, 438 | Uid: uid, 439 | RoomId: roomId, 440 | Time: time.Now().UnixNano() / 1e6, // 13位 10位 => now.Unix() 441 | } 442 | 443 | if status == msgTypeSend || status == msgTypePrivateChat { 444 | data.AvatarId = avatarId 445 | 446 | data.Content = content 447 | if helper.MbStrLen(content) > 800 { 448 | // 直接截断 449 | data.Content = string([]rune(content)[:800]) 450 | } 451 | 452 | toUid, _ := strconv.Atoi(toUidStr) 453 | 454 | // 保存消息 455 | intUid, _ := strconv.Atoi(uid) 456 | 457 | if imageUrl != "" { 458 | // 存在图片 459 | models.SaveContent(map[string]interface{}{ 460 | "user_id": intUid, 461 | "to_user_id": toUid, 462 | "content": data.Content, 463 | "room_id": data.RoomId, 464 | "image_url": imageUrl, 465 | }) 466 | } else { 467 | models.SaveContent(map[string]interface{}{ 468 | "user_id": intUid, 469 | "to_user_id": toUid, 470 | "content": data.Content, 471 | "room_id": data.RoomId, 472 | }) 473 | } 474 | 475 | } 476 | 477 | if status == msgTypeGetOnlineUser { 478 | ro := rooms[roomIdInt] 479 | data.Count = len(ro) 480 | data.List = ro 481 | } 482 | 483 | jsonStrServeMsg := msg{ 484 | Status: status, 485 | Data: data, 486 | Conn: conn, 487 | } 488 | serveMsgStr, _ := json.Marshal(jsonStrServeMsg) 489 | 490 | return serveMsgStr, jsonStrServeMsg 491 | } 492 | 493 | func getRoomId() (string, int) { 494 | // 读取clientMsg需要加锁 495 | clientMsgLock.Lock() 496 | roomId := clientMsg.Data.RoomId 497 | clientMsgLock.Unlock() 498 | 499 | roomIdInt, _ := strconv.Atoi(roomId) 500 | return roomId, roomIdInt 501 | } 502 | 503 | // =======================对外方法===================================== 504 | 505 | func GetOnlineUserCount() int { 506 | num := 0 507 | for i := 1; i <= roomCount; i++ { 508 | num = num + GetOnlineRoomUserCount(i) 509 | } 510 | return num 511 | } 512 | 513 | func GetOnlineRoomUserCount(roomId int) int { 514 | return len(rooms[roomId]) 515 | } 516 | -------------------------------------------------------------------------------- /static/javascripts/Public.js: -------------------------------------------------------------------------------- 1 | 2 | let ws_protocol = document.location.protocol == "https:" ? "wss" : "ws" 3 | 4 | const websocketHeartbeatJsOptions = { 5 | url: ws_protocol + "://"+ window.location.host +"/ws", 6 | pingTimeout: 15000, 7 | pongTimeout: 10000, 8 | reconnectTimeout: 2000, 9 | pingMsg: "heartbeat" 10 | } 11 | 12 | let websocketHeartbeatJs = new WebsocketHeartbeatJs(websocketHeartbeatJsOptions); 13 | 14 | let ws = websocketHeartbeatJs; 15 | // let ws = new WebSocket("ws://"+ window.location.host +"/ws"); 16 | 17 | function _time(time = +new Date()) { 18 | var date = new Date(time + 8 * 3600 * 1000); // 增加8小时 19 | return date.toJSON().substr(0, 19).replace('T', ' '); 20 | //return date.toJSON().substr(0, 19).replace('T', ' ').replace(/-/g, '/'); 21 | } 22 | 23 | function WebSocketConnect(userInfo,toUserInfo = null) { 24 | if ("WebSocket" in window) { 25 | //console.log("您的浏览器支持 WebSocket!"); 26 | 27 | if ( userInfo.uid <= 0 ) 28 | { 29 | alert('参数错误,请刷新页面重试');return false; 30 | } 31 | 32 | // 打开一个 web socket 33 | // let ws = new WebSocket("ws://127.0.0.1:8322/ws"); 34 | 35 | let send_data = JSON.stringify({ 36 | "status": toUserInfo ? 5 : 1, 37 | "data": { 38 | "uid": userInfo.uid.toString(), 39 | "room_id": userInfo.room_id, 40 | "avatar_id": userInfo.avatar_id, 41 | "username": userInfo.username, 42 | "to_user": toUserInfo 43 | } 44 | }) 45 | 46 | ws.onopen = function () { 47 | // layer.msg("websocket 连接已建立"); 48 | chat_info.html(chat_info.html() + 49 | '
  • ' + 50 | "✅ websocket 连接已建立 " + 51 | '
  • '); 52 | ws.send(send_data); 53 | //console.log("send_data 发送数据", send_data) 54 | toLow(); 55 | }; 56 | 57 | // if ( toUserInfo ) 58 | // { 59 | // let to_user_send_data = JSON.stringify({ 60 | // "status": toUserInfo ? 5 : 1, 61 | // "data": { 62 | // "uid": toUserInfo.uid, 63 | // "room_id": toUserInfo.room_id, 64 | // "avatar_id": toUserInfo.avatar_id, 65 | // "username": toUserInfo.username, 66 | // "to_user": toUserInfo 67 | // } 68 | // }) 69 | // ws.onopen = function () { 70 | // ws.send(to_user_send_data); 71 | // console.log("to_user_send_data 发送数据", to_user_send_data) 72 | // }; 73 | // } 74 | 75 | 76 | let chat_info = $('.main .chat_info') 77 | let isServeClose = 0; 78 | 79 | ws.onmessage = function (evt) { 80 | var received_msg = JSON.parse(evt.data); 81 | 82 | // let myDate = new Date(); 83 | // let time = myDate.toLocaleDateString() + myDate.toLocaleTimeString() 84 | let time = _time(received_msg.data.time) 85 | 86 | switch(received_msg.status) 87 | { 88 | case 1: 89 | chat_info.html(chat_info.html() + 90 | '
  • ' + 91 | "【" + 92 | received_msg.data.username + 93 | "】" + 94 | time + 95 | " 加入了房间" + 96 | '
  • '); 97 | break; 98 | case 2: 99 | chat_info.html(chat_info.html() + 100 | '
  • ' + 101 | "【" + 102 | received_msg.data.username + 103 | "】" + 104 | time + 105 | " 离开了房间" + 106 | '
  • '); 107 | break; 108 | case 3: 109 | if ( received_msg.data.uid != userInfo.uid && !isPrivateChat()) 110 | { 111 | chat_info.html(chat_info.html() + 112 | '
  • ' + 115 | received_msg.data.username + 116 | '' + 117 | time + 118 | '
    ' + 119 | received_msg.data.content + 120 | '
  • '); 121 | } 122 | break; 123 | case -1: 124 | ws.close() // 主动close掉 125 | isServeClose = 1 126 | console.log("client 连接已关闭..."); 127 | break; 128 | case 4: 129 | $('.popover-title').html('在线用户 '+ received_msg.data.count +' 人') 130 | 131 | $.each(received_msg.data.list,function (index, value) { 132 | 133 | if ( received_msg.data.uid == value.uid ) 134 | { 135 | // 禁止点击 136 | $('.ul-user-list').html($('.ul-user-list').html() + 137 | '
  • ' + " " + 140 | value.username + 141 | '' + 142 | '
  • ' 143 | ) 144 | }else{ 145 | $('.ul-user-list').html($('.ul-user-list').html() + 146 | '
  • ' + " " + 149 | value.username + 150 | '' + 151 | '
  • ' 152 | ) 153 | } 154 | 155 | }) 156 | //console.log("在线用户",received_msg); 157 | break; 158 | case 5: 159 | // 私聊通知 160 | if (!isPrivateChat()) 161 | { 162 | layer.msg(received_msg.data.username+':'+ received_msg.data.content); 163 | } 164 | break; 165 | default: 166 | } 167 | // console.log("数据已接收...", received_msg); 168 | 169 | if ( !(received_msg.data === "heartbeat ok") ){ 170 | // 滚动条滚到最下面 171 | toLow(); 172 | } 173 | 174 | }; 175 | 176 | ws.onclose = function (evt) { 177 | // 关闭 websocket 178 | if ( isServeClose === 1 ){ 179 | chat_info.html(chat_info.html() + 180 | '
  • ' + 181 | "❌ 与服务器连接断开,请检查是否在浏览器中打开了多个聊天界面" + 182 | '
  • '); 183 | }else{ 184 | chat_info.html(chat_info.html() + 185 | '
  • ' + 186 | "❌ 与服务器连接断开,正在尝试重新连接,请稍后..." + 187 | '
  • '); 188 | } 189 | // let c = ws.close() // 主动close掉 190 | console.log("serve 连接已关闭... " + _time(),evt); 191 | // console.log(c); 192 | toLow(); 193 | }; 194 | 195 | ws.onerror = function (evt) { 196 | // ws.close() 197 | console.log("触发 onerror",evt) 198 | } 199 | 200 | ws.onreconnect = (e) => { 201 | console.log('reconnecting...'); 202 | } 203 | 204 | } else { 205 | // 浏览器不支持 WebSocket 206 | alert("您的浏览器不支持 WebSocket!"); 207 | } 208 | } 209 | 210 | $(document).ready(function(){ 211 | // ------------------------选择聊天室页面----------------------------------------------- 212 | 213 | // 在页面即将卸载之前关闭WebSocket连接 214 | window.addEventListener("beforeunload", function() { 215 | console.log("beforeunload close"); 216 | ws.close(); 217 | }); 218 | // 用户信息提交 219 | 220 | $('#userinfo_sub').click(function(event) { 221 | var userName = $('.rooms .user_name input').val(); // 用户昵称 222 | var userPortrait = $('.rooms .user_portrait img').attr('portrait_id'); // 用户头像id 223 | if(userName=='') { // 如果不填用户昵称,就是以前的昵称 224 | userName = $('.rooms .user_name input').attr('placeholder'); 225 | } 226 | 227 | 228 | // 下面是测试用的代码 229 | 230 | 231 | $('.userinfo a b').text(userName); // 修改标题栏的用户昵称 232 | $('.rooms .user_name input').val(''); // 昵称输入框清空 233 | $('.rooms .user_name input').attr('placeholder', userName); // 昵称输入框默认显示用户昵称 234 | $('.topnavlist .popover').not($(this).next('.popover')).removeClass('show'); // 关掉用户面板 235 | $('.clapboard').addClass('hidden'); // 关掉模糊背景 236 | }); 237 | 238 | // 设置主题 239 | 240 | 241 | $('.theme img').click(function(event) { 242 | var theme_id = $(this).attr('theme_id'); 243 | $('.clapboard').click(); // 关掉用户模糊背景 244 | 245 | 246 | 247 | 248 | // 下面是测试用的代码 249 | 250 | 251 | $('body').css('background-image', 'url(images/theme/' + theme_id + '_bg.jpg)'); // 设置背景 252 | }); 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | // --------------------聊天室内页面---------------------------------------------------- 276 | 277 | // 获取在线用户列表 278 | $(document).on('click', '.a-user-list', function(e) { 279 | $('.ul-user-list').html('') 280 | let send_data = JSON.stringify({ 281 | "status": 4, 282 | "data": { 283 | "uid": $('.room').attr('data-uid').toString(), 284 | "username": $('.room').attr('data-username'), 285 | "avatar_id": $('.room').attr('data-avatar_id'), 286 | "room_id": $('.room').attr('data-room_id'), 287 | } 288 | }) 289 | ws.send(send_data); 290 | }) 291 | 292 | // 发送图片 293 | 294 | $('.imgFileBtn').change(function(event) { 295 | 296 | var formData = new FormData(); 297 | formData.append('file', $(this)[0].files[0]); 298 | $.ajax({ 299 | url: '/img-kr-upload', 300 | type: 'POST', 301 | beforeSend: function (xhr) { 302 | // 在请求发送之前执行的代码 303 | console.log('请求即将发送'); 304 | 305 | // 在请求发送之前调用 layer 的加载动画 306 | var index = layer.load(1, { // 1 是加载动画的样式,layer 提供了多种样式 307 | shade: [0.5, '#000'], // 遮罩层颜色和透明度 308 | time: 25000, // 最大显示时间(毫秒),超过此时间自动关闭 309 | success: function(layero, index) { 310 | // 加载动画加载完成时的回调 311 | console.log('加载动画已显示'); 312 | } 313 | }); 314 | // 将加载动画的索引存储到全局变量或闭包中,方便后续关闭 315 | window.layerIndex = index; 316 | 317 | }, 318 | cache: false, 319 | data: formData, 320 | processData: false, 321 | contentType: false 322 | }).done(function(res) { 323 | console.log(res) 324 | 325 | var str = '' 326 | 327 | let to_uid = "0" 328 | let status = 3 329 | if (isPrivateChat()) { 330 | // 私聊 331 | to_uid = getQueryVariable("uid") 332 | status = 5 333 | } 334 | 335 | sends_message($('.room').attr('data-username'), $('.room').attr('data-avatar_id'), str); // sends_message(昵称,头像id,聊天内容); 336 | 337 | let send_data = JSON.stringify({ 338 | "status": status, 339 | "data": { 340 | "uid": $('.room').attr('data-uid').toString(), 341 | "username": $('.room').attr('data-username'), 342 | "avatar_id": $('.room').attr('data-avatar_id'), 343 | "room_id": $('.room').attr('data-room_id'), 344 | "image_url": res.data.url, 345 | "content": str, 346 | "to_uid" : to_uid, 347 | } 348 | }) 349 | 350 | console.log("send_data",send_data) 351 | ws.send(send_data); 352 | 353 | 354 | // 滚动条滚到最下面 355 | toLow(); 356 | 357 | // 解决input上传文件选择同一文件change事件不生效 358 | event.target.value='' 359 | 360 | layer.close(window.layerIndex); 361 | }).fail(function(res) {}); 362 | 363 | 364 | 365 | }); 366 | 367 | // 发送消息 368 | 369 | //$('.text input').focus(); 370 | $("#emojionearea2")[0].emojioneArea.setFocus() 371 | $('#subxx').click(function(event) { 372 | //var str = $('.text input').val(); // 获取聊天内容 373 | var str = $("#emojionearea2")[0].emojioneArea.getText() // 获取聊天内容 374 | str = str.replace(/\/g,'>'); 376 | str = str.replace(/\n/g,'
    '); 377 | str = str.replace(/\[em_([0-9]*)\]/g,''); 378 | 379 | if($.trim(str)!=='') { 380 | 381 | let to_uid = "0" 382 | let status = 3 383 | if (isPrivateChat()) { 384 | // 私聊 385 | to_uid = getQueryVariable("uid") 386 | status = 5 387 | } 388 | 389 | 390 | sends_message($('.room').attr('data-username'), $('.room').attr('data-avatar_id'), str); // sends_message(昵称,头像id,聊天内容); 391 | 392 | let send_data = JSON.stringify({ 393 | "status": status, 394 | "data": { 395 | "uid": $('.room').attr('data-uid').toString(), 396 | "username": $('.room').attr('data-username'), 397 | "avatar_id": $('.room').attr('data-avatar_id'), 398 | "room_id": $('.room').attr('data-room_id'), 399 | "content": str, 400 | "image_url" : "", 401 | "to_uid" : to_uid, 402 | } 403 | }) 404 | 405 | ws.send(send_data); 406 | 407 | // 滚动条滚到最下面 408 | toLow(); 409 | 410 | } 411 | 412 | $("#emojionearea2")[0].emojioneArea.setText("") 413 | $("#emojionearea2")[0].emojioneArea.setFocus() 414 | }); 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | // -----下边的代码不用管--------------------------------------- 445 | 446 | 447 | 448 | jQuery('.scrollbar-macosx').scrollbar(); 449 | $('.topnavlist li a.a-user-list').click(function(event) { 450 | $('.topnavlist .popover').not($(this).next('.popover')).removeClass('show'); 451 | $(this).next('.popover').toggleClass('show'); 452 | if($(this).next('.popover').attr('class')!='popover fade bottom in') { 453 | $('.clapboard').removeClass('hidden'); 454 | }else{ 455 | $('.clapboard').click(); 456 | } 457 | }); 458 | $('.clapboard').click(function(event) { 459 | $('.topnavlist .popover').removeClass('show'); 460 | $(this).addClass('hidden'); 461 | $('.user_portrait img').attr('portrait_id', $('.user_portrait img').attr('ptimg')); 462 | $('.user_portrait img').attr('src', '/static/images/user/' + $('.user_portrait img').attr('ptimg') + '.png'); 463 | $('.select_portrait img').removeClass('t'); 464 | $('.select_portrait img').eq($('.user_portrait img').attr('ptimg')-1).addClass('t'); 465 | $('.rooms .user_name input').val(''); 466 | }); 467 | $('.select_portrait img').hover(function() { 468 | var portrait_id = $(this).attr('portrait_id'); 469 | $('.user_portrait img').attr('src', '/static/images/user/' + portrait_id + '.png'); 470 | }, function() { 471 | var t_id = $('.user_portrait img').attr('portrait_id'); 472 | $('.user_portrait img').attr('src', '/static/images/user/' + t_id + '.png'); 473 | }); 474 | $('.select_portrait img').click(function(event) { 475 | var portrait_id = $(this).attr('portrait_id'); 476 | $('.user_portrait img').attr('portrait_id', portrait_id); 477 | $('.select_portrait img').removeClass('t'); 478 | $(this).addClass('t'); 479 | }); 480 | $('.face_btn,.faces').hover(function() { 481 | $('.faces').addClass('show'); 482 | }, function() { 483 | $('.faces').removeClass('show'); 484 | }); 485 | $('.faces img').click(function(event) { 486 | if($(this).attr('alt')!='') { 487 | $('.text input').val($('.text input').val() + '[em_' + $(this).attr('alt') + ']'); 488 | } 489 | $('.faces').removeClass('show'); 490 | $('.text input').focus(); 491 | }); 492 | $('.imgFileico').click(function(event) { 493 | $('.imgFileBtn').click(); 494 | }); 495 | function sends_message (userName, userPortrait, message) { 496 | if(message!='') { 497 | 498 | let myDate = new Date(); 499 | let time = myDate.toLocaleDateString() + myDate.toLocaleTimeString() 500 | $('.main .chat_info').html($('.main .chat_info').html() + '
  • ' + userName + ''+ time +'
    ' + message +'
  • '); 501 | } 502 | } 503 | $('.text input').keypress(function(e) { 504 | if (e.which == 13){ 505 | $('#subxx').click(); 506 | } 507 | }); 508 | 509 | 510 | function replaceImg() { 511 | $(".load-img").each(function () { 512 | let realImgUrl = $(this).attr("data-src"); 513 | if (realImgUrl !== "") { 514 | $(this).attr("src", $(this).attr("data-src")) 515 | } 516 | }); 517 | } 518 | setTimeout(replaceImg, 1500); 519 | 520 | }); 521 | 522 | function getQueryVariable(variable) 523 | { 524 | var query = window.location.search.substring(1); 525 | var vars = query.split("&"); 526 | for (var i=0;i 0 536 | } 537 | 538 | function toLow() { 539 | $('.scrollbar-macosx.scroll-content.scroll-scrolly_visible').animate({ 540 | scrollTop: $('.scrollbar-macosx.scroll-content.scroll-scrolly_visible').prop('scrollHeight') 541 | }, 500); 542 | } 543 | 544 | 545 | -------------------------------------------------------------------------------- /static/emoji/emojionearea.min.css: -------------------------------------------------------------------------------- 1 | .dropdown-menu.textcomplete-dropdown[data-strategy=emojionearea]{position:absolute;z-index:1000;min-width:160px;padding:5px 0;margin:2px 0 0;font-size:14px;text-align:left;list-style:none;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.15);-moz-border-radius:4px;-webkit-border-radius:4px;border-radius:4px;-moz-box-shadow:0 6px 12px rgba(0,0,0,.175);-webkit-box-shadow:0 6px 12px rgba(0,0,0,.175);box-shadow:0 6px 12px rgba(0,0,0,.175)}.dropdown-menu.textcomplete-dropdown[data-strategy=emojionearea] li.textcomplete-item{font-size:14px;padding:1px 3px;border:0}.dropdown-menu.textcomplete-dropdown[data-strategy=emojionearea] li.textcomplete-item a{text-decoration:none;display:block;height:100%;line-height:1.8em;padding:0 1.54em 0 .615em;color:#4f4f4f}.dropdown-menu.textcomplete-dropdown[data-strategy=emojionearea] li.textcomplete-item.active,.dropdown-menu.textcomplete-dropdown[data-strategy=emojionearea] li.textcomplete-item:hover{background-color:#e4e4e4}.dropdown-menu.textcomplete-dropdown[data-strategy=emojionearea] li.textcomplete-item.active a,.dropdown-menu.textcomplete-dropdown[data-strategy=emojionearea] li.textcomplete-item:hover a{color:#333}.dropdown-menu.textcomplete-dropdown[data-strategy=emojionearea] li.textcomplete-item .emojioneemoji{font-size:inherit;height:2ex;width:2.1ex;min-height:20px;min-width:20px;display:inline-block;margin:0 5px .2ex 0;line-height:normal;vertical-align:middle;max-width:100%;top:0}.emojionearea-text .emojioneemoji,.emojionearea-text [class*=emojione-]{font-size:inherit;height:2ex;width:2.1ex;min-height:20px;min-width:20px;display:inline-block;margin:-.2ex .15em .2ex;line-height:normal;vertical-align:middle;max-width:100%;top:0}.emojionearea,.emojionearea *{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box}.emojionearea.emojionearea-disable{position:relative;background-color:#eee;-moz-user-select:-moz-none;-ms-user-select:none;-webkit-user-select:none;user-select:none}.emojionearea.emojionearea-disable:before{content:"";display:block;top:0;left:0;right:0;bottom:0;z-index:1;opacity:.3;position:absolute;background-color:#eee}.emojionearea,.emojionearea.form-control{display:block;position:relative!important;width:100%;height:auto;padding:0;font-size:14px;background-color:#FFF;border:1px solid #CCC;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075);-moz-transition:border-color .15s ease-in-out,-moz-box-shadow .15s ease-in-out;-o-transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-transition:border-color .15s ease-in-out,-webkit-box-shadow .15s ease-in-out;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}.emojionearea.focused{border-color:#66AFE9;outline:0;-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}.emojionearea .emojionearea-editor{display:block;height:auto;min-height:8em;max-height:15em;overflow:auto;padding:6px 24px 6px 12px;line-height:1.42857143;font-size:inherit;color:#555;background-color:transparent;border:0;cursor:text;margin-right:1px;-moz-border-radius:0;-webkit-border-radius:0;border-radius:0;-moz-box-shadow:none;-webkit-box-shadow:none;box-shadow:none}.emojionearea .emojionearea-editor:empty:before{content:attr(placeholder);display:block;color:#BBB}.emojionearea .emojionearea-editor:focus{border:0;outline:0;-moz-box-shadow:none;-webkit-box-shadow:none;box-shadow:none}.emojionearea .emojionearea-editor .emojioneemoji,.emojionearea .emojionearea-editor [class*=emojione-]{font-size:inherit;height:2ex;width:2.1ex;min-height:20px;min-width:20px;display:inline-block;margin:-.2ex .15em .2ex;line-height:normal;vertical-align:middle;max-width:100%;top:0}.emojionearea.emojionearea-inline{height:34px}.emojionearea.emojionearea-inline>.emojionearea-editor{height:32px;min-height:20px;overflow:hidden;white-space:nowrap;position:absolute;top:0;left:12px;right:24px;padding:6px 0}.emojionearea.emojionearea-inline>.emojionearea-button{top:4px}.emojionearea .emojionearea-button{z-index:5;position:absolute;right:3px;top:3px;width:24px;height:24px;opacity:.6;cursor:pointer;-moz-transition:opacity .3s ease-in-out;-o-transition:opacity .3s ease-in-out;-webkit-transition:opacity .3s ease-in-out;transition:opacity .3s ease-in-out}.emojionearea .emojionearea-button:hover{opacity:1}.emojionearea .emojionearea-button>div{display:block;width:24px;height:24px;position:absolute;-moz-transition:all .4s ease-in-out;-o-transition:all .4s ease-in-out;-webkit-transition:all .4s ease-in-out;transition:all .4s ease-in-out}.emojionearea .emojionearea-button>div.emojionearea-button-open{background-position:0 -24px;filter:alpha(enabled=false);opacity:1}.emojionearea .emojionearea-button>div.emojionearea-button-close{background-position:0 0;-webkit-transform:rotate(-45deg);-o-transform:rotate(-45deg);transform:rotate(-45deg);filter:alpha(Opacity=0);opacity:0}.emojionearea .emojionearea-button.active>div.emojionearea-button-open{-webkit-transform:rotate(45deg);-o-transform:rotate(45deg);transform:rotate(45deg);filter:alpha(Opacity=0);opacity:0}.emojionearea .emojionearea-button.active>div.emojionearea-button-close{-webkit-transform:rotate(0);-o-transform:rotate(0);transform:rotate(0);filter:alpha(enabled=false);opacity:1}.emojionearea .emojionearea-picker{background:#FFF;position:absolute;-moz-box-shadow:0 1px 5px rgba(0,0,0,.32);-webkit-box-shadow:0 1px 5px rgba(0,0,0,.32);box-shadow:0 1px 5px rgba(0,0,0,.32);-moz-border-radius:5px;-webkit-border-radius:5px;border-radius:5px;height:276px;width:316px;top:-15px;right:-15px;z-index:90;-moz-transition:all .25s ease-in-out;-o-transition:all .25s ease-in-out;-webkit-transition:all .25s ease-in-out;transition:all .25s ease-in-out;filter:alpha(Opacity=0);opacity:0;-moz-user-select:-moz-none;-ms-user-select:none;-webkit-user-select:none;user-select:none}.emojionearea .emojionearea-picker.hidden{display:none}.emojionearea .emojionearea-picker .emojionearea-wrapper{position:relative;height:276px;width:316px}.emojionearea .emojionearea-picker .emojionearea-wrapper:after{content:"";display:block;position:absolute;background-repeat:no-repeat;z-index:91}.emojionearea .emojionearea-picker .emojionearea-filters{width:100%;position:absolute;z-index:95;background:#F5F7F9;padding:0 0 0 7px;height:40px}.emojionearea .emojionearea-picker .emojionearea-filters .emojionearea-filter{display:block;float:left;height:40px;width:32px;padding:7px 1px 0;cursor:pointer;-webkit-filter:grayscale(1);filter:grayscale(1)}.emojionearea .emojionearea-picker .emojionearea-filters .emojionearea-filter.active{background:#fff}.emojionearea .emojionearea-picker .emojionearea-filters .emojionearea-filter.active,.emojionearea .emojionearea-picker .emojionearea-filters .emojionearea-filter:hover{-webkit-filter:grayscale(0);filter:grayscale(0)}.emojionearea .emojionearea-picker .emojionearea-filters .emojionearea-filter>i{width:24px;height:24px;top:0}.emojionearea .emojionearea-picker .emojionearea-filters .emojionearea-filter>img{width:24px;height:24px;margin:0 3px}.emojionearea .emojionearea-picker .emojionearea-search-panel{height:30px;position:absolute;z-index:95;top:40px;left:0;right:0;padding:5px 0 5px 8px}.emojionearea .emojionearea-picker .emojionearea-search-panel .emojionearea-tones{float:right;margin-right:10px;margin-top:-1px}.emojionearea .emojionearea-picker .emojionearea-tones-panel .emojionearea-tones{position:absolute;top:4px;left:171px}.emojionearea .emojionearea-picker .emojionearea-search{float:left;padding:0;height:20px;width:160px}.emojionearea .emojionearea-picker .emojionearea-search>input{outline:0;width:160px;min-width:160px;height:20px}.emojionearea .emojionearea-picker .emojionearea-tones{padding:0;width:120px;height:20px}.emojionearea .emojionearea-picker .emojionearea-tones>.btn-tone{display:inline-block;padding:0;border:0;vertical-align:middle;outline:0;background:0 0;cursor:pointer;position:relative}.emojionearea .emojionearea-picker .emojionearea-tones>.btn-tone.btn-tone-0,.emojionearea .emojionearea-picker .emojionearea-tones>.btn-tone.btn-tone-0:after{background-color:#ffcf3e}.emojionearea .emojionearea-picker .emojionearea-tones>.btn-tone.btn-tone-1,.emojionearea .emojionearea-picker .emojionearea-tones>.btn-tone.btn-tone-1:after{background-color:#fae3c5}.emojionearea .emojionearea-picker .emojionearea-tones>.btn-tone.btn-tone-2,.emojionearea .emojionearea-picker .emojionearea-tones>.btn-tone.btn-tone-2:after{background-color:#e2cfa5}.emojionearea .emojionearea-picker .emojionearea-tones>.btn-tone.btn-tone-3,.emojionearea .emojionearea-picker .emojionearea-tones>.btn-tone.btn-tone-3:after{background-color:#daa478}.emojionearea .emojionearea-picker .emojionearea-tones>.btn-tone.btn-tone-4,.emojionearea .emojionearea-picker .emojionearea-tones>.btn-tone.btn-tone-4:after{background-color:#a78058}.emojionearea .emojionearea-picker .emojionearea-tones>.btn-tone.btn-tone-5,.emojionearea .emojionearea-picker .emojionearea-tones>.btn-tone.btn-tone-5:after{background-color:#5e4d43}.emojionearea .emojionearea-picker .emojionearea-tones.emojionearea-tones-bullet>.btn-tone,.emojionearea .emojionearea-picker .emojionearea-tones.emojionearea-tones-square>.btn-tone{width:20px;height:20px;margin:0;background-color:transparent}.emojionearea .emojionearea-picker .emojionearea-tones.emojionearea-tones-bullet>.btn-tone:after,.emojionearea .emojionearea-picker .emojionearea-tones.emojionearea-tones-square>.btn-tone:after{content:"";position:absolute;display:block;top:4px;left:4px;width:12px;height:12px}.emojionearea .emojionearea-picker .emojionearea-tones.emojionearea-tones-bullet>.btn-tone.active:after,.emojionearea .emojionearea-picker .emojionearea-tones.emojionearea-tones-square>.btn-tone.active:after{top:0;left:0;width:20px;height:20px}.emojionearea .emojionearea-picker .emojionearea-tones.emojionearea-tones-checkbox>.btn-tone,.emojionearea .emojionearea-picker .emojionearea-tones.emojionearea-tones-radio>.btn-tone{width:16px;height:16px;margin:0 2px}.emojionearea .emojionearea-picker .emojionearea-tones.emojionearea-tones-checkbox>.btn-tone.active:after,.emojionearea .emojionearea-picker .emojionearea-tones.emojionearea-tones-radio>.btn-tone.active:after{content:"";position:absolute;display:block;background-color:transparent;border:2px solid #fff;width:8px;height:8px;top:2px;left:2px;box-sizing:initial}.emojionearea .emojionearea-picker .emojionearea-scroll-area .emojionearea-category-block:after,.emojionearea .emojionearea-picker .emojionearea-scroll-area .emojionearea-category-block:before,.emojionearea .emojionearea-picker .emojionearea-scroll-area .emojionearea-category-block>.emojionearea-category:after,.emojionearea .emojionearea-picker .emojionearea-scroll-area .emojionearea-category-block>.emojionearea-category:before,.emojionearea .emojionearea-picker .emojionearea-scroll-area .emojionearea-category-title:after,.emojionearea .emojionearea-picker .emojionearea-scroll-area .emojionearea-category-title:before{content:" ";clear:both;display:block}.emojionearea .emojionearea-picker .emojionearea-tones.emojionearea-tones-bullet>.btn-tone,.emojionearea .emojionearea-picker .emojionearea-tones.emojionearea-tones-bullet>.btn-tone:after,.emojionearea .emojionearea-picker .emojionearea-tones.emojionearea-tones-radio>.btn-tone,.emojionearea .emojionearea-picker .emojionearea-tones.emojionearea-tones-radio>.btn-tone:after{-moz-border-radius:100%;-webkit-border-radius:100%;border-radius:100%}.emojionearea .emojionearea-picker .emojionearea-tones.emojionearea-tones-checkbox>.btn-tone,.emojionearea .emojionearea-picker .emojionearea-tones.emojionearea-tones-checkbox>.btn-tone:after,.emojionearea .emojionearea-picker .emojionearea-tones.emojionearea-tones-square>.btn-tone,.emojionearea .emojionearea-picker .emojionearea-tones.emojionearea-tones-square>.btn-tone:after{-moz-border-radius:1px;-webkit-border-radius:1px;border-radius:1px}.emojionearea .emojionearea-picker .emojionearea-scroll-area{height:236px;overflow:auto;overflow-x:hidden;width:100%;position:absolute;padding:0 0 5px}.emojionearea .emojionearea-picker .emojionearea-search-panel+.emojionearea-scroll-area{height:206px}.emojionearea .emojionearea-picker .emojionearea-scroll-area .emojionearea-emojis-list{z-index:1}.emojionearea .emojionearea-picker .emojionearea-scroll-area .emojionearea-category-title{display:block;font-family:Arial,'Helvetica Neue',Helvetica,sans-serif;font-size:13px;font-weight:400;color:#b2b2b2;background:#FFF;line-height:20px;margin:0;padding:7px 0 5px 6px}.emojionearea .emojionearea-picker .emojionearea-scroll-area .emojionearea-category-block{padding:0 0 0 7px}.emojionearea .emojionearea-picker .emojionearea-scroll-area .emojionearea-category-block>.emojionearea-category{padding:0!important}.emojionearea .emojionearea-picker .emojionearea-scroll-area [class*=emojione-]{-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box;margin:0;width:24px;height:24px;top:0}.emojionearea .emojionearea-picker .emojionearea-scroll-area .emojibtn{-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box;width:24px;height:24px;float:left;display:block;margin:1px;padding:3px}.emojionearea .emojionearea-picker .emojionearea-scroll-area .emojibtn:hover{-moz-border-radius:4px;-webkit-border-radius:4px;border-radius:4px;background-color:#e4e4e4;cursor:pointer}.emojionearea .emojionearea-picker .emojionearea-scroll-area .emojibtn i,.emojionearea .emojionearea-picker .emojionearea-scroll-area .emojibtn img{float:left;display:block;width:24px;height:24px}.emojionearea .emojionearea-picker .emojionearea-scroll-area .emojibtn img.lazy-emoji{filter:alpha(Opacity=0);opacity:0}.emojionearea .emojionearea-picker.emojionearea-filters-position-top .emojionearea-filters{top:0;-moz-border-radius-topleft:5px;-webkit-border-top-left-radius:5px;border-top-left-radius:5px;-moz-border-radius-topright:5px;-webkit-border-top-right-radius:5px;border-top-right-radius:5px}.emojionearea .emojionearea-picker.emojionearea-filters-position-top.emojionearea-search-position-top .emojionearea-scroll-area{bottom:0}.emojionearea .emojionearea-picker.emojionearea-filters-position-top.emojionearea-search-position-bottom .emojionearea-scroll-area{top:40px}.emojionearea .emojionearea-picker.emojionearea-filters-position-top.emojionearea-search-position-bottom .emojionearea-search-panel{top:initial;bottom:0}.emojionearea .emojionearea-picker.emojionearea-filters-position-bottom .emojionearea-filters{bottom:0;-moz-border-radius-bottomleft:5px;-webkit-border-bottom-left-radius:5px;border-bottom-left-radius:5px;-moz-border-radius-bottomright:5px;-webkit-border-bottom-right-radius:5px;border-bottom-right-radius:5px}.emojionearea .emojionearea-picker.emojionearea-filters-position-bottom.emojionearea-search-position-bottom .emojionearea-scroll-area{top:0}.emojionearea .emojionearea-picker.emojionearea-filters-position-bottom.emojionearea-search-position-bottom .emojionearea-search-panel,.emojionearea .emojionearea-picker.emojionearea-filters-position-bottom.emojionearea-search-position-top .emojionearea-scroll-area{top:initial;bottom:40px}.emojionearea .emojionearea-picker.emojionearea-filters-position-bottom.emojionearea-search-position-top .emojionearea-search-panel{top:0}.emojionearea .emojionearea-picker.emojionearea-picker-position-top{margin-top:-286px;right:-14px}.emojionearea .emojionearea-picker.emojionearea-picker-position-top .emojionearea-wrapper:after{width:19px;height:10px;background-position:-2px -49px;bottom:-10px;right:20px}.emojionearea .emojionearea-picker.emojionearea-picker-position-top.emojionearea-filters-position-bottom .emojionearea-wrapper:after{background-position:-2px -80px}.emojionearea .emojionearea-picker.emojionearea-picker-position-left,.emojionearea .emojionearea-picker.emojionearea-picker-position-right{margin-right:-326px;top:-8px}.emojionearea .emojionearea-picker.emojionearea-picker-position-left .emojionearea-wrapper:after,.emojionearea .emojionearea-picker.emojionearea-picker-position-right .emojionearea-wrapper:after{width:10px;height:19px;background-position:0 -60px;top:13px;left:-10px}.emojionearea .emojionearea-picker.emojionearea-picker-position-left.emojionearea-filters-position-bottom .emojionearea-wrapper:after,.emojionearea .emojionearea-picker.emojionearea-picker-position-right.emojionearea-filters-position-bottom .emojionearea-wrapper:after{background-position:right -60px}.emojionearea .emojionearea-picker.emojionearea-picker-position-bottom{margin-top:10px;right:-14px;top:47px}.emojionearea .emojionearea-picker.emojionearea-picker-position-bottom .emojionearea-wrapper:after{width:19px;height:10px;background-position:-2px -100px;top:-10px;right:20px}.emojionearea .emojionearea-picker.emojionearea-picker-position-bottom.emojionearea-filters-position-bottom .emojionearea-wrapper:after{background-position:-2px -90px}.emojionearea .emojionearea-button.active+.emojionearea-picker{filter:alpha(enabled=false);opacity:1}.emojionearea .emojionearea-button.active+.emojionearea-picker-position-top{margin-top:-269px}.emojionearea .emojionearea-button.active+.emojionearea-picker-position-left,.emojionearea .emojionearea-button.active+.emojionearea-picker-position-right{margin-right:-309px}.emojionearea .emojionearea-button.active+.emojionearea-picker-position-bottom{margin-top:-7px}.emojionearea.emojionearea-standalone{display:inline-block;width:auto;box-shadow:none}.emojionearea.emojionearea-standalone .emojionearea-editor{min-height:33px;position:relative;padding:6px 42px 6px 6px}.emojionearea.emojionearea-standalone .emojionearea-editor::before{content:"";position:absolute;top:4px;left:50%;bottom:4px;border-left:1px solid #e6e6e6}.emojionearea.emojionearea-standalone .emojionearea-editor.has-placeholder .emojioneemoji{opacity:.4}.emojionearea.emojionearea-standalone .emojionearea-button{top:0;right:0;bottom:0;left:0;width:auto;height:auto}.emojionearea.emojionearea-standalone .emojionearea-button>div{right:6px;top:5px}.emojionearea.emojionearea-standalone .emojionearea-picker.emojionearea-picker-position-bottom .emojionearea-wrapper:after,.emojionearea.emojionearea-standalone .emojionearea-picker.emojionearea-picker-position-top .emojionearea-wrapper:after{right:23px}.emojionearea.emojionearea-standalone .emojionearea-picker.emojionearea-picker-position-left .emojionearea-wrapper:after,.emojionearea.emojionearea-standalone .emojionearea-picker.emojionearea-picker-position-right .emojionearea-wrapper:after{top:15px}.emojionearea .emojionearea-button>div,.emojionearea .emojionearea-picker .emojionearea-wrapper:after{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABcAAABuCAYAAADMB4ipAAAHfElEQVRo3u1XS1NT2Rb+9uOcQF4YlAJzLymFUHaLrdxKULvEUNpdTnRqD532f+AHMLMc94gqR1Zbt8rBnUh3YXipPGKwRDoWgXvrYiFUlEdIkPPYZ/dAkwox5yQCVt/bzRrBPnt9e+211/etFeDQDu3ArL+/X37OeqmRWoH7+vpItfWawStF1tfXR+zW9xW5ne0p8loOcAKuCdwpRft60C8a+X5zTvebCqcAvmidf1GGHtqhHdpf1qqKzsrKipyensbi4iKWl5cBAMFgEG1tbYhGo2hpadlbmxseHpaDg4MAgI6ODng8HgBAPp/H/Pw8AODatWvo7e2tvUHrui7v3r2L+fl5XL58GVeuXIHH49m1N5/Py0ePHmF0dBQdHR24desWVFXdtYdXAn/48CHm5+dx8+ZNRKPRigEUDpuenpb3799H4YaOnWh5eVmOj48jFoshGo0STdPkwMCAXF5elqV7BgYGpKZpMhqNklgshrGxMbx580Y6gicSCTDGEIvFAADpdBqpVArJZLK4J5lMIpVKIZ1OAwBisRgYY0gkEs6Rp1IphMNh+Hw+AgCGYQAANE0r7in8Xfjm8/lIOBzGq1evnMHX19fR1NRU/D8UCoFzjnA4XFwLh8PgnCMUChXXmpqakM1mUfVBS62xsZHk83lZWi1nz579ZA0AhBDO4A0NDchkMsWSJIRAURRiVy26rktVVUkmk0EgEHAGP3XqFKamppDP56Vpmrhz5w5u374t/X4/OP+w3TRNZLNZ6LoO0zSRz+dlf38/Ll686Jzz8+fPQwiBeDwOt9tNrl+/jkwmU6yaQpVkMhncuHEDbrebxONxCCEQiUScIw8Gg+TBgwdyZGQEyWRSdnV1kVQqJYeGhrC6ugrGGEKhEHp7e3Hy5EmSTCblvXv30NPTg2AwSA6M/vF4HCMjI7b0/yzh8vv9AIBsNrt34aokuQsLC7skt729varkHtqftUFf++FHsrq0QN3eBvp68Tfvf9Mv12oFCYU7G//e9nVuO7dpNbe2W4M//yQr0p8yRvyBo1Zr++lwLcCt7afD/sBRizJGavrB1dDYYh47Htrq+Kb7jBNwxzfdZ44dD201NLaYVUkU7ozQpuAJBkARwnRZpunN5zaa5hJjiXLH05GeiMd7JEM5zzHGNQBGZvk/Iv0yYVWMvK0zKk1Dl6ahW5RQobjqdjy+wEZn9PKF0n2d0csXPL7AhuKq26GECtPQLdPQZVtn1LlB69p7yRVVSEiDEGJwRd12e4+8PR3piRQidnuPvOWKuk0IMSSkwRVV6Np7WVVbSqvGsgSnlKkAFNPQXdrOtuKqcxtcUTUAhmUJnVJmlleJo3CVHmAaOlPUOmYJkxFKibQsSRkXhr4juKIKO2BHVSwcoLrqCVdUYho6K3YYRRWmoUtdey/tgKtK7rUffiQAsLq08MnbNLe2WwBgB/zHzueFyD8nwlIfbvdx8eU0WV1aKD1cVAMs9+F2j9gUPEEKemEJIe3AnXy4XfkBoNKSZHNthWfX31EA69VKttyHVyIOY1wRwmS6tqNsrr31vXo5k/bUu4gT2cp9lhbm0rzCJpeUUrE0vS63+c7/6uXMbDUWl/ssLczNFrVFddUT09AZpUy1LKvO0DVfPrfR9HxqfNbuEe185l9MFX3o6tIC5YpKFLWOfdQQ93Zu49j0+FDCDtjOp1yaOQCYhs4Y40wI05XfWj8yPT40Ua2ey33mEmMTtp2IUEq0nW3FKeJPGPjRp1Iz2QUuLUu66txG9NLVSK3gBZ+C1lcE54oqKOOCK6rm8QU2unu+u1ANuNynvFsBAG1ubbdMQ5eGviMAFDuP0w3sfMpvQEtb24fOQncU1bXl8R7JnOu+ZNv97XxKJwY6+PNPsrm13drObVqUMlMIU5OWpVHOc96Go5lTnV2fzC/VfAozD7HTCa6olBBa1Imlhbmq2lLuQ5xaW6nCPfnln0Yt7bDUhzhps8cfKH5//uTXmvS81OeLdqI/ZoROzSZrHqG/OvOPzxuhK5VgJTvV2bW3EdqJRABwrvvS/kfoSkoZvXT1YEbociHr7vnuYEfogpBFL109HKH/h0fomnXg3Lff79r7/MmvVbWG7gX4QObzc99+Tz7mHKah05KcW6ahQ9feS6cbMCdgt7eBWJagjCuUAC5tZzuouuo0Spm0hElc9R4cbf4bVl8v1p6WUmCuqEwIs34ruxaeeTy4uJVd67As08UVlVmWoG5vA7FLG3WMmHEupVTyW+vh2cn4DADMTsaTuc21LiGEhzHOnQ6gNtMrJSBMCKHkNt999WLi0S7hejEZH81n174WpukiIMw0dKq66p3Bw50RwhUVXFGJKUy28Xal48VkfKrSlWenhsc23q2cEB9SR7iiItwZIbbgHn8AlDFCCMW7laXjqZnHjkNpaubJzNuVpWZCKChjxOMPVH/QlaW0f/G3ZLqWWl6ce/bvlddp7yFD/w8Z+njoX1+GoZMjgzMAMDkyeLAMnRh+uKveJ0YGD4ahEyODFRk6OfrL/hj67GnckaHPng7vjaGzyYmaGDr77KktQ38H8tqx8Wja+WIAAAAASUVORK5CYII=)!important}.emojionearea.emojionearea-standalone .emojionearea-editor.has-placeholder{background-repeat:no-repeat;background-position:20px 4px;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMAQMAAABsu86kAAAABlBMVEUAAAC/v79T5hyIAAAAAXRSTlMAQObYZgAAABNJREFUCNdjYGNgQEb/P4AQqiAASiUEG6Vit44AAAAASUVORK5CYII=)!important} -------------------------------------------------------------------------------- /static/rolling/js/rolling.js: -------------------------------------------------------------------------------- 1 | /** 2 | * jQuery CSS Customizable Scrollbar 3 | * 4 | * Copyright 2015, Yuriy Khabarov 5 | * Dual licensed under the MIT or GPL Version 2 licenses. 6 | * 7 | * If you found bug, please contact me via email <13real008@gmail.com> 8 | * 9 | * @author Yuriy Khabarov aka Gromo 10 | * @version 0.2.8 11 | * @url https://github.com/gromo/jquery.scrollbar/ 12 | * 13 | */ 14 | ; 15 | 16 | 17 | 18 | (function (root, factory) { 19 | if (typeof define === 'function' && define.amd) { 20 | define(['jquery'], factory); 21 | } else { 22 | factory(root.jQuery); 23 | } 24 | }(this, function ($) { 25 | 'use strict'; 26 | 27 | // init flags & variables 28 | var debug = false; 29 | 30 | var browser = { 31 | data: { 32 | index: 0, 33 | name: 'scrollbar' 34 | }, 35 | macosx: navigator.platform.toLowerCase().indexOf('mac') !== -1, 36 | mobile: /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent), 37 | overlay: null, 38 | scroll: null, 39 | scrolls: [], 40 | webkit: /WebKit/.test(navigator.userAgent) 41 | }; 42 | 43 | browser.scrolls.add = function (instance) { 44 | this.remove(instance).push(instance); 45 | }; 46 | browser.scrolls.remove = function (instance) { 47 | while ($.inArray(instance, this) >= 0) { 48 | this.splice($.inArray(instance, this), 1); 49 | } 50 | return this; 51 | }; 52 | 53 | var defaults = { 54 | "autoScrollSize": true, // automatically calculate scrollsize 55 | "autoUpdate": true, // update scrollbar if content/container size changed 56 | "debug": false, // debug mode 57 | "disableBodyScroll": false, // disable body scroll if mouse over container 58 | "duration": 200, // scroll animate duration in ms 59 | "ignoreMobile": false, // ignore mobile devices 60 | "ignoreOverlay": false, // ignore browsers with overlay scrollbars (mobile, MacOS) 61 | "scrollStep": 30, // scroll step for scrollbar arrows 62 | "showArrows": false, // add class to show arrows 63 | "stepScrolling": true, // when scrolling to scrollbar mousedown position 64 | 65 | "scrollx": null, // horizontal scroll element 66 | "scrolly": null, // vertical scroll element 67 | 68 | "onDestroy": null, // callback function on destroy, 69 | "onInit": null, // callback function on first initialization 70 | "onScroll": null, // callback function on content scrolling 71 | "onUpdate": null // callback function on init/resize (before scrollbar size calculation) 72 | }; 73 | 74 | 75 | var BaseScrollbar = function (container) { 76 | 77 | if (!browser.scroll) { 78 | browser.overlay = isScrollOverlaysContent(); 79 | browser.scroll = getBrowserScrollSize(); 80 | updateScrollbars(); 81 | 82 | $(window).resize(function () { 83 | var forceUpdate = false; 84 | if (browser.scroll && (browser.scroll.height || browser.scroll.width)) { 85 | var scroll = getBrowserScrollSize(); 86 | if (scroll.height !== browser.scroll.height || scroll.width !== browser.scroll.width) { 87 | browser.scroll = scroll; 88 | forceUpdate = true; // handle page zoom 89 | } 90 | } 91 | updateScrollbars(forceUpdate); 92 | }); 93 | } 94 | 95 | this.container = container; 96 | this.namespace = '.scrollbar_' + browser.data.index++; 97 | this.options = $.extend({}, defaults, window.jQueryScrollbarOptions || {}); 98 | this.scrollTo = null; 99 | this.scrollx = {}; 100 | this.scrolly = {}; 101 | 102 | container.data(browser.data.name, this); 103 | browser.scrolls.add(this); 104 | }; 105 | 106 | BaseScrollbar.prototype = { 107 | 108 | destroy: function () { 109 | 110 | if (!this.wrapper) { 111 | return; 112 | } 113 | 114 | this.container.removeData(browser.data.name); 115 | browser.scrolls.remove(this); 116 | 117 | // init variables 118 | var scrollLeft = this.container.scrollLeft(); 119 | var scrollTop = this.container.scrollTop(); 120 | 121 | this.container.insertBefore(this.wrapper).css({ 122 | "height": "", 123 | "margin": "", 124 | "max-height": "" 125 | }) 126 | .removeClass('scroll-content scroll-scrollx_visible scroll-scrolly_visible') 127 | .off(this.namespace) 128 | .scrollLeft(scrollLeft) 129 | .scrollTop(scrollTop); 130 | 131 | this.scrollx.scroll.removeClass('scroll-scrollx_visible').find('div').andSelf().off(this.namespace); 132 | this.scrolly.scroll.removeClass('scroll-scrolly_visible').find('div').andSelf().off(this.namespace); 133 | 134 | this.wrapper.remove(); 135 | 136 | $(document).add('body').off(this.namespace); 137 | 138 | if ($.isFunction(this.options.onDestroy)){ 139 | this.options.onDestroy.apply(this, [this.container]); 140 | } 141 | }, 142 | init: function (options) { 143 | 144 | // init variables 145 | var S = this, 146 | c = this.container, 147 | cw = this.containerWrapper || c, 148 | namespace = this.namespace, 149 | o = $.extend(this.options, options || {}), 150 | s = {x: this.scrollx, y: this.scrolly}, 151 | w = this.wrapper; 152 | 153 | var initScroll = { 154 | "scrollLeft": c.scrollLeft(), 155 | "scrollTop": c.scrollTop() 156 | }; 157 | 158 | // do not init if in ignorable browser 159 | if ((browser.mobile && o.ignoreMobile) 160 | || (browser.overlay && o.ignoreOverlay) 161 | || (browser.macosx && !browser.webkit) // still required to ignore nonWebKit browsers on Mac 162 | ) { 163 | return false; 164 | } 165 | 166 | // init scroll container 167 | if (!w) { 168 | this.wrapper = w = $('
    ').addClass('scroll-wrapper').addClass(c.attr('class')) 169 | .css('position', c.css('position') == 'absolute' ? 'absolute' : 'relative') 170 | .insertBefore(c).append(c); 171 | 172 | if (c.is('textarea')) { 173 | this.containerWrapper = cw = $('
    ').insertBefore(c).append(c); 174 | w.addClass('scroll-textarea'); 175 | } 176 | 177 | cw.addClass('scroll-content').css({ 178 | "height": "auto", 179 | "margin-bottom": browser.scroll.height * -1 + 'px', 180 | "margin-right": browser.scroll.width * -1 + 'px', 181 | "max-height": "" 182 | }); 183 | 184 | c.on('scroll' + namespace, function (event) { 185 | if ($.isFunction(o.onScroll)) { 186 | o.onScroll.call(S, { 187 | "maxScroll": s.y.maxScrollOffset, 188 | "scroll": c.scrollTop(), 189 | "size": s.y.size, 190 | "visible": s.y.visible 191 | }, { 192 | "maxScroll": s.x.maxScrollOffset, 193 | "scroll": c.scrollLeft(), 194 | "size": s.x.size, 195 | "visible": s.x.visible 196 | }); 197 | } 198 | s.x.isVisible && s.x.scroll.bar.css('left', c.scrollLeft() * s.x.kx + 'px'); 199 | s.y.isVisible && s.y.scroll.bar.css('top', c.scrollTop() * s.y.kx + 'px'); 200 | }); 201 | 202 | /* prevent native scrollbars to be visible on #anchor click */ 203 | w.on('scroll' + namespace, function () { 204 | w.scrollTop(0).scrollLeft(0); 205 | }); 206 | 207 | if (o.disableBodyScroll) { 208 | var handleMouseScroll = function (event) { 209 | isVerticalScroll(event) ? 210 | s.y.isVisible && s.y.mousewheel(event) : 211 | s.x.isVisible && s.x.mousewheel(event); 212 | }; 213 | w.on('MozMousePixelScroll' + namespace, handleMouseScroll); 214 | w.on('mousewheel' + namespace, handleMouseScroll); 215 | 216 | if (browser.mobile) { 217 | w.on('touchstart' + namespace, function (event) { 218 | var touch = event.originalEvent.touches && event.originalEvent.touches[0] || event; 219 | var originalTouch = { 220 | "pageX": touch.pageX, 221 | "pageY": touch.pageY 222 | }; 223 | var originalScroll = { 224 | "left": c.scrollLeft(), 225 | "top": c.scrollTop() 226 | }; 227 | $(document).on('touchmove' + namespace, function (event) { 228 | var touch = event.originalEvent.targetTouches && event.originalEvent.targetTouches[0] || event; 229 | c.scrollLeft(originalScroll.left + originalTouch.pageX - touch.pageX); 230 | c.scrollTop(originalScroll.top + originalTouch.pageY - touch.pageY); 231 | event.preventDefault(); 232 | }); 233 | $(document).on('touchend' + namespace, function () { 234 | $(document).off(namespace); 235 | }); 236 | }); 237 | } 238 | } 239 | if ($.isFunction(o.onInit)){ 240 | o.onInit.apply(this, [c]); 241 | } 242 | } else { 243 | cw.css({ 244 | "height": "auto", 245 | "margin-bottom": browser.scroll.height * -1 + 'px', 246 | "margin-right": browser.scroll.width * -1 + 'px', 247 | "max-height": "" 248 | }); 249 | } 250 | 251 | // init scrollbars & recalculate sizes 252 | $.each(s, function (d, scrollx) { 253 | 254 | var scrollCallback = null; 255 | var scrollForward = 1; 256 | var scrollOffset = (d === 'x') ? 'scrollLeft' : 'scrollTop'; 257 | var scrollStep = o.scrollStep; 258 | var scrollTo = function () { 259 | var currentOffset = c[scrollOffset](); 260 | c[scrollOffset](currentOffset + scrollStep); 261 | if (scrollForward == 1 && (currentOffset + scrollStep) >= scrollToValue) 262 | currentOffset = c[scrollOffset](); 263 | if (scrollForward == -1 && (currentOffset + scrollStep) <= scrollToValue) 264 | currentOffset = c[scrollOffset](); 265 | if (c[scrollOffset]() == currentOffset && scrollCallback) { 266 | scrollCallback(); 267 | } 268 | } 269 | var scrollToValue = 0; 270 | 271 | if (!scrollx.scroll) { 272 | 273 | scrollx.scroll = S._getScroll(o['scroll' + d]).addClass('scroll-' + d); 274 | 275 | if(o.showArrows){ 276 | scrollx.scroll.addClass('scroll-element_arrows_visible'); 277 | } 278 | 279 | scrollx.mousewheel = function (event) { 280 | 281 | if (!scrollx.isVisible || (d === 'x' && isVerticalScroll(event))) { 282 | return true; 283 | } 284 | if (d === 'y' && !isVerticalScroll(event)) { 285 | s.x.mousewheel(event); 286 | return true; 287 | } 288 | 289 | var delta = event.originalEvent.wheelDelta * -1 || event.originalEvent.detail; 290 | var maxScrollValue = scrollx.size - scrollx.visible - scrollx.offset; 291 | 292 | if ((delta > 0 && scrollToValue < maxScrollValue) || (delta < 0 && scrollToValue > 0)) { 293 | scrollToValue = scrollToValue + delta; 294 | if (scrollToValue < 0) 295 | scrollToValue = 0; 296 | if (scrollToValue > maxScrollValue) 297 | scrollToValue = maxScrollValue; 298 | 299 | S.scrollTo = S.scrollTo || {}; 300 | S.scrollTo[scrollOffset] = scrollToValue; 301 | setTimeout(function () { 302 | if (S.scrollTo) { 303 | c.stop().animate(S.scrollTo, 240, 'linear', function () { 304 | scrollToValue = c[scrollOffset](); 305 | }); 306 | S.scrollTo = null; 307 | } 308 | }, 1); 309 | } 310 | 311 | event.preventDefault(); 312 | return false; 313 | }; 314 | 315 | scrollx.scroll 316 | .on('MozMousePixelScroll' + namespace, scrollx.mousewheel) 317 | .on('mousewheel' + namespace, scrollx.mousewheel) 318 | .on('mouseenter' + namespace, function () { 319 | scrollToValue = c[scrollOffset](); 320 | }); 321 | 322 | // handle arrows & scroll inner mousedown event 323 | scrollx.scroll.find('.scroll-arrow, .scroll-element_track') 324 | .on('mousedown' + namespace, function (event) { 325 | 326 | if (event.which != 1) // lmb 327 | return true; 328 | 329 | scrollForward = 1; 330 | 331 | var data = { 332 | "eventOffset": event[(d === 'x') ? 'pageX' : 'pageY'], 333 | "maxScrollValue": scrollx.size - scrollx.visible - scrollx.offset, 334 | "scrollbarOffset": scrollx.scroll.bar.offset()[(d === 'x') ? 'left' : 'top'], 335 | "scrollbarSize": scrollx.scroll.bar[(d === 'x') ? 'outerWidth' : 'outerHeight']() 336 | }; 337 | var timeout = 0, timer = 0; 338 | 339 | if ($(this).hasClass('scroll-arrow')) { 340 | scrollForward = $(this).hasClass("scroll-arrow_more") ? 1 : -1; 341 | scrollStep = o.scrollStep * scrollForward; 342 | scrollToValue = scrollForward > 0 ? data.maxScrollValue : 0; 343 | } else { 344 | scrollForward = (data.eventOffset > (data.scrollbarOffset + data.scrollbarSize) ? 1 345 | : (data.eventOffset < data.scrollbarOffset ? -1 : 0)); 346 | scrollStep = Math.round(scrollx.visible * 0.75) * scrollForward; 347 | scrollToValue = (data.eventOffset - data.scrollbarOffset - 348 | (o.stepScrolling ? (scrollForward == 1 ? data.scrollbarSize : 0) 349 | : Math.round(data.scrollbarSize / 2))); 350 | scrollToValue = c[scrollOffset]() + (scrollToValue / scrollx.kx); 351 | } 352 | 353 | S.scrollTo = S.scrollTo || {}; 354 | S.scrollTo[scrollOffset] = o.stepScrolling ? c[scrollOffset]() + scrollStep : scrollToValue; 355 | 356 | if (o.stepScrolling) { 357 | scrollCallback = function () { 358 | scrollToValue = c[scrollOffset](); 359 | clearInterval(timer); 360 | clearTimeout(timeout); 361 | timeout = 0; 362 | timer = 0; 363 | }; 364 | timeout = setTimeout(function () { 365 | timer = setInterval(scrollTo, 40); 366 | }, o.duration + 100); 367 | } 368 | 369 | setTimeout(function () { 370 | if (S.scrollTo) { 371 | c.animate(S.scrollTo, o.duration); 372 | S.scrollTo = null; 373 | } 374 | }, 1); 375 | 376 | return S._handleMouseDown(scrollCallback, event); 377 | }); 378 | 379 | // handle scrollbar drag'n'drop 380 | scrollx.scroll.bar.on('mousedown' + namespace, function (event) { 381 | 382 | if (event.which != 1) // lmb 383 | return true; 384 | 385 | var eventPosition = event[(d === 'x') ? 'pageX' : 'pageY']; 386 | var initOffset = c[scrollOffset](); 387 | 388 | scrollx.scroll.addClass('scroll-draggable'); 389 | 390 | $(document).on('mousemove' + namespace, function (event) { 391 | var diff = parseInt((event[(d === 'x') ? 'pageX' : 'pageY'] - eventPosition) / scrollx.kx, 10); 392 | c[scrollOffset](initOffset + diff); 393 | }); 394 | 395 | return S._handleMouseDown(function () { 396 | scrollx.scroll.removeClass('scroll-draggable'); 397 | scrollToValue = c[scrollOffset](); 398 | }, event); 399 | }); 400 | } 401 | }); 402 | 403 | // remove classes & reset applied styles 404 | $.each(s, function (d, scrollx) { 405 | var scrollClass = 'scroll-scroll' + d + '_visible'; 406 | var scrolly = (d == "x") ? s.y : s.x; 407 | 408 | scrollx.scroll.removeClass(scrollClass); 409 | scrolly.scroll.removeClass(scrollClass); 410 | cw.removeClass(scrollClass); 411 | }); 412 | 413 | // calculate init sizes 414 | $.each(s, function (d, scrollx) { 415 | $.extend(scrollx, (d == "x") ? { 416 | "offset": parseInt(c.css('left'), 10) || 0, 417 | "size": c.prop('scrollWidth'), 418 | "visible": w.width() 419 | } : { 420 | "offset": parseInt(c.css('top'), 10) || 0, 421 | "size": c.prop('scrollHeight'), 422 | "visible": w.height() 423 | }); 424 | }); 425 | 426 | // update scrollbar visibility/dimensions 427 | this._updateScroll('x', this.scrollx); 428 | this._updateScroll('y', this.scrolly); 429 | 430 | if ($.isFunction(o.onUpdate)){ 431 | o.onUpdate.apply(this, [c]); 432 | } 433 | 434 | // calculate scroll size 435 | $.each(s, function (d, scrollx) { 436 | 437 | var cssOffset = (d === 'x') ? 'left' : 'top'; 438 | var cssFullSize = (d === 'x') ? 'outerWidth' : 'outerHeight'; 439 | var cssSize = (d === 'x') ? 'width' : 'height'; 440 | var offset = parseInt(c.css(cssOffset), 10) || 0; 441 | 442 | var AreaSize = scrollx.size; 443 | var AreaVisible = scrollx.visible + offset; 444 | 445 | var scrollSize = scrollx.scroll.size[cssFullSize]() + (parseInt(scrollx.scroll.size.css(cssOffset), 10) || 0); 446 | 447 | if (o.autoScrollSize) { 448 | scrollx.scrollbarSize = parseInt(scrollSize * AreaVisible / AreaSize, 10); 449 | scrollx.scroll.bar.css(cssSize, scrollx.scrollbarSize + 'px'); 450 | } 451 | 452 | scrollx.scrollbarSize = scrollx.scroll.bar[cssFullSize](); 453 | scrollx.kx = ((scrollSize - scrollx.scrollbarSize) / (AreaSize - AreaVisible)) || 1; 454 | scrollx.maxScrollOffset = AreaSize - AreaVisible; 455 | }); 456 | 457 | c.scrollLeft(initScroll.scrollLeft).scrollTop(initScroll.scrollTop).trigger('scroll'); 458 | }, 459 | 460 | /** 461 | * Get scrollx/scrolly object 462 | * 463 | * @param {Mixed} scroll 464 | * @returns {jQuery} scroll object 465 | */ 466 | _getScroll: function (scroll) { 467 | var types = { 468 | advanced: [ 469 | '
    ', 470 | '
    ', 471 | '
    ', 472 | '
    ', 473 | '
    ', 474 | '
    ', // required! used for scrollbar size calculation ! 475 | '
    ', 476 | '
    ', // used for handling scrollbar click 477 | '
    ', 478 | '
    ', 479 | '
    ', 480 | '
    ', // required 481 | '
    ', 482 | '
    ', 483 | '
    ', 484 | '
    ', 485 | '
    ', 486 | '
    ', 487 | '
    ', 488 | '
    ' 489 | ].join(''), 490 | simple: [ 491 | '
    ', 492 | '
    ', 493 | '
    ', // required! used for scrollbar size calculation ! 494 | '
    ', // used for handling scrollbar click 495 | '
    ', // required 496 | '
    ', 497 | '
    ' 498 | ].join('') 499 | }; 500 | if (types[scroll]) { 501 | scroll = types[scroll]; 502 | } 503 | if (!scroll) { 504 | scroll = types['simple']; 505 | } 506 | if (typeof (scroll) == 'string') { 507 | scroll = $(scroll).appendTo(this.wrapper); 508 | } else { 509 | scroll = $(scroll); 510 | } 511 | $.extend(scroll, { 512 | bar: scroll.find('.scroll-bar'), 513 | size: scroll.find('.scroll-element_size'), 514 | track: scroll.find('.scroll-element_track') 515 | }); 516 | return scroll; 517 | }, 518 | 519 | _handleMouseDown: function(callback, event) { 520 | 521 | var namespace = this.namespace; 522 | 523 | $(document).on('blur' + namespace, function () { 524 | $(document).add('body').off(namespace); 525 | callback && callback(); 526 | }); 527 | $(document).on('dragstart' + namespace, function (event) { 528 | event.preventDefault(); 529 | return false; 530 | }); 531 | $(document).on('mouseup' + namespace, function () { 532 | $(document).add('body').off(namespace); 533 | callback && callback(); 534 | }); 535 | $('body').on('selectstart' + namespace, function (event) { 536 | event.preventDefault(); 537 | return false; 538 | }); 539 | 540 | event && event.preventDefault(); 541 | return false; 542 | }, 543 | 544 | _updateScroll: function (d, scrollx) { 545 | 546 | var container = this.container, 547 | containerWrapper = this.containerWrapper || container, 548 | scrollClass = 'scroll-scroll' + d + '_visible', 549 | scrolly = (d === 'x') ? this.scrolly : this.scrollx, 550 | offset = parseInt(this.container.css((d === 'x') ? 'left' : 'top'), 10) || 0, 551 | wrapper = this.wrapper; 552 | 553 | var AreaSize = scrollx.size; 554 | var AreaVisible = scrollx.visible + offset; 555 | 556 | scrollx.isVisible = (AreaSize - AreaVisible) > 1; // bug in IE9/11 with 1px diff 557 | if (scrollx.isVisible) { 558 | scrollx.scroll.addClass(scrollClass); 559 | scrolly.scroll.addClass(scrollClass); 560 | containerWrapper.addClass(scrollClass); 561 | } else { 562 | scrollx.scroll.removeClass(scrollClass); 563 | scrolly.scroll.removeClass(scrollClass); 564 | containerWrapper.removeClass(scrollClass); 565 | } 566 | 567 | if (d === 'y') { 568 | if(container.is('textarea') || AreaSize < AreaVisible){ 569 | containerWrapper.css({ 570 | "height": (AreaVisible + browser.scroll.height) + 'px', 571 | "max-height": "none" 572 | }); 573 | } else { 574 | containerWrapper.css({ 575 | //"height": "auto", // do not reset height value: issue with height:100%! 576 | "max-height": (AreaVisible + browser.scroll.height) + 'px' 577 | }); 578 | } 579 | } 580 | 581 | if (scrollx.size != container.prop('scrollWidth') 582 | || scrolly.size != container.prop('scrollHeight') 583 | || scrollx.visible != wrapper.width() 584 | || scrolly.visible != wrapper.height() 585 | || scrollx.offset != (parseInt(container.css('left'), 10) || 0) 586 | || scrolly.offset != (parseInt(container.css('top'), 10) || 0) 587 | ) { 588 | $.extend(this.scrollx, { 589 | "offset": parseInt(container.css('left'), 10) || 0, 590 | "size": container.prop('scrollWidth'), 591 | "visible": wrapper.width() 592 | }); 593 | $.extend(this.scrolly, { 594 | "offset": parseInt(container.css('top'), 10) || 0, 595 | "size": this.container.prop('scrollHeight'), 596 | "visible": wrapper.height() 597 | }); 598 | this._updateScroll(d === 'x' ? 'y' : 'x', scrolly); 599 | } 600 | } 601 | }; 602 | 603 | var CustomScrollbar = BaseScrollbar; 604 | 605 | /* 606 | * Extend jQuery as plugin 607 | * 608 | * @param {Mixed} command to execute 609 | * @param {Mixed} arguments as Array 610 | * @return {jQuery} 611 | */ 612 | $.fn.scrollbar = function (command, args) { 613 | if (typeof command !== 'string') { 614 | args = command; 615 | command = 'init'; 616 | } 617 | if (typeof args === 'undefined') { 618 | args = []; 619 | } 620 | if (!$.isArray(args)) { 621 | args = [args]; 622 | } 623 | this.not('body, .scroll-wrapper').each(function () { 624 | var element = $(this), 625 | instance = element.data(browser.data.name); 626 | if (instance || command === 'init') { 627 | if (!instance) { 628 | instance = new CustomScrollbar(element); 629 | } 630 | if (instance[command]) { 631 | instance[command].apply(instance, args); 632 | } 633 | } 634 | }); 635 | return this; 636 | }; 637 | 638 | /** 639 | * Connect default options to global object 640 | */ 641 | $.fn.scrollbar.options = defaults; 642 | 643 | 644 | /** 645 | * Check if scroll content/container size is changed 646 | */ 647 | 648 | var updateScrollbars = (function () { 649 | var timer = 0, 650 | timerCounter = 0; 651 | 652 | return function (force) { 653 | var i, container, options, scroll, wrapper, scrollx, scrolly; 654 | for (i = 0; i < browser.scrolls.length; i++) { 655 | scroll = browser.scrolls[i]; 656 | container = scroll.container; 657 | options = scroll.options; 658 | wrapper = scroll.wrapper; 659 | scrollx = scroll.scrollx; 660 | scrolly = scroll.scrolly; 661 | if (force || (options.autoUpdate && wrapper && wrapper.is(':visible') && 662 | (container.prop('scrollWidth') != scrollx.size || container.prop('scrollHeight') != scrolly.size || wrapper.width() != scrollx.visible || wrapper.height() != scrolly.visible))) { 663 | scroll.init(); 664 | 665 | if (options.debug) { 666 | window.console && console.log({ 667 | scrollHeight: container.prop('scrollHeight') + ':' + scroll.scrolly.size, 668 | scrollWidth: container.prop('scrollWidth') + ':' + scroll.scrollx.size, 669 | visibleHeight: wrapper.height() + ':' + scroll.scrolly.visible, 670 | visibleWidth: wrapper.width() + ':' + scroll.scrollx.visible 671 | }, true); 672 | timerCounter++; 673 | } 674 | } 675 | } 676 | if (debug && timerCounter > 10) { 677 | window.console && console.log('Scroll updates exceed 10'); 678 | updateScrollbars = function () {}; 679 | } else { 680 | clearTimeout(timer); 681 | timer = setTimeout(updateScrollbars, 300); 682 | } 683 | }; 684 | })(); 685 | 686 | /* ADDITIONAL FUNCTIONS */ 687 | /** 688 | * Get native browser scrollbar size (height/width) 689 | * 690 | * @param {Boolean} actual size or CSS size, default - CSS size 691 | * @returns {Object} with height, width 692 | */ 693 | function getBrowserScrollSize(actualSize) { 694 | 695 | if (browser.webkit && !actualSize) { 696 | return { 697 | "height": 0, 698 | "width": 0 699 | }; 700 | } 701 | 702 | if (!browser.data.outer) { 703 | var css = { 704 | "border": "none", 705 | "box-sizing": "content-box", 706 | "height": "200px", 707 | "margin": "0", 708 | "padding": "0", 709 | "width": "200px" 710 | }; 711 | browser.data.inner = $("
    ").css($.extend({}, css)); 712 | browser.data.outer = $("
    ").css($.extend({ 713 | "left": "-1000px", 714 | "overflow": "scroll", 715 | "position": "absolute", 716 | "top": "-1000px" 717 | }, css)).append(browser.data.inner).appendTo("body"); 718 | } 719 | 720 | browser.data.outer.scrollLeft(1000).scrollTop(1000); 721 | 722 | return { 723 | "height": Math.ceil((browser.data.outer.offset().top - browser.data.inner.offset().top) || 0), 724 | "width": Math.ceil((browser.data.outer.offset().left - browser.data.inner.offset().left) || 0) 725 | }; 726 | } 727 | 728 | /** 729 | * Check if native browser scrollbars overlay content 730 | * 731 | * @returns {Boolean} 732 | */ 733 | function isScrollOverlaysContent() { 734 | var scrollSize = getBrowserScrollSize(true); 735 | return !(scrollSize.height || scrollSize.width); 736 | } 737 | 738 | function isVerticalScroll(event) { 739 | var e = event.originalEvent; 740 | if (e.axis && e.axis === e.HORIZONTAL_AXIS) 741 | return false; 742 | if (e.wheelDeltaX) 743 | return false; 744 | return true; 745 | } 746 | 747 | 748 | /** 749 | * Extend AngularJS as UI directive 750 | * and expose a provider for override default config 751 | * 752 | */ 753 | if (window.angular) { 754 | (function (angular) { 755 | angular.module('jQueryScrollbar', []) 756 | .provider('jQueryScrollbar', function () { 757 | var defaultOptions = defaults; 758 | return { 759 | setOptions: function (options) { 760 | angular.extend(defaultOptions, options); 761 | }, 762 | $get: function () { 763 | return { 764 | options: angular.copy(defaultOptions) 765 | }; 766 | } 767 | }; 768 | }) 769 | .directive('jqueryScrollbar', function (jQueryScrollbar, $parse) { 770 | return { 771 | "restrict": "AC", 772 | "link": function (scope, element, attrs) { 773 | var model = $parse(attrs.jqueryScrollbar), 774 | options = model(scope); 775 | element.scrollbar(options || jQueryScrollbar.options) 776 | .on('$destroy', function () { 777 | element.scrollbar('destroy'); 778 | }); 779 | } 780 | }; 781 | }); 782 | })(window.angular); 783 | } 784 | })); --------------------------------------------------------------------------------