should be complete with initial read posts code

This commit is contained in:
2025-12-20 15:51:18 -07:00
parent 80bd0e03fd
commit 9e6bf2feda
6 changed files with 176 additions and 38 deletions
+82 -8
View File
@@ -17,12 +17,15 @@ import (
"io"
"mime/multipart"
"net/http"
"reflect"
"strconv"
"strings"
"git.erbosoft.com/amy/amsterdam/database"
"git.erbosoft.com/amy/amsterdam/htmlcheck"
"git.erbosoft.com/amy/amsterdam/ui"
"github.com/CloudyKit/jet/v6"
"github.com/labstack/gommon/log"
)
/* Conferences displayes the list of conferences in a community.
@@ -325,18 +328,25 @@ func AttachmentUpload(ctxt ui.AmContext) (string, any, error) {
return ui.ErrorPage(ctxt, errors.New("invalid button clicked on form"))
}
/* breakRange breaks up a post range into two elements.
* Parameters:
* topic - The topic within which the range is defined.
* into - The 2-element array into which the range will be filled.
* param - The range parameter to be broken up.
* sep - The separator character to use
*/
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)
v, err := strconv.ParseInt(strings.TrimSpace(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)
v, err = strconv.ParseInt(strings.TrimSpace(rstr[1]), 10, 32)
if err != nil {
return fmt.Errorf("posts not found: %s in topic %d", param, topic.Number)
}
@@ -357,6 +367,63 @@ func breakRange(topic *database.Topic, into []int32, param string, sep string) e
return nil
}
func templateExtractUserName(args jet.Arguments) reflect.Value {
rc := "<<ERROR>>"
post := args.Get(0).Convert(reflect.TypeFor[*database.PostHeader]()).Interface().(*database.PostHeader)
user, err := database.AmGetUser(post.CreatorUid)
if err == nil {
rc = user.Username
} else {
log.Errorf("templateExtractUserName failed to get user #%d: %v", post.CreatorUid, err)
}
return reflect.ValueOf(rc)
}
func templatePostText(args jet.Arguments) reflect.Value {
post := args.Get(0).Convert(reflect.TypeFor[*database.PostHeader]()).Interface().(*database.PostHeader)
rc, err := post.Text()
if err != nil {
log.Errorf("templatePostText could not get post text from post #%d: %v", post.PostId, err)
rc = ""
}
return reflect.ValueOf(rc)
}
func templateOverrideLine(args jet.Arguments) reflect.Value {
post := args.Get(0).Convert(reflect.TypeFor[*database.PostHeader]()).Interface().(*database.PostHeader)
ctxt := args.Get(1).Convert(reflect.TypeFor[ui.AmContext]()).Interface().(ui.AmContext)
rc := ""
if post.IsScribbled() {
scr_date := ""
scr_user, err := database.AmGetUser(*post.ScribbleUid)
if err == nil {
var p *database.UserPrefs
p, err = ctxt.CurrentUser().Prefs()
if err == nil {
scr_date = p.Localizer().Strftime("%b %e, %Y %r", *post.ScribbleDate)
}
}
if err == nil {
rc = fmt.Sprintf("(Scribbled by %s on %s)", scr_user.Username, scr_date)
} else {
rc = fmt.Sprintf("<<<%v>>>", err)
}
} else if post.Hidden {
rc = fmt.Sprintf("(Hidden Message: %d Lines)", *post.LineCount)
}
return reflect.ValueOf(rc)
}
func templateOverrideLink(args jet.Arguments) reflect.Value {
post := args.Get(0).Convert(reflect.TypeFor[*database.PostHeader]()).Interface().(*database.PostHeader)
root := args.Get(1).Convert(reflect.TypeFor[string]()).String()
rc := ""
if post.Hidden {
rc = fmt.Sprintf("%s?r=%d&ac=1", root, post.Num)
}
return reflect.ValueOf(rc)
}
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.
if ctxt.HasParameter("rst") {
@@ -420,7 +487,7 @@ func ReadPosts(ctxt ui.AmContext) (string, any, error) {
} else if count < ctxt.Globals().PostsPerPage {
pin = postRange[0] - 1
postRange[0] = max(0, postRange[0]-ctxt.Globals().OldPostsAtTop)
if pin < postRange[0] {
if pin < postRange[0] || pin >= postRange[1] {
pin = -1
}
}
@@ -434,28 +501,35 @@ func ReadPosts(ctxt ui.AmContext) (string, any, error) {
}
// 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)
summaryLine := fmt.Sprintf("%d Total; %d New; Last: %s", topic.TopMessage+1, topic.TopMessage-lastRead, prefs.Localizer().Strftime("%b %e, %Y %r", topic.LastUpdate))
plc := database.AmCreatePostLinkContext("", ctxt.GetScratch("currentAlias").(string), topic.Number)
topicConferenceRef := plc.AsString()
plc.Community = comm.Alias
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_confRef", topicConferenceRef)
ctxt.VarMap().SetFunc("post_getOverrideLine", templateOverrideLine)
ctxt.VarMap().SetFunc("post.getOverrideLink", templateOverrideLink)
ctxt.VarMap().SetFunc("post_getText", templatePostText)
ctxt.VarMap().SetFunc("post_getUserName", templateExtractUserName)
ctxt.VarMap().Set("post_stem", fmt.Sprintf("/comm/%s/conf/%s/r/%d", comm.Alias, ctxt.GetScratch("currentAlias").(string), topic.Number))
ctxt.VarMap().Set("post_max", topic.TopMessage)
ctxt.VarMap().Set("post_topicPermalink", fmt.Sprintf("/go/%s", topicPostRef))
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("topicListLink", fmt.Sprintf("/comm/%s/conf/%s", comm.Alias, ctxt.GetScratch("currentAlias").(string)))
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()
+37
View File
@@ -29,12 +29,49 @@ type PostHeader struct {
Pseud *string `db:"pseud"` // post's "pseud" (name/header)
}
type PostData struct {
PostId int64 `db:"postid"` // ID of the post
Data *string `db:"data"` // actual post data
}
var ErrNoPostData = errors.New("no post data")
// IsScribbled returns true if the post has been scribbled, false if not.
func (p *PostHeader) IsScribbled() bool {
return p.ScribbleUid != nil && p.ScribbleDate != nil
}
/* SetAttachment sets the attachment data for a post.
* Parameters:
* fileName - Name of the original attachment file.
* mimeType - MIME type of the attachment data.
* length - Length of the attachment data in bytes.
* data - The attachment data itself.
* Returns:
* Standard Go error status.
*/
func (p *PostHeader) SetAttachment(fileName string, mimeType string, length int32, data []byte) error {
_, err := amdb.Exec("INSERT INTO postattach (postid, datalen, filename, mimetype, data) VALUES (?, ?, ?, ?, ?)",
p.PostId, length, fileName, mimeType, data)
return err
}
// Text returns the text associated with a post.
func (p *PostHeader) Text() (string, error) {
var dbdata []PostData
err := amdb.Select(&dbdata, "SELECT * FROM postdata WHERE postid = ?", p.PostId)
if err != nil {
return "", err
}
if len(dbdata) > 1 {
return "", fmt.Errorf("too many data records (%d) for post #%d", len(dbdata), p.PostId)
}
if len(dbdata) == 0 || dbdata[0].Data == nil {
return "", ErrNoPostData
}
return *dbdata[0].Data, nil
}
func AmGetPost(postId int64) (*PostHeader, error) {
var dbdata []PostHeader
err := amdb.Select(&dbdata, "SELECT * FROM posts WHERE postid = ?", postId)
+13
View File
@@ -121,6 +121,18 @@ func extractCommunityLogo(a jet.Arguments) reflect.Value {
return reflect.ValueOf(rc)
}
// displayDateTime formats a date and time value.
func displayDateTime(a jet.Arguments) reflect.Value {
timeval := a.Get(0).Convert(reflect.TypeFor[time.Time]()).Interface().(time.Time)
ctxt := a.Get(1).Convert(reflect.TypeFor[AmContext]()).Interface().(AmContext)
prefs, err := ctxt.CurrentUser().Prefs()
if err == nil {
loc := prefs.Localizer()
return reflect.ValueOf(loc.Strftime("%b %e, %Y %r", timeval))
}
return reflect.ValueOf(fmt.Sprintf("<<%v>>", err))
}
// displayActivity displays an activity string formatted to the user's preferences.
func displayActivity(a jet.Arguments) reflect.Value {
timeval := a.Get(0).Convert(reflect.TypeFor[*time.Time]()).Interface().(*time.Time)
@@ -273,6 +285,7 @@ func SetupTemplates() {
views.AddGlobalFunc("MakeYearRange", makeYearRange)
views.AddGlobalFunc("ExtractCommunityLogo", extractCommunityLogo)
views.AddGlobalFunc("DisplayActivity", displayActivity)
views.AddGlobalFunc("DisplayDateTime", displayDateTime)
views.AddGlobalFunc("DisplayMemberCount", displayMemberCount)
views.AddGlobalFunc("DisplayFullName", displayFullName)
views.AddGlobalFunc("DisplayExpandCat", displayExpandCat)
+24 -16
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="{{ stem }}"
<a href="{{ topicListLink }}"
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,23 +41,23 @@
<!-- Navigation Bar -->
<div class="flex justify-between items-center mb-4">
<form method="GET" action="{{ stem }}/r/{{ topicNum }}" class="flex items-center gap-2">
<form method="GET" action="{{ post_stem }}" 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">
<div class="flex items-center gap-2 text-sm" name="top">
<span>[</span>
<a href="{{ stem }}/r/{{ topicNum }}?r=0,-1" class="text-blue-700 hover:text-blue-900">View All</a>
<a href="{{ post_stem }}?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>
<a href="{{ post_stem }}?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>
<a href="{{ post_stem }}?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>
<a href="{{ post_stem }}?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>
@@ -67,32 +67,40 @@
<!-- Permalink Info -->
<div class="text-center text-xs text-gray-600 mb-6">
<a href="/go/{{ topicPermalink }}" class="text-blue-700 hover:text-blue-900">[Permalink to this topic]</a>
<a href="{{ post_topicPermalink }}" class="text-blue-700 hover:text-blue-900">[Permalink to this topic]</a>
<span class="mx-2">·</span>
<a href="/go/{{ postsPermalink }}" class="text-blue-700 hover:text-blue-900">[Permalink to these posts]</a>
<a href="{{ postsPermalink }}" class="text-blue-700 hover:text-blue-900">[Permalink to these posts]</a>
</div>
<!-- Messages -->
<div class="space-y-6 mb-8">
{{ range i, p := posts }}
{{ post_userName := "" }}
{{ post_text := "" }}
{{ post_overrideLine := "" }}
{{ post_overrideLink := "" }}
{{ range i, post_cur := posts }}
{{ post_userName = post_getUserName(post_cur) }}
{{ post_text = post_getText(post_cur) }}
{{ post_overrideLine = post_getOverrideLine(post_cur, .) }}
{{ post_overrideLink = post_getOverrideLink(post_cur, post_topicPermalink) }}
{{ .SubRender("singlepost.jet") | raw }}
{{ if pin == p.Num }}<hr/>{{ end }}
{{ if pin == post_cur.Num }}<hr/>{{ end }}
{{ end }}
</div>
<!-- Bottom Navigation -->
<div class="flex justify-end items-center mb-6 text-sm">
<div class="flex justify-end items-center mb-6 text-sm" name="bottom">
<span>[</span>
<a href="{{ stem }}/r/{{ topicNum }}?r=0,-1" class="text-blue-700 hover:text-blue-900 mx-2">View All</a>
<a href="{{ post_stem }}?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>
<a href="{{ post_stem }}?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>
<a href="{{ post_stem }}?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>
<a href="{{ post_stem }}?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>
+17 -11
View File
@@ -9,21 +9,27 @@
<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>
<a href="{{ post_stem }}?r={{ post_cur.Num }}&ac=1" class="text-blue-700 hover:text-blue-900 font-mono">{{ post_cur.Num }}</a> of
<a href="{{ post_stem }}?r={{ post_max }}&ac=1" class="text-blue-700 hover:text-blue-900 font-mono">{{ post_max }}</a>
🔗 <a href="{{ post_topicPermalink }}.{{ post_cur.Num }}" class="ml-2 text-xs text-blue-700 hover:text-blue-900">&lt;{{ post_confRef }}.{{ post_cur.Num }}&gt;</a>
</div>
</div>
<div class="mb-2">
<strong class="text-lg">The first one</strong>
<strong class="text-lg">{{ post_cur.Pseud }}</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>)
<a href="/user/{{ post_userName }}" target="_blank" class="text-blue-700 hover:text-blue-900">{{ post_userName }}</a>,
{{ DisplayDateTime(post_cur.Posted, .) }}</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>
{{ if post_overrideLine != "" }}
<div class="mb-2">
{{ if post_overrideLink != "" }}
<a href="{{ post_overrideLink }}" target="_blank" class="text-blue-700 hover:text-blue-900"><span class="italic font-bold">{{ post_overrideLine }}</span></a>
{{ else }}
<span class="italic text-bold">{{ post_overrideLine }}</span>
{{ end }}
</div>
{{ else }}
<pre class="amsPost font-mono text-sm whitespace-pre-wrap bg-gray-50 p-4 rounded border border-gray-200">{{ post_text | postRewrite | raw }}</pre>
{{ end }}
</div>
+3 -3
View File
@@ -44,15 +44,15 @@ func AmNewPool(parent context.Context, workers, queueSize int) *WorkerPool {
tasks: make(chan Task, queueSize),
}
for i := range workers {
p.wg.Add(1)
go p.worker(i)
p.wg.Go(func() {
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():