diff --git a/conference.go b/conference.go index c90ebe9..817a6b6 100644 --- a/conference.go +++ b/conference.go @@ -98,6 +98,11 @@ func Topics(ctxt ui.AmContext) (string, any, error) { return ui.ErrorPage(ctxt, err) } + hotlistTest, err := database.AmIsInHotlist(ctxt.Ctx(), ctxt.CurrentUser(), comm.Id, conf.ConfId) + if err != nil { + return ui.ErrorPage(ctxt, err) + } + traverser := ui.NewTopicTraverser(topics) ctxt.SetSession("topic.traverser", traverser) @@ -116,6 +121,7 @@ func Topics(ctxt ui.AmContext) (string, any, error) { } ctxt.VarMap().Set("canCreate", conf.TestPermission("Conference.Create", myLevel)) + ctxt.VarMap().Set("showHotlist", !hotlistTest) ctxt.VarMap().Set("conferenceName", conf.Name) ctxt.VarMap().Set("urlBack", fmt.Sprintf("/comm/%s/conf", comm.Alias)) ctxt.VarMap().Set("urlStem", urlStem) diff --git a/conference_ops.go b/conference_ops.go index f99b7b3..aaeefaa 100644 --- a/conference_ops.go +++ b/conference_ops.go @@ -122,7 +122,25 @@ func AttachmentSend(ctxt ui.AmContext) (string, any, error) { return "bytes", data, nil } -/* HideTopic hides or shows rthe current topic for the current user. +/* AddToHotlist adds the current community and conference to the user's hotlist.. + * Parameters: + * ctxt - The AmContext for the request. + * Returns: + * Command string dictating what to be rendered. + * Data as a parameter for the command string. + * Standard Go error status. + */ +func AddToHotlist(ctxt ui.AmContext) (string, any, error) { + comm := ctxt.CurrentCommunity() + conf := ctxt.GetScratch("currentConference").(*database.Conference) + err := database.AmAppendToHotlist(ctxt.Ctx(), ctxt.CurrentUser(), comm.Id, conf.ConfId) + if err != nil { + return ui.ErrorPage(ctxt, err) + } + return "redirect", fmt.Sprintf("/comm/%s/conf/%s", comm.Alias, ctxt.GetScratch("currentAlias")), nil +} + +/* HideTopic hides or shows the current topic for the current user. * Parameters: * ctxt - The AmContext for the request. * Returns: diff --git a/database/conference.go b/database/conference.go index 3ee8056..fde7ecd 100644 --- a/database/conference.go +++ b/database/conference.go @@ -383,7 +383,7 @@ func AmGetConferenceByAliasInCommunity(ctx context.Context, cid int32, alias str err := row.Scan(&confid) switch err { case nil: - AmGetConference(ctx, confid) + return AmGetConference(ctx, confid) case sql.ErrNoRows: return nil, errors.New("conference not found") } diff --git a/database/hotlist.go b/database/hotlist.go index 187887a..d92dc25 100644 --- a/database/hotlist.go +++ b/database/hotlist.go @@ -9,7 +9,11 @@ // The database package contains database management and storage logic. package database -import "context" +import ( + "context" + "database/sql" + "errors" +) // ConferenceHotlist represents a user's conference hotlist. type ConferenceHotlist struct { @@ -19,6 +23,8 @@ type ConferenceHotlist struct { ConfId int32 `db:"confid"` } +const HOTLIST_SEQUENCE_SPACING = 100 + // Community gets the community pointer from the hotlist. func (h *ConferenceHotlist) Community(ctx context.Context) (*Community, error) { return AmGetCommunity(ctx, h.CommId) @@ -62,3 +68,105 @@ func AmCopyConferenceHotlist(ctx context.Context, from, to *User) error { success = true return nil } + +// AmReorderHotlist exchanges the position of two items on the user's hotlist. +func AmReorderHotlist(ctx context.Context, u *User, seq1, seq2 int16) error { + success := false + tx := amdb.MustBegin() + defer func() { + if !success { + tx.Rollback() + } + }() + + _, err := tx.ExecContext(ctx, "UPDATE confhotlist SET sequence = -1 WHERE uid = ? AND sequence = ?", u.Uid, seq1) + if err == nil { + _, err = tx.ExecContext(ctx, "UPDATE confhotlist SET sequence = ? WHERE uid = ? AND sequence = ?", seq1, u.Uid, seq2) + if err == nil { + _, err = tx.ExecContext(ctx, "UPDATE confhotlist SET sequence = ? WHERE uid = ? AND sequence = -1", seq2, u.Uid) + } + } + if err != nil { + return err + } + if err = tx.Commit(); err != nil { + return err + } + success = true + return nil +} + +// AmRemoveEntryFromHotlist removes an entry from the user's hotlist. +func AmRemoveEntryFromHotlist(ctx context.Context, u *User, seq int16) error { + success := false + tx := amdb.MustBegin() + defer func() { + if !success { + tx.Rollback() + } + }() + + _, err := tx.ExecContext(ctx, "DELETE FROM confhotlist WHERE uid = ? AND sequence = ?", u.Uid, seq) + if err == nil { + _, err = tx.ExecContext(ctx, "UPDATE confhotlist SET sequence = sequence - ? WHERE uid = ? AND sequence > ?", HOTLIST_SEQUENCE_SPACING, u.Uid, seq) + } + if err != nil { + return err + } + if err = tx.Commit(); err != nil { + return err + } + success = true + return nil +} + +// AmAppendToHotlist adds a community/conference ID to the end of the user's hotlist. +func AmAppendToHotlist(ctx context.Context, u *User, commid, confid int32) error { + success := false + tx := amdb.MustBegin() + defer func() { + if !success { + tx.Rollback() + } + }() + + var newseq int16 + row := tx.QueryRowContext(ctx, "SELECT sequence FROM confhotlist WHERE uid = ? AND commid = ? AND confid = ?", u.Uid, commid, confid) + err := row.Scan(&newseq) + if err == nil { + return errors.New("community/conference already exist in hotlist") + } else if err != sql.ErrNoRows { + return err + } + row = tx.QueryRowContext(ctx, "SELECT MAX(sequence) FROM confhotlist WHERE uid = ?", u.Uid) + err = row.Scan(&newseq) + if err == sql.ErrNoRows { + newseq = 0 + } else if err != nil { + return err + } + _, err = tx.ExecContext(ctx, "INSERT INTO confhotlist (uid, sequence, commid, confid) VALUES (?, ?, ?, ?)", + u.Uid, newseq+HOTLIST_SEQUENCE_SPACING, commid, confid) + if err != nil { + return err + } + if err = tx.Commit(); err != nil { + return err + } + success = true + return nil +} + +// AmIsInHotlist returns true if the community/conference pair is in the hotlist. +func AmIsInHotlist(ctx context.Context, u *User, commid, confid int32) (bool, error) { + row := amdb.QueryRowContext(ctx, "SELECT sequence FROM confhotlist WHERE uid = ? AND commid = ? AND confid = ?", u.Uid, commid, confid) + var tmp int16 + err := row.Scan(&tmp) + switch err { + case nil: + return true, nil + case sql.ErrNoRows: + return false, nil + } + return false, err +} diff --git a/docs/MISSINGFUNCS.md b/docs/MISSINGFUNCS.md index e42d904..af5d69c 100644 --- a/docs/MISSINGFUNCS.md +++ b/docs/MISSINGFUNCS.md @@ -43,10 +43,10 @@ - Manage Communities on communities sidebox - ~~Conference Hotlist sidebox~~ - ~~"New" flag on Conference Hotlist sidebox~~ -- Manage on Conference Hotlist sidebox +- ~~Manage on Conference Hotlist sidebox~~ - Sidebox configuration - Topics view: - Find - Manage - - Add to Hotlist/Remove from Hotlist + - ~~Add to Hotlist/Remove from Hotlist~~ diff --git a/main.go b/main.go index 1d1d286..f1240d9 100644 --- a/main.go +++ b/main.go @@ -71,6 +71,7 @@ func setupEcho() *echo.Echo { e.POST("/find", ui.AmWrap(Find)) e.GET("/user/:uname", ui.AmWrap(ShowProfile)) e.POST("/quick_email", ui.AmWrap(QuickEMail)) + e.GET("/hotlist", ui.AmWrap(Hotlist)) e.GET("/sysadmin", ui.AmWrap(SysAdminMenu)) e.GET("/create_comm", ui.AmWrap(CreateCommunityForm)) e.POST("/create_comm", ui.AmWrap(CreateCommunity)) @@ -100,6 +101,7 @@ func setupEcho() *echo.Echo { confGroup.GET("", ui.AmWrap(Topics)) confGroup.GET("/new_topic", ui.AmWrap(NewTopicForm)) confGroup.POST("/new_topic", ui.AmWrap(NewTopic)) + confGroup.GET("/hotlist", ui.AmWrap(AddToHotlist)) confGroup.GET("/r/:topic", ui.AmWrap(ReadPosts), ui.SetTopic) confGroup.POST("/r/:topic", ui.AmWrap(PostInTopic), ui.SetTopic) opsGroup := confGroup.Group("/op/:topic", ui.SetTopic) diff --git a/ui/views/hotlist.jet b/ui/views/hotlist.jet new file mode 100644 index 0000000..0b7fee4 --- /dev/null +++ b/ui/views/hotlist.jet @@ -0,0 +1,74 @@ +{* + * Amsterdam Web Communities System + * 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 + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + *} +
+ +
+

Your Conference Hotlist

+
+
+ + +
+
+ + + {{ range i, hl := hotlist }} + + + + + + + {{ end }} + +
+ {{ if i < (len(hotlist) - 1) }} + ⬇️ + {{ else }} +   + {{ end }} + + {{ if i > 0 }} + ⬆️ + {{ else }} +   + {{ end }} + + + +
{{ conferences[i] }}
+
({{ communities[i] }})
+
+
+
+ + +
+
+

How to update the hotlist:

+
+
+ ⬇️ + Click this symbol to move the specified conference down in your hotlist. +
+
+ ⬆️ + Click this symbol to move the specified conference up in your hotlist. +
+
+ + Click this symbol to remove the specified conference from your hotlist. +
+
+
+
+
diff --git a/ui/views/sb_ftrconf.jet b/ui/views/sb_ftrconf.jet index e325606..e9f138a 100644 --- a/ui/views/sb_ftrconf.jet +++ b/ui/views/sb_ftrconf.jet @@ -32,7 +32,7 @@ {{ if sb.Flags["canManage"] }}
- [ Manage ] + [ Manage ]
{{ end }} diff --git a/ui/views/topiclist.jet b/ui/views/topiclist.jet index 0991b20..bb3e4a3 100644 --- a/ui/views/topiclist.jet +++ b/ui/views/topiclist.jet @@ -40,8 +40,8 @@ class="bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded text-sm font-medium transition-colors"> Manage - {{ if false }}{* TODO *} - Add to Hotlist diff --git a/userdata.go b/userdata.go index f932c04..c783367 100644 --- a/userdata.go +++ b/userdata.go @@ -476,3 +476,69 @@ func QuickEMail(ctxt ui.AmContext) (string, any, error) { msg.Send() return "redirect", "/user/" + user.Username, nil } + +/* Hotlist displays and edits the user's conference hotlist. + * Parameters: + * ctxt - The AmContext for the request. + * Returns: + * Command string dictating what to be rendered. + * Data as a parameter for the command string. + * Standard Go error status. + */ +func Hotlist(ctxt ui.AmContext) (string, any, error) { + me := ctxt.CurrentUser() + if me.IsAnon { + return ui.ErrorPage(ctxt, errors.New("you are not logged in")) + } + hotlist, err := database.AmGetConferenceHotlist(ctxt.Ctx(), me) + if err != nil { + return ui.ErrorPage(ctxt, err) + } + + if ctxt.HasParameter("m") { + index := ctxt.QueryParamInt("m", -1) + dir := ctxt.QueryParamInt("n", 0) + if index >= 0 && (index+dir) != index { + err := database.AmReorderHotlist(ctxt.Ctx(), me, hotlist[index].Sequence, hotlist[index+dir].Sequence) + if err != nil { + return ui.ErrorPage(ctxt, err) + } + tmp := hotlist[index].CommId + hotlist[index].CommId = hotlist[index+dir].CommId + hotlist[index+dir].CommId = tmp + tmp = hotlist[index].ConfId + hotlist[index].ConfId = hotlist[index+dir].ConfId + hotlist[index+dir].ConfId = tmp + } + } else if ctxt.HasParameter("d") { + index := ctxt.QueryParamInt("d", -1) + if index >= 0 { + err := database.AmRemoveEntryFromHotlist(ctxt.Ctx(), me, hotlist[index].Sequence) + if err != nil { + return ui.ErrorPage(ctxt, err) + } + hotlist = append(hotlist[:index], hotlist[index+1:]...) + } + } + + communities := make([]string, len(hotlist)) + conferences := make([]string, len(hotlist)) + for i := range hotlist { + comm, err := hotlist[i].Community(ctxt.Ctx()) + if err != nil { + return ui.ErrorPage(ctxt, err) + } + communities[i] = comm.Name + conf, err := hotlist[i].Conference(ctxt.Ctx()) + if err != nil { + return ui.ErrorPage(ctxt, err) + } + conferences[i] = conf.Name + } + + ctxt.VarMap().Set("hotlist", hotlist) + ctxt.VarMap().Set("communities", communities) + ctxt.VarMap().Set("conferences", conferences) + ctxt.VarMap().Set("amsterdam_pageTitle", "Your Conference Hotlist") + return "framed_template", "hotlist.jet", nil +}