additional work on reading posts - still not quite there yet but getting closer
This commit is contained in:
+3
-2
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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"><General.4.0></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"><General.4.1></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"><General.4.2></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"><General.4.3></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>
|
||||
|
||||
@@ -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"><General.4.0></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
@@ -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)
|
||||
}
|
||||
}()
|
||||
})
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
Reference in New Issue
Block a user