From 9e6bf2fedaf27a49464ea1469e2397b315a88ff7 Mon Sep 17 00:00:00 2001 From: Amy Gale Ruth Bowersox Date: Sat, 20 Dec 2025 15:51:18 -0700 Subject: [PATCH] should be complete with initial read posts code --- conference.go | 90 +++++++++++++++++++++++++++++++++++++---- database/post.go | 37 +++++++++++++++++ ui/templates.go | 13 ++++++ ui/views/posts.jet | 40 ++++++++++-------- ui/views/singlepost.jet | 28 ++++++++----- util/workerpool.go | 6 +-- 6 files changed, 176 insertions(+), 38 deletions(-) diff --git a/conference.go b/conference.go index 7a6869b..f2ac48b 100644 --- a/conference.go +++ b/conference.go @@ -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 := "<>" + 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() diff --git a/database/post.go b/database/post.go index 0e97a93..38582c6 100644 --- a/database/post.go +++ b/database/post.go @@ -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) diff --git a/ui/templates.go b/ui/templates.go index 2b72acf..3d5340d 100644 --- a/ui/templates.go +++ b/ui/templates.go @@ -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) diff --git a/ui/views/posts.jet b/ui/views/posts.jet index 2a89f8a..a4d307d 100644 --- a/ui/views/posts.jet +++ b/ui/views/posts.jet @@ -18,7 +18,7 @@
- Topic List Hide Topic @@ -41,23 +41,23 @@
-
+
-
+
[ - View All + View All {{ if rangeStart > 0 }} | - Scroll Up {{ pageSize }} + Scroll Up {{ pageSize }} {{ end }} {{ if rangeEnd < post_max }} | - Scroll Down {{ pageSize }} + Scroll Down {{ pageSize }} | - Scroll To End + Scroll To End {{ end }} | Bottom @@ -67,32 +67,40 @@
- {{ 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 }}
{{ end }} + {{ if pin == post_cur.Num }}
{{ end }} {{ end }}
-
+
[ - View All + View All {{ if rangeStart > 0 }} | - Scroll Up {{ pageSize }} + Scroll Up {{ pageSize }} {{ end }} {{ if rangeEnd < post_max }} | - Scroll Down {{ pageSize }} + Scroll Down {{ pageSize }} | - Scroll To End + Scroll To End {{ end }} | Top diff --git a/ui/views/singlepost.jet b/ui/views/singlepost.jet index f34e71a..0fd7486 100644 --- a/ui/views/singlepost.jet +++ b/ui/views/singlepost.jet @@ -9,21 +9,27 @@
- The first one + {{ post_cur.Pseud }} ( - Administrator, - Nov 20, 2025 10:47:17 PM) + {{ post_userName }}, + {{ DisplayDateTime(post_cur.Posted, .) }})
-
This is a test.
-This is only a test.
-If this had been an actual emergency, we would all be 
-dead by now.
+ {{ if post_overrideLine != "" }} +
+ {{ if post_overrideLink != "" }} + {{ post_overrideLine }} + {{ else }} + {{ post_overrideLine }} + {{ end }} +
+ {{ else }} +
{{ post_text | postRewrite | raw }}
+ {{ end }}
diff --git a/util/workerpool.go b/util/workerpool.go index 13ad960..9204830 100644 --- a/util/workerpool.go +++ b/util/workerpool.go @@ -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():