feat: integrate user profile pictures with circular avatar display and upload in settings
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 57s

This commit is contained in:
sandrodach committed 2026-10-25 21:37:06 +01:00
1 parent 8b043c4809
commit 444442fdf0
13 files changed
+367 -65

No files matched your search

+27 -19
View File
@@ -81,6 +81,7 @@ func (s *Store) migrate() error {
discord_webhook_url TEXT NOT NULL DEFAULT '',
custom_webhook_url TEXT NOT NULL DEFAULT '',
notify_before_minutes INTEGER NOT NULL DEFAULT 30,
avatar_url TEXT NOT NULL DEFAULT '',
created_at TEXT NOT NULL
);
@@ -313,6 +314,7 @@ func (s *Store) migrate() error {
"ALTER TABLE users ADD COLUMN discord_webhook_url TEXT NOT NULL DEFAULT '';",
"ALTER TABLE users ADD COLUMN custom_webhook_url TEXT NOT NULL DEFAULT '';",
"ALTER TABLE users ADD COLUMN notify_before_minutes INTEGER NOT NULL DEFAULT 30;",
"ALTER TABLE users ADD COLUMN avatar_url TEXT NOT NULL DEFAULT '';",
"ALTER TABLE thoughts ADD COLUMN task_list_id TEXT NOT NULL DEFAULT '';",
"ALTER TABLE thoughts ADD COLUMN kanban_status TEXT NOT NULL DEFAULT 'todo';",
@@ -486,7 +488,7 @@ func (s *Store) EnsureDefaultUser() (*domain.User, error) {
// --- USERS ---
const userSelectCols = `id, email, username, password_hash, display_name, role, is_active, alarm_channels, alarm_email, discord_webhook_url, custom_webhook_url, notify_before_minutes, created_at`
const userSelectCols = `id, email, username, password_hash, display_name, role, is_active, alarm_channels, alarm_email, discord_webhook_url, custom_webhook_url, notify_before_minutes, avatar_url, created_at`
func parseAlarmChannels(raw string) []string {
parts := strings.Split(raw, ",")
@@ -509,7 +511,7 @@ func scanUser(row interface{ Scan(dest ...any) error }) (*domain.User, error) {
err := row.Scan(
&u.ID, &u.Email, &u.Username, &u.PasswordHash, &u.DisplayName,
&u.Role, &isActiveInt, &channelsRaw, &u.AlarmEmail,
&u.DiscordWebhookURL, &u.CustomWebhookURL, &u.NotifyBeforeMinutes, &u.CreatedAt,
&u.DiscordWebhookURL, &u.CustomWebhookURL, &u.NotifyBeforeMinutes, &u.AvatarURL, &u.CreatedAt,
)
if err != nil {
return nil, err
@@ -559,11 +561,12 @@ func (s *Store) CreateUserWithUsername(email, username, passwordHash, displayNam
DiscordWebhookURL: "",
CustomWebhookURL: "",
NotifyBeforeMinutes: 30,
AvatarURL: "",
CreatedAt: domain.NowISO(),
}
_, err := s.db.Exec(
`INSERT INTO users (id, email, username, password_hash, display_name, role, is_active, alarm_channels, alarm_email, discord_webhook_url, custom_webhook_url, notify_before_minutes, created_at)
VALUES (?, ?, ?, ?, ?, ?, 1, '', ?, '', '', 30, ?)`,
`INSERT INTO users (id, email, username, password_hash, display_name, role, is_active, alarm_channels, alarm_email, discord_webhook_url, custom_webhook_url, notify_before_minutes, avatar_url, created_at)
VALUES (?, ?, ?, ?, ?, ?, 1, '', ?, '', '', 30, '', ?)`,
u.ID, u.Email, u.Username, u.PasswordHash, u.DisplayName, u.Role, u.AlarmEmail, u.CreatedAt,
)
if err != nil {
@@ -714,7 +717,7 @@ func (s *Store) UpdateUser(
discordWebhookURL *string,
notifyBeforeMinutes *int,
) (*domain.User, error) {
return s.UpdateUserFull(userID, displayName, email, role, isActive, nil, nil, discordWebhookURL, nil, notifyBeforeMinutes)
return s.UpdateUserFull(userID, displayName, email, role, isActive, nil, nil, discordWebhookURL, nil, notifyBeforeMinutes, nil)
}
func (s *Store) UpdateUserFull(
@@ -724,6 +727,7 @@ func (s *Store) UpdateUserFull(
alarmChannels *[]string,
alarmEmail, discordWebhookURL, customWebhookURL *string,
notifyBeforeMinutes *int,
avatarURL *string,
) (*domain.User, error) {
u, err := s.GetUserByID(userID)
if err != nil {
@@ -756,14 +760,17 @@ func (s *Store) UpdateUserFull(
if notifyBeforeMinutes != nil {
u.NotifyBeforeMinutes = *notifyBeforeMinutes
}
if avatarURL != nil {
u.AvatarURL = strings.TrimSpace(*avatarURL)
}
activeInt := 0
if u.IsActive {
activeInt = 1
}
channelsStr := strings.Join(u.AlarmChannels, ",")
_, err = s.db.Exec(
`UPDATE users SET display_name = ?, email = ?, role = ?, is_active = ?, alarm_channels = ?, alarm_email = ?, discord_webhook_url = ?, custom_webhook_url = ?, notify_before_minutes = ? WHERE id = ?`,
u.DisplayName, u.Email, u.Role, activeInt, channelsStr, u.AlarmEmail, u.DiscordWebhookURL, u.CustomWebhookURL, u.NotifyBeforeMinutes, userID,
`UPDATE users SET display_name = ?, email = ?, role = ?, is_active = ?, alarm_channels = ?, alarm_email = ?, discord_webhook_url = ?, custom_webhook_url = ?, notify_before_minutes = ?, avatar_url = ? WHERE id = ?`,
u.DisplayName, u.Email, u.Role, activeInt, channelsStr, u.AlarmEmail, u.DiscordWebhookURL, u.CustomWebhookURL, u.NotifyBeforeMinutes, u.AvatarURL, userID,
)
if err != nil {
return nil, err
@@ -2196,7 +2203,7 @@ func (s *Store) DeleteDocShare(currentUserID, docID, targetID string) error {
func (s *Store) GetCollaborators(currentUserID string) ([]domain.User, error) {
rows, err := s.db.Query(
`SELECT id, email, display_name, role, is_active, created_at FROM users WHERE id != ? AND is_active = 1 ORDER BY display_name ASC`,
`SELECT id, email, display_name, role, is_active, avatar_url, created_at FROM users WHERE id != ? AND is_active = 1 ORDER BY display_name ASC`,
currentUserID,
)
if err != nil {
@@ -2208,7 +2215,7 @@ func (s *Store) GetCollaborators(currentUserID string) ([]domain.User, error) {
for rows.Next() {
var u domain.User
var activeInt int
if err := rows.Scan(&u.ID, &u.Email, &u.DisplayName, &u.Role, &activeInt, &u.CreatedAt); err == nil {
if err := rows.Scan(&u.ID, &u.Email, &u.DisplayName, &u.Role, &activeInt, &u.AvatarURL, &u.CreatedAt); err == nil {
u.IsActive = activeInt == 1
users = append(users, u)
}
@@ -2936,7 +2943,7 @@ func (s *Store) GetTaskListShares(taskListID string) ([]domain.TaskListShare, er
if shares[i].TargetType == "team" {
_ = s.db.QueryRow(`SELECT name FROM teams WHERE id = ?`, shares[i].TargetID).Scan(&shares[i].TargetName)
} else {
_ = s.db.QueryRow(`SELECT display_name, email FROM users WHERE id = ?`, shares[i].TargetID).Scan(&shares[i].TargetName, &shares[i].Email)
_ = s.db.QueryRow(`SELECT display_name, email, avatar_url FROM users WHERE id = ?`, shares[i].TargetID).Scan(&shares[i].TargetName, &shares[i].Email, &shares[i].AvatarURL)
}
}
return shares, nil
@@ -2946,25 +2953,26 @@ func (s *Store) getMembersForTaskList(tl *domain.TaskList) []domain.TaskListMemb
memberMap := make(map[string]domain.TaskListMember)
// 1. Owner
var ownerName, ownerEmail string
_ = s.db.QueryRow(`SELECT display_name, email FROM users WHERE id = ?`, tl.UserID).Scan(&ownerName, &ownerEmail)
var ownerName, ownerEmail, ownerAvatar string
_ = s.db.QueryRow(`SELECT display_name, email, avatar_url FROM users WHERE id = ?`, tl.UserID).Scan(&ownerName, &ownerEmail, &ownerAvatar)
memberMap[tl.UserID] = domain.TaskListMember{
UserID: tl.UserID,
DisplayName: ownerName,
Email: ownerEmail,
AvatarURL: ownerAvatar,
Role: "owner",
}
// 2. Team members if tl.TeamID != ""
if tl.TeamID != "" {
rows, err := s.db.Query(
`SELECT u.id, u.display_name, u.email FROM team_members tm JOIN users u ON u.id = tm.user_id WHERE tm.team_id = ?`,
`SELECT u.id, u.display_name, u.email, u.avatar_url FROM team_members tm JOIN users u ON u.id = tm.user_id WHERE tm.team_id = ?`,
tl.TeamID,
)
if err == nil {
for rows.Next() {
var m domain.TaskListMember
if err := rows.Scan(&m.UserID, &m.DisplayName, &m.Email); err == nil {
if err := rows.Scan(&m.UserID, &m.DisplayName, &m.Email, &m.AvatarURL); err == nil {
m.Role = "member"
memberMap[m.UserID] = m
}
@@ -2977,13 +2985,13 @@ func (s *Store) getMembersForTaskList(tl *domain.TaskList) []domain.TaskListMemb
for _, sh := range tl.Shares {
if sh.TargetType == "team" {
rows, err := s.db.Query(
`SELECT u.id, u.display_name, u.email FROM team_members tm JOIN users u ON u.id = tm.user_id WHERE tm.team_id = ?`,
`SELECT u.id, u.display_name, u.email, u.avatar_url FROM team_members tm JOIN users u ON u.id = tm.user_id WHERE tm.team_id = ?`,
sh.TargetID,
)
if err == nil {
for rows.Next() {
var m domain.TaskListMember
if err := rows.Scan(&m.UserID, &m.DisplayName, &m.Email); err == nil {
if err := rows.Scan(&m.UserID, &m.DisplayName, &m.Email, &m.AvatarURL); err == nil {
m.Role = "member"
memberMap[m.UserID] = m
}
@@ -2992,7 +3000,7 @@ func (s *Store) getMembersForTaskList(tl *domain.TaskList) []domain.TaskListMemb
}
} else if sh.TargetType == "user" {
var m domain.TaskListMember
if err := s.db.QueryRow(`SELECT id, display_name, email FROM users WHERE id = ?`, sh.TargetID).Scan(&m.UserID, &m.DisplayName, &m.Email); err == nil {
if err := s.db.QueryRow(`SELECT id, display_name, email, avatar_url FROM users WHERE id = ?`, sh.TargetID).Scan(&m.UserID, &m.DisplayName, &m.Email, &m.AvatarURL); err == nil {
m.Role = "member"
memberMap[m.UserID] = m
}
@@ -3466,7 +3474,7 @@ func (s *Store) RemoveTeamMember(teamID, userID string) error {
func (s *Store) GetTeamMembers(teamID string) ([]domain.TeamMember, error) {
rows, err := s.db.Query(
`SELECT tm.team_id, tm.user_id, u.display_name, u.email, tm.role, tm.joined_at
`SELECT tm.team_id, tm.user_id, u.display_name, u.email, u.avatar_url, tm.role, tm.joined_at
FROM team_members tm
JOIN users u ON u.id = tm.user_id
WHERE tm.team_id = ?
@@ -3481,7 +3489,7 @@ func (s *Store) GetTeamMembers(teamID string) ([]domain.TeamMember, error) {
members := make([]domain.TeamMember, 0)
for rows.Next() {
var m domain.TeamMember
if err := rows.Scan(&m.TeamID, &m.UserID, &m.DisplayName, &m.Email, &m.Role, &m.JoinedAt); err == nil {
if err := rows.Scan(&m.TeamID, &m.UserID, &m.DisplayName, &m.Email, &m.AvatarURL, &m.Role, &m.JoinedAt); err == nil {
members = append(members, m)
}
}