From e42ba392c6f53e21ee56f32cd4abc5ee67ef4dc1 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Mon, 7 Jun 2021 15:52:24 -0400 Subject: [PATCH] Support Web Monetized split content Ref T770 --- account.go | 17 +++ collections.go | 11 ++ database.go | 16 ++- feed.go | 4 + handle.go | 52 +++++++++ less/core.less | 21 ++++ less/post-temp.less | 19 +++ monetization.go | 160 ++++++++++++++++++++++++++ postrender.go | 46 +++++++- posts.go | 44 ++++--- routes.go | 2 + static/img/paidarticle.svg | 78 +++++++++++++ static/js/webmonetization.js | 94 +++++++++++++++ templates/chorus-collection-post.tmpl | 9 ++ templates/collection-post.tmpl | 9 ++ templates/include/post-render.tmpl | 2 +- templates/include/posts.tmpl | 15 ++- users.go | 3 +- 18 files changed, 578 insertions(+), 24 deletions(-) create mode 100644 monetization.go create mode 100644 static/img/paidarticle.svg create mode 100644 static/js/webmonetization.js diff --git a/account.go b/account.go index 65d39c7..72d12ee 100644 --- a/account.go +++ b/account.go @@ -199,6 +199,23 @@ func signupWithRegistration(app *App, signup userRegistration, w http.ResponseWr }, } + var coll *Collection + if signup.Monetization != "" { + if coll == nil { + coll, err = app.db.GetCollection(signup.Alias) + if err != nil { + log.Error("Unable to get new collection '%s' for monetization on signup: %v", signup.Alias, err) + return nil, err + } + } + err = app.db.SetCollectionAttribute(coll.ID, "monetization_pointer", signup.Monetization) + if err != nil { + log.Error("Unable to add monetization on signup: %v", err) + return nil, err + } + coll.Monetization = signup.Monetization + } + var token string if reqJSON && !signup.Web { token, err = app.db.GetAccessToken(u.ID) diff --git a/collections.go b/collections.go index 30795d5..52fa089 100644 --- a/collections.go +++ b/collections.go @@ -353,6 +353,17 @@ func (c *Collection) RenderMathJax() bool { return c.db.CollectionHasAttribute(c.ID, "render_mathjax") } +func (c *Collection) MonetizationURL() string { + if c.Monetization == "" { + return "" + } + return strings.Replace(c.Monetization, "$", "https://", 1) +} + +func (c CollectionPage) DisplayMonetization() string { + return displayMonetization(c.Monetization, c.Alias) +} + func newCollection(app *App, w http.ResponseWriter, r *http.Request) error { reqJSON := IsJSON(r) alias := r.FormValue("alias") diff --git a/database.go b/database.go index 7042b63..fefc3c1 100644 --- a/database.go +++ b/database.go @@ -813,6 +813,7 @@ func (db *datastore) GetCollectionBy(condition string, value interface{}) (*Coll c.Signature = signature.String c.Format = format.String c.Public = c.IsPublic() + c.Monetization = db.GetCollectionAttribute(c.ID, "monetization_pointer") c.db = db @@ -1182,7 +1183,7 @@ func (db *datastore) GetPosts(cfg *config.Config, c *Collection, page int, inclu } p.extractData() p.augmentContent(c) - p.formatContent(cfg, c, includeFuture) + p.formatContent(cfg, c, includeFuture, false) posts = append(posts, p.processPost()) } @@ -1247,7 +1248,7 @@ func (db *datastore) GetPostsTagged(cfg *config.Config, c *Collection, tag strin } p.extractData() p.augmentContent(c) - p.formatContent(cfg, c, includeFuture) + p.formatContent(cfg, c, includeFuture, false) posts = append(posts, p.processPost()) } @@ -1652,6 +1653,14 @@ func (db *datastore) GetCollections(u *User, hostName string) (*[]Collection, er c.URL = c.CanonicalURL() c.Public = c.IsPublic() + /* + // NOTE: future functionality + if visibility != nil { // TODO: && visibility == CollPublic { + // Add Monetization info when retrieving all public collections + c.Monetization = db.GetCollectionAttribute(c.ID, "monetization_pointer") + } + */ + colls = append(colls, c) } err = rows.Err() @@ -1698,6 +1707,9 @@ func (db *datastore) GetPublicCollections(hostName string) (*[]Collection, error c.URL = c.CanonicalURL() c.Public = c.IsPublic() + // Add Monetization information + c.Monetization = db.GetCollectionAttribute(c.ID, "monetization_pointer") + colls = append(colls, c) } err = rows.Err() diff --git a/feed.go b/feed.go index 3062e26..32c6591 100644 --- a/feed.go +++ b/feed.go @@ -97,6 +97,10 @@ func ViewFeed(app *App, w http.ResponseWriter, req *http.Request) error { var title, permalink string for _, p := range *coll.Posts { + // Add necessary path back to the web browser for Web Monetization if needed + p.Collection = coll.CollectionObj // augmentReadingDestination requires a populated Collection field + p.augmentReadingDestination() + // Create the item for the feed title = p.PlainDisplayTitle() permalink = fmt.Sprintf("%s%s", baseUrl, p.Slug.String) feed.Items = append(feed.Items, &Item{ diff --git a/handle.go b/handle.go index 4c454ec..1cbf114 100644 --- a/handle.go +++ b/handle.go @@ -574,6 +574,38 @@ func (h *Handler) All(f handlerFunc) http.HandlerFunc { } } +func (h *Handler) PlainTextAPI(f handlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + h.handleTextError(w, r, func() error { + // TODO: return correct "success" status + status := 200 + start := time.Now() + + defer func() { + if e := recover(); e != nil { + log.Error("%s:\n%s", e, debug.Stack()) + status = http.StatusInternalServerError + w.WriteHeader(status) + fmt.Fprintf(w, "Something didn't work quite right. The robots have alerted the humans.") + } + + log.Info(fmt.Sprintf("\"%s %s\" %d %s \"%s\" \"%s\"", r.Method, r.RequestURI, status, time.Since(start), r.UserAgent(), r.Host)) + }() + + err := f(h.app.App(), w, r) + if err != nil { + if err, ok := err.(impart.HTTPError); ok { + status = err.Status + } else { + status = http.StatusInternalServerError + } + } + + return err + }()) + } +} + func (h *Handler) OAuth(f handlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { h.handleOAuthError(w, r, func() error { @@ -842,6 +874,26 @@ func (h *Handler) handleError(w http.ResponseWriter, r *http.Request, err error) h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app.App(), r)) } +func (h *Handler) handleTextError(w http.ResponseWriter, r *http.Request, err error) { + if err == nil { + return + } + + if err, ok := err.(impart.HTTPError); ok { + if err.Status >= 300 && err.Status < 400 { + sendRedirect(w, err.Status, err.Message) + return + } + + w.WriteHeader(err.Status) + fmt.Fprintf(w, http.StatusText(err.Status)) + return + } + + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprintf(w, "This is an unhelpful error message for a miscellaneous internal error.") +} + func (h *Handler) handleOAuthError(w http.ResponseWriter, r *http.Request, err error) { if err == nil { return diff --git a/less/core.less b/less/core.less index 6401ceb..75a801b 100644 --- a/less/core.less +++ b/less/core.less @@ -393,6 +393,14 @@ body { } } +img { + &.paid { + height: 0.86em; + vertical-align: middle; + margin-bottom: 0.1em; + } +} + nav#full-nav { margin: 0; @@ -743,6 +751,19 @@ input, button, select.inputform, textarea.inputform, a.btn { } } +.btn.cta.secondary, input[type=submit].secondary { + background: transparent; + color: @primary; + &:hover { + background-color: #f9f9f9; + } +} + +.btn.cta.disabled { + background-color: desaturate(@primary, 100%) !important; + border-color: desaturate(@primary, 100%) !important; +} + div.flat-select { display: inline-block; position: relative; diff --git a/less/post-temp.less b/less/post-temp.less index 7ab5d92..aec7d26 100644 --- a/less/post-temp.less +++ b/less/post-temp.less @@ -37,6 +37,25 @@ body#post article, pre, .hljs { font-size: 1.2em; } +p.split { + color: #6161FF; + font-style: italic; + font-size: 0.86em; +} + +#readmore-sell { + padding: 1em 1em 2em; + background-color: #fafafa; + p.split { + color: black; + font-style: normal; + font-size: 1.4em; + } + .cta + .cta { + margin-left: 0.5em; + } +} + /* Post mixins */ .article-code() { background-color: #f8f8f8; diff --git a/monetization.go b/monetization.go new file mode 100644 index 0000000..92375c2 --- /dev/null +++ b/monetization.go @@ -0,0 +1,160 @@ +/* + * Copyright © 2020-2021 A Bunch Tell LLC. + * + * This file is part of WriteFreely. + * + * WriteFreely is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, included + * in the LICENSE file in this source code package. + */ + +package writefreely + +import ( + "bytes" + "fmt" + "github.com/gorilla/mux" + "github.com/writeas/impart" + "github.com/writeas/web-core/log" + "io/ioutil" + "net/http" + "net/url" + "os" + "strings" +) + +func displayMonetization(monetization, alias string) string { + if monetization == "" { + return "" + } + + ptrURL, err := url.Parse(strings.Replace(monetization, "$", "https://", 1)) + if err == nil { + if strings.HasSuffix(ptrURL.Host, ".xrptipbot.com") { + // xrp tip bot doesn't support stream receipts, so return plain pointer + return monetization + } + } + + u := os.Getenv("PAYMENT_HOST") + if u == "" { + return "$webmonetization.org/api/receipts/" + url.PathEscape(monetization) + } + u += "/" + alias + return u +} + +func handleSPSPEndpoint(app *App, w http.ResponseWriter, r *http.Request) error { + idStr := r.FormValue("id") + id, err := url.QueryUnescape(idStr) + if err != nil { + log.Error("Unable to unescape: %s", err) + return err + } + + var c *Collection + if strings.IndexRune(id, '.') > 0 && app.cfg.App.SingleUser { + c, err = app.db.GetCollectionByID(1) + } else { + c, err = app.db.GetCollection(id) + } + if err != nil { + return err + } + + pointer := c.Monetization + if pointer == "" { + err := impart.HTTPError{http.StatusNotFound, "No monetization pointer."} + return err + } + + fmt.Fprintf(w, pointer) + return nil +} + +func handleGetSplitContent(app *App, w http.ResponseWriter, r *http.Request) error { + var collID int64 + var collLookupID string + var coll *Collection + var err error + vars := mux.Vars(r) + if collAlias := vars["alias"]; collAlias != "" { + // Fetch collection information, since an alias is provided + coll, err = app.db.GetCollection(collAlias) + if err != nil { + return err + } + collID = coll.ID + collLookupID = coll.Alias + } + + p, err := app.db.GetPost(vars["post"], collID) + if err != nil { + return err + } + + receipt := r.FormValue("receipt") + if receipt == "" { + return impart.HTTPError{http.StatusBadRequest, "No `receipt` given."} + } + err = verifyReceipt(receipt, collLookupID) + if err != nil { + return err + } + + d := struct { + Content string `json:"body"` + HTMLContent string `json:"html_body"` + }{} + + if exc := strings.Index(p.Content, shortCodePaid); exc > -1 { + baseURL := "" + if coll != nil { + baseURL = coll.CanonicalURL() + } + + d.Content = p.Content[exc+len(shortCodePaid):] + d.HTMLContent = applyMarkdown([]byte(d.Content), baseURL, app.cfg) + } + + return impart.WriteSuccess(w, d, http.StatusOK) +} + +func verifyReceipt(receipt, id string) error { + receiptsHost := os.Getenv("RECEIPTS_HOST") + if receiptsHost == "" { + receiptsHost = "https://webmonetization.org/api/receipts/verify?id=" + id + } else { + receiptsHost = fmt.Sprintf("%s/receipts?id=%s", receiptsHost, id) + } + + log.Info("Verifying receipt %s at %s", receipt, receiptsHost) + r, err := http.NewRequest("POST", receiptsHost, bytes.NewBufferString(receipt)) + if err != nil { + log.Error("Unable to create new request to %s: %s", receiptsHost, err) + return err + } + + resp, err := http.DefaultClient.Do(r) + if err != nil { + log.Error("Unable to Do() request to %s: %s", receiptsHost, err) + return err + } + if resp != nil && resp.Body != nil { + defer resp.Body.Close() + } + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + log.Error("Unable to read %s response body: %s", receiptsHost, err) + return err + } + log.Info("Status : %s", resp.Status) + log.Info("Response: %s", body) + + if resp.StatusCode != http.StatusOK { + log.Error("Bad response from %s:\nStatus: %d\n%s", receiptsHost, resp.StatusCode, string(body)) + return impart.HTTPError{resp.StatusCode, string(body)} + } + return nil +} diff --git a/postrender.go b/postrender.go index 55d0cdf..8e71109 100644 --- a/postrender.go +++ b/postrender.go @@ -42,12 +42,46 @@ var ( mentionReg = regexp.MustCompile(`@([A-Za-z0-9._%+-]+)(@[A-Za-z0-9.-]+\.[A-Za-z]+)\b`) ) -func (p *Post) formatContent(cfg *config.Config, c *Collection, isOwner bool) { +func (p *Post) handlePremiumContent(c *Collection, isOwner, postPage bool, cfg *config.Config) { + if c.Monetization != "" { + // User has Web Monetization enabled, so split content if it exists + spl := strings.Index(p.Content, shortCodePaid) + p.IsPaid = spl > -1 + if postPage { + // We're viewing the individual post + if isOwner { + p.Content = strings.Replace(p.Content, shortCodePaid, "\n\n"+`

Your subscriber content begins here.

`+"\n\n", 1) + } else { + if spl > -1 { + p.Content = p.Content[:spl+len(shortCodePaid)] + p.Content = strings.Replace(p.Content, shortCodePaid, "\n\n"+`

Continue reading with a Coil membership.

`+"\n\n", 1) + } + } + } else { + // We've viewing the post on the collection landing + if spl > -1 { + baseURL := c.CanonicalURL() + if isOwner { + baseURL = "/" + c.Alias + "/" + } + + p.Content = p.Content[:spl+len(shortCodePaid)] + p.HTMLExcerpt = template.HTML(applyMarkdown([]byte(p.Content[:spl]), baseURL, cfg)) + } + } + } +} + +func (p *Post) formatContent(cfg *config.Config, c *Collection, isOwner bool, isPostPage bool) { baseURL := c.CanonicalURL() // TODO: redundant if !isSingleUser { baseURL = "/" + c.Alias + "/" } + + p.handlePremiumContent(c, isOwner, isPostPage, cfg) + p.Content = strings.Replace(p.Content, "<!--paid-->", "", 1) + p.HTMLTitle = template.HTML(applyBasicMarkdown([]byte(p.Title.String))) p.HTMLContent = template.HTML(applyMarkdown([]byte(p.Content), baseURL, cfg)) if exc := strings.Index(string(p.Content), ""); exc > -1 { @@ -55,8 +89,8 @@ func (p *Post) formatContent(cfg *config.Config, c *Collection, isOwner bool) { } } -func (p *PublicPost) formatContent(cfg *config.Config, isOwner bool) { - p.Post.formatContent(cfg, &p.Collection.Collection, isOwner) +func (p *PublicPost) formatContent(cfg *config.Config, isOwner bool, isPostPage bool) { + p.Post.formatContent(cfg, &p.Collection.Collection, isOwner, isPostPage) } func (p *Post) augmentContent(c *Collection) { @@ -78,6 +112,12 @@ func (p *PublicPost) augmentContent() { p.Post.augmentContent(&p.Collection.Collection) } +func (p *PublicPost) augmentReadingDestination() { + if p.IsPaid { + p.HTMLContent += template.HTML("\n\n" + `

` + localStr("Read more...", p.Language.String) + ` ($)

`) + } +} + func applyMarkdown(data []byte, baseURL string, cfg *config.Config) string { return applyMarkdownSpecial(data, false, baseURL, cfg) } diff --git a/posts.go b/posts.go index f60c3af..6ceaaee 100644 --- a/posts.go +++ b/posts.go @@ -48,6 +48,8 @@ const ( postIDLen = 10 postMetaDateFormat = "2006-01-02 15:04:05" + + shortCodePaid = "" ) type ( @@ -109,6 +111,7 @@ type ( HTMLExcerpt template.HTML `db:"content" json:"-"` Tags []string `json:"tags"` Images []string `json:"images,omitempty"` + IsPaid bool `json:"paid"` OwnerName string `json:"owner,omitempty"` } @@ -129,6 +132,20 @@ type ( Collection *CollectionObj `json:"collection,omitempty"` } + CollectionPostPage struct { + *PublicPost + page.StaticPage + IsOwner bool + IsPinned bool + IsCustomDomain bool + Monetization string + PinnedPosts *[]PublicPost + IsFound bool + IsAdmin bool + CanInvite bool + Silenced bool + } + RawPost struct { Id, Slug string Title string @@ -269,6 +286,14 @@ func (p *Post) HasTitleLink() bool { return hasLink } +func (c CollectionPostPage) DisplayMonetization() string { + if c.Collection == nil { + log.Info("CollectionPostPage.DisplayMonetization: c.Collection is nil") + return "" + } + return displayMonetization(c.Monetization, c.Collection.Alias) +} + func handleViewPost(app *App, w http.ResponseWriter, r *http.Request) error { vars := mux.Vars(r) friendlyID := vars["post"] @@ -1154,7 +1179,8 @@ func (p *PublicPost) ActivityObject(app *App) *activitystreams.Object { o.Name = p.DisplayTitle() p.augmentContent() if p.HTMLContent == template.HTML("") { - p.formatContent(cfg, false) + p.formatContent(cfg, false, false) + p.augmentReadingDestination() } o.Content = string(p.HTMLContent) if p.Language.Valid { @@ -1502,20 +1528,8 @@ Are you sure it was ever here?`, p.extractData() p.Content = strings.Replace(p.Content, "", "", 1) // TODO: move this to function - p.formatContent(app.cfg, cr.isCollOwner) - tp := struct { - *PublicPost - page.StaticPage - IsOwner bool - IsPinned bool - IsCustomDomain bool - Monetization string - PinnedPosts *[]PublicPost - IsFound bool - IsAdmin bool - CanInvite bool - Silenced bool - }{ + p.formatContent(app.cfg, cr.isCollOwner, true) + tp := CollectionPostPage{ PublicPost: p, StaticPage: pageForReq(app, r), IsOwner: cr.isCollOwner, diff --git a/routes.go b/routes.go index 1244e97..213958d 100644 --- a/routes.go +++ b/routes.go @@ -134,6 +134,7 @@ func InitRoutes(apper Apper, r *mux.Router) *mux.Router { // Handle collections write.HandleFunc("/api/collections", handler.All(newCollection)).Methods("POST") apiColls := write.PathPrefix("/api/collections/").Subrouter() + apiColls.HandleFunc("/monetization-pointer", handler.PlainTextAPI(handleSPSPEndpoint)).Methods("GET") apiColls.HandleFunc("/"+host, handler.AllReader(fetchCollection)).Methods("GET") apiColls.HandleFunc("/{alias:[0-9a-zA-Z\\-]+}", handler.AllReader(fetchCollection)).Methods("GET") apiColls.HandleFunc("/{alias:[0-9a-zA-Z\\-]+}", handler.All(existingCollection)).Methods("POST", "DELETE") @@ -141,6 +142,7 @@ func InitRoutes(apper Apper, r *mux.Router) *mux.Router { apiColls.HandleFunc("/{alias}/posts", handler.All(newPost)).Methods("POST") apiColls.HandleFunc("/{alias}/posts/{post}", handler.AllReader(fetchPost)).Methods("GET") apiColls.HandleFunc("/{alias}/posts/{post:[a-zA-Z0-9]{10}}", handler.All(existingPost)).Methods("POST") + apiColls.HandleFunc("/{alias}/posts/{post}/splitcontent", handler.AllReader(handleGetSplitContent)).Methods("GET", "POST") apiColls.HandleFunc("/{alias}/posts/{post}/{property}", handler.AllReader(fetchPostProperty)).Methods("GET") apiColls.HandleFunc("/{alias}/collect", handler.All(addPost)).Methods("POST") apiColls.HandleFunc("/{alias}/pin", handler.All(pinPost)).Methods("POST") diff --git a/static/img/paidarticle.svg b/static/img/paidarticle.svg new file mode 100644 index 0000000..788e208 --- /dev/null +++ b/static/img/paidarticle.svg @@ -0,0 +1,78 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/static/js/webmonetization.js b/static/js/webmonetization.js new file mode 100644 index 0000000..bbd828c --- /dev/null +++ b/static/js/webmonetization.js @@ -0,0 +1,94 @@ +/* + * Copyright © 2020-2021 A Bunch Tell LLC. + * + * This file is part of WriteFreely. + * + * WriteFreely is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, included + * in the LICENSE file in this source code package. + */ + +let unlockingSplitContent = false; +let unlockedSplitContent = false; +let pendingSplitContent = false; + +function showWMPaywall($content, $split) { + let $readmoreSell = document.createElement('div') + $readmoreSell.id = 'readmore-sell'; + $content.insertAdjacentElement('beforeend', $readmoreSell); + $readmoreSell.appendChild($split); + $readmoreSell.insertAdjacentHTML("beforeend", '\n\n

For $5 per month, you can read this and other great writing across our site and other websites that support Web Monetization.

') + $readmoreSell.insertAdjacentHTML("beforeend", '\n\n

Get started Learn more

') +} + +function initMonetization() { + let $content = document.querySelector('.e-content') + let $post = document.getElementById('post-body') + let $split = $post.querySelector('.split') + if (document.monetization === undefined || $split == null) { + if ($split) { + showWMPaywall($content, $split) + } + return + } + + document.monetization.addEventListener('monetizationstop', function(event) { + if (pendingSplitContent) { + // We've seen the 'pending' activity, so we can assume things will work + document.monetization.removeEventListener('monetizationstop', progressHandler) + return + } + + // We're getting 'stop' without ever starting, so display the paywall. + showWMPaywall($content, $split) + }); + + document.monetization.addEventListener('monetizationpending', function (event) { + pendingSplitContent = true + }) + + let progressHandler = function(event) { + if (unlockedSplitContent) { + document.monetization.removeEventListener('monetizationprogress', progressHandler) + return + } + if (!unlockingSplitContent && !unlockedSplitContent) { + unlockingSplitContent = true + getSplitContent(event.detail.receipt, function (status, data) { + unlockingSplitContent = false + if (status == 200) { + $split.textContent = "Your subscriber perks start here." + $split.insertAdjacentHTML("afterend", "\n\n"+data.data.html_body) + } else { + $split.textContent = "Something went wrong while unlocking subscriber content." + } + unlockedSplitContent = true + }) + } + } + + function getSplitContent(receipt, callback) { + let params = "receipt="+encodeURIComponent(receipt) + + let http = new XMLHttpRequest(); + http.open("POST", "/api/collections/" + window.collAlias + "/posts/" + window.postSlug + "/splitcontent", true); + + // Send the proper header information along with the request + http.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); + + http.onreadystatechange = function () { + if (http.readyState == 4) { + callback(http.status, JSON.parse(http.responseText)); + } + } + http.send(params); + } + + document.monetization.addEventListener('monetizationstart', function() { + if (!unlockedSplitContent) { + $split.textContent = "Unlocking subscriber content..." + } + document.monetization.removeEventListener('monetizationstart', progressHandler) + }); + document.monetization.addEventListener('monetizationprogress', progressHandler); +} \ No newline at end of file diff --git a/templates/chorus-collection-post.tmpl b/templates/chorus-collection-post.tmpl index 22f2d8f..537e4ef 100644 --- a/templates/chorus-collection-post.tmpl +++ b/templates/chorus-collection-post.tmpl @@ -142,4 +142,13 @@ function unpinPost(e, postID) { })(); } catch (e) { /* ¯\_(ツ)_/¯ */ } + + {{if and .Monetization (not .IsOwner)}} + + + {{end}} {{end}} diff --git a/templates/collection-post.tmpl b/templates/collection-post.tmpl index bba2936..5d56abd 100644 --- a/templates/collection-post.tmpl +++ b/templates/collection-post.tmpl @@ -132,4 +132,13 @@ function unpinPost(e, postID) { })(); } catch (e) { /* ¯\_(ツ)_/¯ */ } + + {{if and .Monetization (not .IsOwner)}} + + + {{end}} {{end}} diff --git a/templates/include/post-render.tmpl b/templates/include/post-render.tmpl index beb98aa..5b84845 100644 --- a/templates/include/post-render.tmpl +++ b/templates/include/post-render.tmpl @@ -1,7 +1,7 @@ {{define "collection-meta"}} {{if .Monetization -}} - + {{- end}} {{end}} diff --git a/templates/include/posts.tmpl b/templates/include/posts.tmpl index b1ccbf2..c3401fa 100644 --- a/templates/include/posts.tmpl +++ b/templates/include/posts.tmpl @@ -1,7 +1,13 @@ {{ define "posts" }} {{ range $el := .Posts }}
{{if .IsScheduled}}

Scheduled

{{end}} - {{if .Title.String}}

{{if .HasTitleLink}}{{.HTMLTitle}} {{else}}{{end}} + {{if .Title.String}}

+ {{- if .HasTitleLink -}} + {{.HTMLTitle}} + {{- else -}} + {{- if .IsPaid}}{{template "paid-badge" .}}{{end -}} + + {{- end}} {{if $.IsOwner}} {{if $.CanPin}}{{end}} @@ -24,7 +30,10 @@ {{if $.Format.ShowDates}}{{end}} {{else}}

- {{if $.Format.ShowDates}}{{end}} + {{if $.Format.ShowDates -}} + {{- if .IsPaid}}{{template "paid-badge" .}}{{end -}} + + {{- end}} {{if $.IsOwner}} {{if not $.Format.ShowDates}}{{end}} @@ -51,3 +60,5 @@ {{localstr "Read more..." .Language.String}}{{else}}
{{if and (and (not $.IsOwner) (not $.Format.ShowDates)) (not .Title.String)}}{{end}}{{.HTMLContent}}
{{end}}

{{ end }} {{ end }} + +{{define "paid-badge"}} {{end}} \ No newline at end of file diff --git a/users.go b/users.go index fe2f2c8..cc6764f 100644 --- a/users.go +++ b/users.go @@ -45,7 +45,8 @@ type ( Signup bool `json:"signup" schema:"signup"` // Feature fields - Description string `json:"description" schema:"description"` + Description string `json:"description" schema:"description"` + Monetization string `json:"monetization" schema:"monetization"` } // AuthUser contains information for a newly authenticated user (either