additional work on reading posts - still not quite there yet but getting closer

This commit is contained in:
2025-12-19 23:23:58 -07:00
parent 57d664dcb1
commit 80bd0e03fd
11 changed files with 325 additions and 151 deletions
+3 -2
View File
@@ -11,6 +11,7 @@
package main
import (
"context"
"errors"
"fmt"
"net/http"
@@ -334,12 +335,12 @@ func EditCommunityLogo(ctxt ui.AmContext) (string, any, error) {
}
defer func() {
if happy {
go func() {
ampool.Submit(func(context.Context) {
err := database.AmDeleteImage(int32(id))
if err != nil {
log.Errorf("unable to delete image ID %d: %v", id, err)
}
}()
})
}
}()
}
+97 -40
View File
@@ -11,6 +11,7 @@
package main
import (
"context"
"errors"
"fmt"
"io"
@@ -324,24 +325,61 @@ func AttachmentUpload(ctxt ui.AmContext) (string, any, error) {
return ui.ErrorPage(ctxt, errors.New("invalid button clicked on form"))
}
func breakRange(topic *database.Topic, into []int32, param string, sep string) error {
rstr := strings.Split(param, sep)
if len(rstr) == 0 {
return fmt.Errorf("posts not found: %s in topic %d", param, topic.Number)
}
v, err := strconv.ParseInt(rstr[0], 10, 32)
if err != nil {
return fmt.Errorf("posts not found: %s in topic %d", param, topic.Number)
}
into[0] = int32(v)
if len(rstr) > 1 {
v, err = strconv.ParseInt(rstr[1], 10, 32)
if err != nil {
return fmt.Errorf("posts not found: %s in topic %d", param, topic.Number)
}
into[1] = int32(v)
} else {
into[1] = into[0]
}
if into[1] < 0 {
into[1] = topic.TopMessage
}
if into[0] > into[1] {
t := into[0]
into[0] = into[1]
into[1] = t
}
into[0] = max(into[0], 0)
into[1] = min(into[1], topic.TopMessage)
return nil
}
func ReadPosts(ctxt ui.AmContext) (string, any, error) {
// If we need to reset a topic's last read count (as with "Next & Keep New"), spin the task off into a goroutine.
// If we need to reset a topic's last read count (as with "Next & Keep New"), spin the task off.
if ctxt.HasParameter("rst") {
rst := strings.Split(ctxt.Parameter("rst"), ",")
if len(rst) >= 2 {
topicId, e1 := strconv.ParseInt(rst[0], 10, 32)
lastRead, e2 := strconv.ParseInt(rst[1], 10, 32)
if e1 == nil && e2 == nil {
user := ctxt.CurrentUser()
go func() {
user := ctxt.CurrentUser()
ampool.Submit(func(context.Context) {
topicId, e1 := strconv.ParseInt(rst[0], 10, 32)
lastRead, e2 := strconv.ParseInt(rst[1], 10, 32)
if e1 == nil && e2 == nil {
topic, _ := database.AmGetTopic(int32(topicId))
if topic != nil {
topic.SetLastRead(user, int32(lastRead))
}
}()
}
}
})
}
}
// Get user prefs.
prefs, err := ctxt.CurrentUser().Prefs()
if err != nil {
return ui.ErrorPage(ctxt, err)
}
// Locate community, conference, and topic.
comm := ctxt.CurrentCommunity()
conf := ctxt.GetScratch("currentConference").(*database.Conference)
@@ -355,44 +393,25 @@ func ReadPosts(ctxt ui.AmContext) (string, any, error) {
}
// Determine the range of posts to display. The "pin" is the post number after which we display the horizontal line separating old and new posts.
lastRead, err := topic.GetLastRead(ctxt.CurrentUser())
if err != nil {
ctxt.SetRC(http.StatusNotFound)
return ui.ErrorPage(ctxt, fmt.Errorf("posts not found in topic %d - %v", topic.Number, err))
}
postRange := make([]int32, 2)
var pin int32 = -1
resetLastRead := false
if ctxt.HasParameter("r") {
rstr := strings.Split(ctxt.Parameter("r"), ",")
if len(rstr) == 0 {
if err := breakRange(topic, postRange, ctxt.Parameter("r"), ","); err != nil {
ctxt.SetRC(http.StatusNotFound)
return ui.ErrorPage(ctxt, fmt.Errorf("posts not found: %s in topic %d", ctxt.Parameter("r"), topic.Number))
return ui.ErrorPage(ctxt, err)
}
v, err := strconv.ParseInt(rstr[0], 10, 32)
if err != nil {
} else if ctxt.HasParameter("rgo") {
if err := breakRange(topic, postRange, ctxt.Parameter("rgo"), "-"); err != nil {
ctxt.SetRC(http.StatusNotFound)
return ui.ErrorPage(ctxt, fmt.Errorf("posts not found: %s in topic %d", ctxt.Parameter("r"), topic.Number))
}
postRange[0] = int32(v)
if len(rstr) > 1 {
v, err = strconv.ParseInt(rstr[1], 10, 32)
if err != nil {
ctxt.SetRC(http.StatusNotFound)
return ui.ErrorPage(ctxt, fmt.Errorf("posts not found: %s in topic %d", ctxt.Parameter("r"), topic.Number))
}
postRange[1] = int32(v)
} else {
postRange[1] = postRange[0]
}
if postRange[1] < 0 {
postRange[1] = topic.TopMessage
}
if postRange[0] > postRange[1] {
t := postRange[0]
postRange[0] = postRange[1]
postRange[1] = t
return ui.ErrorPage(ctxt, err)
}
} else {
lastRead, err := topic.GetLastRead(ctxt.CurrentUser())
if err != nil {
ctxt.SetRC(http.StatusNotFound)
return ui.ErrorPage(ctxt, fmt.Errorf("posts not found in topic %d - %v", topic.Number, err))
}
postRange[0] = lastRead + 1
postRange[1] = topic.TopMessage
count := postRange[1] - postRange[0] + 1
@@ -400,11 +419,49 @@ func ReadPosts(ctxt ui.AmContext) (string, any, error) {
postRange[0] = postRange[1] - ctxt.Globals().PostsPerPage + 1
} else if count < ctxt.Globals().PostsPerPage {
pin = postRange[0] - 1
postRange[0] -= ctxt.Globals().OldPostsAtTop
postRange[0] = max(0, postRange[0])
postRange[0] = max(0, postRange[0]-ctxt.Globals().OldPostsAtTop)
if pin < postRange[0] {
pin = -1
}
}
resetLastRead = true
}
// Load the actual posts.
posts, err := database.AmGetPostRange(topic, postRange[0], postRange[1])
if err != nil {
return ui.ErrorPage(ctxt, fmt.Errorf("internal error getting posts <%d:%d-%d> - %v", topic.Number, postRange[0], postRange[1], err))
}
// Determine other required data.
loc := prefs.Localizer()
summaryLine := fmt.Sprintf("%d Total; %d New; Last: %s", topic.TopMessage+1, topic.TopMessage-lastRead, loc.Strftime("%b %e, %Y %r", topic.LastUpdate))
plc := database.AmCreatePostLinkContext(comm.Alias, ctxt.GetScratch("currentAlias").(string), topic.Number)
topicPostRef := plc.AsString()
plc.FirstPost = postRange[0]
plc.LastPost = postRange[1]
postsPostRef := plc.AsString()
// Render the output.
ctxt.VarMap().Set("stem", fmt.Sprintf("/comm/%s/conf/%s", comm.Alias, ctxt.GetScratch("currentAlias").(string)))
ctxt.VarMap().Set("topicName", topic.Name)
ctxt.VarMap().Set("summaryLine", summaryLine)
ctxt.VarMap().Set("lastRead", lastRead)
ctxt.VarMap().Set("pageSize", ctxt.Globals().PostsPerPage)
ctxt.VarMap().Set("post_max", topic.TopMessage)
ctxt.VarMap().Set("posts", posts)
ctxt.VarMap().Set("postsPermalink", fmt.Sprintf("/go/%s", postsPostRef))
ctxt.VarMap().Set("pin", pin)
ctxt.VarMap().Set("rangeEnd", postRange[1])
ctxt.VarMap().Set("rangeStart", postRange[0])
ctxt.VarMap().Set("topicNum", topic.Number)
ctxt.VarMap().Set("topicPermalink", fmt.Sprintf("/go/%s", topicPostRef))
ctxt.VarMap().Set("amsterdam_pageTitle", fmt.Sprintf("%s: %s", topic.Name, summaryLine))
if resetLastRead {
user := ctxt.CurrentUser()
ampool.Submit(func(context.Context) {
topic.SetLastRead(user, topic.TopMessage)
})
}
return "framed_template", "posts.jet", nil
}
+9
View File
@@ -49,3 +49,12 @@ func AmGetPost(postId int64) (*PostHeader, error) {
}
return &(dbdata[0]), nil
}
func AmGetPostRange(topic *Topic, first, last int32) ([]PostHeader, error) {
var rc []PostHeader
err := amdb.Select(&rc, "SELECT * FROM posts WHERE topicid = ? AND num >= ? AND num <= ? ORDER BY num", topic.TopicId, first, last)
if err != nil {
return nil, err
}
return rc, nil
}
+38
View File
@@ -11,6 +11,7 @@ package database
import (
"errors"
"fmt"
"math"
"strconv"
"strings"
@@ -53,6 +54,43 @@ func (d *PostLinkData) VerifyNames() error {
return nil
}
// AsString converts the post link data to a string reference.
func (d *PostLinkData) AsString() string {
var b strings.Builder
if d.Community != "" {
b.WriteString(d.Community)
b.WriteString("!")
}
wrote := false
if d.Conference != "" {
b.WriteString(d.Conference)
b.WriteString(".")
wrote = true
}
needDot := false
if d.Topic > 0 {
needDot = true
b.WriteString(fmt.Sprintf("%d", d.Topic))
if !wrote {
b.WriteString(".")
needDot = false
}
}
if d.FirstPost >= 0 {
s := ""
if d.LastPost < 0 || d.LastPost == d.FirstPost {
s = fmt.Sprintf("%d", d.FirstPost)
} else {
s = fmt.Sprintf("%d-%d", d.FirstPost, d.LastPost)
}
if needDot {
b.WriteString(".")
}
b.WriteString(s)
}
return b.String()
}
// Maximum lengths of the components.
const (
maxLinkLength = 130
+11
View File
@@ -23,6 +23,7 @@ import (
"git.erbosoft.com/amy/amsterdam/email"
"git.erbosoft.com/amy/amsterdam/htmlcheck"
"git.erbosoft.com/amy/amsterdam/ui"
"git.erbosoft.com/amy/amsterdam/util"
"github.com/labstack/echo-contrib/session"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
@@ -102,6 +103,9 @@ func setupEcho() *echo.Echo {
return e
}
// ampool is the worker pool for one-shot background tasks.
var ampool *util.WorkerPool
// main is Ye Olde Main Function.
func main() {
// Configure the system.
@@ -127,6 +131,13 @@ func main() {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()
// Set up ampool.
ampool = util.AmNewPool(ctx, 4, 128)
go func() {
<-ctx.Done()
ampool.Shutdown()
}()
// Start server
go func() {
if err := e.Start(":1323"); err != nil && err != http.ErrServerClosed {
+11 -6
View File
@@ -12,6 +12,7 @@ package ui
import (
"bytes"
"context"
"fmt"
"mime/multipart"
"net/http"
@@ -38,6 +39,7 @@ type AmContext interface {
ClearCommunityContext()
ClearLoginCookie()
ClearSession()
Ctx() context.Context
CurrentCommunity() *database.Community
CurrentUser() *database.User
CurrentUserId() int32
@@ -123,6 +125,11 @@ func (c *amContext) ClearSession() {
c.effectiveLevel = 0
}
// Ctx returns the current context.Context for the request.
func (c *amContext) Ctx() context.Context {
return c.echoContext.Request().Context()
}
// CurrentCommunity returns the current community, if one's been set.
func (c *amContext) CurrentCommunity() *database.Community {
if c.community == nil {
@@ -213,11 +220,9 @@ func (c *amContext) HasParameter(name string) bool {
if s != "" {
return true
}
if c.echoContext.Request().Method == "POST" {
s = c.echoContext.FormValue(name)
if s != "" {
return true
}
s = c.echoContext.FormValue(name)
if s != "" {
return true
}
return false
}
@@ -255,7 +260,7 @@ func (c *amContext) OutputType() string {
*/
func (c *amContext) Parameter(name string) string {
rc := c.echoContext.QueryParam(name)
if rc == "" && c.echoContext.Request().Method == "POST" {
if rc == "" {
rc = c.echoContext.FormValue(name)
}
return rc
+1
View File
@@ -135,6 +135,7 @@ func SetConference(next echo.HandlerFunc) echo.HandlerFunc {
myLevel = lvl
}
ctxt.SetScratch("currentConference", conf)
ctxt.SetScratch("currentAlias", ctxt.URLParam("confid"))
ctxt.SetScratch("levelInConference", myLevel)
return next(c)
}
+29 -101
View File
@@ -18,7 +18,7 @@
<!-- Topic Controls -->
<div class="flex justify-between items-center mb-4 gap-2 flex-wrap">
<div class="flex gap-2 flex-wrap">
<a href="/TODO"
<a href="{{ stem }}"
class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded text-sm font-medium transition-colors">Topic List</a>
<a href="/TODO"
class="bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded text-sm font-medium transition-colors">Hide Topic</a>
@@ -41,17 +41,24 @@
<!-- Navigation Bar -->
<div class="flex justify-between items-center mb-4">
<form method="GET" action="/TODO" class="flex items-center gap-2">
<input type="hidden" name="cc" value="2">
<input type="hidden" name="conf" value="2">
<input type="hidden" name="top" value="4">
<input type="text" name="pxg" value="" size="6" maxlength="13" placeholder="Go to..."
<form method="GET" action="{{ stem }}/r/{{ topicNum }}" class="flex items-center gap-2">
<input type="text" name="rgo" value="" size="6" maxlength="13" placeholder="Go to..."
class="px-3 py-2 border border-gray-300 rounded font-mono text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
<button type="submit" name="go" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded text-sm font-medium transition-colors">Go</button>
</form>
<div class="flex items-center gap-2 text-sm">
<span>[</span>
<a href="/TODO" class="text-blue-700 hover:text-blue-900">View All</a>
<a href="{{ stem }}/r/{{ topicNum }}?r=0,-1" class="text-blue-700 hover:text-blue-900">View All</a>
{{ if rangeStart > 0 }}
<span>|</span>
<a href="{{ stem }}/r/{{ topicNum }}?r={{ rangeStart - pageSize - 1 }},{{ rangeStart - 1 }}" class="text-blue-700 hover:text-blue-900 mx-2">Scroll Up {{ pageSize }}</a>
{{ end }}
{{ if rangeEnd < post_max }}
<span>|</span>
<a href="{{ stem }}/r/{{ topicNum }}?r={{ rangeEnd + 1 }},{{ rangeEnd + pageSize + 1 }}" class="text-blue-700 hover:text-blue-900 mx-2">Scroll Down {{ pageSize }}</a>
<span>|</span>
<a href="{{ stem }}/r/{{ topicNum }}?r={{ post_max - pageSize - 1 }},{{ post_max }}" class="text-blue-700 hover:text-blue-900 mx-2">Scroll To End</a>
{{ end }}
<span>|</span>
<a href="#bottom" class="text-blue-700 hover:text-blue-900">Bottom</a>
<span>]</span>
@@ -67,105 +74,26 @@
<!-- Messages -->
<div class="space-y-6 mb-8">
<!-- Post 0 -->
<div class="border-2 border-gray-300 rounded-lg p-4 bg-white">
<div class="flex justify-between items-start mb-3">
<div class="text-sm text-gray-600">
<a href="http://necrovenice:8080/venice/conf/posts.js.vs?cc=2&conf=2&top=4&shac=1&p1=0" class="text-blue-700 hover:text-blue-900 font-mono">0</a> of
<a href="http://necrovenice:8080/venice/conf/posts.js.vs?cc=2&conf=2&top=4&shac=1&p1=3" class="text-blue-700 hover:text-blue-900 font-mono">3</a>
<span class="ml-2 text-xs">&lt;General.4.0&gt;</span>
<a href="http://necrovenice:8080/venice/go/Piazza!General.4.0" class="ml-2 text-xs text-blue-700 hover:text-blue-900">[Permalink]</a>
</div>
</div>
<div class="mb-2">
<strong class="text-lg">The first one</strong>
<span class="text-gray-600 text-sm ml-2">(<em>
<a href="http://necrovenice:8080/venice/user/Administrator" target="_blank" class="text-blue-700 hover:text-blue-900">Administrator</a>,
Nov 20, 2025 10:47:17 PM</em>)
</span>
</div>
<pre class="font-mono text-sm whitespace-pre-wrap bg-gray-50 p-4 rounded border border-gray-200">This is a test.
This is <em>only</em> a test.
If this had been an actual emergency, we would all be
dead by now.</pre>
</div>
<!-- Post 1 -->
<div class="border-2 border-gray-300 rounded-lg p-4 bg-white">
<div class="flex justify-between items-start mb-3">
<div class="text-sm text-gray-600">
<a href="http://necrovenice:8080/venice/conf/posts.js.vs?cc=2&conf=2&top=4&shac=1&p1=1" class="text-blue-700 hover:text-blue-900 font-mono">1</a> of
<a href="http://necrovenice:8080/venice/conf/posts.js.vs?cc=2&conf=2&top=4&shac=1&p1=3" class="text-blue-700 hover:text-blue-900 font-mono">3</a>
<span class="ml-2 text-xs">&lt;General.4.1&gt;</span>
<a href="http://necrovenice:8080/venice/go/Piazza!General.4.1" class="ml-2 text-xs text-blue-700 hover:text-blue-900">[Permalink]</a>
</div>
</div>
<div class="mb-2">
<strong class="text-lg">Sample text is here</strong>
<span class="text-gray-600 text-sm ml-2">(<em>
<a href="http://necrovenice:8080/venice/user/Administrator" target="_blank" class="text-blue-700 hover:text-blue-900">Administrator</a>,
Nov 20, 2025 10:48:21 PM</em>)
</span>
</div>
<pre class="font-mono text-sm whitespace-pre-wrap bg-gray-50 p-4 rounded border border-gray-200">Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Nulla maximus, quam sit amet dictum tristique, mi nibh
tempor dolor, pretium finibus purus nunc nec mauris. In
hendrerit a erat at sodales. Fusce dictum metus id
augue interdum dapibus. Mauris maximus elementum arcu
eu ultricies. Nullam mollis lorem ac ipsum accumsan
tincidunt. Proin gravida nibh fringilla tellus gravida,
viverra pellentesque metus luctus. Vivamus quis pretium
magna...</pre>
</div>
<!-- Post 2 - Truncated for space -->
<div class="border-2 border-gray-300 rounded-lg p-4 bg-white">
<div class="flex justify-between items-start mb-3">
<div class="text-sm text-gray-600">
<a href="http://necrovenice:8080/venice/conf/posts.js.vs?cc=2&conf=2&top=4&shac=1&p1=2" class="text-blue-700 hover:text-blue-900 font-mono">2</a> of
<a href="http://necrovenice:8080/venice/conf/posts.js.vs?cc=2&conf=2&top=4&shac=1&p1=3" class="text-blue-700 hover:text-blue-900 font-mono">3</a>
<span class="ml-2 text-xs">&lt;General.4.2&gt;</span>
<a href="http://necrovenice:8080/venice/go/Piazza!General.4.2" class="ml-2 text-xs text-blue-700 hover:text-blue-900">[Permalink]</a>
</div>
</div>
<div class="mb-2">
<strong class="text-lg">Reposted Tiedrich</strong>
<span class="text-gray-600 text-sm ml-2">(<em>
<a href="http://necrovenice:8080/venice/user/Administrator" target="_blank" class="text-blue-700 hover:text-blue-900">Administrator</a>,
Nov 20, 2025 10:57:04 PM</em>)
</span>
</div>
<div class="font-mono text-sm whitespace-pre-wrap bg-gray-50 p-4 rounded border border-gray-200">
<h3 class="font-bold mb-2">friday: the further adventures of some fucking idiot</h3>
<div>[Content with HTML links preserved]</div>
</div>
</div>
<!-- Post 3 -->
<div class="border-2 border-gray-300 rounded-lg p-4 bg-white" id="bottom">
<div class="flex justify-between items-start mb-3">
<div class="text-sm text-gray-600">
<a href="http://necrovenice:8080/venice/conf/posts.js.vs?cc=2&conf=2&top=4&shac=1&p1=3" class="text-blue-700 hover:text-blue-900 font-mono">3</a> of
<a href="http://necrovenice:8080/venice/conf/posts.js.vs?cc=2&conf=2&top=4&shac=1&p1=3" class="text-blue-700 hover:text-blue-900 font-mono">3</a>
<span class="ml-2 text-xs">&lt;General.4.3&gt;</span>
<a href="http://necrovenice:8080/venice/go/Piazza!General.4.3" class="ml-2 text-xs text-blue-700 hover:text-blue-900">[Permalink]</a>
</div>
</div>
<div class="mb-2">
<strong class="text-lg">Look</strong>
<span class="text-gray-600 text-sm ml-2">(<em>
<a href="http://necrovenice:8080/venice/user/Administrator" target="_blank" class="text-blue-700 hover:text-blue-900">Administrator</a>,
Nov 20, 2025 10:57:33 PM</em>)
</span>
</div>
<pre class="font-mono text-sm whitespace-pre-wrap bg-gray-50 p-4 rounded border border-gray-200">Let's not worry about all this.</pre>
</div>
{{ range i, p := posts }}
{{ .SubRender("singlepost.jet") | raw }}
{{ if pin == p.Num }}<hr/>{{ end }}
{{ end }}
</div>
<!-- Bottom Navigation -->
<div class="flex justify-end items-center mb-6 text-sm">
<span>[</span>
<a href="/TODO" class="text-blue-700 hover:text-blue-900 mx-2">View All</a>
<a href="{{ stem }}/r/{{ topicNum }}?r=0,-1" class="text-blue-700 hover:text-blue-900 mx-2">View All</a>
{{ if rangeStart > 0 }}
<span>|</span>
<a href="{{ stem }}/r/{{ topicNum }}?r={{ rangeStart - pageSize - 1 }},{{ rangeStart - 1 }}" class="text-blue-700 hover:text-blue-900 mx-2">Scroll Up {{ pageSize }}</a>
{{ end }}
{{ if rangeEnd < post_max }}
<span>|</span>
<a href="{{ stem }}/r/{{ topicNum }}?r={{ rangeEnd + 1 }},{{ rangeEnd + pageSize + 1 }}" class="text-blue-700 hover:text-blue-900 mx-2">Scroll Down {{ pageSize }}</a>
<span>|</span>
<a href="{{ stem }}/r/{{ topicNum }}?r={{ post_max - pageSize - 1 }},{{ post_max }}" class="text-blue-700 hover:text-blue-900 mx-2">Scroll To End</a>
{{ end }}
<span>|</span>
<a href="#top" class="text-blue-700 hover:text-blue-900 mx-2">Top</a>
<span>]</span>
+29
View File
@@ -0,0 +1,29 @@
{*
* 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/.
*}
<div class="border-2 border-gray-300 rounded-lg p-4 bg-white">
<div class="flex justify-between items-start mb-3">
<div class="text-sm text-gray-600">
<a href="http://necrovenice:8080/venice/conf/posts.js.vs?cc=2&conf=2&top=4&shac=1&p1=0" class="text-blue-700 hover:text-blue-900 font-mono">0</a> of
<a href="http://necrovenice:8080/venice/conf/posts.js.vs?cc=2&conf=2&top=4&shac=1&p1=3" class="text-blue-700 hover:text-blue-900 font-mono">3</a>
<span class="ml-2 text-xs">&lt;General.4.0&gt;</span>
<a href="http://necrovenice:8080/venice/go/Piazza!General.4.0" class="ml-2 text-xs text-blue-700 hover:text-blue-900">[Permalink]</a>
</div>
</div>
<div class="mb-2">
<strong class="text-lg">The first one</strong>
<span class="text-gray-600 text-sm ml-2">(<em>
<a href="http://necrovenice:8080/venice/user/Administrator" target="_blank" class="text-blue-700 hover:text-blue-900">Administrator</a>,
Nov 20, 2025 10:47:17 PM</em>)
</span>
</div>
<pre class="font-mono text-sm whitespace-pre-wrap bg-gray-50 p-4 rounded border border-gray-200">This is a test.
This is <em>only</em> a test.
If this had been an actual emergency, we would all be
dead by now.</pre>
</div>
+3 -2
View File
@@ -10,6 +10,7 @@
package main
import (
"context"
"errors"
"fmt"
"net/http"
@@ -298,12 +299,12 @@ func ProfilePhoto(ctxt ui.AmContext) (string, any, error) {
}
defer func() {
if happy {
go func() {
ampool.Submit(func(context.Context) {
err := database.AmDeleteImage(int32(id))
if err != nil {
log.Errorf("unable to delete image ID %d: %v", id, err)
}
}()
})
}
}()
}
+94
View File
@@ -0,0 +1,94 @@
/*
* 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/.
*/
// Package util contains utility definitions.
package util
import (
"context"
"sync"
log "github.com/sirupsen/logrus"
)
// Task is a function that can be submitted as a one-shot task.
type Task func(ctx context.Context)
// WorkerPool is a pool that can be used to submit one-shot background tasks.
type WorkerPool struct {
ctx context.Context // context
cancel context.CancelFunc // cancellation function
tasks chan Task // our task queue
wg sync.WaitGroup // wait group for shutdown
}
/* AmNewPool creates a new WorkerPool.
* Parameters:
* parent - The parent context for the worker pool.
* workers - The number of worker goroutines to spawn.
* queueSize - The size of the task queue.
* Returns:
* Pointer to the new WorkerPool.
*/
func AmNewPool(parent context.Context, workers, queueSize int) *WorkerPool {
ctx, cancel := context.WithCancel(parent)
p := WorkerPool{
ctx: ctx,
cancel: cancel,
tasks: make(chan Task, queueSize),
}
for i := range workers {
p.wg.Add(1)
go p.worker(i)
}
return &p
}
// worker is the worker goroutine for a pool.
func (p *WorkerPool) worker(id int) {
defer p.wg.Done()
for {
select {
case <-p.ctx.Done():
return
case task, ok := <-p.tasks:
if !ok {
return
}
func() {
defer func() {
if r := recover(); r != nil {
log.Errorf("worker %d panic: %v", id, r)
}
}()
task(p.ctx)
}()
}
}
}
// Submit queues a task for the worker pool.
func (p *WorkerPool) Submit(task Task) bool {
select {
case p.tasks <- task:
return true
case <-p.ctx.Done():
return false
default:
// queue is full
return false
}
}
// Shutdown shuts down the worker pool.
func (p *WorkerPool) Shutdown() {
p.cancel()
close(p.tasks)
p.wg.Wait()
}