diff --git a/database/sidebox.go b/database/sidebox.go index 6e73747..dd849fd 100644 --- a/database/sidebox.go +++ b/database/sidebox.go @@ -22,6 +22,13 @@ type Sidebox struct { Param *string `db:"param"` } +// Known sidebox IDs. +const ( + SideboxIDCommunities = int32(1) + SideboxIDConferences = int32(2) + SideboxIDOnlineUsers = int32(3) +) + // 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) diff --git a/top.go b/top.go index 9af3396..4d14d69 100644 --- a/top.go +++ b/top.go @@ -10,6 +10,7 @@ package main import ( + "context" "fmt" "net/http" "reflect" @@ -19,197 +20,142 @@ import ( "git.erbosoft.com/amy/amsterdam/ui" "github.com/CloudyKit/jet/v6" "github.com/labstack/echo/v4" + log "github.com/sirupsen/logrus" ) -// RenderedSideboxItem is an item for display inside a rendered sidebox. -type RenderedSideboxItem struct { - Text string - Text2 string - Link *string - Flags map[string]bool -} - -// LinkX dereferences the Link pointer safely. -func (item *RenderedSideboxItem) LinkX() string { - if item.Link == nil { - return "" - } - return *item.Link -} - -// RenderedSidebox is the data for a single rendered sidebox. -type RenderedSidebox struct { - TemplateName string - Title string - Subtext *string - Items []RenderedSideboxItem - Flags map[string]bool -} - -/* buildCommunitiesSidebox creates the data for the "My/Featured Communities" sidebox. - * Parameters: - * uid - UID of the user rendering the page. - * out - The RenderedSidebox to be built. - * in - The sidebox data from the database. - * Returns: - * Standard Go error status. +/*---------------------------------------------------------------------------- + * Sidebox rendering + *---------------------------------------------------------------------------- */ -func buildCommunitiesSidebox(ctxt ui.AmContext, uid int32, out *RenderedSidebox, in *database.Sidebox) error { - user, err := database.AmGetUser(ctxt.Ctx(), uid) - if err == nil { - var g *database.Globals - g, err = database.AmGlobals(ctxt.Ctx()) - if err == nil { - if user.IsAnon { - out.Title = "Featured Communities" - } else { - out.Title = "Your Communities" - } - var l []*database.Community - l, err = database.AmGetCommunitiesForUser(ctxt.Ctx(), uid) - if err == nil { - out.Items = make([]RenderedSideboxItem, len(l)) - for i, c := range l { - out.Items[i].Text = c.Name - lk := fmt.Sprintf("/comm/%s/profile", c.Alias) - out.Items[i].Link = &lk - out.Items[i].Flags = make(map[string]bool) - var level uint16 - level, err = database.AmGetCommunityAccessLevel(ctxt.Ctx(), uid, c.Id) - if err == nil && database.AmTestPermission("Community.ShowAdmin", level) { - out.Items[i].Flags["admin"] = true - } - } - out.Flags = make(map[string]bool) - if user.IsAnon { - out.Flags["canManage"] = false - out.Flags["canCreate"] = false - } else { - out.Flags["canManage"] = true - out.Flags["canCreate"] = user.BaseLevel >= uint16(g.CommunityCreateLevel) - } - out.TemplateName = "sb_ftrcomm.jet" - } + +// SideboxRendering is a wrapper interface used to handle rendering a sidebox's variables. +type SideboxRendering interface { + SetVar(string, any) +} + +// SideboxRenderFunc "renders" a sidebox by outputing variables through an adapter. +type SideboxRenderFunc func(context.Context, *database.User, *DisplaySidebox, *string, SideboxRendering) error + +// DisplaySidebox is the structure used to display a sidebox. +type DisplaySidebox struct { + Title string // title to display + TitleAnon string // title to display if user is anon + TemplateName string // name of template to render + Renderer SideboxRenderFunc // rendering function +} + +// renderSBCommunities renders the Communities sidebox. +func renderSBCommunities(ctx context.Context, u *database.User, sb *DisplaySidebox, param *string, rx SideboxRendering) error { + g, err := database.AmGlobals(ctx) + if err != nil { + return err + } + l, err := database.AmGetCommunitiesForUser(ctx, u.Uid) + if err != nil { + return err + } + rx.SetVar("communities", l) + isAdmin := make([]bool, len(l)) + for i, c := range l { + isAdmin[i] = false + level, err := database.AmGetCommunityAccessLevel(ctx, u.Uid, c.Id) + if err == nil && database.AmTestPermission("Community.ShowAdmin", level) { + isAdmin[i] = true } } - _ = in - return err -} - -/* buildFeaturedConferences creates the data for the "Featured Conferences" sidebox. - * Parameters: - * ctxt - AmContext for the operation. - * uid - UID of the user rendering the page. - * out - The RenderedSidebox to be built. - * in - The sidebox data from the database. - * Returns: - * Standard Go error status. - */ -func buildFeaturedConferences(ctxt ui.AmContext, uid int32, out *RenderedSidebox, in *database.Sidebox) error { - user, err := database.AmGetUser(ctxt.Ctx(), uid) - if err == nil { - if user.IsAnon { - out.Title = "Featured Conferences" - } else { - out.Title = "Your Conference Hotlist" - } - var hl []database.ConferenceHotlist - hl, err := database.AmGetConferenceHotlist(ctxt.Ctx(), user) - if err == nil { - out.Items = make([]RenderedSideboxItem, len(hl)) - for i, h := range hl { - comm, err := h.Community(ctxt.Ctx()) - if err != nil { - break - } - conf, err := h.Conference(ctxt.Ctx()) - if err != nil { - break - } - alias, err := conf.Aliases(ctxt.Ctx()) - if err != nil { - break - } - out.Items[i].Text = conf.Name - out.Items[i].Text2 = comm.Name - lk := fmt.Sprintf("/comm/%s/conf/%s", comm.Alias, alias[0]) - out.Items[i].Link = &lk - out.Items[i].Flags = make(map[string]bool) - out.Items[i].Flags["new"] = false - if !user.IsAnon { - nnew, err := conf.UnreadMessages(ctxt.Ctx(), user) - if err == nil { - out.Items[i].Flags["new"] = (nnew > 0) - } - } - } - out.Flags = make(map[string]bool) - out.Flags["canManage"] = !(user.IsAnon) - out.TemplateName = "sb_ftrconf.jet" - } - } - _ = in - return err -} - -/* buildUsersOnline creates the data for the "Users Online" sidebox. - * Parameters: - * uid - UID of the user rendering the page. - * out - The RenderedSidebox to be built. - * in - The sidebox data from the database. - * Returns: - * Standard Go error status. - */ -func buildUsersOnline(uid int32, out *RenderedSidebox, in *database.Sidebox) error { - out.Title = "Users Online" - out.TemplateName = "sb_online.jet" - anons, users, maxUsers := ui.AmSessions() - cap := len(users) + 1 - if anons > 0 { - cap++ - } - out.Items = make([]RenderedSideboxItem, cap) - out.Items[0].Text = fmt.Sprintf("%d total (max %d)", len(users)+anons, maxUsers) - out.Items[0].Flags = make(map[string]bool) - out.Items[0].Flags["nobullet"] = true - out.Items[0].Flags["bold"] = true - b := 1 - if anons > 0 { - out.Items[1].Text = fmt.Sprintf("Not logged in (%d)", anons) - out.Items[1].Flags = make(map[string]bool) - b++ - } - for i, n := range users { - out.Items[b+i].Text = n - lk := fmt.Sprintf("/user/%s", n) - out.Items[b+i].Link = &lk - out.Items[b+i].Flags = make(map[string]bool) - out.Items[b+i].Flags["bold"] = true - } - _ = uid - _ = in + rx.SetVar("isAdmin", isAdmin) + rx.SetVar("canManage", !(u.IsAnon)) + rx.SetVar("canCreate", !(u.IsAnon) && u.BaseLevel >= uint16(g.CommunityCreateLevel)) return nil } -/* buildRenderedSidebox creates a RenderedSidebox for the data in the database. - * Parameters: - * uid - UID of the user rendering the page. - * out - The RenderedSidebox to be built. - * in - The sidebox data from the database. - * Returns: - * Standard Go error status. - */ -func buildRenderedSidebox(ctxt ui.AmContext, uid int32, out *RenderedSidebox, in *database.Sidebox) error { - switch in.Boxid { - case 1: - return buildCommunitiesSidebox(ctxt, uid, out, in) - case 2: - return buildFeaturedConferences(ctxt, uid, out, in) - case 3: - return buildUsersOnline(uid, out, in) - default: - return fmt.Errorf("unknown sidebox boxid: %d", in.Boxid) +// renderSBConferences renders the Conferences sidebox. +func renderSBConferences(ctx context.Context, u *database.User, sb *DisplaySidebox, param *string, rx SideboxRendering) error { + hl, err := database.AmGetConferenceHotlist(ctx, u) + if err != nil { + return err } + comm := make([]*database.Community, len(hl)) + conf := make([]*database.Conference, len(hl)) + alias := make([]string, len(hl)) + newFlag := make([]bool, len(hl)) + for i, h := range hl { + if comm[i], err = h.Community(ctx); err != nil { + return err + } + if conf[i], err = h.Conference(ctx); err != nil { + return err + } + var a []string + if a, err = conf[i].Aliases(ctx); err != nil { + return err + } + alias[i] = a[0] + newFlag[i] = false + if !u.IsAnon { + nnew, err := conf[i].UnreadMessages(ctx, u) + if err == nil { + newFlag[i] = (nnew > 0) + } + } + } + rx.SetVar("comm", comm) + rx.SetVar("conf", conf) + rx.SetVar("alias", alias) + rx.SetVar("newFlag", newFlag) + rx.SetVar("canManage", !(u.IsAnon)) + return nil +} + +// renderSBOnlineUsers renders the Online Users sidebox. +func renderSBOnlineUsers(ctx context.Context, u *database.User, sb *DisplaySidebox, param *string, rx SideboxRendering) error { + anons, users, maxUsers := ui.AmSessions() + rx.SetVar("total", len(users)+anons) + rx.SetVar("maxUsers", maxUsers) + rx.SetVar("anons", anons) + rx.SetVar("users", users) + return nil +} + +// sideboxRegistry contains a registry of all known sideboxes. +var sideboxRegistry map[int32]*DisplaySidebox + +// init sets up the sidebox registry. +func init() { + sideboxRegistry = make(map[int32]*DisplaySidebox) + sb1 := DisplaySidebox{ + Title: "Your Communities", + TitleAnon: "Featured Communities", + TemplateName: "sb_comm.jet", + Renderer: renderSBCommunities, + } + sideboxRegistry[database.SideboxIDCommunities] = &sb1 + sb2 := DisplaySidebox{ + Title: "Your Conference Hotlist", + TitleAnon: "Featured Conferences", + TemplateName: "sb_conf.jet", + Renderer: renderSBConferences, + } + sideboxRegistry[database.SideboxIDConferences] = &sb2 + sb3 := DisplaySidebox{ + Title: "Users Online", + TitleAnon: "Users Online", + TemplateName: "sb_online.jet", + Renderer: renderSBOnlineUsers, + } + sideboxRegistry[database.SideboxIDOnlineUsers] = &sb3 + log.Infof("sidebox registry has %d entries", len(sideboxRegistry)) +} + +// sbRender is a context used for controlling adding variables for sideboxes. +type sbRender struct { + ctxt ui.AmContext // the UI context + id int32 // ID of sidebox being rendered +} + +// SetVar sets a sidebox rendering value into the context. +func (rx *sbRender) SetVar(name string, value any) { + rx.ctxt.VarMap().Set(fmt.Sprintf("sb%d_%s", rx.id, name), value) } // templateGetTopic returns the pointer to the topic. @@ -252,20 +198,26 @@ func TopPage(ctxt ui.AmContext) (string, any) { ctxt.VarMap().SetFunc("post_topicLink", templateTopicLink) // Retrieve the sideboxes and create the data to be presented. - uid := ctxt.CurrentUserId() - sboxes, err := database.AmGetSideboxes(ctxt.Ctx(), uid) + sboxes, err := database.AmGetSideboxes(ctxt.Ctx(), ctxt.CurrentUserId()) if err != nil { return "error", err } - - rc := make([]RenderedSidebox, len(sboxes)) - for i, sb := range sboxes { - err = buildRenderedSidebox(ctxt, uid, &(rc[i]), sb) - if err != nil { - return "error", err + disp := make([]*DisplaySidebox, 0, len(sboxes)) + rx := sbRender{ctxt: ctxt, id: 0} + for _, sb := range sboxes { + dsb, ok := sideboxRegistry[sb.Boxid] + if ok { + rx.id = sb.Boxid + err := dsb.Renderer(ctxt.Ctx(), ctxt.CurrentUser(), dsb, sb.Param, &rx) + if err != nil { + return "error", err + } + disp = append(disp, dsb) + } else { + log.Errorf("TopPage: unknown sidebox ID %d", sb.Boxid) } } - ctxt.VarMap().Set("sideboxes", rc) + ctxt.VarMap().Set("sideboxes", disp) // Final data set. ctxt.SetLeftMenu("top") diff --git a/ui/views/sb_comm.jet b/ui/views/sb_comm.jet new file mode 100644 index 0000000..bb9365c --- /dev/null +++ b/ui/views/sb_comm.jet @@ -0,0 +1,37 @@ +{* + * 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/. + *} + +
+ {{ if len(sb1_communities) > 0 }} + {{ range i, c := sb1_communities }} +
+ 🟣 + {{ c.Name }} + {{ if sb1_isAdmin[i] }}👑{{ end }} +
+ {{ end }} + {{ else }} +
You are not a member of any communities.
+ {{ end }} +
+{{ if sb1_canManage || sb1_canCreate }} +
+ + [ + {{ if sb1_canManage }} + Manage + {{ if sb1_canCreate }}|{{ end }} + {{ end }} + {{ if sb1_canCreate }} + Create New + {{ end }} + ] + +
+{{ end }} diff --git a/ui/views/sb_conf.jet b/ui/views/sb_conf.jet new file mode 100644 index 0000000..0629e38 --- /dev/null +++ b/ui/views/sb_conf.jet @@ -0,0 +1,32 @@ +{* + * 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/. + *} + +
+ {{ if len(sb2_comm) > 0 }} + {{ range i, c := sb2_comm }} +
+ 🟣 +
+ {{ sb2_conf[i].Name }} + ({{ c.Name }}) +
+ {{ if sb2_newFlag[i] }}🔔{{ end }} +
+ {{ end }} + {{ else }} +
No conferences in hotlist.
+ {{ end }} +
+{{ if sb2_canManage }} +
+ + [ Manage ] + +
+{{ end }} diff --git a/ui/views/sb_ftrcomm.jet b/ui/views/sb_ftrcomm.jet deleted file mode 100644 index 7047bdf..0000000 --- a/ui/views/sb_ftrcomm.jet +++ /dev/null @@ -1,45 +0,0 @@ -{* - * 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/. - *} - -{{ sb := .GetScratch("__sidebox") }} -
-
-

{{ sb.Title }}

-
-
-
- {{ if len(sb.Items) > 0 }} - {{ range sb.Items }} -
- 🟣 - {{ .Text }} - {{ if .Flags["admin"] }}👑{{ end }} -
- {{ end }} - {{ else }} -
You are not a member of any communities.
- {{ end }} -
- {{ if sb.Flags["canManage"] || sb.Flags["canCreate"] }} -
- - [ - {{ if sb.Flags["canManage"] }} - Manage - {{ if sb.Flags["canCreate"] }}|{{ end }} - {{ end }} - {{ if sb.Flags["canCreate"] }} - Create New - {{ end }} - ] - -
- {{ end }} -
-
diff --git a/ui/views/sb_ftrconf.jet b/ui/views/sb_ftrconf.jet deleted file mode 100644 index e9f138a..0000000 --- a/ui/views/sb_ftrconf.jet +++ /dev/null @@ -1,40 +0,0 @@ -{* - * Amsterdam Web Communities System - * Copyright (c) 2025 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/. - *} - -{{ sb := .GetScratch("__sidebox") }} -
-
-

{{ sb.Title }}:

-
-
-
- {{ if len(sb.Items) > 0 }} - {{ range i, item := sb.Items }} -
- 🟣 -
- {{ item.Text }} - ({{ item.Text2 }}) -
- {{ if item.Flags["new"] }}🔔{{ end }} -
- {{ end }} - {{ else }} -
No conferences in hotlist.
- {{ end }} -
- {{ if sb.Flags["canManage"] }} -
- - [ Manage ] - -
- {{ end }} -
-
diff --git a/ui/views/sb_online.jet b/ui/views/sb_online.jet index 1987b43..ed1570d 100644 --- a/ui/views/sb_online.jet +++ b/ui/views/sb_online.jet @@ -6,36 +6,21 @@ * 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/. *} - -{{ sb := .GetScratch("__sidebox") }} -
-
-

{{ sb.Title }}:

+ +
+
+ {{ sb3_total }} total (max {{ sb3_maxUsers }})
-
-
- {{ if len(sb.Items) > 0 }} - {{ range sb.Items }} -
- {{ if ! .Flags["nobullet"] }}🟣{{ end }} - {{ if .LinkX() == "" }} - {{ if .Flags["bold"] }} - {{ .Text }} - {{ else }} - {{ .Text }} - {{ end }} - {{ else }} - {{ if .Flags["bold"] }} - {{ .Text }} - {{ else }} - {{ .Text }} - {{ end }} - {{ end }} -
- {{ end }} - {{ else }} -
You are not a member of any communities.
- {{ end }} + {{ if sb3_anons > 0 }} +
+ 🟣 + Not logged in ({{ sb3_anons }})
-
+ {{ end }} + {{ range _, u := sb3_users }} +
+ 🟣 + {{ u }} +
+ {{ end }}
diff --git a/ui/views/top.jet b/ui/views/top.jet index ea7c83b..2c1570d 100644 --- a/ui/views/top.jet +++ b/ui/views/top.jet @@ -53,11 +53,16 @@
- {{ range i, s := sideboxes }} - {{ .SetScratch("__sidebox", s) }} - {{ include s.TemplateName }} + {{ range _, s := sideboxes }} +
+
+

{{ if .CurrentUser().IsAnon }}{{ s.TitleAnon }}{{ else }}{{ s.Title }}{{ end }}:

+
+
+ {{ include s.TemplateName }} +
+
{{ end }} - {{ if !.CurrentUser().IsAnon }}