1 Commits

Author SHA1 Message Date
orangix
b78ed44730 allow external origin to access media files 2026-01-25 07:23:08 +01:00
19 changed files with 180 additions and 279 deletions

View File

@@ -5,6 +5,7 @@ import (
"time"
"codeberg.org/rimgo/rimgo/utils"
"github.com/microcosm-cc/bluemonday"
"github.com/tidwall/gjson"
)
@@ -100,13 +101,17 @@ func parseAlbum(data gjson.Result) (Album, error) {
url := value.Get("url").String()
url = strings.ReplaceAll(url, "https://i.imgur.com", "")
description := value.Get("metadata.description").String()
description = strings.ReplaceAll(description, "\n", "<br>")
description = bluemonday.UGCPolicy().Sanitize(description)
media = append(media, Media{
Id: value.Get("id").String(),
Name: value.Get("name").String(),
MimeType: value.Get("mime_type").String(),
Type: value.Get("type").String(),
Title: value.Get("metadata.title").String(),
Description: value.Get("metadata.description").String(),
Description: description,
Url: url,
})

View File

@@ -1,114 +1,145 @@
package api
import (
"encoding/json"
"errors"
"fmt"
"regexp"
"strings"
"sync"
"time"
"codeberg.org/rimgo/rimgo/utils"
"github.com/dustin/go-humanize"
"github.com/microcosm-cc/bluemonday"
"github.com/patrickmn/go-cache"
"github.com/tidwall/gjson"
"gitlab.com/golang-commonmark/linkify"
)
type Comment struct {
ID int `json:"id"`
ParentID int `json:"parent_id"`
Comment string `json:"comment"`
PostID string `json:"post_id"`
UpvoteCount int `json:"upvote_count"`
DownvoteCount int `json:"downvote_count"`
PointCount int `json:"point_count"`
Vote struct{} `json:"vote"` // ???
PlatformID int `json:"platform_id"`
Platform string `json:"platform"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt time.Time `json:"deleted_at"`
Next struct{} `json:"next"` // ???
Comments commentArray `json:"comments"`
AccountID int `json:"account_id"`
Account _ApiUser `json:"account"`
Post commentPost `json:"post"`
}
type _ApiUser struct {
ID int `json:"id"`
Username string `json:"username"`
Avatar string `json:"avatar"`
Comments []Comment
User User
Post Submission
Id string
Comment string
Upvotes int64
Downvotes int64
Platform string
CreatedAt string
RelTime string
UpdatedAt string
DeletedAt string
}
func (client *Client) FetchComments(galleryID string) ([]Comment, error) {
cacheData, found := client.Cache.Get(galleryID + "-comments")
if found {
return cacheData.(commentArray), nil
return cacheData.([]Comment), nil
}
data, err := utils.GetJSONNew("https://api.imgur.com/comment/v1/comments?client_id=" + client.ClientID + "&filter[post]=eq:" + galleryID + "&include=account,adconfig&per_page=30&sort=best")
data, err := utils.GetJSON("https://api.imgur.com/comment/v1/comments?client_id=" + client.ClientID + "&filter[post]=eq:" + galleryID + "&include=account,adconfig&per_page=30&sort=best")
if err != nil {
return []Comment{}, nil
}
var parsed commentApiResponse
err = json.Unmarshal(data, &parsed)
if err != nil {
return []Comment{}, err
}
client.Cache.Set(galleryID+"-comments", parsed.Data, cache.DefaultExpiration)
return parsed.Data, nil
}
func parseComment(data json.RawMessage, out *Comment) {
err := json.Unmarshal(data, &out)
if err != nil {
panic(err)
}
}
type commentArray []Comment
func (arr *commentArray) UnmarshalJSON(data []byte) error {
var rawArr []json.RawMessage
err := json.Unmarshal(data, &rawArr)
if err != nil {
return err
}
*arr = make(commentArray, len(rawArr))
panics := make([]error, 0, len(rawArr))
wg := sync.WaitGroup{}
var handlePanic = func() {
if v := recover(); v != nil {
v, ok := v.(error)
if !ok {
v = fmt.Errorf("%v", v)
}
panics = append(panics, v)
}
}
for i, value := range rawArr {
comments := make([]Comment, 0)
data.Get("data").ForEach(
func(key, value gjson.Result) bool {
wg.Add(1)
go func() {
defer handlePanic()
defer wg.Done()
parseComment(value, &(*arr)[i])
comments = append(comments, parseComment(value))
}()
}
return true
},
)
wg.Wait()
if len(panics) != 0 {
return errors.Join(panics...)
client.Cache.Set(galleryID+"-comments", comments, cache.DefaultExpiration)
return comments, nil
}
var imgurRe = regexp.MustCompile(`https?://imgur\.com/(gallery|a)?/(.*)`)
var imgurRe2 = regexp.MustCompile(`https?://imgur\.com/(.*)`)
var imgRe = regexp.MustCompile(`https?://i\.imgur\.com/(.*)\.(png|gif|jpe?g|webp)`)
var vidRe = regexp.MustCompile(`https?://i\.imgur\.com/(.*)\.(mp4|webm)`)
var vidFormatRe = regexp.MustCompile(`\.(mp4|webm)`)
var iImgurRe = regexp.MustCompile(`https?://i\.imgur\.com`)
func parseComment(data gjson.Result) Comment {
createdTime, _ := time.Parse("2006-01-02T15:04:05Z", data.Get("created_at").String())
createdAt := createdTime.Format("January 2, 2006 3:04 PM")
updatedAt, _ := utils.FormatDate(data.Get("updated_at").String())
deletedAt, _ := utils.FormatDate(data.Get("deleted_at").String())
userAvatar := strings.ReplaceAll(data.Get("account.avatar").String(), "https://i.imgur.com", "")
wg := sync.WaitGroup{}
comments := make([]Comment, 0)
data.Get("comments").ForEach(
func(key, value gjson.Result) bool {
wg.Add(1)
go func() {
defer wg.Done()
comments = append(comments, parseComment(value))
}()
return true
},
)
wg.Wait()
comment := data.Get("comment").String()
comment = strings.ReplaceAll(comment, "\n", "<br>")
for _, match := range imgRe.FindAllString(comment, -1) {
img := iImgurRe.ReplaceAllString(match, "")
img = `<img src="` + img + `" class="comment__media" loading="lazy"/>`
comment = strings.Replace(comment, match, img, 1)
}
return nil
}
for _, match := range vidRe.FindAllString(comment, -1) {
vid := iImgurRe.ReplaceAllString(match, "")
vid = `<video class="comment__media" controls loop preload="none" poster="` + vidFormatRe.ReplaceAllString(vid, ".webp") + `"><source type="` + strings.Split(vid, ".")[1] + `" src="` + vid + `" /></video>`
comment = strings.Replace(comment, match, vid, 1)
}
for _, l := range linkify.Links(comment) {
origLink := comment[l.Start:l.End]
link := `<a href="` + origLink + `">` + origLink + `</a>`
comment = strings.Replace(comment, origLink, link, 1)
}
comment = imgurRe.ReplaceAllString(comment, "/$1/$2")
comment = imgurRe2.ReplaceAllString(comment, "/$1")
type commentPost Submission
p := bluemonday.UGCPolicy()
p.AllowImages()
p.AllowElements("video", "source")
p.AllowAttrs("src", "tvpe").OnElements("source")
p.AllowAttrs("controls", "loop", "preload", "poster").OnElements("video")
p.AllowAttrs("class", "loading").OnElements("img", "video")
p.RequireNoReferrerOnLinks(true)
p.RequireNoFollowOnLinks(true)
p.RequireCrossOriginAnonymous(true)
comment = p.Sanitize(comment)
func (post *commentPost) UnmarshalJSON(data []byte) error {
*post = commentPost(parseSubmission(gjson.Parse(string(data))))
return nil
}
type commentApiResponse struct {
Data commentArray `json:"data"`
return Comment{
Comments: comments,
User: User{
Id: data.Get("account.id").Int(),
Username: data.Get("account.username").String(),
Avatar: userAvatar,
},
Post: parseSubmission(data.Get("post")),
Id: data.Get("id").String(),
Comment: comment,
Upvotes: data.Get("upvote_count").Int(),
Downvotes: data.Get("downvote_count").Int(),
Platform: data.Get("platform").String(),
CreatedAt: createdAt,
RelTime: humanize.Time(createdTime),
UpdatedAt: updatedAt,
DeletedAt: deletedAt,
}
}

View File

@@ -1,7 +1,6 @@
package api
import (
"encoding/json"
"io"
"net/http"
"strings"
@@ -155,7 +154,7 @@ func (client *Client) FetchUserFavorites(username string, sort string, page stri
func (client *Client) FetchUserComments(username string) ([]Comment, error) {
cacheData, found := client.Cache.Get(username + "-usercomments")
if found {
return cacheData.(commentArray), nil
return cacheData.([]Comment), nil
}
req, err := http.NewRequest("GET", "https://api.imgur.com/comment/v1/comments", nil)
@@ -177,19 +176,23 @@ func (client *Client) FetchUserComments(username string) ([]Comment, error) {
return []Comment{}, err
}
data, err := io.ReadAll(res.Body)
body, err := io.ReadAll(res.Body)
if err != nil {
return []Comment{}, err
}
var parsed commentApiResponse
err = json.Unmarshal(data, &parsed)
if err != nil {
return []Comment{}, err
}
data := gjson.Parse(string(body))
client.Cache.Set(username+"-usercomments", parsed.Data, cache.DefaultExpiration)
return parsed.Data, nil
comments := make([]Comment, 0)
data.Get("data").ForEach(
func(key, value gjson.Result) bool {
comments = append(comments, parseComment(value))
return true
},
)
client.Cache.Set(username+"-usercomments", comments, cache.DefaultExpiration)
return comments, nil
}
func parseSubmission(value gjson.Result) Submission {

View File

@@ -23,13 +23,13 @@ func wrapHandler(h handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if v := recover(); v != nil {
pages.RenderError(w, r, 500, fmt.Sprint(v))
utils.RenderError(w, r, 500, fmt.Sprint(v))
}
}()
err := h(w, r)
if err != nil {
fmt.Println(err)
pages.RenderError(w, r, 500, err.Error())
utils.RenderError(w, r, 500, err.Error())
}
})
}
@@ -61,14 +61,14 @@ func main() {
if os.Getenv("ENV") == "dev" {
app.Handle("GET /errors/429", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
pages.RenderError(w, r, 429)
utils.RenderError(w, r, 429)
}))
app.Handle("GET /errors/429/img", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Location", "/static/img/error-429.png")
w.WriteHeader(302)
}))
app.Handle("GET /errors/404", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
pages.RenderError(w, r, 404)
utils.RenderError(w, r, 404)
}))
app.Handle("GET /errors/404/img", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Location", "/static/img/error-404.png")

View File

@@ -24,10 +24,10 @@ func HandleEmbed(w http.ResponseWriter, r *http.Request) error {
post, err = ApiClient.FetchMedia(r.PathValue("postID"))
}
if err != nil && err.Error() == "ratelimited by imgur" {
return RenderError(w, r, 429)
return utils.RenderError(w, r, 429)
}
if err != nil && post.Id == "" && strings.Contains(err.Error(), "404") {
return RenderError(w, r, 404)
return utils.RenderError(w, r, 404)
}
if err != nil {
return err

View File

@@ -35,6 +35,9 @@ func HandleUserAvatar(w http.ResponseWriter, r *http.Request) error {
func handleMedia(w http.ResponseWriter, r *http.Request, url string) error {
utils.SetHeaders(w)
if !utils.Config.RestrictiveCORS {
w.Header().Set("Access-Control-Allow-Origin", "*")
}
path := r.URL.Path
if utils.Config.ForceWebp &&
@@ -73,9 +76,9 @@ func handleMedia(w http.ResponseWriter, r *http.Request, url string) error {
}
if res.StatusCode == 404 || strings.Contains(res.Request.URL.String(), "error/404") {
return RenderError(w, r, 404)
return utils.RenderError(w, r, 404)
} else if res.StatusCode == 429 {
return RenderError(w, r, 429)
return utils.RenderError(w, r, 429)
}
w.Header().Set("Accept-Ranges", "bytes")

View File

@@ -55,10 +55,10 @@ func HandlePost(w http.ResponseWriter, r *http.Request) error {
post, err = ApiClient.FetchMedia(postId)
}
if err != nil && err.Error() == "ratelimited by imgur" {
return RenderError(w, r, 429)
return utils.RenderError(w, r, 429)
}
if err != nil && post.Id == "" && strings.Contains(err.Error(), "404") {
return RenderError(w, r, 404)
return utils.RenderError(w, r, 404)
}
if err != nil {
return err

View File

@@ -78,7 +78,7 @@ func HandleUserRSS(w http.ResponseWriter, r *http.Request) error {
submissions, err := ApiClient.FetchSubmissions(user, "newest", "1")
if err != nil && err.Error() == "ratelimited by imgur" {
return RenderError(w, r, 429)
return utils.RenderError(w, r, 429)
}
if err != nil {
return err

View File

@@ -26,13 +26,13 @@ func HandleTag(w http.ResponseWriter, r *http.Request) error {
tag, err := ApiClient.FetchTag(r.PathValue("tag"), r.URL.Query().Get("sort"), page)
if err != nil && err.Error() == "ratelimited by imgur" {
return RenderError(w, r, 429)
return utils.RenderError(w, r, 429)
}
if err != nil {
return err
}
if tag.Display == "" {
return RenderError(w, r, 404)
return utils.RenderError(w, r, 404)
}
return render.Render(w, "tag", map[string]any{

View File

@@ -26,18 +26,18 @@ func HandleUser(w http.ResponseWriter, r *http.Request) error {
user, err := ApiClient.FetchUser(r.PathValue("userID"))
if err != nil && err.Error() == "ratelimited by imgur" {
return RenderError(w, r, 429)
return utils.RenderError(w, r, 429)
}
if err != nil {
return err
}
if user.Username == "" {
return RenderError(w, r, 404)
return utils.RenderError(w, r, 404)
}
submissions, err := ApiClient.FetchSubmissions(r.PathValue("userID"), "newest", page)
if err != nil && err.Error() == "ratelimited by imgur" {
return RenderError(w, r, 429)
return utils.RenderError(w, r, 429)
}
if err != nil {
return err
@@ -60,18 +60,18 @@ func HandleUserComments(w http.ResponseWriter, r *http.Request) error {
user, err := ApiClient.FetchUser(r.PathValue("userID"))
if err != nil && err.Error() == "ratelimited by imgur" {
return RenderError(w, r, 429)
return utils.RenderError(w, r, 429)
}
if err != nil {
return err
}
if user.Username == "" {
return RenderError(w, r, 404)
return utils.RenderError(w, r, 404)
}
comments, err := ApiClient.FetchUserComments(r.PathValue("userID"))
if err != nil && err.Error() == "ratelimited by imgur" {
return RenderError(w, r, 429)
return utils.RenderError(w, r, 429)
}
if err != nil {
return err
@@ -101,18 +101,18 @@ func HandleUserFavorites(w http.ResponseWriter, r *http.Request) error {
user, err := ApiClient.FetchUser(r.PathValue("userID"))
if err != nil && err.Error() == "ratelimited by imgur" {
return RenderError(w, r, 429)
return utils.RenderError(w, r, 429)
}
if err != nil {
return err
}
if user.Username == "" {
return RenderError(w, r, 404)
return utils.RenderError(w, r, 404)
}
favorites, err := ApiClient.FetchUserFavorites(r.PathValue("userID"), "newest", page)
if err != nil && err.Error() == "ratelimited by imgur" {
return RenderError(w, r, 429)
return utils.RenderError(w, r, 429)
}
if err != nil {
return err

View File

@@ -1,21 +1,10 @@
package render
import (
"time"
"codeberg.org/rimgo/rimgo/utils"
"github.com/dustin/go-humanize"
"github.com/mailgun/raymond/v2"
)
import "github.com/mailgun/raymond/v2"
func (r *renderer) registerHelpers() {
funcmap := map[string]any{
"noteq": noteq,
"ifNonZeroTime": ifNonZeroTime,
"relTime": relTime,
"rewriteUrl": rewriteUrl,
"sanitizeDescription": sanitizeDescription,
"sanitizeComment": sanitizeComment,
}
raymond.RegisterHelpers(funcmap)
}
@@ -26,19 +15,3 @@ func noteq(a, b any, options *raymond.Options) any {
}
return ""
}
func ifNonZeroTime(v any, options *raymond.Options) any {
if v.(time.Time).IsZero() {
return ""
}
return options.Fn()
}
func relTime(date time.Time) string {
return humanize.Time(date)
}
func rewriteUrl(link string) string {
r, err := utils.RewriteUrl(link)
if err != nil {
panic(err)
}
return r
}

View File

@@ -1,53 +0,0 @@
package render
import (
"regexp"
"strings"
"github.com/microcosm-cc/bluemonday"
"gitlab.com/golang-commonmark/linkify"
)
var imgurRe = regexp.MustCompile(`https?://imgur\.com/(gallery|a)?/(.*)`)
var imgurRe2 = regexp.MustCompile(`https?://imgur\.com/(.*)`)
var imgRe = regexp.MustCompile(`https?://i\.imgur\.com/(.*)\.(png|gif|jpe?g|webp)`)
var vidRe = regexp.MustCompile(`https?://i\.imgur\.com/(.*)\.(mp4|webm)`)
var vidFormatRe = regexp.MustCompile(`\.(mp4|webm)`)
var iImgurRe = regexp.MustCompile(`https?://i\.imgur\.com`)
func sanitizeDescription(src string) string {
src = strings.ReplaceAll(src, "\n", "<br>")
return bluemonday.UGCPolicy().Sanitize(src)
}
func sanitizeComment(src string) string {
src = strings.ReplaceAll(src, "\n", "<br>")
for _, match := range imgRe.FindAllString(src, -1) {
img := iImgurRe.ReplaceAllString(match, "")
img = `<img src="` + img + `" class="comment__media" loading="lazy"/>`
src = strings.Replace(src, match, img, 1)
}
for _, match := range vidRe.FindAllString(src, -1) {
vid := iImgurRe.ReplaceAllString(match, "")
vid = `<video class="comment__media" controls loop preload="none" poster="` + vidFormatRe.ReplaceAllString(vid, ".webp") + `"><source type="` + strings.Split(vid, ".")[1] + `" src="` + vid + `" /></video>`
src = strings.Replace(src, match, vid, 1)
}
for _, l := range linkify.Links(src) {
origLink := (src)[l.Start:l.End]
link := `<a href="` + origLink + `">` + origLink + `</a>`
src = strings.Replace(src, origLink, link, 1)
}
src = imgurRe.ReplaceAllString(src, "/$1/$2")
src = imgurRe2.ReplaceAllString(src, "/$1")
p := bluemonday.UGCPolicy()
p.AllowImages()
p.AllowElements("video", "source")
p.AllowAttrs("src", "tvpe").OnElements("source")
p.AllowAttrs("controls", "loop", "preload", "poster").OnElements("video")
p.AllowAttrs("class", "loading").OnElements("img", "video")
p.RequireNoReferrerOnLinks(true)
p.RequireNoFollowOnLinks(true)
p.RequireCrossOriginAnonymous(true)
return p.Sanitize(src)
}

View File

@@ -12,6 +12,7 @@ type config struct {
ProtocolDetection bool
Secure bool
ForceWebp bool
RestrictiveCORS bool
ImageCache bool
CleanupInterval time.Duration
CacheDir string
@@ -39,6 +40,7 @@ func LoadConfig() {
ProtocolDetection: envBool("PROTOCOL_DETECTION"),
Secure: envBool("SECURE"),
ForceWebp: envBool("FORCE_WEBP"),
RestrictiveCORS: envBool("RESTRICTIVE_CORS"),
Privacy: map[string]interface{}{
"set": os.Getenv("PRIVACY_NOT_COLLECTED") != "",
"policy": os.Getenv("PRIVACY_POLICY"),

View File

@@ -1,4 +1,4 @@
package pages
package utils
import (
"fmt"
@@ -8,7 +8,6 @@ import (
"codeberg.org/rimgo/rimgo/render"
"codeberg.org/rimgo/rimgo/static"
"codeberg.org/rimgo/rimgo/utils"
)
func RenderError(w http.ResponseWriter, r *http.Request, code int, str ...string) (err error) {
@@ -22,7 +21,7 @@ func RenderError(w http.ResponseWriter, r *http.Request, code int, str ...string
if code != 500 {
codeStr = strconv.Itoa(code)
}
if !utils.Accepts(r, "text/html") && r.PathValue("extension") != "" {
if !Accepts(r, "text/html") && r.PathValue("extension") != "" {
w.Header().Set("Content-Type", "image/png")
w.WriteHeader(code)
file, _ := static.GetFiles().Open("img/error-" + codeStr + ".png")

View File

@@ -1,7 +1,6 @@
package utils
import (
"encoding/json"
"fmt"
"io"
"net/http"
@@ -10,42 +9,6 @@ import (
"github.com/tidwall/gjson"
)
func GetJSONNew(url string) (json.RawMessage, error) {
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return json.RawMessage{}, err
}
SetReqHeaders(req)
client := http.Client{}
res, err := client.Do(req)
if err != nil {
return json.RawMessage{}, err
}
rateLimitRemaining := res.Header.Get("X-RateLimit-UserRemaining")
if rateLimitRemaining != "" {
ratelimit, _ := strconv.Atoi(rateLimitRemaining)
if ratelimit <= 0 {
return json.RawMessage{}, fmt.Errorf("ratelimited by imgur")
}
}
body, err := io.ReadAll(res.Body)
if err != nil {
return json.RawMessage{}, err
}
switch res.StatusCode {
case 200:
return body, nil
case 429:
return json.RawMessage{}, fmt.Errorf("ratelimited by imgur")
default:
return json.RawMessage{}, fmt.Errorf("received status %s, expected 200 OK.\n%s", res.Status, string(body))
}
}
func GetJSON(url string) (gjson.Result, error) {
req, err := http.NewRequest("GET", url, nil)
if err != nil {

View File

@@ -1,25 +0,0 @@
package utils
import (
"fmt"
"net/url"
"strings"
)
func RewriteUrl(link string) (string, error) {
url, err := url.Parse(link)
if err != nil {
return "", err
}
path := url.Path
if !strings.HasPrefix(path, "/") {
path = "/" + path
}
switch url.Host {
case "", "imgur.com", "www.imgur.com", "i.imgur.com":
return path, nil
case "i.stack.imgur.com":
return "/stack" + path, nil
}
return "", fmt.Errorf("unknown host %s", url.Host)
}

View File

@@ -1,22 +1,22 @@
<div class="flex flex-col gap-2">
<div class="flex gap-2 items-center">
{{#noteq this.Account.Username "[deleted]"}}
<img src="{{rewriteUrl(this.Account.Avatar)}}" class="rounded-full" width="24" height="24" loading="lazy">
<a href="/user/{{this.Account.Username}}">
<p class="whitespace-nowrap text-ellipsis overflow-hidden"><b>{{this.Account.Username}}</b></p>
{{#noteq this.User.Username "[deleted]"}}
<img src="{{this.User.Avatar}}" class="rounded-full" width="24" height="24" loading="lazy">
<a href="/user/{{this.User.Username}}">
<p class="whitespace-nowrap text-ellipsis overflow-hidden"><b>{{this.User.Username}}</b></p>
</a>
{{/noteq}}
{{#equal this.Account.Username "[deleted]"}}
{{#equal this.User.Username "[deleted]"}}
<p class="whitespace-nowrap text-ellipsis overflow-hidden"><b>[deleted]</b></p>
{{/equal}}
</div>
<div>
<p>{{{sanitizeComment(this.Comment)}}}</p>
<p>{{{this.Comment}}}</p>
<div class="flex gap-2">
<span title="{{this.CreatedAt}}">{{relTime(this.CreatedAt)}}</span>
{{#ifNonZeroTime this.DeletedAt}}
<span title="{{this.CreatedAt}}">{{this.RelTime}}</span>
{{#if this.DeletedAt}}
<span class="text-md">(deleted {{this.DeletedAt}})</span>
{{/ifNonZeroTime}}
{{/if}}
|
<img class="invert icon" src="/static/icons/PhArrowFatUp.svg" alt="Likes" width="24px" height="24px"> {{this.Upvotes}}
<img class="invert icon" src="/static/icons/PhArrowFatDown.svg" alt="Dislikes" width="24px" height="24px"> {{this.Downvotes}}

View File

@@ -3,13 +3,13 @@
<img class="object-cover block w-full h-[300px] sm:w-[120px] sm:h-[140px] rounded-lg rounded-b-none sm:rounded-b-lg" src="{{this.Post.Cover.Url}}" alt="">
<div class="flex flex-col gap-2 bg-slate-600 p-4 rounded-lg rounded-t-none sm:rounded-t-lg w-full">
<div class="flex flex-col h-full">
<p class="md-container">{{{sanitizeComment(this.Comment)}}}</p>
<p class="md-container">{{{this.Comment}}}</p>
<div class="grow"></div>
<div class="flex gap-2">
<span title="{{this.CreatedAt}}">{{relTime(this.CreatedAt)}}</span>
{{#ifNonZeroTime this.DeletedAt}}
<span title="{{this.CreatedAt}}">{{this.RelTime}}</span>
{{#if this.DeletedAt}}
<span class="text-md">(deleted {{this.DeletedAt}})</span>
{{/ifNonZeroTime}}
{{/if}}
|
<img class="invert icon" src="/static/icons/PhArrowFatUp.svg" alt="Likes" width="24px" height="24px">
{{this.Upvotes}}

View File

@@ -75,7 +75,7 @@
{{/equal}}
{{#if this.Description}}
<p>{{{sanitizeDescription(this.Description)}}}</p>
<p>{{{this.Description}}}</p>
{{/if}}
{{/each}}
</div>