diff --git a/database/sidebox.go b/database/sidebox.go index dd849fd..5e962d6 100644 --- a/database/sidebox.go +++ b/database/sidebox.go @@ -1,6 +1,6 @@ /* * Amsterdam Web Communities System - * Copyright (c) 2025 Erbosoft Metaverse Design Solutions, All Rights Reserved + * Copyright (c) 2025-2026 Erbosoft Metaverse Design Solutions, All Rights Reserved * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this @@ -15,13 +15,16 @@ import ( "github.com/jmoiron/sqlx" ) +// Sidebox represents a user sidebox. type Sidebox struct { - Uid int32 `db:"uid"` - Boxid int32 `db:"boxid"` - Sequence int32 `db:"sequence"` - Param *string `db:"param"` + Uid int32 `db:"uid"` // ID of the user + Boxid int32 `db:"boxid"` // ID of the sidebox + Sequence int32 `db:"sequence"` // sequence number of the sidebox + Param *string `db:"param"` // parameter string } +const SIDEBOX_SEQUENCE_SPACING = 100 + // Known sidebox IDs. const ( SideboxIDCommunities = int32(1) @@ -29,9 +32,12 @@ const ( SideboxIDOnlineUsers = int32(3) ) +// maxSidebox is the maximum sidebox index. +const maxSidebox = SideboxIDOnlineUsers + // copySideboxes copies sideboxes from one user to another. func copySideboxes(ctx context.Context, tx *sqlx.Tx, toUid int32, fromUid int32) error { - sbox := make([]Sidebox, 0, 3) + sbox := make([]Sidebox, 0, maxSidebox) err := tx.SelectContext(ctx, &sbox, "SELECT * from sideboxes WHERE uid = ?", fromUid) if err == nil { for _, sb := range sbox { @@ -57,3 +63,93 @@ func AmGetSideboxes(ctx context.Context, uid int32) ([]*Sidebox, error) { err := amdb.SelectContext(ctx, &sboxes, "SELECT * FROM sideboxes WHERE uid = ? ORDER BY SEQUENCE", uid) return sboxes, err } + +// AmReorderSideboxes changes the position of two sideboxes on the user's list. +func AmReorderSideboxes(ctx context.Context, uid int32, seq1, seq2 int32) error { + success := false + tx := amdb.MustBegin() + defer func() { + if !success { + tx.Rollback() + } + }() + + _, err := tx.ExecContext(ctx, "UPDATE sideboxes SET sequence = -1 WHERE uid = ? AND sequence = ?", uid, seq1) + if err == nil { + _, err = tx.ExecContext(ctx, "UPDATE sideboxes SET sequence = ? WHERE uid = ? AND sequence = ?", seq1, uid, seq2) + if err == nil { + _, err = tx.ExecContext(ctx, "UPDATE sideboxes SET sequence = ? WHERE uid = ? AND sequence = -1", seq2, uid) + } + } + if err != nil { + return err + } + if err = tx.Commit(); err != nil { + return err + } + success = true + return nil +} + +// AmRemoveSidebox removes a sidebox from the user configuration. +func AmRemoveSidebox(ctx context.Context, uid int32, boxid int32) error { + success := false + tx := amdb.MustBegin() + defer func() { + if !success { + tx.Rollback() + } + }() + + // Get the old sequence number. + row := tx.QueryRowContext(ctx, "SELECT sequence FROM sideboxes WHERE uid = ? AND boxid = ?", uid, boxid) + var oldseq int32 + err := row.Scan(&oldseq) + if err != nil { + return err + } + + // Delete the sidebox entry. + _, err = tx.ExecContext(ctx, "DELETE FROM sideboxes WHERE uid = ? AND boxid = ?", uid, boxid) + if err == nil { + // Renumber the other sideboxes to close the gap. + _, err = tx.ExecContext(ctx, "UPDATE sideboxes SET sequence = sequence - ? WHERE uid = ? AND sequence > ?", SIDEBOX_SEQUENCE_SPACING, uid, oldseq) + } + if err != nil { + return err + } + if err = tx.Commit(); err != nil { + return err + } + success = true + return nil +} + +// AmAppendSidebox appends a new sidebox to the existing user's configuration. +func AmAppendSidebox(ctx context.Context, uid int32, boxid int32, param *string) error { + success := false + tx := amdb.MustBegin() + defer func() { + if !success { + tx.Rollback() + } + }() + + row := tx.QueryRowContext(ctx, "SELECT MAX(sequence) FROM sideboxes WHERE uid = ?", uid) + var topseq int32 + err := row.Scan(&topseq) + if err != nil { + return err + } + + _, err = tx.ExecContext(ctx, "INSERT INTO sideboxes (uid, boxid, sequence, param) VALUES (?, ?, ?, ?)", + uid, boxid, topseq+SIDEBOX_SEQUENCE_SPACING, param) + if err != nil { + return err + } + if err = tx.Commit(); err != nil { + return err + } + success = true + return nil +} diff --git a/docs/MISSINGFUNCS.md b/docs/MISSINGFUNCS.md index 2fcf1e7..b3536ba 100644 --- a/docs/MISSINGFUNCS.md +++ b/docs/MISSINGFUNCS.md @@ -55,7 +55,7 @@ _(italicized items can be deferred)_ - ~~Conference Hotlist sidebox~~ - ~~"New" flag on Conference Hotlist sidebox~~ - ~~Manage on Conference Hotlist sidebox~~ -- Sidebox configuration +- ~~Sidebox configuration~~ - Topics view: - ~~Find~~ - Manage: diff --git a/main.go b/main.go index d720676..d74d10c 100644 --- a/main.go +++ b/main.go @@ -16,6 +16,7 @@ import ( "net/http" "os" "os/signal" + "syscall" "time" "git.erbosoft.com/amy/amsterdam/config" @@ -72,6 +73,8 @@ func setupEcho() *echo.Echo { e.GET("/user/:uname", ui.AmWrap(ShowProfile)) e.POST("/quick_email", ui.AmWrap(QuickEMail)) e.GET("/hotlist", ui.AmWrap(Hotlist)) + e.GET("/sideboxes", ui.AmWrap(ManageSideboxes)) + e.POST("/sideboxes", ui.AmWrap(AddSidebox)) e.GET("/sysadmin", ui.AmWrap(SysAdminMenu)) e.GET("/create_comm", ui.AmWrap(CreateCommunityForm)) e.POST("/create_comm", ui.AmWrap(CreateCommunity)) @@ -175,8 +178,8 @@ func main() { closer = ui.SetupAmContext() defer closer() - // Set up to trap SIGINT and shut down gracefully - ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) + // Set up to trap SIGINT/SIGTERM and shut down gracefully + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() // Set up ampool. diff --git a/top.go b/top.go index 4d14d69..9dc7013 100644 --- a/top.go +++ b/top.go @@ -12,8 +12,11 @@ package main import ( "context" "fmt" + "maps" "net/http" "reflect" + "slices" + "strings" "git.erbosoft.com/amy/amsterdam/config" "git.erbosoft.com/amy/amsterdam/database" @@ -38,6 +41,7 @@ type SideboxRenderFunc func(context.Context, *database.User, *DisplaySidebox, *s // DisplaySidebox is the structure used to display a sidebox. type DisplaySidebox struct { + BoxId int32 // box ID Title string // title to display TitleAnon string // title to display if user is anon TemplateName string // name of template to render @@ -124,6 +128,7 @@ var sideboxRegistry map[int32]*DisplaySidebox func init() { sideboxRegistry = make(map[int32]*DisplaySidebox) sb1 := DisplaySidebox{ + BoxId: database.SideboxIDCommunities, Title: "Your Communities", TitleAnon: "Featured Communities", TemplateName: "sb_comm.jet", @@ -131,6 +136,7 @@ func init() { } sideboxRegistry[database.SideboxIDCommunities] = &sb1 sb2 := DisplaySidebox{ + BoxId: database.SideboxIDConferences, Title: "Your Conference Hotlist", TitleAnon: "Featured Conferences", TemplateName: "sb_conf.jet", @@ -138,6 +144,7 @@ func init() { } sideboxRegistry[database.SideboxIDConferences] = &sb2 sb3 := DisplaySidebox{ + BoxId: database.SideboxIDOnlineUsers, Title: "Users Online", TitleAnon: "Users Online", TemplateName: "sb_online.jet", @@ -205,8 +212,7 @@ func TopPage(ctxt ui.AmContext) (string, any) { disp := make([]*DisplaySidebox, 0, len(sboxes)) rx := sbRender{ctxt: ctxt, id: 0} for _, sb := range sboxes { - dsb, ok := sideboxRegistry[sb.Boxid] - if ok { + if dsb, ok := sideboxRegistry[sb.Boxid]; ok { rx.id = sb.Boxid err := dsb.Renderer(ctxt.Ctx(), ctxt.CurrentUser(), dsb, sb.Param, &rx) if err != nil { @@ -274,3 +280,101 @@ func JumpToShortcut(ctxt ui.AmContext) (string, any) { } return "redirect", targetURL } + +/* ManageSideboxes displays the "sidebox management" page. + * Parameters: + * ctxt - The AmContext for the request. + * Returns: + * Command string dictating what to be rendered. + * Data as a parameter for the command string. + */ +func ManageSideboxes(ctxt ui.AmContext) (string, any) { + if ctxt.CurrentUser().IsAnon { + return "error", ELOGIN + } + + // Retrieve the sideboxes from the database. + sboxes, err := database.AmGetSideboxes(ctxt.Ctx(), ctxt.CurrentUserId()) + if err != nil { + return "error", err + } + + if ctxt.HasParameter("m") { + index := ctxt.QueryParamInt("m", -1) + if index == -1 { + return "error", EINVAL + } + delta := ctxt.QueryParamInt("n", 0) + if delta == 0 { + return "error", EINVAL + } + err = database.AmReorderSideboxes(ctxt.Ctx(), ctxt.CurrentUserId(), sboxes[index].Sequence, sboxes[index+delta].Sequence) + if err != nil { + return "error", err + } + tmp := sboxes[index] + sboxes[index] = sboxes[index+delta] + sboxes[index+delta] = tmp + } else if ctxt.HasParameter("d") { + index := ctxt.QueryParamInt("d", -1) + if index == -1 { + return "error", EINVAL + } + err = database.AmRemoveSidebox(ctxt.Ctx(), ctxt.CurrentUserId(), sboxes[index].Boxid) + if err != nil { + return "error", err + } + sboxes = append(sboxes[:index], sboxes[index+1:]...) + } + + // Create the display list. + avail := maps.Clone(sideboxRegistry) + disp := make([]*DisplaySidebox, 0, len(sboxes)) + for _, sb := range sboxes { + if dsb, ok := avail[sb.Boxid]; ok { + disp = append(disp, dsb) + delete(avail, sb.Boxid) + } else { + log.Errorf("TopPage: unknown sidebox ID %d", sb.Boxid) + } + } + ctxt.VarMap().Set("sideboxes", disp) + + // Create the list of available sideboxes to add. + addList := make([]*DisplaySidebox, 0, len(avail)) + for _, v := range avail { + addList = append(addList, v) + } + if len(addList) > 0 { + slices.SortFunc(addList, func(a, b *DisplaySidebox) int { + return strings.Compare(strings.ToLower(a.Title), strings.ToLower(b.Title)) + }) + } + ctxt.VarMap().Set("addList", addList) + + ctxt.SetFrameTitle("Your Front Page Configuration") + return "framed", "sideboxes.jet" +} + +/* AddSidebox performs the "add sidebox" operation. + * Parameters: + * ctxt - The AmContext for the request. + * Returns: + * Command string dictating what to be rendered. + * Data as a parameter for the command string. + */ +func AddSidebox(ctxt ui.AmContext) (string, any) { + if ctxt.CurrentUser().IsAnon { + return "error", ELOGIN + } + + newId, err := ctxt.FormFieldInt("sbid") + if err != nil { + return "error", err + } + err = database.AmAppendSidebox(ctxt.Ctx(), ctxt.CurrentUserId(), int32(newId), nil) + if err != nil { + return "error", err + } + return "redirect", "/sideboxes" +} diff --git a/ui/views/hotlist.jet b/ui/views/hotlist.jet index 0b7fee4..e70c7e5 100644 --- a/ui/views/hotlist.jet +++ b/ui/views/hotlist.jet @@ -13,6 +13,14 @@
| + {{ if i < (len(sideboxes) - 1) }} + ⬇️ + {{ else }} + + {{ end }} + | ++ {{ if i > 0 }} + ⬆️ + {{ else }} + + {{ end }} + | ++ ❌ + | +
+ {{ sb.Title }}
+ |
+