Compare commits

..

No commits in common. "jocadbz" and "master" have entirely different histories.

12 changed files with 267 additions and 404 deletions

3
.gitignore vendored
View File

@ -1,8 +1,5 @@
config/config.json config/config.json
config/about_page.htmlbody config/about_page.htmlbody
# Testing
files/
# nano # nano
.swp .swp

View File

@ -4,6 +4,5 @@
"db_username": "threadr_user", "db_username": "threadr_user",
"db_password": "threadr_password", "db_password": "threadr_password",
"db_database": "threadr_db", "db_database": "threadr_db",
"db_svr_host": "localhost:3306", "db_svr_host": "localhost:3306"
"file_storage_dir": "files"
} }

View File

@ -4,9 +4,7 @@ import (
"context" "context"
"database/sql" "database/sql"
"html/template" "html/template"
"log"
"net/http" "net/http"
"github.com/gorilla/sessions" "github.com/gorilla/sessions"
) )
@ -27,7 +25,6 @@ type Config struct {
DBPassword string `json:"db_password"` DBPassword string `json:"db_password"`
DBDatabase string `json:"db_database"` DBDatabase string `json:"db_database"`
DBServerHost string `json:"db_svr_host"` DBServerHost string `json:"db_svr_host"`
FileStorageDir string `json:"file_storage_dir"`
} }
type App struct { type App struct {
@ -48,21 +45,24 @@ func (app *App) SessionMW(next http.HandlerFunc) http.HandlerFunc {
HttpOnly: true, HttpOnly: true,
} }
} }
if _, ok := session.Values["user_id"].(int); ok {
// Skip IP and User-Agent check for WebSocket connections
if r.URL.Query().Get("ws") != "true" {
if session.Values["user_ip"] != r.RemoteAddr || session.Values["user_agent"] != r.UserAgent() {
session.Values = make(map[interface{}]interface{})
session.Options.MaxAge = -1
session.Save(r, w)
http.Redirect(w, r, app.Config.ThreadrDir+"/login/?error=session", http.StatusFound)
return
}
}
ctx := context.WithValue(r.Context(), "session", session)
r = r.WithContext(ctx)
} else {
ctx := context.WithValue(r.Context(), "session", session) ctx := context.WithValue(r.Context(), "session", session)
r = r.WithContext(ctx) r = r.WithContext(ctx)
next(w, r)
if err := session.Save(r, w); err != nil {
/*
Ok, so here's the thing
Errors coming from this function here "can" be ignored.
They mostly come from errors while setting cookies, so in some
environments this will trigger a lot, but they are harmless.
*/
log.Printf("Error saving session in SessionMW: %v", err)
} }
next(w, r)
} }
} }

View File

@ -1,32 +0,0 @@
package handlers
import (
"fmt"
"net/http"
"path/filepath"
"strconv"
"threadr/models"
)
func FileHandler(app *App) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
fileIDStr := r.URL.Query().Get("id")
fileID, err := strconv.ParseInt(fileIDStr, 10, 64)
if err != nil {
http.NotFound(w, r)
return
}
file, err := models.GetFileByID(app.DB, fileID)
if err != nil || file == nil {
http.NotFound(w, r)
return
}
fileExt := filepath.Ext(file.OriginalName)
fileName := fmt.Sprintf("%d%s", fileID, fileExt)
filePath := filepath.Join(app.Config.FileStorageDir, fileName)
http.ServeFile(w, r, filePath)
}
}

View File

@ -1,15 +1,9 @@
package handlers package handlers
import ( import (
"crypto/sha256"
"fmt"
"io"
"log" "log"
"net/http" "net/http"
"os"
"path/filepath"
"threadr/models" "threadr/models"
"github.com/gorilla/sessions" "github.com/gorilla/sessions"
) )
@ -23,74 +17,15 @@ func ProfileEditHandler(app *App) http.HandlerFunc {
} }
if r.Method == http.MethodPost { if r.Method == http.MethodPost {
// Handle file upload
file, handler, err := r.FormFile("pfp")
if err == nil {
defer file.Close()
// Create a hash of the file
h := sha256.New()
if _, err := io.Copy(h, file); err != nil {
log.Printf("Error hashing file: %v", err)
http.Error(w, "Failed to process file", http.StatusInternalServerError)
return
}
fileHash := fmt.Sprintf("%x", h.Sum(nil))
// Create file record in the database
fileRecord := models.File{
OriginalName: handler.Filename,
Hash: fileHash,
HashAlgorithm: "sha256",
}
fileID, err := models.CreateFile(app.DB, fileRecord)
if err != nil {
log.Printf("Error creating file record: %v", err)
http.Error(w, "Failed to save file information", http.StatusInternalServerError)
return
}
// Save the file to disk
fileExt := filepath.Ext(handler.Filename)
newFileName := fmt.Sprintf("%d%s", fileID, fileExt)
filePath := filepath.Join(app.Config.FileStorageDir, newFileName)
// Reset file pointer
file.Seek(0, 0)
dst, err := os.Create(filePath)
if err != nil {
log.Printf("Error creating file on disk: %v", err)
http.Error(w, "Failed to save file", http.StatusInternalServerError)
return
}
defer dst.Close()
if _, err := io.Copy(dst, file); err != nil {
log.Printf("Error saving file to disk: %v", err)
http.Error(w, "Failed to save file", http.StatusInternalServerError)
return
}
// Update user's pfp_file_id
err = models.UpdateUserPfp(app.DB, userID, fileID)
if err != nil {
log.Printf("Error updating user pfp: %v", err)
http.Error(w, "Failed to update profile", http.StatusInternalServerError)
return
}
}
// Update other profile fields
displayName := r.FormValue("display_name") displayName := r.FormValue("display_name")
pfpURL := r.FormValue("pfp_url")
bio := r.FormValue("bio") bio := r.FormValue("bio")
err = models.UpdateUserProfile(app.DB, userID, displayName, bio) err := models.UpdateUserProfile(app.DB, userID, displayName, pfpURL, bio)
if err != nil { if err != nil {
log.Printf("Error updating profile: %v", err) log.Printf("Error updating profile: %v", err)
http.Error(w, "Failed to update profile", http.StatusInternalServerError) http.Error(w, "Failed to update profile", http.StatusInternalServerError)
return return
} }
http.Redirect(w, r, app.Config.ThreadrDir+"/profile/", http.StatusFound) http.Redirect(w, r, app.Config.ThreadrDir+"/profile/", http.StatusFound)
return return
} }

64
main.go
View File

@ -49,6 +49,26 @@ func createTablesIfNotExist(db *sql.DB) error {
return fmt.Errorf("error creating boards table: %v", err) return fmt.Errorf("error creating boards table: %v", err)
} }
// Create users table
_, err = db.Exec(`
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(255) NOT NULL UNIQUE,
display_name VARCHAR(255),
pfp_url VARCHAR(255),
bio TEXT,
authentication_string VARCHAR(128) NOT NULL,
authentication_salt VARCHAR(255) NOT NULL,
authentication_algorithm VARCHAR(50) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
verified BOOLEAN DEFAULT FALSE,
permissions BIGINT DEFAULT 0
)`)
if err != nil {
return fmt.Errorf("error creating users table: %v", err)
}
// Create threads table (without type field) // Create threads table (without type field)
_, err = db.Exec(` _, err = db.Exec(`
CREATE TABLE threads ( CREATE TABLE threads (
@ -179,41 +199,6 @@ func createTablesIfNotExist(db *sql.DB) error {
return fmt.Errorf("error creating chat_messages table: %v", err) return fmt.Errorf("error creating chat_messages table: %v", err)
} }
// Create files table (Hope this does not break anything)
_, err = db.Exec(`
CREATE TABLE files (
id INT AUTO_INCREMENT PRIMARY KEY,
original_name VARCHAR(255) NOT NULL,
hash VARCHAR(255) NOT NULL,
hash_algorithm VARCHAR(50) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)`)
if err != nil {
return fmt.Errorf("error creating files table: %v", err)
}
// Create users table (KEEP THIS HERE!)
// Otherwise SQL bitches about the foreign key.
_, err = db.Exec(`
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(255) NOT NULL UNIQUE,
display_name VARCHAR(255),
pfp_file_id INT,
bio TEXT,
authentication_string VARCHAR(128) NOT NULL,
authentication_salt VARCHAR(255) NOT NULL,
authentication_algorithm VARCHAR(50) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
verified BOOLEAN DEFAULT FALSE,
permissions BIGINT DEFAULT 0,
FOREIGN KEY (pfp_file_id) REFERENCES files(id)
)`)
if err != nil {
return fmt.Errorf("error creating users table: %v", err)
}
log.Println("Database tables created.") log.Println("Database tables created.")
return nil return nil
} }
@ -298,14 +283,6 @@ func main() {
} }
defer db.Close() defer db.Close()
// Create the file directory
// TODO: Wouldn't this be better suited on the initialize function?
// Discussion pending.
err = os.MkdirAll(config.FileStorageDir, 0700)
if err != nil {
log.Fatal("Error creating file storage directory:", err)
}
// Perform initialization if the flag is set // Perform initialization if the flag is set
if *initialize { if *initialize {
log.Println("Initializing database...") log.Println("Initializing database...")
@ -383,7 +360,6 @@ func main() {
http.HandleFunc(config.ThreadrDir+"/signup/", app.SessionMW(handlers.SignupHandler(app))) http.HandleFunc(config.ThreadrDir+"/signup/", app.SessionMW(handlers.SignupHandler(app)))
http.HandleFunc(config.ThreadrDir+"/accept_cookie/", app.SessionMW(handlers.AcceptCookieHandler(app))) http.HandleFunc(config.ThreadrDir+"/accept_cookie/", app.SessionMW(handlers.AcceptCookieHandler(app)))
http.HandleFunc(config.ThreadrDir+"/chat/", app.SessionMW(app.RequireLoginMW(handlers.ChatHandler(app)))) http.HandleFunc(config.ThreadrDir+"/chat/", app.SessionMW(app.RequireLoginMW(handlers.ChatHandler(app))))
http.HandleFunc(config.ThreadrDir+"/file", app.SessionMW(handlers.FileHandler(app)))
log.Println("Server starting on :8080") log.Println("Server starting on :8080")
log.Fatal(http.ListenAndServe(":8080", nil)) log.Fatal(http.ListenAndServe(":8080", nil))

View File

@ -2,19 +2,18 @@ package models
import ( import (
"database/sql" "database/sql"
"regexp"
"time" "time"
) )
type ChatMessage struct { type ChatMessage struct {
ID int `json:"id"` ID int
UserID int `json:"userId"` UserID int
Content string `json:"content"` Content string
ReplyTo int `json:"replyTo"` ReplyTo int // -1 if not a reply
Timestamp time.Time `json:"timestamp"` Timestamp time.Time
Username string `json:"username"` Username string // For display, fetched from user
PfpFileID sql.NullInt64 `json:"pfpFileId"` PfpURL string // For display, fetched from user
Mentions []string `json:"mentions"` Mentions []string // List of mentioned usernames
} }
func CreateChatMessage(db *sql.DB, msg ChatMessage) error { func CreateChatMessage(db *sql.DB, msg ChatMessage) error {
@ -25,7 +24,7 @@ func CreateChatMessage(db *sql.DB, msg ChatMessage) error {
func GetRecentChatMessages(db *sql.DB, limit int) ([]ChatMessage, error) { func GetRecentChatMessages(db *sql.DB, limit int) ([]ChatMessage, error) {
query := ` query := `
SELECT cm.id, cm.user_id, cm.content, cm.reply_to, cm.timestamp, u.username, u.pfp_file_id SELECT cm.id, cm.user_id, cm.content, cm.reply_to, cm.timestamp, u.username, u.pfp_url
FROM chat_messages cm FROM chat_messages cm
JOIN users u ON cm.user_id = u.id JOIN users u ON cm.user_id = u.id
ORDER BY cm.timestamp DESC ORDER BY cm.timestamp DESC
@ -40,7 +39,8 @@ func GetRecentChatMessages(db *sql.DB, limit int) ([]ChatMessage, error) {
for rows.Next() { for rows.Next() {
var msg ChatMessage var msg ChatMessage
var timestampStr string var timestampStr string
err := rows.Scan(&msg.ID, &msg.UserID, &msg.Content, &msg.ReplyTo, &timestampStr, &msg.Username, &msg.PfpFileID) var pfpURL sql.NullString
err := rows.Scan(&msg.ID, &msg.UserID, &msg.Content, &msg.ReplyTo, &timestampStr, &msg.Username, &pfpURL)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -48,6 +48,10 @@ func GetRecentChatMessages(db *sql.DB, limit int) ([]ChatMessage, error) {
if err != nil { if err != nil {
msg.Timestamp = time.Time{} msg.Timestamp = time.Time{}
} }
if pfpURL.Valid {
msg.PfpURL = pfpURL.String
}
// Parse mentions from content (simple @username detection)
msg.Mentions = extractMentions(msg.Content) msg.Mentions = extractMentions(msg.Content)
messages = append(messages, msg) messages = append(messages, msg)
} }
@ -56,14 +60,15 @@ func GetRecentChatMessages(db *sql.DB, limit int) ([]ChatMessage, error) {
func GetChatMessageByID(db *sql.DB, id int) (*ChatMessage, error) { func GetChatMessageByID(db *sql.DB, id int) (*ChatMessage, error) {
query := ` query := `
SELECT cm.id, cm.user_id, cm.content, cm.reply_to, cm.timestamp, u.username, u.pfp_file_id SELECT cm.id, cm.user_id, cm.content, cm.reply_to, cm.timestamp, u.username, u.pfp_url
FROM chat_messages cm FROM chat_messages cm
JOIN users u ON cm.user_id = u.id JOIN users u ON cm.user_id = u.id
WHERE cm.id = ?` WHERE cm.id = ?`
row := db.QueryRow(query, id) row := db.QueryRow(query, id)
var msg ChatMessage var msg ChatMessage
var timestampStr string var timestampStr string
err := row.Scan(&msg.ID, &msg.UserID, &msg.Content, &msg.ReplyTo, &timestampStr, &msg.Username, &msg.PfpFileID) var pfpURL sql.NullString
err := row.Scan(&msg.ID, &msg.UserID, &msg.Content, &msg.ReplyTo, &timestampStr, &msg.Username, &pfpURL)
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return nil, nil return nil, nil
} }
@ -74,6 +79,9 @@ func GetChatMessageByID(db *sql.DB, id int) (*ChatMessage, error) {
if err != nil { if err != nil {
msg.Timestamp = time.Time{} msg.Timestamp = time.Time{}
} }
if pfpURL.Valid {
msg.PfpURL = pfpURL.String
}
msg.Mentions = extractMentions(msg.Content) msg.Mentions = extractMentions(msg.Content)
return &msg, nil return &msg, nil
} }
@ -99,11 +107,26 @@ func GetUsernamesMatching(db *sql.DB, prefix string) ([]string, error) {
// Simple utility to extract mentions from content // Simple utility to extract mentions from content
func extractMentions(content string) []string { func extractMentions(content string) []string {
re := regexp.MustCompile(`@(\w+)`) var mentions []string
matches := re.FindAllStringSubmatch(content, -1) var currentMention string
mentions := make([]string, len(matches)) inMention := false
for i, match := range matches {
mentions[i] = match[1] for _, char := range content {
if char == '@' {
inMention = true
currentMention = "@"
} else if inMention && (char == ' ' || char == '\n' || char == '\t') {
if len(currentMention) > 1 {
mentions = append(mentions, currentMention)
}
inMention = false
currentMention = ""
} else if inMention {
currentMention += string(char)
}
}
if inMention && len(currentMention) > 1 {
mentions = append(mentions, currentMention)
} }
return mentions return mentions
} }

View File

@ -1,35 +0,0 @@
package models
import (
"database/sql"
)
type File struct {
ID int
OriginalName string
Hash string
HashAlgorithm string
}
func GetFileByID(db *sql.DB, id int64) (*File, error) {
query := "SELECT id, original_name, hash, hash_algorithm FROM files WHERE id = ?"
row := db.QueryRow(query, id)
file := &File{}
err := row.Scan(&file.ID, &file.OriginalName, &file.Hash, &file.HashAlgorithm)
if err == sql.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, err
}
return file, nil
}
func CreateFile(db *sql.DB, file File) (int64, error) {
query := "INSERT INTO files (original_name, hash, hash_algorithm) VALUES (?, ?, ?)"
result, err := db.Exec(query, file.OriginalName, file.Hash, file.HashAlgorithm)
if err != nil {
return 0, err
}
return result.LastInsertId()
}

View File

@ -11,7 +11,7 @@ type User struct {
ID int ID int
Username string Username string
DisplayName string DisplayName string
PfpFileID sql.NullInt64 PfpURL string
Bio string Bio string
AuthenticationString string AuthenticationString string
AuthenticationSalt string AuthenticationSalt string
@ -23,14 +23,15 @@ type User struct {
} }
func GetUserByID(db *sql.DB, id int) (*User, error) { func GetUserByID(db *sql.DB, id int) (*User, error) {
query := "SELECT id, username, display_name, pfp_file_id, bio, authentication_string, authentication_salt, authentication_algorithm, created_at, updated_at, verified, permissions FROM users WHERE id = ?" query := "SELECT id, username, display_name, pfp_url, bio, authentication_string, authentication_salt, authentication_algorithm, created_at, updated_at, verified, permissions FROM users WHERE id = ?"
row := db.QueryRow(query, id) row := db.QueryRow(query, id)
user := &User{} user := &User{}
var displayName sql.NullString var displayName sql.NullString
var pfpURL sql.NullString
var bio sql.NullString var bio sql.NullString
var createdAtString sql.NullString var createdAtString sql.NullString
var updatedAtString sql.NullString var updatedAtString sql.NullString
err := row.Scan(&user.ID, &user.Username, &displayName, &user.PfpFileID, &bio, &user.AuthenticationString, &user.AuthenticationSalt, &user.AuthenticationAlgorithm, &createdAtString, &updatedAtString, &user.Verified, &user.Permissions) err := row.Scan(&user.ID, &user.Username, &displayName, &pfpURL, &bio, &user.AuthenticationString, &user.AuthenticationSalt, &user.AuthenticationAlgorithm, &createdAtString, &updatedAtString, &user.Verified, &user.Permissions)
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return nil, nil return nil, nil
} }
@ -42,6 +43,11 @@ func GetUserByID(db *sql.DB, id int) (*User, error) {
} else { } else {
user.DisplayName = "" user.DisplayName = ""
} }
if pfpURL.Valid {
user.PfpURL = pfpURL.String
} else {
user.PfpURL = ""
}
if bio.Valid { if bio.Valid {
user.Bio = bio.String user.Bio = bio.String
} else { } else {
@ -67,14 +73,15 @@ func GetUserByID(db *sql.DB, id int) (*User, error) {
} }
func GetUserByUsername(db *sql.DB, username string) (*User, error) { func GetUserByUsername(db *sql.DB, username string) (*User, error) {
query := "SELECT id, username, display_name, pfp_file_id, bio, authentication_string, authentication_salt, authentication_algorithm, created_at, updated_at, verified, permissions FROM users WHERE username = ?" query := "SELECT id, username, display_name, pfp_url, bio, authentication_string, authentication_salt, authentication_algorithm, created_at, updated_at, verified, permissions FROM users WHERE username = ?"
row := db.QueryRow(query, username) row := db.QueryRow(query, username)
user := &User{} user := &User{}
var displayName sql.NullString var displayName sql.NullString
var pfpURL sql.NullString
var bio sql.NullString var bio sql.NullString
var createdAtString sql.NullString var createdAtString sql.NullString
var updatedAtString sql.NullString var updatedAtString sql.NullString
err := row.Scan(&user.ID, &user.Username, &displayName, &user.PfpFileID, &bio, &user.AuthenticationString, &user.AuthenticationSalt, &user.AuthenticationAlgorithm, &createdAtString, &updatedAtString, &user.Verified, &user.Permissions) err := row.Scan(&user.ID, &user.Username, &displayName, &pfpURL, &bio, &user.AuthenticationString, &user.AuthenticationSalt, &user.AuthenticationAlgorithm, &createdAtString, &updatedAtString, &user.Verified, &user.Permissions)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -83,6 +90,11 @@ func GetUserByUsername(db *sql.DB, username string) (*User, error) {
} else { } else {
user.DisplayName = "" user.DisplayName = ""
} }
if pfpURL.Valid {
user.PfpURL = pfpURL.String
} else {
user.PfpURL = ""
}
if bio.Valid { if bio.Valid {
user.Bio = bio.String user.Bio = bio.String
} else { } else {
@ -133,15 +145,9 @@ func CreateUser(db *sql.DB, username, password string) error {
return err return err
} }
func UpdateUserProfile(db *sql.DB, userID int, displayName, bio string) error { func UpdateUserProfile(db *sql.DB, userID int, displayName, pfpURL, bio string) error {
query := "UPDATE users SET display_name = ?, bio = ?, updated_at = NOW() WHERE id = ?" query := "UPDATE users SET display_name = ?, pfp_url = ?, bio = ?, updated_at = NOW() WHERE id = ?"
_, err := db.Exec(query, displayName, bio, userID) _, err := db.Exec(query, displayName, pfpURL, bio, userID)
return err
}
func UpdateUserPfp(db *sql.DB, userID int, pfpFileID int64) error {
query := "UPDATE users SET pfp_file_id = ? WHERE id = ?"
_, err := db.Exec(query, pfpFileID, userID)
return err return err
} }

View File

@ -224,8 +224,8 @@
{{range .Messages}} {{range .Messages}}
<div class="chat-message" id="msg-{{.ID}}"> <div class="chat-message" id="msg-{{.ID}}">
<div class="chat-message-header"> <div class="chat-message-header">
{{if .PfpFileID.Valid}} {{if .PfpURL}}
<img src="{{$.BasePath}}/file?id={{.PfpFileID.Int64}}" alt="PFP" class="chat-message-pfp"> <img src="{{.PfpURL}}" alt="PFP" class="chat-message-pfp">
{{else}} {{else}}
<div class="chat-message-pfp" style="background-color: #001858;"></div> <div class="chat-message-pfp" style="background-color: #001858;"></div>
{{end}} {{end}}
@ -262,7 +262,7 @@
let replyUsername = ''; let replyUsername = '';
function connectWebSocket() { function connectWebSocket() {
ws = new WebSocket('ws://' + window.location.host + '{{.BasePath}}/chat/?ws=true'); ws = new WebSocket('ws://' + window.location.host + '{{.BasePath}}/chat/?ws=true', [], { credentials: 'include' });
ws.onmessage = function(event) { ws.onmessage = function(event) {
const msg = JSON.parse(event.data); const msg = JSON.parse(event.data);
appendMessage(msg); appendMessage(msg);
@ -298,26 +298,21 @@
const messages = document.getElementById('chat-messages'); const messages = document.getElementById('chat-messages');
const msgDiv = document.createElement('div'); const msgDiv = document.createElement('div');
msgDiv.className = 'chat-message'; msgDiv.className = 'chat-message';
msgDiv.id = 'msg-' + msg.id; msgDiv.id = 'msg-' + msg.ID;
let pfpHTML = ''; let pfpHTML = msg.PfpURL ? `<img src="${msg.PfpURL}" alt="PFP" class="chat-message-pfp">` : `<div class="chat-message-pfp" style="background-color: #001858;"></div>`;
if (msg.pfpFileId && msg.pfpFileId.Valid) { let replyHTML = msg.ReplyTo > 0 ? `<div class="chat-message-reply" onclick="scrollToMessage(${msg.ReplyTo})">Replying to ${msg.Username}</div>` : '';
pfpHTML = `<img src="{{.BasePath}}/file?id=${msg.pfpFileId.Int64}&t=${new Date().getTime()}" alt="PFP" class="chat-message-pfp">`;
} else {
pfpHTML = `<div class="chat-message-pfp" style="background-color: #001858;"></div>`;
}
let replyHTML = msg.replyTo > 0 ? `<div class="chat-message-reply" onclick="scrollToMessage(${msg.replyTo})">Replying to ${msg.username}</div>` : '';
// Process content for mentions // Process content for mentions
let content = msg.content.replace(/@[\w]+/g, match => `<span class="chat-message-mention">${match}</span>`); let content = msg.Content.replace(/@[\w]+/g, match => `<span class="chat-message-mention">${match}</span>`);
msgDiv.innerHTML = ` msgDiv.innerHTML = `
<div class="chat-message-header"> <div class="chat-message-header">
${pfpHTML} ${pfpHTML}
<span class="chat-message-username">${msg.username}</span> <span class="chat-message-username">${msg.Username}</span>
<span class="chat-message-timestamp">${new Date(msg.timestamp).toLocaleString()}</span> <span class="chat-message-timestamp">${new Date(msg.Timestamp).toLocaleString()}</span>
</div> </div>
${replyHTML} ${replyHTML}
<div class="chat-message-content">${content}</div> <div class="chat-message-content">${content}</div>
<div class="post-actions"> <div class="post-actions">
<a href="javascript:void(0)" onclick="replyToMessage(${msg.id}, '${msg.username}')">Reply</a> <a href="javascript:void(0)" onclick="replyToMessage(${msg.ID}, '${msg.Username}')">Reply</a>
</div> </div>
`; `;
messages.appendChild(msgDiv); messages.appendChild(msgDiv);
@ -387,7 +382,6 @@
if (atIndex !== -1 && (caretPos === text.length || text[caretPos] === ' ')) { if (atIndex !== -1 && (caretPos === text.length || text[caretPos] === ' ')) {
const prefix = text.substring(atIndex + 1, caretPos); const prefix = text.substring(atIndex + 1, caretPos);
autocompletePrefix = prefix; autocompletePrefix = prefix;
// TODO: Fix this.
const response = await fetch('{{.BasePath}}/chat/?autocomplete=true&prefix=' + encodeURIComponent(prefix)); const response = await fetch('{{.BasePath}}/chat/?autocomplete=true&prefix=' + encodeURIComponent(prefix));
const usernames = await response.json(); const usernames = await response.json();
if (usernames.length > 0) { if (usernames.length > 0) {

View File

@ -14,8 +14,8 @@
<section> <section>
<p>Username: {{.User.Username}}</p> <p>Username: {{.User.Username}}</p>
<p>Display Name: {{.DisplayName}}</p> <p>Display Name: {{.DisplayName}}</p>
{{if .User.PfpFileID.Valid}} {{if .User.PfpURL}}
<img src="{{.BasePath}}/file?id={{.User.PfpFileID.Int64}}" alt="Profile Picture"> <img src="{{.User.PfpURL}}" alt="Profile Picture">
{{end}} {{end}}
<p>Bio: {{.User.Bio}}</p> <p>Bio: {{.User.Bio}}</p>
<p>Joined: {{.User.CreatedAt}}</p> <p>Joined: {{.User.CreatedAt}}</p>

View File

@ -12,11 +12,11 @@
<h2>Edit Profile</h2> <h2>Edit Profile</h2>
</header> </header>
<section> <section>
<form method="post" action="{{.BasePath}}/profile/edit/" enctype="multipart/form-data"> <form method="post" action="{{.BasePath}}/profile/edit/">
<label for="display_name">Display Name:</label> <label for="display_name">Display Name:</label>
<input type="text" id="display_name" name="display_name" value="{{.User.DisplayName}}"><br> <input type="text" id="display_name" name="display_name" value="{{.User.DisplayName}}"><br>
<label for="pfp">Profile Picture:</label> <label for="pfp_url">Profile Picture URL:</label>
<input type="file" id="pfp" name="pfp"><br> <input type="text" id="pfp_url" name="pfp_url" value="{{.User.PfpURL}}"><br>
<label for="bio">Bio:</label> <label for="bio">Bio:</label>
<textarea id="bio" name="bio">{{.User.Bio}}</textarea><br> <textarea id="bio" name="bio">{{.User.Bio}}</textarea><br>
<input type="submit" value="Save"> <input type="submit" value="Save">