From 50ceab88ecae9858d942a2e445963d64406f5174 Mon Sep 17 00:00:00 2001 From: lowcarbdev Date: Sun, 30 Nov 2025 11:02:36 -0700 Subject: [PATCH] Add settings modal and option to show calls --- frontend/src/App.jsx | 64 +++++++++- frontend/src/components/SettingsModal.jsx | 142 ++++++++++++++++++++++ internal/auth.go | 7 ++ internal/handlers.go | 28 +++++ internal/settings.go | 110 +++++++++++++++++ main.go | 2 + 6 files changed, 348 insertions(+), 5 deletions(-) create mode 100644 frontend/src/components/SettingsModal.jsx create mode 100644 internal/settings.go diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index fa5d29b..1289b3b 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -11,6 +11,7 @@ import DateFilter from './components/DateFilter' import Upload from './components/Upload' import Search from './components/Search' import ChangePasswordModal from './components/ChangePasswordModal' +import SettingsModal from './components/SettingsModal' import './App.css' const API_BASE = import.meta.env.VITE_API_URL || 'http://localhost:8081/api' @@ -27,8 +28,14 @@ function App() { const [dateRange, setDateRange] = useState({ min: null, max: null }) const [showUpload, setShowUpload] = useState(false) const [showPasswordModal, setShowPasswordModal] = useState(false) + const [showSettingsModal, setShowSettingsModal] = useState(false) const [searchFilter, setSearchFilter] = useState('') const [version, setVersion] = useState('...') + const [settings, setSettings] = useState({ + conversations: { + show_calls: true + } + }) // Mobile sidebar state const [showSidebar, setShowSidebar] = useState(true) @@ -50,11 +57,16 @@ function App() { : 'conversations' useEffect(() => { + fetchSettings() fetchDateRange() - fetchConversations() fetchVersion() }, []) + useEffect(() => { + // Fetch conversations after settings are loaded + fetchConversations() + }, [startDate, endDate, settings]) + const fetchVersion = async () => { try { const response = await axios.get(`${API_BASE}/version`) @@ -65,9 +77,20 @@ function App() { } } - useEffect(() => { - fetchConversations() - }, [startDate, endDate]) + const fetchSettings = async () => { + try { + const response = await axios.get(`${API_BASE}/settings`) + setSettings(response.data) + } catch (error) { + console.error('Failed to fetch settings:', error) + // Use default settings if fetch fails + setSettings({ + conversations: { + show_calls: true + } + }) + } + } // Sync selected conversation from URL and manage sidebar visibility useEffect(() => { @@ -112,7 +135,9 @@ function App() { if (endDate) params.end = endDate.toISOString() const response = await axios.get(`${API_BASE}/conversations`, { params }) - setConversations(response.data || []) + const conversationList = response.data || [] + + setConversations(conversationList) } catch (error) { console.error('Error fetching conversations:', error) } finally { @@ -197,6 +222,13 @@ function App() { Version {version} + setShowSettingsModal(true)}> + + + + + Settings + setShowPasswordModal(true)}> @@ -370,6 +402,17 @@ function App() { /> )} + {/* Settings Modal */} + setShowSettingsModal(false)} + onSettingsUpdated={(newSettings) => { + setSettings(newSettings) + // Reload conversations if show_calls setting changed + fetchConversations() + }} + /> + {/* Change Password Modal */} {showPasswordModal && ( )} + + {/* Settings Modal */} + setShowSettingsModal(false)} + onSettingsUpdated={(newSettings) => { + setSettings(newSettings) + // Reload conversations if show_calls setting changed + fetchConversations() + }} + /> ) } diff --git a/frontend/src/components/SettingsModal.jsx b/frontend/src/components/SettingsModal.jsx new file mode 100644 index 0000000..b755aee --- /dev/null +++ b/frontend/src/components/SettingsModal.jsx @@ -0,0 +1,142 @@ +import { useState, useEffect } from 'react' +import axios from 'axios' + +const API_BASE = import.meta.env.VITE_API_URL || 'http://localhost:8081/api' + +function SettingsModal({ show, onClose, onSettingsUpdated }) { + const [settings, setSettings] = useState({ + conversations: { + show_calls: true + } + }) + const [loading, setLoading] = useState(true) + const [saving, setSaving] = useState(false) + const [error, setError] = useState('') + + useEffect(() => { + if (show) { + fetchSettings() + } + }, [show]) + + const fetchSettings = async () => { + try { + setLoading(true) + const response = await axios.get(`${API_BASE}/settings`) + setSettings(response.data) + } catch (err) { + console.error('Failed to fetch settings:', err) + setError('Failed to load settings') + } finally { + setLoading(false) + } + } + + const handleSave = async () => { + try { + setSaving(true) + setError('') + await axios.put(`${API_BASE}/settings`, settings) + + // Notify parent component that settings were updated + if (onSettingsUpdated) { + onSettingsUpdated(settings) + } + + onClose() + } catch (err) { + console.error('Failed to save settings:', err) + setError('Failed to save settings') + } finally { + setSaving(false) + } + } + + const handleToggleShowCalls = () => { + setSettings({ + ...settings, + conversations: { + ...settings.conversations, + show_calls: !settings.conversations.show_calls + } + }) + } + + if (!show) return null + + return ( +
+
+
+
+
Settings
+ +
+
+ {loading ? ( +
+
+ Loading... +
+
+ ) : ( + <> + {error && ( +
+ {error} +
+ )} + +
Conversations
+ +
+ + +
+ When enabled, phone calls will appear in the conversation list alongside messages. +
+
+ + )} +
+
+ + +
+
+
+
+ ) +} + +export default SettingsModal diff --git a/internal/auth.go b/internal/auth.go index f04703e..b7687bd 100644 --- a/internal/auth.go +++ b/internal/auth.go @@ -41,6 +41,13 @@ func InitAuthDB(filepath string) error { FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ); + CREATE TABLE IF NOT EXISTS settings ( + user_id TEXT PRIMARY KEY, + settings_json TEXT NOT NULL DEFAULT '{}', + updated_at INTEGER NOT NULL, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE + ); + CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON sessions(user_id); CREATE INDEX IF NOT EXISTS idx_sessions_expires_at ON sessions(expires_at); ` diff --git a/internal/handlers.go b/internal/handlers.go index 45774dc..aa9b62a 100644 --- a/internal/handlers.go +++ b/internal/handlers.go @@ -191,6 +191,22 @@ func HandleMessages(c echo.Context) error { } } + // Get user ID from context to fetch settings + userID, ok := c.Get("user_id").(string) + if !ok { + return c.JSON(http.StatusUnauthorized, map[string]string{ + "error": "User not authenticated", + }) + } + + // Fetch user settings to check if calls should be shown + settings, err := GetUserSettings(userID) + if err != nil { + slog.Error("Error getting user settings", "error", err) + // If we can't get settings, default to showing calls + settings = GetDefaultSettings() + } + activities, err := GetActivityByAddress(userDB, address, startDate, endDate, limit, offset) if err != nil { slog.Error("Error getting activity", "error", err) @@ -198,6 +214,18 @@ func HandleMessages(c echo.Context) error { "error": "Failed to get activity", }) } + + // Filter out calls if show_calls setting is false + if !settings.Conversations.ShowCalls { + filteredActivities := []ActivityItem{} + for _, activity := range activities { + if activity.Type != "call" { + filteredActivities = append(filteredActivities, activity) + } + } + activities = filteredActivities + } + return c.JSON(http.StatusOK, activities) } diff --git a/internal/settings.go b/internal/settings.go new file mode 100644 index 0000000..e76cf59 --- /dev/null +++ b/internal/settings.go @@ -0,0 +1,110 @@ +package internal + +import ( + "database/sql" + "encoding/json" + "net/http" + "time" + + "github.com/labstack/echo/v4" +) + +// Settings represents user settings stored as JSON +type Settings struct { + Conversations ConversationSettings `json:"conversations"` +} + +// ConversationSettings contains settings for the conversation view +type ConversationSettings struct { + ShowCalls bool `json:"show_calls"` +} + +// GetDefaultSettings returns the default settings +func GetDefaultSettings() Settings { + return Settings{ + Conversations: ConversationSettings{ + ShowCalls: true, + }, + } +} + +// GetUserSettings retrieves settings for a user +func GetUserSettings(userID string) (Settings, error) { + var settingsJSON string + var updatedAt int64 + + err := authDB.QueryRow( + "SELECT settings_json, updated_at FROM settings WHERE user_id = ?", + userID, + ).Scan(&settingsJSON, &updatedAt) + + if err == sql.ErrNoRows { + // Return default settings if no settings exist + return GetDefaultSettings(), nil + } + + if err != nil { + return Settings{}, err + } + + var settings Settings + if err := json.Unmarshal([]byte(settingsJSON), &settings); err != nil { + return Settings{}, err + } + + return settings, nil +} + +// SaveUserSettings saves settings for a user +func SaveUserSettings(userID string, settings Settings) error { + settingsJSON, err := json.Marshal(settings) + if err != nil { + return err + } + + now := time.Now().Unix() + + _, err = authDB.Exec(` + INSERT INTO settings (user_id, settings_json, updated_at) + VALUES (?, ?, ?) + ON CONFLICT(user_id) DO UPDATE SET + settings_json = excluded.settings_json, + updated_at = excluded.updated_at + `, userID, string(settingsJSON), now) + + return err +} + +// HandleGetSettings handles GET /api/settings +func HandleGetSettings(c echo.Context) error { + userID := c.Get("user_id").(string) + + settings, err := GetUserSettings(userID) + if err != nil { + return c.JSON(http.StatusInternalServerError, map[string]string{ + "error": "Failed to get settings", + }) + } + + return c.JSON(http.StatusOK, settings) +} + +// HandleUpdateSettings handles PUT /api/settings +func HandleUpdateSettings(c echo.Context) error { + userID := c.Get("user_id").(string) + + var settings Settings + if err := c.Bind(&settings); err != nil { + return c.JSON(http.StatusBadRequest, map[string]string{ + "error": "Invalid settings data", + }) + } + + if err := SaveUserSettings(userID, settings); err != nil { + return c.JSON(http.StatusInternalServerError, map[string]string{ + "error": "Failed to save settings", + }) + } + + return c.JSON(http.StatusOK, settings) +} diff --git a/main.go b/main.go index e4cc11f..7ba218c 100644 --- a/main.go +++ b/main.go @@ -74,6 +74,8 @@ func main() { protected.GET("/progress", internal.HandleProgress) protected.GET("/media", internal.HandleMedia) protected.GET("/search", internal.HandleSearch) + protected.GET("/settings", internal.HandleGetSettings) + protected.PUT("/settings", internal.HandleUpdateSettings) // Health check e.GET("/api/health", func(c echo.Context) error {