From 110e9179211bcc807da3c62ab47e6ca1cde0d55e Mon Sep 17 00:00:00 2001 From: Amy Gale Ruth Bowersox Date: Mon, 23 Feb 2026 22:41:59 -0700 Subject: [PATCH] Pictures in Posts now works! --- conference.go | 29 ++++++++++++++ config/config.go | 13 ++++++ config/default.yaml | 1 + docs/MISSINGFUNCS.md | 2 +- login.go | 44 +++++++++++++++++++- main.go | 3 ++ ui/dialogs/profile.yaml | 2 - ui/images.go | 23 ++++++++++- ui/views/posts.jet | 4 ++ ui/views/profile.jet | 3 +- ui/views/singlepost.jet | 89 +++++++++++++++++++++++------------------ userdata.go | 26 +++++++++++- 12 files changed, 193 insertions(+), 46 deletions(-) diff --git a/conference.go b/conference.go index b49715a..89324aa 100644 --- a/conference.go +++ b/conference.go @@ -436,6 +436,23 @@ func templateBozo(args jet.Arguments) reflect.Value { return reflect.ValueOf(rc) } +// templateProfileImage returns the user profile image associated with this post. +func templateProfileImage(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 := "/img/builtin/no-user.png" + user, err := post.Creator(ctxt.Ctx()) + if err == nil { + ci, err := user.ContactInfo(ctxt.Ctx()) + if err == nil { + if ci.PhotoURL != nil { + rc = *ci.PhotoURL + } + } + } + return reflect.ValueOf(rc) +} + /* ReadPosts displays posts in a topic. * Parameters: * ctxt - The AmContext for the request. @@ -555,6 +572,16 @@ func ReadPosts(ctxt ui.AmContext) (string, any) { } ctxt.VarMap().Set("pseud", pseud) + // Get property flags from conference and user. + cflags, err := conf.Flags(ctxt.Ctx()) + if err != nil { + return "error", err + } + uflags, err := ctxt.CurrentUser().Flags(ctxt.Ctx()) + if err != nil { + return "error", err + } + // Set permission and status flags. confHidePerm := conf.TestPermission("Conference.Hide", myLevel) ctxt.VarMap().Set("canFreeze", confHidePerm) @@ -568,6 +595,7 @@ func ReadPosts(ctxt ui.AmContext) (string, any) { confPostPerm := conf.TestPermission("Conference.Post", myLevel) ctxt.VarMap().Set("canPost", (!(topic.Frozen || topic.Archived) || confHidePerm) && confPostPerm) ctxt.VarMap().Set("showFrozenArchiveMessages", confPostPerm) + ctxt.VarMap().Set("showPics", cflags.Get(database.ConferenceFlagPicturesInPosts) && uflags.Get(database.UserFlagPicturesInPosts)) // Set advanced controls. advancedControls := ctxt.HasParameter("ac") && (len(posts) == 1) @@ -622,6 +650,7 @@ func ReadPosts(ctxt ui.AmContext) (string, any) { ctxt.VarMap().SetFunc("post_getUserName", templateExtractUserName) ctxt.VarMap().SetFunc("post_getAttachmentInfo", templateAttachmentInfo) ctxt.VarMap().SetFunc("post_isBozo", templateBozo) + ctxt.VarMap().SetFunc("post_profileImage", templateProfileImage) ctxt.VarMap().Set("post_stem", fmt.Sprintf("%s/r/%d", urlStem, topic.Number)) ctxt.VarMap().Set("post_max", topic.TopMessage) ctxt.VarMap().Set("posts", posts) diff --git a/config/config.go b/config/config.go index 66075d9..b462753 100644 --- a/config/config.go +++ b/config/config.go @@ -90,6 +90,7 @@ type AmConfig struct { CountryList struct { Prioritize string `yaml:"prioritize"` } `yaml:"countryList"` + VeniceCompatibleImageURLs bool `yaml:"veniceCompatibleImageURLs"` } `yaml:"rendering"` Posting struct { ExternalDictionary string `yaml:"externalDictionary"` @@ -198,6 +199,17 @@ func overlayInt(loaded int, defaulted int) int { return defaulted } +/* overlayOptionFlag is a helper that takes a loaded or defaulted option flag and returns it. + * Parameters: + * loaded - The option flag loaded from a configuration file. + * defaulted - The default value of this option flag. + * Returns: + * Combined value. + */ +func overlayOptionFlag(loaded, defaulted bool) bool { + return loaded || defaulted +} + /* overlayConfig takes two configuration structures and overlays them to create the third. * Parameters: * dest - Points to the destination copnfiguration structure. @@ -230,6 +242,7 @@ func overlayConfig(dest *AmConfig, loaded *AmConfig, defaults *AmConfig) { dest.Rendering.TemplateDir = overlayString(loaded.Rendering.TemplateDir, defaults.Rendering.TemplateDir) dest.Rendering.CookieKey = overlayString(loaded.Rendering.CookieKey, defaults.Rendering.CookieKey) dest.Rendering.CountryList.Prioritize = overlayString(loaded.Rendering.CountryList.Prioritize, defaults.Rendering.CountryList.Prioritize) + dest.Rendering.VeniceCompatibleImageURLs = overlayOptionFlag(loaded.Rendering.VeniceCompatibleImageURLs, defaults.Rendering.VeniceCompatibleImageURLs) dest.Posting.ExternalDictionary = overlayString(loaded.Posting.ExternalDictionary, defaults.Posting.ExternalDictionary) dest.Posting.Uploads.MaxSize = overlayString(loaded.Posting.Uploads.MaxSize, defaults.Posting.Uploads.MaxSize) dest.Posting.Uploads.NoCompressTypes = overlayStringArray(loaded.Posting.Uploads.NoCompressTypes, defaults.Posting.Uploads.NoCompressTypes) diff --git a/config/default.yaml b/config/default.yaml index 6a2e346..93fdac4 100644 --- a/config/default.yaml +++ b/config/default.yaml @@ -43,6 +43,7 @@ rendering: cookiekey: ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz countryList: prioritize: US + veniceCompatibleImageURLs: false posting: externalDictionary: "" uploads: diff --git a/docs/MISSINGFUNCS.md b/docs/MISSINGFUNCS.md index 840a00e..81d015c 100644 --- a/docs/MISSINGFUNCS.md +++ b/docs/MISSINGFUNCS.md @@ -72,7 +72,7 @@ _(italicized items can be deferred)_ - Import Messages - ~~Delete Conference~~ - ~~Add to Hotlist/Remove from Hotlist~~ -- Actually implement pictures in posts +- ~~Actually implement pictures in posts~~ - ~~Related to bugs in Export Messages caused by bad data:~~ - ~~Provide a per-conference flag that will set BuggyAttachment behavior~~ - ~~New feature: remove attachment from message (requires Conference.Nuke permission)~~ diff --git a/login.go b/login.go index b863391..99b6eff 100644 --- a/login.go +++ b/login.go @@ -1,6 +1,6 @@ /* * Amsterdam Web Communities System - * Copyright (c) 2025 Erbosoft Metaverse Design Solutions, All Rights Reserved + * 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 @@ -34,6 +34,12 @@ var ENOACCOUNT error = errors.New("you cannot create a new account while logged func LoginForm(ctxt ui.AmContext) (string, any) { // Get target URI. target := ctxt.Parameter("tgt") + if target == "" { + v := ctxt.GetSession("lastKnownGood") + if v != nil { + target = v.(string) + } + } if target == "" { target = "/" } @@ -63,6 +69,12 @@ func Login(ctxt ui.AmContext) (string, any) { if err == nil { dlg.LoadFromForm(ctxt) target := dlg.Field("tgt").Value + if target == "" { + v := ctxt.GetSession("lastKnownGood") + if v != nil { + target = v.(string) + } + } if target == "" { target = "/" } @@ -166,6 +178,12 @@ func Logout(ctxt ui.AmContext) (string, any) { func VerifyEmailForm(ctxt ui.AmContext) (string, any) { // Get target URI. target := ctxt.Parameter("tgt") + if target == "" { + v := ctxt.GetSession("lastKnownGood") + if v != nil { + target = v.(string) + } + } if target == "" { target = "/" } @@ -222,6 +240,12 @@ func VerifyEMail(ctxt ui.AmContext) (string, any) { if err == nil { dlg.LoadFromForm(ctxt) target := dlg.Field("tgt").Value + if target == "" { + v := ctxt.GetSession("lastKnownGood") + if v != nil { + target = v.(string) + } + } if target == "" { target = "/" } @@ -276,6 +300,12 @@ func VerifyEMail(ctxt ui.AmContext) (string, any) { func NewAccountUserAgreement(ctxt ui.AmContext) (string, any) { // Get target URI. target := ctxt.Parameter("tgt") + if target == "" { + v := ctxt.GetSession("lastKnownGood") + if v != nil { + target = v.(string) + } + } if target == "" { target = "/" } @@ -302,6 +332,12 @@ func NewAccountUserAgreement(ctxt ui.AmContext) (string, any) { func NewAccountForm(ctxt ui.AmContext) (string, any) { // Get target URI. target := ctxt.Parameter("tgt") + if target == "" { + v := ctxt.GetSession("lastKnownGood") + if v != nil { + target = v.(string) + } + } if target == "" { target = "/" } @@ -337,6 +373,12 @@ func NewAccount(ctxt ui.AmContext) (string, any) { if err == nil { dlg.LoadFromForm(ctxt) target := dlg.Field("tgt").Value + if target == "" { + v := ctxt.GetSession("lastKnownGood") + if v != nil { + target = v.(string) + } + } if target == "" { target = "/" } diff --git a/main.go b/main.go index 310f32a..af8db2f 100644 --- a/main.go +++ b/main.go @@ -54,6 +54,9 @@ func setupEcho() *echo.Echo { e.RouteNotFound("/*", ui.AmWrap(AmNotFoundHandler), uiset...) e.Match(GetAndPost, "/TODO/*", ui.AmWrap(NotImplPage), uiset...) e.GET("/img/*", ui.AmServeImage) + if config.GlobalConfig.Rendering.VeniceCompatibleImageURLs { + e.GET("/venice/imagedata/:id", ui.AmServeVeniceCompatibleImage) + } e.GET("/static/*", ui.AmStaticFileHandler()) e.GET("/go/:postlink", ui.AmWrap(JumpToShortcut)) diff --git a/ui/dialogs/profile.yaml b/ui/dialogs/profile.yaml index 9f9a0d6..d0e464e 100644 --- a/ui/dialogs/profile.yaml +++ b/ui/dialogs/profile.yaml @@ -23,14 +23,12 @@ fields: - type: "password" name: "pass1" caption: "Password" - required: true size: 32 maxlength: 128 - type: "password" name: "pass2" caption: "Password" subcaption: "(retype)" - required: true size: 32 maxlength: 128 - type: "text" diff --git a/ui/images.go b/ui/images.go index 4a250d0..8223202 100644 --- a/ui/images.go +++ b/ui/images.go @@ -1,6 +1,6 @@ /* * Amsterdam Web Communities System - * Copyright (c) 2025 Erbosoft Metaverse Design Solutions, All Rights Reserved + * 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 @@ -89,6 +89,27 @@ func AmServeImage(c echo.Context) error { return c.String(http.StatusNotFound, err.Error()) } +/* AmServeVeniceCompatibleImage serves an image from the image store under a Venice-compatible URI. + * Parameters: + * c - The Echo context for this request. + * Returns: + * Standard Go error return. + */ +func AmServeVeniceCompatibleImage(c echo.Context) error { + id, err := strconv.Atoi(c.Param("id")) + if err == nil { + var img *database.ImageStore + img, err = database.AmLoadImage(c.Request().Context(), int32(id)) + if err == nil { + return c.Blob(http.StatusOK, img.MimeType, img.Data) + } + } + if err == nil { + err = fmt.Errorf("image not found: %s", c.Request().URL.Path) + } + return c.String(http.StatusNotFound, err.Error()) +} + /* AmProcessUploadedImage takes an image and resizes it to a specified size, returning its data. * Parameters: * fileheader - The multipart file header from the uploaded file. diff --git a/ui/views/posts.jet b/ui/views/posts.jet index 57295ce..dcde4a5 100644 --- a/ui/views/posts.jet +++ b/ui/views/posts.jet @@ -110,6 +110,7 @@ {{ post_overrideLink := "" }} {{ post_attach := nil }} {{ post_bozo := false }} + {{ post_profile_pic := "" }} {{ range i, p := posts }} {{ post_cur = p }} {{ post_userName = post_getUserName(p, .) }} @@ -118,6 +119,9 @@ {{ post_overrideLink = post_getOverrideLink(p, post_topicPermalink) }} {{ post_attach = post_getAttachmentInfo(p, .) }} {{ post_bozo = post_isBozo(p, post_topic, .) }} + {{ if showPics }} + {{ post_profile_pic = post_profileImage(p, .) }} + {{ end }} {{ include "singlepost.jet" }} {{ if advancedControls }}
diff --git a/ui/views/profile.jet b/ui/views/profile.jet index c774892..bfcba82 100644 --- a/ui/views/profile.jet +++ b/ui/views/profile.jet @@ -20,7 +20,8 @@
- {{ username }}'s photo + {{ username }}'s photo
diff --git a/ui/views/singlepost.jet b/ui/views/singlepost.jet index 43352d6..2196bb4 100644 --- a/ui/views/singlepost.jet +++ b/ui/views/singlepost.jet @@ -7,45 +7,56 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. *}
-
-
- {{ post_cur.Num }} of - {{ post_max }} - 🔗<{{ post_confRef }}.{{ post_cur.Num }}> +
+ {{ if !post_bozo && isset(post_profile_pic) && (post_profile_pic != "") }} +
+ Profile image: {{ post_userName }} +
+ {{ end }} +
+ + {{ if post_bozo }} +
+ + (User filtered; remove filter) + +
+ {{ else }} +
+ {{ post_cur.Pseud | raw }} + ( + {{ post_userName }}, + {{ DisplayDateTime(post_cur.Posted, .) }}) + {{ if post_attach.Filename != "" }} + 📎 + {{ end }} + +
+ {{ if post_overrideLine != "" }} +
+ {{ if post_overrideLink != "" }} + {{ post_overrideLine }} + {{ else }} + {{ post_overrideLine }} + {{ end }} +
+ {{ else }} +
{{ post_text | postRewrite | raw }}
+ {{ end }} + {{ end }}
- {{ if post_bozo }} -
- - (User filtered; remove filter) - -
- {{ else }} -
- {{ post_cur.Pseud | raw }} - ( - {{ post_userName }}, - {{ DisplayDateTime(post_cur.Posted, .) }}) - {{ if post_attach.Filename != "" }} - 📎 - {{ end }} - -
- {{ if post_overrideLine != "" }} -
- {{ if post_overrideLink != "" }} - {{ post_overrideLine }} - {{ else }} - {{ post_overrideLine }} - {{ end }} -
- {{ else }} -
{{ post_text | postRewrite | raw }}
- {{ end }} - {{ end }}
diff --git a/userdata.go b/userdata.go index 696a68a..b97823c 100644 --- a/userdata.go +++ b/userdata.go @@ -1,6 +1,6 @@ /* * Amsterdam Web Communities System - * Copyright (c) 2025 Erbosoft Metaverse Design Solutions, All Rights Reserved + * 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 @@ -45,6 +45,12 @@ func userPhotoURL(ci *database.ContactInfo) string { func EditProfileForm(ctxt ui.AmContext) (string, any) { // Get target URI. target := ctxt.Parameter("tgt") + if target == "" { + v := ctxt.GetSession("lastKnownGood") + if v != nil { + target = v.(string) + } + } if target == "" { target = "/" } @@ -125,6 +131,12 @@ func EditProfile(ctxt ui.AmContext) (string, any) { if err == nil { dlg.LoadFromForm(ctxt) target := dlg.Field("tgt").Value + if target == "" { + v := ctxt.GetSession("lastKnownGood") + if v != nil { + target = v.(string) + } + } if target == "" { target = "/" } @@ -228,6 +240,12 @@ func EditProfile(ctxt ui.AmContext) (string, any) { func ProfilePhotoForm(ctxt ui.AmContext) (string, any) { // Get target URI. target := ctxt.Parameter("tgt") + if target == "" { + v := ctxt.GetSession("lastKnownGood") + if v != nil { + target = v.(string) + } + } if target == "" { target = "/" } @@ -264,6 +282,12 @@ func ProfilePhoto(ctxt ui.AmContext) (string, any) { return "error", err } target := ctxt.FormField("tgt") + if target == "" { + v := ctxt.GetSession("lastKnownGood") + if v != nil { + target = v.(string) + } + } if target == "" { target = "/" }