From 3eb0f6a259f4a040077b46a60166c23b5ec392dd Mon Sep 17 00:00:00 2001 From: Amy Gale Ruth Bowersox Date: Thu, 1 Jan 2026 22:41:43 -0700 Subject: [PATCH] first draft of posting to a topic (including preview) --- conference.go | 114 ++++++++++++++++++++++++++++++++++++++ database/post.go | 83 +++++++++++++++++++++++++++ main.go | 1 + ui/views/posts.jet | 8 +-- ui/views/preview_post.jet | 101 +++++++++++++++++++++++++++++++++ 5 files changed, 302 insertions(+), 5 deletions(-) create mode 100644 ui/views/preview_post.jet diff --git a/conference.go b/conference.go index f933ed1..19cc678 100644 --- a/conference.go +++ b/conference.go @@ -553,3 +553,117 @@ func ReadPosts(ctxt ui.AmContext) (string, any, error) { } return "framed_template", "posts.jet", nil } + +func PostInTopic(ctxt ui.AmContext) (string, any, error) { + // Locate community, conference, and topic. + comm := ctxt.CurrentCommunity() + conf := ctxt.GetScratch("currentConference").(*database.Conference) + var topic *database.Topic = nil + if rawTopic, err := strconv.ParseInt(ctxt.URLParam("topic"), 10, 16); err == nil { + topic, err = database.AmGetTopicByNumber(ctxt.Ctx(), conf, int16(rawTopic)) + } + if topic == nil { + ctxt.SetRC(http.StatusNotFound) + return ui.ErrorPage(ctxt, fmt.Errorf("topic not found: %s", ctxt.URLParam("topic"))) + } + + urlStem := fmt.Sprintf("/comm/%s/conf/%s/r/%d", comm.Alias, ctxt.URLParam("confid"), topic.Number) + if ctxt.FormFieldIsSet("cancel") { + return "redirect", urlStem, nil + } + if ctxt.FormFieldIsSet("preview") { + // Preview the post. + checker, err := htmlcheck.AmNewHTMLChecker(ctxt.Ctx(), "escaper") + if err != nil { + return ui.ErrorPage(ctxt, err) + } + checker.Append(ctxt.FormField("pseud")) + checker.Finish() + v, _ := checker.Value() + ctxt.VarMap().Set("pseud", v) + + // escape the data + postdata := ctxt.FormField("pb") + checker.Reset() + checker.Append(postdata) + checker.Finish() + v, _ = checker.Value() + ctxt.VarMap().Set("pb", v) + + // run the preview + checker, err = htmlcheck.AmNewHTMLChecker(ctxt.Ctx(), "preview") + if err != nil { + return ui.ErrorPage(ctxt, err) + } + checker.SetContext("PostLinkDecoderContext", database.AmCreatePostLinkContext(comm.Alias, ctxt.URLParam("cid"), conf.TopTopic+1)) + checker.Append(postdata) + checker.Finish() + v, _ = checker.Value() + ctxt.VarMap().Set("previewPb", v) + nErr, _ := checker.Counter("spelling") + ctxt.VarMap().Set("nError", nErr) + + ctxt.VarMap().Set("maxPost", ctxt.FormField("xp")) + if ctxt.FormFieldIsSet("attach") { + ctxt.VarMap().Set("attachFile", true) + } + ctxt.VarMap().Set("urlStem", urlStem) + ctxt.VarMap().Set("amsterdam_pageTitle", "Previewing Message") + return "framed_template", "preview_post.jet", nil + } + // Figure out which URL to return to once this post is made. + var returnURL string + if ctxt.FormFieldIsSet("post") { + returnURL = urlStem + } else if ctxt.FormFieldIsSet("postnext") { + returnURL = urlStem // TODO + } else if ctxt.FormFieldIsSet("posttopics") { + returnURL = fmt.Sprintf("/comm/%s/conf/%s", comm.Alias, ctxt.URLParam("confid")) + } else { + return ui.ErrorPage(ctxt, errors.New("unknown post button")) + } + maxPost, err := ctxt.FormFieldInt("xp") + if err != nil { + return ui.ErrorPage(ctxt, err) + } + if int32(maxPost) < topic.TopMessage { + // Slippage detected! Display the slipped posts and another post box. + // TODO + return "framed_template", "???", nil + } + // start by checking the title and pseud + checker, err := htmlcheck.AmNewHTMLChecker(ctxt.Ctx(), "post-pseud") + if err != nil { + return ui.ErrorPage(ctxt, err) + } + checker.Append(ctxt.FormField("pseud")) + checker.Finish() + postPseud, _ := checker.Value() + + // now check the post data itself + checker, err = htmlcheck.AmNewHTMLChecker(ctxt.Ctx(), "post-body") + if err != nil { + return ui.ErrorPage(ctxt, err) + } + checker.SetContext("PostLinkDecoderContext", database.AmCreatePostLinkContext(comm.Alias, ctxt.URLParam("cid"), conf.TopTopic+1)) + checker.Append(ctxt.FormField("pb")) + checker.Finish() + postText, _ := checker.Value() + lines, _ := checker.Lines() + + // Add the post! + hdr, err := database.AmNewPost(ctxt.Ctx(), conf, topic, ctxt.CurrentUser(), postPseud, postText, int32(lines)) + if err != nil { + return ui.ErrorPage(ctxt, err) + } + + if !ctxt.FormFieldIsSet("attach") { + return "redirect", returnURL, nil // no attachment - just redisplay topic list + } + + // go upload the attachment + ctxt.VarMap().Set("target", returnURL) + ctxt.VarMap().Set("post", hdr.PostId) + ctxt.VarMap().Set("amsterdam_pageTitle", "Upload Attachment") + return "framed_template", "attachment_upload.jet", nil +} diff --git a/database/post.go b/database/post.go index fff6d64..712e166 100644 --- a/database/post.go +++ b/database/post.go @@ -118,3 +118,86 @@ func AmGetPostRange(ctx context.Context, topic *Topic, first, last int32) ([]*Po } return rc, nil } + +/* AmNewPost adds a new post to a topic. + * Parameters: + * ctx - Standard Go context value. + * conf - Pointer to conference containing the topic. + * topic - Pointer to topic. + * user - Pointer to user posting the message. + * pseud - Pseud for the new post. + * post - New post text. + * postLines - Number of lines in the post text. + * Returns: + * New post header pointer. + * Standard Go error status. + */ +func AmNewPost(ctx context.Context, conf *Conference, topic *Topic, user *User, pseud string, post string, postLines int32) (*PostHeader, error) { + success := false + tx := amdb.MustBegin() + defer func() { + if !success { + tx.Rollback() + } + }() + unlock := true + tx.ExecContext(ctx, "LOCK TABLES topics WRITE, topicsettings WRITE, posts WRITE, postdata WRITE;") + defer func() { + if unlock { + tx.ExecContext(ctx, "UNLOCK TABLES;") + } + }() + + // Add the post header information. + rs, err := tx.ExecContext(ctx, "INSERT INTO posts (topicid, num, linecount, creator_uid, posted, pseud) VALUES (?, ?, ?, ?, NOW(), ?)", + topic.TopicId, topic.TopMessage+1, postLines, user.Uid, pseud) + if err != nil { + return nil, err + } + xid, err := rs.LastInsertId() + if err != nil { + return nil, err + } + + // Read back the post header. + var dbdata []PostHeader + if err := tx.SelectContext(ctx, &dbdata, "SELECT * FROM posts WHERE postid = ?", xid); err != nil { + return nil, err + } + if len(dbdata) == 0 { + return nil, errors.New("AmNewPost: new post not found") + } + if len(dbdata) > 1 { + return nil, fmt.Errorf("AmNewPost: too many entries (%d) for post ID %d", len(dbdata), xid) + } + hdr := &(dbdata[0]) + + // Add the post data. + _, err = tx.ExecContext(ctx, "INSERT INTO postdata (postid, data) VALUES (?, ?)", int32(xid), hdr.PostId) + if err != nil { + return nil, err + } + + // Update the topic. + _, err = tx.ExecContext(ctx, "UPDATE topics SET top_message = ?, lastupdate = ? WHERE topicid = ?", hdr.Num, hdr.Posted, topic.TopicId) + if err != nil { + return nil, err + } + topic.TopMessage = hdr.Num + topic.LastUpdate = hdr.Posted + + tx.ExecContext(ctx, "UNLOCK TABLES;") + unlock = false + + // update the "last posted" date in the conference settings + _, err = conf.TouchPost(ctx, tx, user, hdr.Posted) + if err != nil { + return nil, err + } + + if err = tx.Commit(); err != nil { + return nil, err + } + success = true + return hdr, nil +} diff --git a/main.go b/main.go index 464f366..a47c5ad 100644 --- a/main.go +++ b/main.go @@ -99,6 +99,7 @@ func setupEcho() *echo.Echo { confGroup.GET("/new_topic", ui.AmWrap(NewTopicForm)) confGroup.POST("/new_topic", ui.AmWrap(NewTopic)) confGroup.GET("/r/:topic", ui.AmWrap(ReadPosts)) + confGroup.POST("/r/:topic", ui.AmWrap(PostInTopic)) return e } diff --git a/ui/views/posts.jet b/ui/views/posts.jet index 7bcc59f..9db0228 100644 --- a/ui/views/posts.jet +++ b/ui/views/posts.jet @@ -106,11 +106,8 @@

Post Message in "{{ topicName }}":

-
- - - - + +
@@ -137,6 +134,7 @@
+
diff --git a/ui/views/preview_post.jet b/ui/views/preview_post.jet new file mode 100644 index 0000000..3900f91 --- /dev/null +++ b/ui/views/preview_post.jet @@ -0,0 +1,101 @@ +{* + * Amsterdam Web Communities System + * Copyright (c) 2025-2026 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/. + *} +
+
+

Previewing Post

+
+
+ + +
+ + {{ if nError == 0 }} +
+

+ Your post did not contain any spelling errors. +

+
+ {{ else }} +
+

+ {{ if nError == 1 }} + There was 1 spelling error in your post. + {{ else }} + There were {{ nError }} spelling errors in your post. + {{ end }} +

+
+ {{ end }} + + +
+
{{ previewPb | postRewrite | raw }}
+
+ +
+
+ + +
+ + +
+ +
+ +
+ +
+ + +
+
+
+ + +
+
+ + HTML Guide +
+ +
+ + +
+ + + + + +
+
+ + + +
+

Review Your Post:

+
    +
  • Check the preview above to see how your post will appear to others
  • +
  • Spelling errors are highlighted in red - you can edit your message to fix them
  • +
  • Click "Preview Again" to see updated changes before posting
  • +
  • When satisfied, click "Add Topic" to submit your message
  • +
+
+
+ + + +