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
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -334,12 +335,12 @@ func EditCommunityLogo(ctxt ui.AmContext) (string, any, error) {
|
|||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
if happy {
|
if happy {
|
||||||
go func() {
|
ampool.Submit(func(context.Context) {
|
||||||
err := database.AmDeleteImage(int32(id))
|
err := database.AmDeleteImage(int32(id))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("unable to delete image ID %d: %v", id, err)
|
log.Errorf("unable to delete image ID %d: %v", id, err)
|
||||||
}
|
}
|
||||||
}()
|
})
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|||||||
+97
-40
@@ -11,6 +11,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -324,24 +325,61 @@ func AttachmentUpload(ctxt ui.AmContext) (string, any, error) {
|
|||||||
return ui.ErrorPage(ctxt, errors.New("invalid button clicked on form"))
|
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) {
|
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") {
|
if ctxt.HasParameter("rst") {
|
||||||
rst := strings.Split(ctxt.Parameter("rst"), ",")
|
rst := strings.Split(ctxt.Parameter("rst"), ",")
|
||||||
if len(rst) >= 2 {
|
if len(rst) >= 2 {
|
||||||
topicId, e1 := strconv.ParseInt(rst[0], 10, 32)
|
user := ctxt.CurrentUser()
|
||||||
lastRead, e2 := strconv.ParseInt(rst[1], 10, 32)
|
ampool.Submit(func(context.Context) {
|
||||||
if e1 == nil && e2 == nil {
|
topicId, e1 := strconv.ParseInt(rst[0], 10, 32)
|
||||||
user := ctxt.CurrentUser()
|
lastRead, e2 := strconv.ParseInt(rst[1], 10, 32)
|
||||||
go func() {
|
if e1 == nil && e2 == nil {
|
||||||
topic, _ := database.AmGetTopic(int32(topicId))
|
topic, _ := database.AmGetTopic(int32(topicId))
|
||||||
if topic != nil {
|
if topic != nil {
|
||||||
topic.SetLastRead(user, int32(lastRead))
|
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.
|
// Locate community, conference, and topic.
|
||||||
comm := ctxt.CurrentCommunity()
|
comm := ctxt.CurrentCommunity()
|
||||||
conf := ctxt.GetScratch("currentConference").(*database.Conference)
|
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.
|
// 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)
|
postRange := make([]int32, 2)
|
||||||
var pin int32 = -1
|
var pin int32 = -1
|
||||||
|
resetLastRead := false
|
||||||
if ctxt.HasParameter("r") {
|
if ctxt.HasParameter("r") {
|
||||||
rstr := strings.Split(ctxt.Parameter("r"), ",")
|
if err := breakRange(topic, postRange, ctxt.Parameter("r"), ","); err != nil {
|
||||||
if len(rstr) == 0 {
|
|
||||||
ctxt.SetRC(http.StatusNotFound)
|
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)
|
} else if ctxt.HasParameter("rgo") {
|
||||||
if err != nil {
|
if err := breakRange(topic, postRange, ctxt.Parameter("rgo"), "-"); err != nil {
|
||||||
ctxt.SetRC(http.StatusNotFound)
|
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)
|
||||||
}
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
} else {
|
} 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[0] = lastRead + 1
|
||||||
postRange[1] = topic.TopMessage
|
postRange[1] = topic.TopMessage
|
||||||
count := postRange[1] - postRange[0] + 1
|
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
|
postRange[0] = postRange[1] - ctxt.Globals().PostsPerPage + 1
|
||||||
} else if count < ctxt.Globals().PostsPerPage {
|
} else if count < ctxt.Globals().PostsPerPage {
|
||||||
pin = postRange[0] - 1
|
pin = postRange[0] - 1
|
||||||
postRange[0] -= ctxt.Globals().OldPostsAtTop
|
postRange[0] = max(0, postRange[0]-ctxt.Globals().OldPostsAtTop)
|
||||||
postRange[0] = max(0, postRange[0])
|
|
||||||
if pin < postRange[0] {
|
if pin < postRange[0] {
|
||||||
pin = -1
|
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
|
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 (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -53,6 +54,43 @@ func (d *PostLinkData) VerifyNames() error {
|
|||||||
return nil
|
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.
|
// Maximum lengths of the components.
|
||||||
const (
|
const (
|
||||||
maxLinkLength = 130
|
maxLinkLength = 130
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import (
|
|||||||
"git.erbosoft.com/amy/amsterdam/email"
|
"git.erbosoft.com/amy/amsterdam/email"
|
||||||
"git.erbosoft.com/amy/amsterdam/htmlcheck"
|
"git.erbosoft.com/amy/amsterdam/htmlcheck"
|
||||||
"git.erbosoft.com/amy/amsterdam/ui"
|
"git.erbosoft.com/amy/amsterdam/ui"
|
||||||
|
"git.erbosoft.com/amy/amsterdam/util"
|
||||||
"github.com/labstack/echo-contrib/session"
|
"github.com/labstack/echo-contrib/session"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
"github.com/labstack/echo/v4/middleware"
|
"github.com/labstack/echo/v4/middleware"
|
||||||
@@ -102,6 +103,9 @@ func setupEcho() *echo.Echo {
|
|||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ampool is the worker pool for one-shot background tasks.
|
||||||
|
var ampool *util.WorkerPool
|
||||||
|
|
||||||
// main is Ye Olde Main Function.
|
// main is Ye Olde Main Function.
|
||||||
func main() {
|
func main() {
|
||||||
// Configure the system.
|
// Configure the system.
|
||||||
@@ -127,6 +131,13 @@ func main() {
|
|||||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
|
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
|
||||||
defer stop()
|
defer stop()
|
||||||
|
|
||||||
|
// Set up ampool.
|
||||||
|
ampool = util.AmNewPool(ctx, 4, 128)
|
||||||
|
go func() {
|
||||||
|
<-ctx.Done()
|
||||||
|
ampool.Shutdown()
|
||||||
|
}()
|
||||||
|
|
||||||
// Start server
|
// Start server
|
||||||
go func() {
|
go func() {
|
||||||
if err := e.Start(":1323"); err != nil && err != http.ErrServerClosed {
|
if err := e.Start(":1323"); err != nil && err != http.ErrServerClosed {
|
||||||
|
|||||||
+11
-6
@@ -12,6 +12,7 @@ package ui
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -38,6 +39,7 @@ type AmContext interface {
|
|||||||
ClearCommunityContext()
|
ClearCommunityContext()
|
||||||
ClearLoginCookie()
|
ClearLoginCookie()
|
||||||
ClearSession()
|
ClearSession()
|
||||||
|
Ctx() context.Context
|
||||||
CurrentCommunity() *database.Community
|
CurrentCommunity() *database.Community
|
||||||
CurrentUser() *database.User
|
CurrentUser() *database.User
|
||||||
CurrentUserId() int32
|
CurrentUserId() int32
|
||||||
@@ -123,6 +125,11 @@ func (c *amContext) ClearSession() {
|
|||||||
c.effectiveLevel = 0
|
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.
|
// CurrentCommunity returns the current community, if one's been set.
|
||||||
func (c *amContext) CurrentCommunity() *database.Community {
|
func (c *amContext) CurrentCommunity() *database.Community {
|
||||||
if c.community == nil {
|
if c.community == nil {
|
||||||
@@ -213,11 +220,9 @@ func (c *amContext) HasParameter(name string) bool {
|
|||||||
if s != "" {
|
if s != "" {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if c.echoContext.Request().Method == "POST" {
|
s = c.echoContext.FormValue(name)
|
||||||
s = c.echoContext.FormValue(name)
|
if s != "" {
|
||||||
if s != "" {
|
return true
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -255,7 +260,7 @@ func (c *amContext) OutputType() string {
|
|||||||
*/
|
*/
|
||||||
func (c *amContext) Parameter(name string) string {
|
func (c *amContext) Parameter(name string) string {
|
||||||
rc := c.echoContext.QueryParam(name)
|
rc := c.echoContext.QueryParam(name)
|
||||||
if rc == "" && c.echoContext.Request().Method == "POST" {
|
if rc == "" {
|
||||||
rc = c.echoContext.FormValue(name)
|
rc = c.echoContext.FormValue(name)
|
||||||
}
|
}
|
||||||
return rc
|
return rc
|
||||||
|
|||||||
@@ -135,6 +135,7 @@ func SetConference(next echo.HandlerFunc) echo.HandlerFunc {
|
|||||||
myLevel = lvl
|
myLevel = lvl
|
||||||
}
|
}
|
||||||
ctxt.SetScratch("currentConference", conf)
|
ctxt.SetScratch("currentConference", conf)
|
||||||
|
ctxt.SetScratch("currentAlias", ctxt.URLParam("confid"))
|
||||||
ctxt.SetScratch("levelInConference", myLevel)
|
ctxt.SetScratch("levelInConference", myLevel)
|
||||||
return next(c)
|
return next(c)
|
||||||
}
|
}
|
||||||
|
|||||||
+29
-101
@@ -18,7 +18,7 @@
|
|||||||
<!-- Topic Controls -->
|
<!-- Topic Controls -->
|
||||||
<div class="flex justify-between items-center mb-4 gap-2 flex-wrap">
|
<div class="flex justify-between items-center mb-4 gap-2 flex-wrap">
|
||||||
<div class="flex 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>
|
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"
|
<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>
|
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 -->
|
<!-- Navigation Bar -->
|
||||||
<div class="flex justify-between items-center mb-4">
|
<div class="flex justify-between items-center mb-4">
|
||||||
<form method="GET" action="/TODO" class="flex items-center gap-2">
|
<form method="GET" action="{{ stem }}/r/{{ topicNum }}" class="flex items-center gap-2">
|
||||||
<input type="hidden" name="cc" value="2">
|
<input type="text" name="rgo" value="" size="6" maxlength="13" placeholder="Go to..."
|
||||||
<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..."
|
|
||||||
class="px-3 py-2 border border-gray-300 rounded font-mono text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
|
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>
|
<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>
|
</form>
|
||||||
<div class="flex items-center gap-2 text-sm">
|
<div class="flex items-center gap-2 text-sm">
|
||||||
<span>[</span>
|
<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>
|
<span>|</span>
|
||||||
<a href="#bottom" class="text-blue-700 hover:text-blue-900">Bottom</a>
|
<a href="#bottom" class="text-blue-700 hover:text-blue-900">Bottom</a>
|
||||||
<span>]</span>
|
<span>]</span>
|
||||||
@@ -67,105 +74,26 @@
|
|||||||
|
|
||||||
<!-- Messages -->
|
<!-- Messages -->
|
||||||
<div class="space-y-6 mb-8">
|
<div class="space-y-6 mb-8">
|
||||||
<!-- Post 0 -->
|
{{ range i, p := posts }}
|
||||||
<div class="border-2 border-gray-300 rounded-lg p-4 bg-white">
|
{{ .SubRender("singlepost.jet") | raw }}
|
||||||
<div class="flex justify-between items-start mb-3">
|
{{ if pin == p.Num }}<hr/>{{ end }}
|
||||||
<div class="text-sm text-gray-600">
|
{{ end }}
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Bottom Navigation -->
|
<!-- Bottom Navigation -->
|
||||||
<div class="flex justify-end items-center mb-6 text-sm">
|
<div class="flex justify-end items-center mb-6 text-sm">
|
||||||
<span>[</span>
|
<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>
|
<span>|</span>
|
||||||
<a href="#top" class="text-blue-700 hover:text-blue-900 mx-2">Top</a>
|
<a href="#top" class="text-blue-700 hover:text-blue-900 mx-2">Top</a>
|
||||||
<span>]</span>
|
<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
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -298,12 +299,12 @@ func ProfilePhoto(ctxt ui.AmContext) (string, any, error) {
|
|||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
if happy {
|
if happy {
|
||||||
go func() {
|
ampool.Submit(func(context.Context) {
|
||||||
err := database.AmDeleteImage(int32(id))
|
err := database.AmDeleteImage(int32(id))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("unable to delete image ID %d: %v", id, err)
|
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