Pictures in Posts now works!

This commit is contained in:
2026-02-23 22:41:59 -07:00
parent c4cecbc8a9
commit 110e917921
12 changed files with 193 additions and 46 deletions
+29
View File
@@ -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)
+13
View File
@@ -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)
+1
View File
@@ -43,6 +43,7 @@ rendering:
cookiekey: ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz
countryList:
prioritize: US
veniceCompatibleImageURLs: false
posting:
externalDictionary: ""
uploads:
+1 -1
View File
@@ -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)~~
+43 -1
View File
@@ -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 = "/"
}
+3
View File
@@ -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))
-2
View File
@@ -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"
+22 -1
View File
@@ -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.
+4
View File
@@ -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 }}
<div class="flex flex-col gap-2">
+2 -1
View File
@@ -20,7 +20,8 @@
<!-- Left Column: Photo and Metadata -->
<div class="flex-shrink-0 w-40">
<div class="border-2 border-gray-300 rounded mb-4">
<img src="{{ photoURL }}" alt="{{ username }}'s photo" class="w-full h-auto">
<img src="{{ photoURL }}" alt="{{ username }}'s photo" class="w-full h-auto rounded-3xl border-2 border-gray-300"
onerror="this.src='/img/builtin/no-user.png'">
</div>
<div class="text-xs text-gray-700 space-y-2">
<div>
+50 -39
View File
@@ -7,45 +7,56 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*}
<div class="flex-1 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="{{ 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"
title="Permalink to this post">🔗&lt;{{ post_confRef }}.{{ post_cur.Num }}&gt;</a>
<div class="flex gap-2">
{{ if !post_bozo && isset(post_profile_pic) && (post_profile_pic != "") }}
<div class="flex-shrink-0">
<img src="{{ post_profile_pic }}" alt="Profile image: {{ post_userName }}"
class="w-16 h-16 rounded-lg border-2 border-gray-300"
onerror="this.src='/img/builtin/no-user.png'">
</div>
{{ end }}
<div class="flex-1">
<div class="flex justify-between items-start mb-3">
<div class="text-sm text-gray-600">
<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"
title="Permalink to this post">🔗&lt;{{ post_confRef }}.{{ post_cur.Num }}&gt;</a>
</div>
</div>
{{ if post_bozo }}
<div class="mt-4 mb-2">
<span class="italic font-bold">
(User filtered; <a class="text-blue-700 hover:text-blue-900" href="{{ post_stem }}?r={{ post_cur.Num }}&ac=1&bozo=0">remove filter</a>)
</span>
</div>
{{ else }}
<div class="mb-2">
<strong class="text-lg">{{ post_cur.Pseud | raw }}</strong>
<span class="text-gray-600 text-sm ml-2">(<em>
<a href="/user/{{ post_userName }}" target="_blank" class="text-blue-700 hover:text-blue-900">{{ post_userName }}</a>,
{{ DisplayDateTime(post_cur.Posted, .) }}</em>)
{{ if post_attach.Filename != "" }}
<a href="/attachment/{{ post_cur.PostId }}" title="(Attachment {{ post_attach.Filename }} - {{ post_attach.Length }} bytes)"
{{ if hasPrefix(post_attach.MIMEType, "text/") || hasPrefix(post_attach.MIMEType, "image/" )}}
target="_blank"
{{ end }}
class="text-lg">📎</a>
{{ end }}
</span>
</div>
{{ 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 }}
{{ end }}
</div>
</div>
{{ if post_bozo }}
<div class="mt-4 mb-2">
<span class="italic font-bold">
(User filtered; <a class="text-blue-700 hover:text-blue-900" href="{{ post_stem }}?r={{ post_cur.Num }}&ac=1&bozo=0">remove filter</a>)
</span>
</div>
{{ else }}
<div class="mb-2">
<strong class="text-lg">{{ post_cur.Pseud | raw }}</strong>
<span class="text-gray-600 text-sm ml-2">(<em>
<a href="/user/{{ post_userName }}" target="_blank" class="text-blue-700 hover:text-blue-900">{{ post_userName }}</a>,
{{ DisplayDateTime(post_cur.Posted, .) }}</em>)
{{ if post_attach.Filename != "" }}
<a href="/attachment/{{ post_cur.PostId }}" title="(Attachment {{ post_attach.Filename }} - {{ post_attach.Length }} bytes)"
{{ if hasPrefix(post_attach.MIMEType, "text/") || hasPrefix(post_attach.MIMEType, "image/" )}}
target="_blank"
{{ end }}
class="text-lg">📎</a>
{{ end }}
</span>
</div>
{{ 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 }}
{{ end }}
</div>
+25 -1
View File
@@ -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 = "/"
}