rebuilt sidebox code tighter

This commit is contained in:
2026-02-16 14:02:49 -07:00
parent 08b0fa6185
commit e1d2b60b2b
8 changed files with 243 additions and 310 deletions
+7
View File
@@ -22,6 +22,13 @@ type Sidebox struct {
Param *string `db:"param"` 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. // copySideboxes copies sideboxes from one user to another.
func copySideboxes(ctx context.Context, tx *sqlx.Tx, toUid int32, fromUid int32) error { func copySideboxes(ctx context.Context, tx *sqlx.Tx, toUid int32, fromUid int32) error {
sbox := make([]Sidebox, 0, 3) sbox := make([]Sidebox, 0, 3)
+143 -191
View File
@@ -10,6 +10,7 @@
package main package main
import ( import (
"context"
"fmt" "fmt"
"net/http" "net/http"
"reflect" "reflect"
@@ -19,197 +20,142 @@ import (
"git.erbosoft.com/amy/amsterdam/ui" "git.erbosoft.com/amy/amsterdam/ui"
"github.com/CloudyKit/jet/v6" "github.com/CloudyKit/jet/v6"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
log "github.com/sirupsen/logrus"
) )
// RenderedSideboxItem is an item for display inside a rendered sidebox. /*----------------------------------------------------------------------------
type RenderedSideboxItem struct { * Sidebox rendering
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.
*/ */
func buildCommunitiesSidebox(ctxt ui.AmContext, uid int32, out *RenderedSidebox, in *database.Sidebox) error {
user, err := database.AmGetUser(ctxt.Ctx(), uid) // SideboxRendering is a wrapper interface used to handle rendering a sidebox's variables.
if err == nil { type SideboxRendering interface {
var g *database.Globals SetVar(string, any)
g, err = database.AmGlobals(ctxt.Ctx()) }
if err == nil {
if user.IsAnon { // SideboxRenderFunc "renders" a sidebox by outputing variables through an adapter.
out.Title = "Featured Communities" type SideboxRenderFunc func(context.Context, *database.User, *DisplaySidebox, *string, SideboxRendering) error
} else {
out.Title = "Your Communities" // DisplaySidebox is the structure used to display a sidebox.
} type DisplaySidebox struct {
var l []*database.Community Title string // title to display
l, err = database.AmGetCommunitiesForUser(ctxt.Ctx(), uid) TitleAnon string // title to display if user is anon
if err == nil { TemplateName string // name of template to render
out.Items = make([]RenderedSideboxItem, len(l)) Renderer SideboxRenderFunc // rendering function
for i, c := range l { }
out.Items[i].Text = c.Name
lk := fmt.Sprintf("/comm/%s/profile", c.Alias) // renderSBCommunities renders the Communities sidebox.
out.Items[i].Link = &lk func renderSBCommunities(ctx context.Context, u *database.User, sb *DisplaySidebox, param *string, rx SideboxRendering) error {
out.Items[i].Flags = make(map[string]bool) g, err := database.AmGlobals(ctx)
var level uint16 if err != nil {
level, err = database.AmGetCommunityAccessLevel(ctxt.Ctx(), uid, c.Id) return err
if err == nil && database.AmTestPermission("Community.ShowAdmin", level) { }
out.Items[i].Flags["admin"] = true l, err := database.AmGetCommunitiesForUser(ctx, u.Uid)
} if err != nil {
} return err
out.Flags = make(map[string]bool) }
if user.IsAnon { rx.SetVar("communities", l)
out.Flags["canManage"] = false isAdmin := make([]bool, len(l))
out.Flags["canCreate"] = false for i, c := range l {
} else { isAdmin[i] = false
out.Flags["canManage"] = true level, err := database.AmGetCommunityAccessLevel(ctx, u.Uid, c.Id)
out.Flags["canCreate"] = user.BaseLevel >= uint16(g.CommunityCreateLevel) if err == nil && database.AmTestPermission("Community.ShowAdmin", level) {
} isAdmin[i] = true
out.TemplateName = "sb_ftrcomm.jet"
}
} }
} }
_ = in rx.SetVar("isAdmin", isAdmin)
return err rx.SetVar("canManage", !(u.IsAnon))
} rx.SetVar("canCreate", !(u.IsAnon) && u.BaseLevel >= uint16(g.CommunityCreateLevel))
/* 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
return nil return nil
} }
/* buildRenderedSidebox creates a RenderedSidebox for the data in the database. // renderSBConferences renders the Conferences sidebox.
* Parameters: func renderSBConferences(ctx context.Context, u *database.User, sb *DisplaySidebox, param *string, rx SideboxRendering) error {
* uid - UID of the user rendering the page. hl, err := database.AmGetConferenceHotlist(ctx, u)
* out - The RenderedSidebox to be built. if err != nil {
* in - The sidebox data from the database. return err
* 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)
} }
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. // templateGetTopic returns the pointer to the topic.
@@ -252,20 +198,26 @@ func TopPage(ctxt ui.AmContext) (string, any) {
ctxt.VarMap().SetFunc("post_topicLink", templateTopicLink) ctxt.VarMap().SetFunc("post_topicLink", templateTopicLink)
// Retrieve the sideboxes and create the data to be presented. // Retrieve the sideboxes and create the data to be presented.
uid := ctxt.CurrentUserId() sboxes, err := database.AmGetSideboxes(ctxt.Ctx(), ctxt.CurrentUserId())
sboxes, err := database.AmGetSideboxes(ctxt.Ctx(), uid)
if err != nil { if err != nil {
return "error", err return "error", err
} }
disp := make([]*DisplaySidebox, 0, len(sboxes))
rc := make([]RenderedSidebox, len(sboxes)) rx := sbRender{ctxt: ctxt, id: 0}
for i, sb := range sboxes { for _, sb := range sboxes {
err = buildRenderedSidebox(ctxt, uid, &(rc[i]), sb) dsb, ok := sideboxRegistry[sb.Boxid]
if err != nil { if ok {
return "error", err 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. // Final data set.
ctxt.SetLeftMenu("top") ctxt.SetLeftMenu("top")
+37
View File
@@ -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/.
*}
<!-- Communities sidebox (BOXID = 1) -->
<div class="space-y-2">
{{ if len(sb1_communities) > 0 }}
{{ range i, c := sb1_communities }}
<div class="flex items-center">
<span class="mr-2">🟣</span>
<a href="/comm/{{ c.Alias }}/profile" class="text-blue-700 hover:text-blue-900 font-bold text-sm">{{ c.Name }}</a>
{{ if sb1_isAdmin[i] }}<span class="ml-1" title="You are a host of this community">👑</span>{{ end }}
</div>
{{ end }}
{{ else }}
<div class="text-small">You are not a member of any communities.</div>
{{ end }}
</div>
{{ if sb1_canManage || sb1_canCreate }}
<div class="mt-3 text-center">
<span class="text-black text-xs font-bold">
[
{{ if sb1_canManage }}
<a href="/manage_comm" class="text-blue-700 hover:text-blue-900">Manage</a>
{{ if sb1_canCreate }}|{{ end }}
{{ end }}
{{ if sb1_canCreate }}
<a href="/create_comm" class="text-blue-700 hover:text-blue-900">Create New</a>
{{ end }}
]
</span>
</div>
{{ end }}
+32
View File
@@ -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/.
*}
<!-- Conferences sidebox (BOXID = 2) -->
<div class="space-y-2">
{{ if len(sb2_comm) > 0 }}
{{ range i, c := sb2_comm }}
<div class="flex items-center">
<span class="mr-2">🟣</span>
<div class="text-sm">
<a href="/comm/{{ c.Alias }}/conf/{{ sb2_alias[i] }}" class="text-blue-700 hover:text-blue-900 font-bold">{{ sb2_conf[i].Name }}</a>
<span class="text-black"> ({{ c.Name }})</span>
</div>
{{ if sb2_newFlag[i] }}<span class="ml-1" title="There are new messages in this conference">🔔</span>{{ end }}
</div>
{{ end }}
{{ else }}
<div class="text-small">No conferences in hotlist.</div>
{{ end }}
</div>
{{ if sb2_canManage }}
<div class="mt-3 text-center">
<span class="text-black text-xs font-bold">
[ <a href="/hotlist" class="text-blue-700 hover:text-blue-900">Manage</a> ]
</span>
</div>
{{ end }}
-45
View File
@@ -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/.
*}
<!-- Communities sidebox -->
{{ sb := .GetScratch("__sidebox") }}
<div class="mb-4">
<div class="bg-blue-600 px-2 py-1 rounded-t">
<h3 class="text-white font-bold text-base">{{ sb.Title }}</h3>
</div>
<div class="bg-blue-400 px-2 py-2 rounded-b">
<div class="space-y-2">
{{ if len(sb.Items) > 0 }}
{{ range sb.Items }}
<div class="flex items-center">
<span class="mr-2">🟣</span>
<a href="{{ .Link }}" class="text-blue-700 hover:text-blue-900 font-bold text-sm">{{ .Text }}</a>
{{ if .Flags["admin"] }}<span class="ml-1" title="You are a host of this community">👑</span>{{ end }}
</div>
{{ end }}
{{ else }}
<div class="text-small">You are not a member of any communities.</div>
{{ end }}
</div>
{{ if sb.Flags["canManage"] || sb.Flags["canCreate"] }}
<div class="mt-3 text-center">
<span class="text-black text-xs font-bold">
[
{{ if sb.Flags["canManage"] }}
<a href="/manage_comm" class="text-blue-700 hover:text-blue-900">Manage</a>
{{ if sb.Flags["canCreate"] }}|{{ end }}
{{ end }}
{{ if sb.Flags["canCreate"] }}
<a href="/create_comm" class="text-blue-700 hover:text-blue-900">Create New</a>
{{ end }}
]
</span>
</div>
{{ end }}
</div>
</div>
-40
View File
@@ -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/.
*}
<!-- Conference Hotlist sidebox -->
{{ sb := .GetScratch("__sidebox") }}
<div class="mb-4">
<div class="bg-blue-600 px-2 py-1 rounded-t">
<h3 class="text-white font-bold text-base">{{ sb.Title }}:</h3>
</div>
<div class="bg-blue-400 px-2 py-2 rounded-b">
<div class="space-y-2">
{{ if len(sb.Items) > 0 }}
{{ range i, item := sb.Items }}
<div class="flex items-center">
<span class="mr-2">🟣</span>
<div class="text-sm">
<a href="{{ item.Link }}" class="text-blue-700 hover:text-blue-900 font-bold">{{ item.Text }}</a>
<span class="text-black"> ({{ item.Text2 }})</span>
</div>
{{ if item.Flags["new"] }}<span class="ml-1" title="There are new messages in this conference">🔔</span>{{ end }}
</div>
{{ end }}
{{ else }}
<div class="text-small">No conferences in hotlist.</div>
{{ end }}
</div>
{{ if sb.Flags["canManage"] }}
<div class="mt-3 text-center">
<span class="text-black text-xs font-bold">
[ <a href="/hotlist" class="text-blue-700 hover:text-blue-900">Manage</a> ]
</span>
</div>
{{ end }}
</div>
</div>
+15 -30
View File
@@ -6,36 +6,21 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this * 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/. * file, You can obtain one at https://mozilla.org/MPL/2.0/.
*} *}
<!-- Users Online --> <!-- Online users sidebox (BOXID = 3) -->
{{ sb := .GetScratch("__sidebox") }} <div class="space-y-1">
<div class="mb-4"> <div class="flex items-center">
<div class="bg-blue-600 px-2 py-1 rounded-t"> <span class="font-bold text-sm">{{ sb3_total }} total (max {{ sb3_maxUsers }})</span>
<h3 class="text-white font-bold text-base">{{ sb.Title }}:</h3>
</div> </div>
<div class="bg-blue-400 px-2 py-2 rounded-b"> {{ if sb3_anons > 0 }}
<div class="space-y-1"> <div class="flex items-center">
{{ if len(sb.Items) > 0 }} <span class="mr-2">🟣</span>
{{ range sb.Items }} <span class="text-sm">Not logged in ({{ sb3_anons }})</span>
<div class="flex items-center">
{{ if ! .Flags["nobullet"] }}<span class="mr-2">🟣</span>{{ end }}
{{ if .LinkX() == "" }}
{{ if .Flags["bold"] }}
<span class="font-bold text-sm">{{ .Text }}</span>
{{ else }}
<span class="text-sm">{{ .Text }}</span>
{{ end }}
{{ else }}
{{ if .Flags["bold"] }}
<a href="{{ .LinkX() }}" class="text-blue-700 hover:text-blue-900 font-bold text-sm">{{ .Text }}</a>
{{ else }}
<a href="{{ .LinkX() }}" class="text-blue-700 hover:text-blue-900 text-sm">{{ .Text }}</a>
{{ end }}
{{ end }}
</div>
{{ end }}
{{ else }}
<div class="text-small">You are not a member of any communities.</div>
{{ end }}
</div> </div>
</div> {{ end }}
{{ range _, u := sb3_users }}
<div class="flex items-center">
<span class="mr-2">🟣</span>
<a href="/user/{{ u }}" class="text-blue-700 hover:text-blue-900 font-bold text-sm">{{ u }}</a>
</div>
{{ end }}
</div> </div>
+9 -4
View File
@@ -53,11 +53,16 @@
<!-- RIGHT SIDEBAR --> <!-- RIGHT SIDEBAR -->
<div class="w-80 p-4"> <div class="w-80 p-4">
{{ range i, s := sideboxes }} {{ range _, s := sideboxes }}
{{ .SetScratch("__sidebox", s) }} <div class="mb-4">
{{ include s.TemplateName }} <div class="bg-blue-600 px-2 py-1 rounded-t">
<h3 class="text-white font-bold text-base">{{ if .CurrentUser().IsAnon }}{{ s.TitleAnon }}{{ else }}{{ s.Title }}{{ end }}:</h3>
</div>
<div class="bg-blue-400 px-2 py-2 rounded-b">
{{ include s.TemplateName }}
</div>
</div>
{{ end }} {{ end }}
{{ if !.CurrentUser().IsAnon }} {{ if !.CurrentUser().IsAnon }}
<div class="text-center"> <div class="text-center">
<a href="/TODO/config-sideboxes" <a href="/TODO/config-sideboxes"