chat working
This commit is contained in:
parent
787ccb4fc1
commit
27b5de93ea
16 changed files with 331 additions and 134 deletions
|
|
@ -5,20 +5,22 @@ import (
|
||||||
"chat/ws"
|
"chat/ws"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/maddalax/htmgo/framework/h"
|
"github.com/maddalax/htmgo/framework/h"
|
||||||
"github.com/maddalax/htmgo/framework/service"
|
"github.com/maddalax/htmgo/framework/service"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Manager struct {
|
type Manager struct {
|
||||||
socketManager *ws.SocketManager
|
socketManager *ws.SocketManager
|
||||||
queries *db.Queries
|
queries *db.Queries
|
||||||
|
service *Service
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewManager(loader *service.Locator) *Manager {
|
func NewManager(locator *service.Locator) *Manager {
|
||||||
return &Manager{
|
return &Manager{
|
||||||
socketManager: service.Get[ws.SocketManager](loader),
|
socketManager: service.Get[ws.SocketManager](locator),
|
||||||
queries: service.Get[db.Queries](loader),
|
queries: service.Get[db.Queries](locator),
|
||||||
|
service: NewService(locator),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -32,61 +34,58 @@ func (m *Manager) StartListener() {
|
||||||
switch event.Type {
|
switch event.Type {
|
||||||
case ws.ConnectedEvent:
|
case ws.ConnectedEvent:
|
||||||
fmt.Printf("User %s connected\n", event.Id)
|
fmt.Printf("User %s connected\n", event.Id)
|
||||||
m.backFill(event.Id)
|
m.backFill(event.Id, event.RoomId)
|
||||||
case ws.DisconnectedEvent:
|
case ws.DisconnectedEvent:
|
||||||
fmt.Printf("User %s disconnected\n", event.Id)
|
fmt.Printf("User %s disconnected\n", event.Id)
|
||||||
case ws.MessageEvent:
|
case ws.MessageEvent:
|
||||||
m.onMessage(event.Id, event.Payload)
|
m.onMessage(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) backFill(socketId string) {
|
func (m *Manager) backFill(socketId string, roomId string) {
|
||||||
messages, _ := m.queries.GetLastMessages(context.Background(), db.GetLastMessagesParams{
|
messages, _ := m.queries.GetLastMessages(context.Background(), db.GetLastMessagesParams{
|
||||||
ChatRoomID: "4ccc3f90a27c9375c98477571034b2e1",
|
ChatRoomID: roomId,
|
||||||
Limit: 50,
|
Limit: 200,
|
||||||
})
|
})
|
||||||
for _, message := range messages {
|
for _, message := range messages {
|
||||||
|
parsed, _ := time.Parse("2006-01-02 15:04:05", message.CreatedAt)
|
||||||
m.socketManager.SendText(socketId,
|
m.socketManager.SendText(socketId,
|
||||||
h.Render(MessageRow(message.Message)),
|
h.Render(MessageRow(&Message{
|
||||||
|
UserId: message.UserID,
|
||||||
|
UserName: message.UserName,
|
||||||
|
Message: message.Message,
|
||||||
|
CreatedAt: parsed,
|
||||||
|
})),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) onMessage(socketId string, payload map[string]any) {
|
func (m *Manager) onMessage(e ws.SocketEvent) {
|
||||||
fmt.Printf("Received message from %s: %v\n", socketId, payload)
|
fmt.Printf("Received message from %s: %v\n", e.Id, e.Payload)
|
||||||
message := payload["message"].(string)
|
message := e.Payload["message"].(string)
|
||||||
|
|
||||||
if message == "" {
|
if message == "" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := context.Background()
|
user, err := m.queries.GetUserBySessionId(context.Background(), e.Id)
|
||||||
|
|
||||||
user, err := m.queries.CreateUser(ctx, uuid.NewString())
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Error creating user: %v\n", err)
|
fmt.Printf("Error getting user: %v\n", err)
|
||||||
return
|
|
||||||
}
|
|
||||||
//chat, _ := m.queries.CreateChatRoom(ctx, "General")
|
|
||||||
|
|
||||||
err = m.queries.InsertMessage(
|
|
||||||
context.Background(),
|
|
||||||
db.InsertMessageParams{
|
|
||||||
ChatRoomID: "4ccc3f90a27c9375c98477571034b2e1",
|
|
||||||
UserID: user.ID,
|
|
||||||
Message: message,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Error inserting message: %v\n", err)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
m.socketManager.BroadcastText(
|
saved := m.service.InsertMessage(
|
||||||
h.Render(MessageRow(message)),
|
&user,
|
||||||
|
e.RoomId,
|
||||||
|
message,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if saved != nil {
|
||||||
|
m.socketManager.BroadcastText(
|
||||||
|
h.Render(MessageRow(saved)),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,20 @@
|
||||||
package chat
|
package chat
|
||||||
|
|
||||||
import "github.com/maddalax/htmgo/framework/h"
|
import (
|
||||||
|
"github.com/maddalax/htmgo/framework/h"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
func MessageRow(text string) *h.Element {
|
func MessageRow(message *Message) *h.Element {
|
||||||
return h.Div(
|
return h.Div(
|
||||||
h.Attribute("hx-swap-oob", "beforeend"),
|
h.Attribute("hx-swap-oob", "beforeend"),
|
||||||
h.Class("flex flex-col gap-2 w-full"),
|
h.Class("flex flex-col gap-2 w-full"),
|
||||||
h.Id("messages"),
|
h.Id("messages"),
|
||||||
h.Pf(text),
|
h.Div(
|
||||||
|
h.Class("flex gap-2 items-center"),
|
||||||
|
h.Pf(message.UserName),
|
||||||
|
h.Pf(message.CreatedAt.In(time.Local).Format("01/02 03:04 PM")),
|
||||||
|
h.Pf(message.Message),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,20 @@ package chat
|
||||||
import (
|
import (
|
||||||
"chat/internal/db"
|
"chat/internal/db"
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/google/uuid"
|
||||||
"github.com/maddalax/htmgo/framework/service"
|
"github.com/maddalax/htmgo/framework/service"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type Message struct {
|
||||||
|
UserId int64 `json:"userId"`
|
||||||
|
UserName string `json:"userName"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
}
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
queries *db.Queries
|
queries *db.Queries
|
||||||
}
|
}
|
||||||
|
|
@ -16,6 +27,54 @@ func NewService(locator *service.Locator) *Service {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) InsertMessage(user *db.User, roomId string, message string) *Message {
|
||||||
|
err := s.queries.InsertMessage(context.Background(), db.InsertMessageParams{
|
||||||
|
UserID: user.ID,
|
||||||
|
Username: user.Name,
|
||||||
|
ChatRoomID: roomId,
|
||||||
|
Message: message,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to insert message: %v\n", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &Message{
|
||||||
|
UserId: user.ID,
|
||||||
|
UserName: user.Name,
|
||||||
|
Message: message,
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) GetUserBySession(sessionId string) (*db.User, error) {
|
||||||
|
user, err := s.queries.GetUserBySessionId(context.Background(), sessionId)
|
||||||
|
return &user, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) CreateUser(name string) (*db.CreateUserRow, error) {
|
||||||
|
nameWithHash := fmt.Sprintf("%s#%s", name, uuid.NewString()[0:4])
|
||||||
|
sessionId := fmt.Sprintf("session-%s-%s", uuid.NewString(), uuid.NewString())
|
||||||
|
user, err := s.queries.CreateUser(context.Background(), db.CreateUserParams{
|
||||||
|
Name: nameWithHash,
|
||||||
|
SessionID: sessionId,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) CreateRoom(name string) (*db.CreateChatRoomRow, error) {
|
||||||
|
room, err := s.queries.CreateChatRoom(context.Background(), db.CreateChatRoomParams{
|
||||||
|
ID: fmt.Sprintf("room-%s-%s", uuid.NewString()[0:8], name),
|
||||||
|
Name: name,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &room, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Service) GetRoom(id string) (*db.ChatRoom, error) {
|
func (s *Service) GetRoom(id string) (*db.ChatRoom, error) {
|
||||||
room, err := s.queries.GetChatRoom(context.Background(), id)
|
room, err := s.queries.GetChatRoom(context.Background(), id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ type Message struct {
|
||||||
ID int64
|
ID int64
|
||||||
ChatRoomID string
|
ChatRoomID string
|
||||||
UserID int64
|
UserID int64
|
||||||
|
Username string
|
||||||
Message string
|
Message string
|
||||||
CreatedAt string
|
CreatedAt string
|
||||||
UpdatedAt string
|
UpdatedAt string
|
||||||
|
|
@ -30,4 +31,5 @@ type User struct {
|
||||||
Name string
|
Name string
|
||||||
CreatedAt string
|
CreatedAt string
|
||||||
UpdatedAt string
|
UpdatedAt string
|
||||||
|
SessionID string
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
-- name: CreateChatRoom :one
|
-- name: CreateChatRoom :one
|
||||||
INSERT INTO chat_rooms (name, created_at, updated_at)
|
INSERT INTO chat_rooms (id, name, created_at, updated_at)
|
||||||
VALUES (?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
VALUES (?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||||
RETURNING id, name, created_at, updated_at, last_message_sent_at;
|
RETURNING id, name, created_at, updated_at, last_message_sent_at;
|
||||||
|
|
||||||
-- name: InsertMessage :exec
|
-- name: InsertMessage :exec
|
||||||
INSERT INTO messages (chat_room_id, user_id, message, created_at, updated_at)
|
INSERT INTO messages (chat_room_id, user_id, username, message, created_at, updated_at)
|
||||||
VALUES (?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||||
RETURNING id, chat_room_id, user_id, message, created_at, updated_at;
|
RETURNING id, chat_room_id, user_id, username, message, created_at, updated_at;
|
||||||
|
|
||||||
-- name: UpdateChatRoomLastMessageSentAt :exec
|
-- name: UpdateChatRoomLastMessageSentAt :exec
|
||||||
UPDATE chat_rooms
|
UPDATE chat_rooms
|
||||||
|
|
@ -24,9 +24,9 @@ FROM chat_rooms
|
||||||
WHERE chat_rooms.id = ?;
|
WHERE chat_rooms.id = ?;
|
||||||
|
|
||||||
-- name: CreateUser :one
|
-- name: CreateUser :one
|
||||||
INSERT INTO users (name, created_at, updated_at)
|
INSERT INTO users (name, session_id, created_at, updated_at)
|
||||||
VALUES (?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
VALUES (?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||||
RETURNING id, name, created_at, updated_at;
|
RETURNING id, name, session_id, created_at, updated_at;
|
||||||
|
|
||||||
-- name: GetLastMessages :many
|
-- name: GetLastMessages :many
|
||||||
SELECT
|
SELECT
|
||||||
|
|
@ -42,3 +42,6 @@ FROM messages
|
||||||
WHERE messages.chat_room_id = ?
|
WHERE messages.chat_room_id = ?
|
||||||
ORDER BY messages.created_at
|
ORDER BY messages.created_at
|
||||||
LIMIT ?;
|
LIMIT ?;
|
||||||
|
|
||||||
|
-- name: GetUserBySessionId :one
|
||||||
|
SELECT * FROM users WHERE session_id = ?;
|
||||||
|
|
|
||||||
|
|
@ -11,11 +11,16 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const createChatRoom = `-- name: CreateChatRoom :one
|
const createChatRoom = `-- name: CreateChatRoom :one
|
||||||
INSERT INTO chat_rooms (name, created_at, updated_at)
|
INSERT INTO chat_rooms (id, name, created_at, updated_at)
|
||||||
VALUES (?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
VALUES (?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||||
RETURNING id, name, created_at, updated_at, last_message_sent_at
|
RETURNING id, name, created_at, updated_at, last_message_sent_at
|
||||||
`
|
`
|
||||||
|
|
||||||
|
type CreateChatRoomParams struct {
|
||||||
|
ID string
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
type CreateChatRoomRow struct {
|
type CreateChatRoomRow struct {
|
||||||
ID string
|
ID string
|
||||||
Name string
|
Name string
|
||||||
|
|
@ -24,8 +29,8 @@ type CreateChatRoomRow struct {
|
||||||
LastMessageSentAt sql.NullString
|
LastMessageSentAt sql.NullString
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) CreateChatRoom(ctx context.Context, name string) (CreateChatRoomRow, error) {
|
func (q *Queries) CreateChatRoom(ctx context.Context, arg CreateChatRoomParams) (CreateChatRoomRow, error) {
|
||||||
row := q.db.QueryRowContext(ctx, createChatRoom, name)
|
row := q.db.QueryRowContext(ctx, createChatRoom, arg.ID, arg.Name)
|
||||||
var i CreateChatRoomRow
|
var i CreateChatRoomRow
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
&i.ID,
|
&i.ID,
|
||||||
|
|
@ -38,17 +43,31 @@ func (q *Queries) CreateChatRoom(ctx context.Context, name string) (CreateChatRo
|
||||||
}
|
}
|
||||||
|
|
||||||
const createUser = `-- name: CreateUser :one
|
const createUser = `-- name: CreateUser :one
|
||||||
INSERT INTO users (name, created_at, updated_at)
|
INSERT INTO users (name, session_id, created_at, updated_at)
|
||||||
VALUES (?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
VALUES (?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||||
RETURNING id, name, created_at, updated_at
|
RETURNING id, name, session_id, created_at, updated_at
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) CreateUser(ctx context.Context, name string) (User, error) {
|
type CreateUserParams struct {
|
||||||
row := q.db.QueryRowContext(ctx, createUser, name)
|
Name string
|
||||||
var i User
|
SessionID string
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateUserRow struct {
|
||||||
|
ID int64
|
||||||
|
Name string
|
||||||
|
SessionID string
|
||||||
|
CreatedAt string
|
||||||
|
UpdatedAt string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (CreateUserRow, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, createUser, arg.Name, arg.SessionID)
|
||||||
|
var i CreateUserRow
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
&i.ID,
|
&i.ID,
|
||||||
&i.Name,
|
&i.Name,
|
||||||
|
&i.SessionID,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
)
|
)
|
||||||
|
|
@ -141,20 +160,43 @@ func (q *Queries) GetLastMessages(ctx context.Context, arg GetLastMessagesParams
|
||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getUserBySessionId = `-- name: GetUserBySessionId :one
|
||||||
|
SELECT id, name, created_at, updated_at, session_id FROM users WHERE session_id = ?
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetUserBySessionId(ctx context.Context, sessionID string) (User, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, getUserBySessionId, sessionID)
|
||||||
|
var i User
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.Name,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
&i.SessionID,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
const insertMessage = `-- name: InsertMessage :exec
|
const insertMessage = `-- name: InsertMessage :exec
|
||||||
INSERT INTO messages (chat_room_id, user_id, message, created_at, updated_at)
|
INSERT INTO messages (chat_room_id, user_id, username, message, created_at, updated_at)
|
||||||
VALUES (?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||||
RETURNING id, chat_room_id, user_id, message, created_at, updated_at
|
RETURNING id, chat_room_id, user_id, username, message, created_at, updated_at
|
||||||
`
|
`
|
||||||
|
|
||||||
type InsertMessageParams struct {
|
type InsertMessageParams struct {
|
||||||
ChatRoomID string
|
ChatRoomID string
|
||||||
UserID int64
|
UserID int64
|
||||||
|
Username string
|
||||||
Message string
|
Message string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) InsertMessage(ctx context.Context, arg InsertMessageParams) error {
|
func (q *Queries) InsertMessage(ctx context.Context, arg InsertMessageParams) error {
|
||||||
_, err := q.db.ExecContext(ctx, insertMessage, arg.ChatRoomID, arg.UserID, arg.Message)
|
_, err := q.db.ExecContext(ctx, insertMessage,
|
||||||
|
arg.ChatRoomID,
|
||||||
|
arg.UserID,
|
||||||
|
arg.Username,
|
||||||
|
arg.Message,
|
||||||
|
)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,13 @@ CREATE TABLE IF NOT EXISTS users
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
name TEXT NOT NULL UNIQUE,
|
name TEXT NOT NULL UNIQUE,
|
||||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
session_id TEXT NOT NULL
|
||||||
) STRICT;
|
) STRICT;
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS chat_rooms
|
CREATE TABLE IF NOT EXISTS chat_rooms
|
||||||
(
|
(
|
||||||
id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))), -- Generates a UUID
|
id TEXT PRIMARY KEY,
|
||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
last_message_sent_at TEXT,
|
last_message_sent_at TEXT,
|
||||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
@ -20,6 +21,7 @@ CREATE TABLE IF NOT EXISTS messages
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
chat_room_id TEXT NOT NULL,
|
chat_room_id TEXT NOT NULL,
|
||||||
user_id INTEGER NOT NULL,
|
user_id INTEGER NOT NULL,
|
||||||
|
username TEXT NOT NULL,
|
||||||
message TEXT NOT NULL,
|
message TEXT NOT NULL,
|
||||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ func main() {
|
||||||
http.FileServerFS(sub)
|
http.FileServerFS(sub)
|
||||||
|
|
||||||
app.Router.Handle("/public/*", http.StripPrefix("/public", http.FileServerFS(sub)))
|
app.Router.Handle("/public/*", http.StripPrefix("/public", http.FileServerFS(sub)))
|
||||||
app.Router.Handle("/chat", ws.Handle())
|
app.Router.Handle("/ws/chat/{id}", ws.Handle())
|
||||||
|
|
||||||
__htmgo.Register(app.Router)
|
__htmgo.Register(app.Router)
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,15 @@
|
||||||
package pages
|
package pages
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
"github.com/maddalax/htmgo/framework/h"
|
"github.com/maddalax/htmgo/framework/h"
|
||||||
"github.com/maddalax/htmgo/framework/hx"
|
"github.com/maddalax/htmgo/framework/hx"
|
||||||
"github.com/maddalax/htmgo/framework/js"
|
"github.com/maddalax/htmgo/framework/js"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ChatRoom(ctx *h.RequestContext) *h.Page {
|
func ChatRoom(ctx *h.RequestContext) *h.Page {
|
||||||
|
roomId := chi.URLParam(ctx.Request, "id")
|
||||||
return h.NewPage(
|
return h.NewPage(
|
||||||
RootPage(
|
RootPage(
|
||||||
h.Div(
|
h.Div(
|
||||||
|
|
@ -14,13 +17,32 @@ func ChatRoom(ctx *h.RequestContext) *h.Page {
|
||||||
h.TriggerChildren(),
|
h.TriggerChildren(),
|
||||||
h.HxExtension("ws"),
|
h.HxExtension("ws"),
|
||||||
),
|
),
|
||||||
h.Attribute("ws-connect", "/chat"),
|
h.Attribute("ws-connect", fmt.Sprintf("/ws/chat/%s", roomId)),
|
||||||
h.Class("flex flex-col gap-4 items-center pt-24 min-h-screen bg-neutral-100"),
|
h.Class("flex flex-row gap-4 min-h-screen bg-neutral-100"),
|
||||||
Form(ctx),
|
|
||||||
|
// Sidebar for connected users
|
||||||
|
UserSidebar(),
|
||||||
|
|
||||||
|
// Chat Area
|
||||||
h.Div(
|
h.Div(
|
||||||
|
h.Class("flex flex-col flex-grow gap-4 bg-white shadow-md rounded-lg p-4"),
|
||||||
|
|
||||||
|
h.OnEvent("hx-on::ws-after-message",
|
||||||
|
// language=JavaScript
|
||||||
|
js.EvalJsOnSibling("#messages", `
|
||||||
|
element.scrollTop = element.scrollHeight;
|
||||||
|
`)),
|
||||||
|
|
||||||
|
// Chat Messages
|
||||||
h.Div(
|
h.Div(
|
||||||
h.Id("messages"),
|
h.Id("messages"),
|
||||||
h.Class("flex flex-col gap-2 w-full"),
|
h.Class("flex flex-col gap-2 overflow-auto grow w-full"),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Chat Input at the bottom
|
||||||
|
h.Div(
|
||||||
|
h.Class("mt-auto"),
|
||||||
|
Form(ctx),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -28,13 +50,27 @@ func ChatRoom(ctx *h.RequestContext) *h.Page {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func UserSidebar() *h.Element {
|
||||||
|
return h.Div(
|
||||||
|
h.Class("w-64 bg-slate-200 p-4 flex flex-col gap-4 rounded-l-lg"),
|
||||||
|
h.H2F("Connected Users", h.Class("text-lg font-bold")),
|
||||||
|
h.Ul(
|
||||||
|
h.Class("flex flex-col gap-2"),
|
||||||
|
// This would be populated dynamically with connected users
|
||||||
|
h.Li(h.Text("User 1"), h.Class("text-slate-700")),
|
||||||
|
h.Li(h.Text("User 2"), h.Class("text-slate-700")),
|
||||||
|
h.Li(h.Text("User 3"), h.Class("text-slate-700")),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
func MessageInput() *h.Element {
|
func MessageInput() *h.Element {
|
||||||
return h.Input("text",
|
return h.Input("text",
|
||||||
h.Id("message-input"),
|
h.Id("message-input"),
|
||||||
h.Required(),
|
h.Required(),
|
||||||
h.Class("p-4 rounded-md border border-slate-200"),
|
h.Class("p-4 rounded-md border border-slate-200 w-full"),
|
||||||
h.Name("message"),
|
h.Name("message"),
|
||||||
h.Placeholder("Message"),
|
h.Placeholder("Type a message..."),
|
||||||
h.HxBeforeWsSend(
|
h.HxBeforeWsSend(
|
||||||
js.SetValue(""),
|
js.SetValue(""),
|
||||||
),
|
),
|
||||||
|
|
@ -44,40 +80,11 @@ func MessageInput() *h.Element {
|
||||||
|
|
||||||
func Form(ctx *h.RequestContext) *h.Element {
|
func Form(ctx *h.RequestContext) *h.Element {
|
||||||
return h.Div(
|
return h.Div(
|
||||||
h.Class("flex flex-col items-center justify-center p-4 gap-6"),
|
h.Class("flex gap-4 items-center"),
|
||||||
h.H2F("Form submission with ws example", h.Class("text-2xl font-bold")),
|
|
||||||
h.Form(
|
h.Form(
|
||||||
h.Attribute("ws-send", ""),
|
h.Attribute("ws-send", ""),
|
||||||
h.Class("flex flex-col gap-2"),
|
h.Class("flex flex-grow"),
|
||||||
h.LabelFor("name", "Your Message"),
|
|
||||||
MessageInput(),
|
MessageInput(),
|
||||||
SubmitButton(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func SubmitButton() *h.Element {
|
|
||||||
buttonClasses := "rounded items-center px-3 py-2 bg-slate-800 text-white w-full text-center"
|
|
||||||
return h.Div(
|
|
||||||
h.HxBeforeRequest(
|
|
||||||
js.RemoveClassOnChildren(".loading", "hidden"),
|
|
||||||
js.SetClassOnChildren(".submit", "hidden"),
|
|
||||||
),
|
|
||||||
h.HxAfterRequest(
|
|
||||||
js.SetClassOnChildren(".loading", "hidden"),
|
|
||||||
js.RemoveClassOnChildren(".submit", "hidden"),
|
|
||||||
),
|
|
||||||
h.Class("flex gap-2 justify-center"),
|
|
||||||
h.Button(
|
|
||||||
h.Class("loading hidden relative text-center", buttonClasses),
|
|
||||||
Spinner(),
|
|
||||||
h.Disabled(),
|
|
||||||
h.Text("Submitting..."),
|
|
||||||
),
|
|
||||||
h.Button(
|
|
||||||
h.Type("submit"),
|
|
||||||
h.Class("submit", buttonClasses),
|
|
||||||
h.Text("Submit"),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -85,7 +92,7 @@ func SubmitButton() *h.Element {
|
||||||
func Spinner(children ...h.Ren) *h.Element {
|
func Spinner(children ...h.Ren) *h.Element {
|
||||||
return h.Div(
|
return h.Div(
|
||||||
h.Children(children...),
|
h.Children(children...),
|
||||||
h.Class("absolute left-1 spinner spinner-border animate-spin inline-block w-6 h-6 border-4 rounded-full border-slate-200 border-t-transparent"),
|
h.Class("spinner spinner-border animate-spin w-4 h-4 border-2 border-t-transparent"),
|
||||||
h.Attribute("role", "status"),
|
h.Attribute("role", "status"),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,26 +4,59 @@ import (
|
||||||
"chat/chat"
|
"chat/chat"
|
||||||
"chat/components"
|
"chat/components"
|
||||||
"github.com/maddalax/htmgo/framework/h"
|
"github.com/maddalax/htmgo/framework/h"
|
||||||
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
func CreateOrJoinRoom(ctx *h.RequestContext) *h.Partial {
|
func CreateOrJoinRoom(ctx *h.RequestContext) *h.Partial {
|
||||||
locator := ctx.ServiceLocator()
|
locator := ctx.ServiceLocator()
|
||||||
service := chat.NewService(locator)
|
service := chat.NewService(locator)
|
||||||
|
|
||||||
chatRoomId := ctx.FormValue("join-chat-room")
|
chatRoomId := ctx.Request.FormValue("join-chat-room")
|
||||||
|
username := ctx.Request.FormValue("username")
|
||||||
|
|
||||||
|
if username == "" {
|
||||||
|
return h.SwapPartial(ctx, components.FormError("Username is required"))
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := service.CreateUser(username)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return h.SwapPartial(ctx, components.FormError("Failed to create user"))
|
||||||
|
}
|
||||||
|
|
||||||
|
var redirect = func(path string) *h.Partial {
|
||||||
|
cookie := &http.Cookie{
|
||||||
|
Name: "session_id",
|
||||||
|
Value: user.SessionID,
|
||||||
|
Path: "/",
|
||||||
|
}
|
||||||
|
return h.SwapManyPartialWithHeaders(
|
||||||
|
ctx,
|
||||||
|
h.NewHeaders(
|
||||||
|
"Set-Cookie", cookie.String(),
|
||||||
|
"HX-Redirect", path,
|
||||||
|
),
|
||||||
|
h.Fragment(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if chatRoomId != "" {
|
if chatRoomId != "" {
|
||||||
room, _ := service.GetRoom(chatRoomId)
|
room, _ := service.GetRoom(chatRoomId)
|
||||||
if room == nil {
|
if room == nil {
|
||||||
return h.SwapPartial(ctx, components.FormError("Room not found"))
|
return h.SwapPartial(ctx, components.FormError("Room not found"))
|
||||||
} else {
|
} else {
|
||||||
return h.RedirectPartial("/chat/" + chatRoomId)
|
return redirect("/chat/" + chatRoomId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
chatRoomName := ctx.FormValue("chat-room-name")
|
chatRoomName := ctx.Request.FormValue("new-chat-room")
|
||||||
if chatRoomName != "" {
|
if chatRoomName != "" {
|
||||||
// create room
|
room, _ := service.CreateRoom(chatRoomName)
|
||||||
|
if room == nil {
|
||||||
|
return h.SwapPartial(ctx, components.FormError("Failed to create room"))
|
||||||
|
} else {
|
||||||
|
return redirect("/chat/" + room.ID)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return h.SwapPartial(ctx, components.FormError("Create a new room or join an existing one"))
|
return h.SwapPartial(ctx, components.FormError("Create a new room or join an existing one"))
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"github.com/coder/websocket"
|
"github.com/coder/websocket"
|
||||||
"github.com/coder/websocket/wsjson"
|
"github.com/coder/websocket/wsjson"
|
||||||
"github.com/google/uuid"
|
"github.com/go-chi/chi/v5"
|
||||||
"github.com/maddalax/htmgo/framework/h"
|
"github.com/maddalax/htmgo/framework/h"
|
||||||
"github.com/maddalax/htmgo/framework/service"
|
"github.com/maddalax/htmgo/framework/service"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
@ -12,8 +12,20 @@ import (
|
||||||
|
|
||||||
func Handle() http.HandlerFunc {
|
func Handle() http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
c, err := websocket.Accept(w, r, nil)
|
|
||||||
cc := r.Context().Value(h.RequestContextKey).(*h.RequestContext)
|
cc := r.Context().Value(h.RequestContextKey).(*h.RequestContext)
|
||||||
|
|
||||||
|
sessionCookie, err := r.Cookie("session_id")
|
||||||
|
|
||||||
|
cookies := r.Cookies()
|
||||||
|
|
||||||
|
println(cookies)
|
||||||
|
// no session
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := websocket.Accept(w, r, nil)
|
||||||
|
|
||||||
locator := cc.ServiceLocator()
|
locator := cc.ServiceLocator()
|
||||||
manager := service.Get[SocketManager](locator)
|
manager := service.Get[SocketManager](locator)
|
||||||
|
|
||||||
|
|
@ -21,22 +33,30 @@ func Handle() http.HandlerFunc {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
id := uuid.NewString()
|
sessionId := sessionCookie.Value
|
||||||
manager.Add(id, c)
|
|
||||||
|
roomId := chi.URLParam(r, "id")
|
||||||
|
|
||||||
|
if roomId == "" {
|
||||||
|
manager.CloseWithError(sessionId, "invalid room")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
manager.Add(roomId, sessionId, c)
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
manager.Disconnect(id)
|
manager.Disconnect(sessionId)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
var v map[string]any
|
var v map[string]any
|
||||||
err = wsjson.Read(context.Background(), c, &v)
|
err = wsjson.Read(context.Background(), c, &v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
manager.CloseWithError(id, "failed to read message")
|
manager.CloseWithError(sessionId, "failed to read message")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if v != nil {
|
if v != nil {
|
||||||
manager.OnMessage(id, v)
|
manager.OnMessage(sessionId, v)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,18 +17,25 @@ const (
|
||||||
|
|
||||||
type SocketEvent struct {
|
type SocketEvent struct {
|
||||||
Id string
|
Id string
|
||||||
|
RoomId string
|
||||||
Type EventType
|
Type EventType
|
||||||
Payload map[string]any
|
Payload map[string]any
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SocketConnection struct {
|
||||||
|
Id string
|
||||||
|
Conn *websocket.Conn
|
||||||
|
RoomId string
|
||||||
|
}
|
||||||
|
|
||||||
type SocketManager struct {
|
type SocketManager struct {
|
||||||
sockets *xsync.MapOf[string, *websocket.Conn]
|
sockets *xsync.MapOf[string, SocketConnection]
|
||||||
listeners []chan SocketEvent
|
listeners []chan SocketEvent
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSocketManager() *SocketManager {
|
func NewSocketManager() *SocketManager {
|
||||||
return &SocketManager{
|
return &SocketManager{
|
||||||
sockets: xsync.NewMapOf[string, *websocket.Conn](),
|
sockets: xsync.NewMapOf[string, SocketConnection](),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -46,26 +53,41 @@ func (manager *SocketManager) dispatch(event SocketEvent) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (manager *SocketManager) OnMessage(id string, message map[string]any) {
|
func (manager *SocketManager) OnMessage(id string, message map[string]any) {
|
||||||
|
socket := manager.Get(id)
|
||||||
|
if socket == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
manager.dispatch(SocketEvent{
|
manager.dispatch(SocketEvent{
|
||||||
Id: id,
|
Id: id,
|
||||||
Type: MessageEvent,
|
Type: MessageEvent,
|
||||||
Payload: message,
|
Payload: message,
|
||||||
|
RoomId: socket.RoomId,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (manager *SocketManager) Add(id string, conn *websocket.Conn) {
|
func (manager *SocketManager) Add(roomId string, id string, conn *websocket.Conn) {
|
||||||
manager.sockets.Store(id, conn)
|
manager.sockets.Store(id, SocketConnection{
|
||||||
|
Id: id,
|
||||||
|
Conn: conn,
|
||||||
|
RoomId: roomId,
|
||||||
|
})
|
||||||
manager.dispatch(SocketEvent{
|
manager.dispatch(SocketEvent{
|
||||||
Id: id,
|
Id: id,
|
||||||
Type: ConnectedEvent,
|
Type: ConnectedEvent,
|
||||||
|
RoomId: roomId,
|
||||||
Payload: map[string]any{},
|
Payload: map[string]any{},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (manager *SocketManager) OnClose(id string) {
|
func (manager *SocketManager) OnClose(id string) {
|
||||||
|
socket := manager.Get(id)
|
||||||
|
if socket == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
manager.dispatch(SocketEvent{
|
manager.dispatch(SocketEvent{
|
||||||
Id: id,
|
Id: id,
|
||||||
Type: DisconnectedEvent,
|
Type: DisconnectedEvent,
|
||||||
|
RoomId: socket.RoomId,
|
||||||
Payload: map[string]any{},
|
Payload: map[string]any{},
|
||||||
})
|
})
|
||||||
manager.sockets.Delete(id)
|
manager.sockets.Delete(id)
|
||||||
|
|
@ -75,7 +97,7 @@ func (manager *SocketManager) CloseWithError(id string, message string) {
|
||||||
conn := manager.Get(id)
|
conn := manager.Get(id)
|
||||||
if conn != nil {
|
if conn != nil {
|
||||||
defer manager.OnClose(id)
|
defer manager.OnClose(id)
|
||||||
conn.Close(websocket.StatusInternalError, message)
|
conn.Conn.Close(websocket.StatusInternalError, message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -83,19 +105,22 @@ func (manager *SocketManager) Disconnect(id string) {
|
||||||
conn := manager.Get(id)
|
conn := manager.Get(id)
|
||||||
if conn != nil {
|
if conn != nil {
|
||||||
defer manager.OnClose(id)
|
defer manager.OnClose(id)
|
||||||
_ = conn.CloseNow()
|
_ = conn.Conn.CloseNow()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (manager *SocketManager) Get(id string) *websocket.Conn {
|
func (manager *SocketManager) Get(id string) *SocketConnection {
|
||||||
conn, _ := manager.sockets.Load(id)
|
conn, ok := manager.sockets.Load(id)
|
||||||
return conn
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &conn
|
||||||
}
|
}
|
||||||
|
|
||||||
func (manager *SocketManager) Broadcast(message []byte, messageType websocket.MessageType) {
|
func (manager *SocketManager) Broadcast(message []byte, messageType websocket.MessageType) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
manager.sockets.Range(func(id string, conn *websocket.Conn) bool {
|
manager.sockets.Range(func(id string, conn SocketConnection) bool {
|
||||||
err := conn.Write(ctx, messageType, message)
|
err := conn.Conn.Write(ctx, messageType, message)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
manager.Disconnect(id)
|
manager.Disconnect(id)
|
||||||
}
|
}
|
||||||
|
|
@ -111,6 +136,6 @@ func (manager *SocketManager) BroadcastText(message string) {
|
||||||
func (manager *SocketManager) SendText(id string, message string) {
|
func (manager *SocketManager) SendText(id string, message string) {
|
||||||
conn := manager.Get(id)
|
conn := manager.Get(id)
|
||||||
if conn != nil {
|
if conn != nil {
|
||||||
_ = conn.Write(context.Background(), websocket.MessageText, []byte(message))
|
_ = conn.Conn.Write(context.Background(), websocket.MessageText, []byte(message))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,8 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type RequestContext struct {
|
type RequestContext struct {
|
||||||
*http.Request
|
Request *http.Request
|
||||||
|
Response http.ResponseWriter
|
||||||
locator *service.Locator
|
locator *service.Locator
|
||||||
isBoosted bool
|
isBoosted bool
|
||||||
currentBrowserUrl string
|
currentBrowserUrl string
|
||||||
|
|
@ -118,9 +119,10 @@ func (app *App) start() {
|
||||||
app.Router.Use(func(h http.Handler) http.Handler {
|
app.Router.Use(func(h http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
cc := &RequestContext{
|
cc := &RequestContext{
|
||||||
locator: app.Opts.ServiceLocator,
|
locator: app.Opts.ServiceLocator,
|
||||||
Request: r,
|
Request: r,
|
||||||
kv: make(map[string]interface{}),
|
Response: w,
|
||||||
|
kv: make(map[string]interface{}),
|
||||||
}
|
}
|
||||||
populateHxFields(cc)
|
populateHxFields(cc)
|
||||||
ctx := context.WithValue(r.Context(), RequestContextKey, cc)
|
ctx := context.WithValue(r.Context(), RequestContextKey, cc)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
package h
|
package h
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/maddalax/htmgo/framework/hx"
|
|
||||||
"html"
|
"html"
|
||||||
"net/http"
|
"net/http"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
|
@ -57,10 +56,6 @@ func SwapPartial(ctx *RequestContext, swap *Element) *Partial {
|
||||||
SwapMany(ctx, swap))
|
SwapMany(ctx, swap))
|
||||||
}
|
}
|
||||||
|
|
||||||
func RedirectPartial(url string) *Partial {
|
|
||||||
return NewPartialWithHeaders(NewHeaders(hx.RedirectHeader, url), Fragment())
|
|
||||||
}
|
|
||||||
|
|
||||||
func SwapManyPartial(ctx *RequestContext, swaps ...*Element) *Partial {
|
func SwapManyPartial(ctx *RequestContext, swaps ...*Element) *Partial {
|
||||||
return NewPartial(
|
return NewPartial(
|
||||||
SwapMany(ctx, swaps...),
|
SwapMany(ctx, swaps...),
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ func CombineHeaders(headers ...*Headers) *Headers {
|
||||||
}
|
}
|
||||||
|
|
||||||
func CurrentPath(ctx *RequestContext) string {
|
func CurrentPath(ctx *RequestContext) string {
|
||||||
current := ctx.Header.Get(hx.CurrentUrlHeader)
|
current := ctx.Request.Header.Get(hx.CurrentUrlHeader)
|
||||||
parsed, err := url.Parse(current)
|
parsed, err := url.Parse(current)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ""
|
return ""
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ func (q *Qs) ToString() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetQueryParam(ctx *RequestContext, key string) string {
|
func GetQueryParam(ctx *RequestContext, key string) string {
|
||||||
value, ok := ctx.URL.Query()[key]
|
value, ok := ctx.Request.URL.Query()[key]
|
||||||
if value == nil || !ok {
|
if value == nil || !ok {
|
||||||
current := ctx.currentBrowserUrl
|
current := ctx.currentBrowserUrl
|
||||||
if current != "" {
|
if current != "" {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue