Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca797cd165 | ||
|
|
15d729c6ac | ||
|
|
af1d1dcd0c | ||
|
|
b21c630208 | ||
|
|
ef68dadcd7 | ||
|
|
8a7348ed6a | ||
|
|
5df5b5f112 | ||
|
|
c9677385f8 | ||
|
|
220109a157 | ||
|
|
2e2d022c9b | ||
|
|
63c4da0b8d | ||
|
|
e1c94ecf15 | ||
|
|
60a9f2cc15 | ||
|
|
06f0f6f014 | ||
|
|
69aba94590 | ||
|
|
52ce1e2660 | ||
|
|
7fc3910009 | ||
|
|
619231e32f | ||
|
|
a22b49a112 | ||
|
|
b77089388f | ||
|
|
b35155b9e5 | ||
|
|
5bb51bb131 | ||
|
|
8a8e0b53fc | ||
|
|
2b00384219 | ||
|
|
bd695c53fd | ||
|
|
24ddf0657b | ||
|
|
e70546fb56 | ||
|
|
4ceb85ae51 | ||
|
|
41b0c8e5ca | ||
|
|
0acdd67e39 | ||
|
|
caca7b8c41 | ||
|
|
24de2dbcb3 | ||
|
|
9a14801990 | ||
|
|
de98ed81cd | ||
|
|
3f7b65dee9 | ||
|
|
8c09aa0ff6 | ||
|
|
ce47f80e8e |
@@ -52,6 +52,7 @@ COPY --from=frontend-build /commento/frontend/build/prod/*.html /commento/
|
|||||||
COPY --from=frontend-build /commento/frontend/build/prod/css/*.css /commento/css/
|
COPY --from=frontend-build /commento/frontend/build/prod/css/*.css /commento/css/
|
||||||
COPY --from=frontend-build /commento/frontend/build/prod/js/*.js /commento/js/
|
COPY --from=frontend-build /commento/frontend/build/prod/js/*.js /commento/js/
|
||||||
COPY --from=frontend-build /commento/frontend/build/prod/images/* /commento/images/
|
COPY --from=frontend-build /commento/frontend/build/prod/images/* /commento/images/
|
||||||
|
COPY --from=frontend-build /commento/frontend/build/prod/fonts/* /commento/fonts/
|
||||||
COPY --from=templates-build /commento/templates/build/prod/templates/ /commento/templates/
|
COPY --from=templates-build /commento/templates/build/prod/templates/ /commento/templates/
|
||||||
COPY --from=db-build /commento/db/build/prod/db/ /commento/db/
|
COPY --from=db-build /commento/db/build/prod/db/ /commento/db/
|
||||||
|
|
||||||
|
|||||||
1
api/Gopkg.lock
generated
1
api/Gopkg.lock
generated
@@ -161,6 +161,7 @@
|
|||||||
"github.com/op/go-logging",
|
"github.com/op/go-logging",
|
||||||
"github.com/russross/blackfriday",
|
"github.com/russross/blackfriday",
|
||||||
"golang.org/x/crypto/bcrypt",
|
"golang.org/x/crypto/bcrypt",
|
||||||
|
"golang.org/x/net/html",
|
||||||
"golang.org/x/oauth2",
|
"golang.org/x/oauth2",
|
||||||
"golang.org/x/oauth2/github",
|
"golang.org/x/oauth2/github",
|
||||||
"golang.org/x/oauth2/google",
|
"golang.org/x/oauth2/google",
|
||||||
|
|||||||
@@ -77,6 +77,11 @@ func commentNewHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if d.RequireIdentification && *x.CommenterToken == "anonymous" {
|
||||||
|
bodyMarshal(w, response{"success": false, "message": errorNotAuthorised.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// logic: (empty column indicates the value doesn't matter)
|
// logic: (empty column indicates the value doesn't matter)
|
||||||
// | anonymous | moderator | requireIdentification | requireModeration | moderateAllAnonymous | approved? |
|
// | anonymous | moderator | requireIdentification | requireModeration | moderateAllAnonymous | approved? |
|
||||||
// |-----------+-----------+-----------------------+-------------------+----------------------+-----------|
|
// |-----------+-----------+-----------------------+-------------------+----------------------+-----------|
|
||||||
@@ -94,7 +99,7 @@ func commentNewHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
if isSpam(*x.Domain, getIp(r), getUserAgent(r), "Anonymous", "", "", *x.Markdown) {
|
if isSpam(*x.Domain, getIp(r), getUserAgent(r), "Anonymous", "", "", *x.Markdown) {
|
||||||
state = "flagged"
|
state = "flagged"
|
||||||
} else {
|
} else {
|
||||||
if d.ModerateAllAnonymous {
|
if d.ModerateAllAnonymous || d.RequireModeration {
|
||||||
state = "unapproved"
|
state = "unapproved"
|
||||||
} else {
|
} else {
|
||||||
state = "approved"
|
state = "approved"
|
||||||
@@ -139,5 +144,11 @@ func commentNewHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
bodyMarshal(w, response{"success": true, "commentHex": commentHex, "state": state, "html": markdownToHtml(*x.Markdown)})
|
// TODO: reuse html in commentNew and do only one markdown to HTML conversion?
|
||||||
|
html := markdownToHtml(*x.Markdown)
|
||||||
|
|
||||||
|
bodyMarshal(w, response{"success": true, "commentHex": commentHex, "state": state, "html": html})
|
||||||
|
if smtpConfigured {
|
||||||
|
go emailNotificationNew(d, path, commenterHex, commentHex, *x.ParentHex, state)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,5 +66,6 @@ func commenterGetByCommenterToken(commenterToken string) (commenter, error) {
|
|||||||
return commenter{}, errorNoSuchToken
|
return commenter{}, errorNoSuchToken
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: use a join instead of two queries?
|
||||||
return commenterGetByHex(commenterHex)
|
return commenterGetByHex(commenterHex)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,5 +67,12 @@ func commenterLoginHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
bodyMarshal(w, response{"success": true, "commenterToken": commenterToken})
|
// TODO: modify commenterLogin to directly return c?
|
||||||
|
c, err := commenterGetByCommenterToken(commenterToken)
|
||||||
|
if err != nil {
|
||||||
|
bodyMarshal(w, response{"success": false, "message": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
bodyMarshal(w, response{"success": true, "commenterToken": commenterToken, "commenter": c})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,10 @@ func commenterNew(email string, name string, link string, photo string, provider
|
|||||||
return "", errorEmailAlreadyExists
|
return "", errorEmailAlreadyExists
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := emailNew(email); err != nil {
|
||||||
|
return "", errorInternal
|
||||||
|
}
|
||||||
|
|
||||||
commenterHex, err := randomHex(32)
|
commenterHex, err := randomHex(32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", errorInternal
|
return "", errorInternal
|
||||||
|
|||||||
@@ -21,5 +21,11 @@ func commenterSelfHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
bodyMarshal(w, response{"success": true, "commenter": c})
|
e, err := emailGet(c.Email)
|
||||||
|
if err != nil {
|
||||||
|
bodyMarshal(w, response{"success": false, "message": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
bodyMarshal(w, response{"success": true, "commenter": c, "email": e})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
var version = "v1.5.0"
|
var version = "v1.6.1"
|
||||||
|
|||||||
66
api/cron_email_notification.go
Normal file
66
api/cron_email_notification.go
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func emailNotificationBegin() error {
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
statement := `
|
||||||
|
SELECT email, sendModeratorNotifications, sendReplyNotifications
|
||||||
|
FROM emails
|
||||||
|
WHERE pendingEmails > 0 AND lastEmailNotificationDate < $1;
|
||||||
|
`
|
||||||
|
rows, err := db.Query(statement, time.Now().UTC().Add(time.Duration(-10)*time.Minute))
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("cannot query domains: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
var email string
|
||||||
|
var sendModeratorNotifications bool
|
||||||
|
var sendReplyNotifications bool
|
||||||
|
if err = rows.Scan(&email, &sendModeratorNotifications, &sendReplyNotifications); err != nil {
|
||||||
|
logger.Errorf("cannot scan email in cron job to send notifications: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := emailQueue[email]; !ok {
|
||||||
|
if err = emailNotificationPendingReset(email); err != nil {
|
||||||
|
logger.Errorf("error resetting pendingEmails: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cont := true
|
||||||
|
kindListMap := map[string][]emailNotification{}
|
||||||
|
for cont {
|
||||||
|
select {
|
||||||
|
case e := <-emailQueue[email]:
|
||||||
|
if _, ok := kindListMap[e.Kind]; !ok {
|
||||||
|
kindListMap[e.Kind] = []emailNotification{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.Kind == "reply" && sendReplyNotifications) || sendModeratorNotifications {
|
||||||
|
kindListMap[e.Kind] = append(kindListMap[e.Kind], e)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
cont = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for kind, list := range kindListMap {
|
||||||
|
go emailNotificationSend(email, kind, list)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(10 * time.Minute)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -6,6 +6,10 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var goMigrations = map[string](func() error){
|
||||||
|
"20190213033530-email-notifications.sql": migrateEmails,
|
||||||
|
}
|
||||||
|
|
||||||
func migrate() error {
|
func migrate() error {
|
||||||
return migrateFromDir(os.Getenv("STATIC") + "/db")
|
return migrateFromDir(os.Getenv("STATIC") + "/db")
|
||||||
}
|
}
|
||||||
@@ -69,6 +73,13 @@ func migrateFromDir(dir string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if fn, ok := goMigrations[file.Name()]; ok {
|
||||||
|
if err = fn(); err != nil {
|
||||||
|
logger.Errorf("cannot execute Go migration associated with SQL %s: %v", f, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
completed++
|
completed++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
37
api/database_migrate_email_notifications.go
Normal file
37
api/database_migrate_email_notifications.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import ()
|
||||||
|
|
||||||
|
func migrateEmails() error {
|
||||||
|
statement := `
|
||||||
|
SELECT commenters.email
|
||||||
|
FROM commenters
|
||||||
|
UNION
|
||||||
|
SELECT owners.email
|
||||||
|
FROM owners
|
||||||
|
UNION
|
||||||
|
SELECT moderators.email
|
||||||
|
FROM moderators;
|
||||||
|
`
|
||||||
|
rows, err := db.Query(statement)
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("cannot get comments: %v", err)
|
||||||
|
return errorDatabaseMigration
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
var email string
|
||||||
|
if err = rows.Scan(&email); err != nil {
|
||||||
|
logger.Errorf("cannot get email from tables during migration: %v", err)
|
||||||
|
return errorDatabaseMigration
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = emailNew(email); err != nil {
|
||||||
|
logger.Errorf("cannot insert email during migration: %v", err)
|
||||||
|
return errorDatabaseMigration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -5,15 +5,16 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type domain struct {
|
type domain struct {
|
||||||
Domain string `json:"domain"`
|
Domain string `json:"domain"`
|
||||||
OwnerHex string `json:"ownerHex"`
|
OwnerHex string `json:"ownerHex"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
CreationDate time.Time `json:"creationDate"`
|
CreationDate time.Time `json:"creationDate"`
|
||||||
State string `json:"state"`
|
State string `json:"state"`
|
||||||
ImportedComments bool `json:"importedComments"`
|
ImportedComments bool `json:"importedComments"`
|
||||||
AutoSpamFilter bool `json:"autoSpamFilter"`
|
AutoSpamFilter bool `json:"autoSpamFilter"`
|
||||||
RequireModeration bool `json:"requireModeration"`
|
RequireModeration bool `json:"requireModeration"`
|
||||||
RequireIdentification bool `json:"requireIdentification"`
|
RequireIdentification bool `json:"requireIdentification"`
|
||||||
ModerateAllAnonymous bool `json:"moderateAllAnonymous"`
|
ModerateAllAnonymous bool `json:"moderateAllAnonymous"`
|
||||||
Moderators []moderator `json:"moderators"`
|
Moderators []moderator `json:"moderators"`
|
||||||
|
EmailNotificationPolicy string `json:"emailNotificationPolicy"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ func domainGet(dmn string) (domain, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
statement := `
|
statement := `
|
||||||
SELECT domain, ownerHex, name, creationDate, state, importedComments, autoSpamFilter, requireModeration, requireIdentification, moderateAllAnonymous
|
SELECT domain, ownerHex, name, creationDate, state, importedComments, autoSpamFilter, requireModeration, requireIdentification, moderateAllAnonymous, emailNotificationPolicy
|
||||||
FROM domains
|
FROM domains
|
||||||
WHERE domain = $1;
|
WHERE domain = $1;
|
||||||
`
|
`
|
||||||
@@ -16,7 +16,7 @@ func domainGet(dmn string) (domain, error) {
|
|||||||
|
|
||||||
var err error
|
var err error
|
||||||
d := domain{}
|
d := domain{}
|
||||||
if err = row.Scan(&d.Domain, &d.OwnerHex, &d.Name, &d.CreationDate, &d.State, &d.ImportedComments, &d.AutoSpamFilter, &d.RequireModeration, &d.RequireIdentification, &d.ModerateAllAnonymous); err != nil {
|
if err = row.Scan(&d.Domain, &d.OwnerHex, &d.Name, &d.CreationDate, &d.State, &d.ImportedComments, &d.AutoSpamFilter, &d.RequireModeration, &d.RequireIdentification, &d.ModerateAllAnonymous, &d.EmailNotificationPolicy); err != nil {
|
||||||
return d, errorNoSuchDomain
|
return d, errorNoSuchDomain
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ func domainList(ownerHex string) ([]domain, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
statement := `
|
statement := `
|
||||||
SELECT domain, ownerHex, name, creationDate, state, importedComments, autoSpamFilter, requireModeration, requireIdentification, moderateAllAnonymous
|
SELECT domain, ownerHex, name, creationDate, state, importedComments, autoSpamFilter, requireModeration, requireIdentification, moderateAllAnonymous, emailNotificationPolicy
|
||||||
FROM domains
|
FROM domains
|
||||||
WHERE ownerHex=$1;
|
WHERE ownerHex=$1;
|
||||||
`
|
`
|
||||||
@@ -24,7 +24,7 @@ func domainList(ownerHex string) ([]domain, error) {
|
|||||||
domains := []domain{}
|
domains := []domain{}
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
d := domain{}
|
d := domain{}
|
||||||
if err = rows.Scan(&d.Domain, &d.OwnerHex, &d.Name, &d.CreationDate, &d.State, &d.ImportedComments, &d.AutoSpamFilter, &d.RequireModeration, &d.RequireIdentification, &d.ModerateAllAnonymous); err != nil {
|
if err = rows.Scan(&d.Domain, &d.OwnerHex, &d.Name, &d.CreationDate, &d.State, &d.ImportedComments, &d.AutoSpamFilter, &d.RequireModeration, &d.RequireIdentification, &d.ModerateAllAnonymous, &d.EmailNotificationPolicy); err != nil {
|
||||||
logger.Errorf("cannot Scan domain: %v", err)
|
logger.Errorf("cannot Scan domain: %v", err)
|
||||||
return nil, errorInternal
|
return nil, errorInternal
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,11 @@ func domainModeratorNew(domain string, email string) error {
|
|||||||
return errorMissingField
|
return errorMissingField
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := emailNew(email); err != nil {
|
||||||
|
logger.Errorf("cannot create email when creating moderator: %v", err)
|
||||||
|
return errorInternal
|
||||||
|
}
|
||||||
|
|
||||||
statement := `
|
statement := `
|
||||||
INSERT INTO
|
INSERT INTO
|
||||||
moderators (domain, email, addDate)
|
moderators (domain, email, addDate)
|
||||||
|
|||||||
@@ -7,11 +7,11 @@ import (
|
|||||||
func domainUpdate(d domain) error {
|
func domainUpdate(d domain) error {
|
||||||
statement := `
|
statement := `
|
||||||
UPDATE domains
|
UPDATE domains
|
||||||
SET name=$2, state=$3, autoSpamFilter=$4, requireModeration=$5, requireIdentification=$6, moderateAllAnonymous=$7
|
SET name=$2, state=$3, autoSpamFilter=$4, requireModeration=$5, requireIdentification=$6, moderateAllAnonymous=$7, emailNotificationPolicy=$8
|
||||||
WHERE domain=$1;
|
WHERE domain=$1;
|
||||||
`
|
`
|
||||||
|
|
||||||
_, err := db.Exec(statement, d.Domain, d.Name, d.State, d.AutoSpamFilter, d.RequireModeration, d.RequireIdentification, d.ModerateAllAnonymous)
|
_, err := db.Exec(statement, d.Domain, d.Name, d.State, d.AutoSpamFilter, d.RequireModeration, d.RequireIdentification, d.ModerateAllAnonymous, d.EmailNotificationPolicy)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf("cannot update non-moderators: %v", err)
|
logger.Errorf("cannot update non-moderators: %v", err)
|
||||||
return errorInternal
|
return errorInternal
|
||||||
|
|||||||
14
api/email.go
Normal file
14
api/email.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type email struct {
|
||||||
|
Email string `json:"email"`
|
||||||
|
UnsubscribeSecretHex string `json:"unsubscribeSecretHex"`
|
||||||
|
LastEmailNotificationDate time.Time `json:"lastEmailNotificationDate"`
|
||||||
|
PendingEmails int `json:"-"`
|
||||||
|
SendReplyNotifications bool `json:"sendReplyNotifications"`
|
||||||
|
SendModeratorNotifications bool `json:"sendModeratorNotifications"`
|
||||||
|
}
|
||||||
59
api/email_get.go
Normal file
59
api/email_get.go
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func emailGet(em string) (email, error) {
|
||||||
|
statement := `
|
||||||
|
SELECT email, unsubscribeSecretHex, lastEmailNotificationDate, pendingEmails, sendReplyNotifications, sendModeratorNotifications
|
||||||
|
FROM emails
|
||||||
|
WHERE email = $1;
|
||||||
|
`
|
||||||
|
row := db.QueryRow(statement, em)
|
||||||
|
|
||||||
|
e := email{}
|
||||||
|
if err := row.Scan(&e.Email, &e.UnsubscribeSecretHex, &e.LastEmailNotificationDate, &e.PendingEmails, &e.SendReplyNotifications, &e.SendModeratorNotifications); err != nil {
|
||||||
|
// TODO: is this the only error?
|
||||||
|
return e, errorNoSuchEmail
|
||||||
|
}
|
||||||
|
|
||||||
|
return e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func emailGetByUnsubscribeSecretHex(unsubscribeSecretHex string) (email, error) {
|
||||||
|
statement := `
|
||||||
|
SELECT email, unsubscribeSecretHex, lastEmailNotificationDate, pendingEmails, sendReplyNotifications, sendModeratorNotifications
|
||||||
|
FROM emails
|
||||||
|
WHERE unsubscribeSecretHex = $1;
|
||||||
|
`
|
||||||
|
row := db.QueryRow(statement, unsubscribeSecretHex)
|
||||||
|
|
||||||
|
e := email{}
|
||||||
|
if err := row.Scan(&e.Email, &e.UnsubscribeSecretHex, &e.LastEmailNotificationDate, &e.PendingEmails, &e.SendReplyNotifications, &e.SendModeratorNotifications); err != nil {
|
||||||
|
// TODO: is this the only error?
|
||||||
|
return e, errorNoSuchUnsubscribeSecretHex
|
||||||
|
}
|
||||||
|
|
||||||
|
return e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func emailGetHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
type request struct {
|
||||||
|
UnsubscribeSecretHex *string `json:"unsubscribeSecretHex"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var x request
|
||||||
|
if err := bodyUnmarshal(r, &x); err != nil {
|
||||||
|
bodyMarshal(w, response{"success": false, "message": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
e, err := emailGetByUnsubscribeSecretHex(*x.UnsubscribeSecretHex)
|
||||||
|
if err != nil {
|
||||||
|
bodyMarshal(w, response{"success": false, "message": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
bodyMarshal(w, response{"success": true, "email": e})
|
||||||
|
}
|
||||||
66
api/email_moderate.go
Normal file
66
api/email_moderate.go
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func emailModerateHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
unsubscribeSecretHex := r.FormValue("unsubscribeSecretHex")
|
||||||
|
e, err := emailGetByUnsubscribeSecretHex(unsubscribeSecretHex)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(w, "error: %v", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
action := r.FormValue("action")
|
||||||
|
if action != "delete" && action != "approve" {
|
||||||
|
fmt.Fprintf(w, "error: invalid action")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
commentHex := r.FormValue("commentHex")
|
||||||
|
if commentHex == "" {
|
||||||
|
fmt.Fprintf(w, "error: invalid commentHex")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
statement := `
|
||||||
|
SELECT domain
|
||||||
|
FROM comments
|
||||||
|
WHERE commentHex = $1;
|
||||||
|
`
|
||||||
|
row := db.QueryRow(statement, commentHex)
|
||||||
|
|
||||||
|
var domain string
|
||||||
|
if err = row.Scan(&domain); err != nil {
|
||||||
|
// TODO: is this the only error?
|
||||||
|
fmt.Fprintf(w, "error: no such comment found (perhaps it has been deleted?)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isModerator, err := isDomainModerator(domain, e.Email)
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("error checking if %s is a moderator: %v", e.Email, err)
|
||||||
|
fmt.Fprintf(w, "error checking if %s is a moderator: %v", e.Email, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isModerator {
|
||||||
|
fmt.Fprintf(w, "error: you're not a moderator for that domain")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if action == "approve" {
|
||||||
|
err = commentApprove(commentHex)
|
||||||
|
} else {
|
||||||
|
err = commentDelete(commentHex)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(w, "error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(w, "comment successfully %sd", action)
|
||||||
|
}
|
||||||
26
api/email_new.go
Normal file
26
api/email_new.go
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func emailNew(email string) error {
|
||||||
|
unsubscribeSecretHex, err := randomHex(32)
|
||||||
|
if err != nil {
|
||||||
|
return errorInternal
|
||||||
|
}
|
||||||
|
|
||||||
|
statement := `
|
||||||
|
INSERT INTO
|
||||||
|
emails (email, unsubscribeSecretHex, lastEmailNotificationDate)
|
||||||
|
VALUES ($1, $2, $3 )
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
`
|
||||||
|
_, err = db.Exec(statement, email, unsubscribeSecretHex, time.Now().UTC())
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("cannot insert email into emails: %v", err)
|
||||||
|
return errorInternal
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
81
api/email_notification.go
Normal file
81
api/email_notification.go
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type emailNotification struct {
|
||||||
|
Email string
|
||||||
|
CommenterName string
|
||||||
|
Domain string
|
||||||
|
Path string
|
||||||
|
Title string
|
||||||
|
CommentHex string
|
||||||
|
Kind string
|
||||||
|
}
|
||||||
|
|
||||||
|
var emailQueue map[string](chan emailNotification) = map[string](chan emailNotification){}
|
||||||
|
|
||||||
|
func emailNotificationPendingResetAll() error {
|
||||||
|
statement := `
|
||||||
|
UPDATE emails
|
||||||
|
SET pendingEmails = 0;
|
||||||
|
`
|
||||||
|
_, err := db.Exec(statement)
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("cannot reset pendingEmails: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func emailNotificationPendingIncrement(email string) error {
|
||||||
|
statement := `
|
||||||
|
UPDATE emails
|
||||||
|
SET pendingEmails = pendingEmails + 1
|
||||||
|
WHERE email = $1;
|
||||||
|
`
|
||||||
|
_, err := db.Exec(statement, email)
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("cannot increment pendingEmails: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func emailNotificationPendingReset(email string) error {
|
||||||
|
statement := `
|
||||||
|
UPDATE emails
|
||||||
|
SET pendingEmails = 0, lastEmailNotificationDate = $2
|
||||||
|
WHERE email = $1;
|
||||||
|
`
|
||||||
|
_, err := db.Exec(statement, email, time.Now().UTC())
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("cannot decrement pendingEmails: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func emailNotificationEnqueue(e emailNotification) error {
|
||||||
|
if err := emailNotificationPendingIncrement(e.Email); err != nil {
|
||||||
|
logger.Errorf("cannot increment pendingEmails when enqueueing: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := emailQueue[e.Email]; !ok {
|
||||||
|
// don't enqueue more than 10 emails as we won't send more than 10 comments
|
||||||
|
// in one email anyway
|
||||||
|
emailQueue[e.Email] = make(chan emailNotification, 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case emailQueue[e.Email] <- e:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
138
api/email_notification_new.go
Normal file
138
api/email_notification_new.go
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import ()
|
||||||
|
|
||||||
|
func emailNotificationModerator(d domain, path string, title string, commenterHex string, commentHex string, state string) {
|
||||||
|
if d.EmailNotificationPolicy == "none" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// We'll need to check again when we're sending in case the comment was
|
||||||
|
// approved midway anyway.
|
||||||
|
if d.EmailNotificationPolicy == "pending-moderation" && state == "approved" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var commenterName string
|
||||||
|
var commenterEmail string
|
||||||
|
if commenterHex == "anonymous" {
|
||||||
|
commenterName = "Anonymous"
|
||||||
|
} else {
|
||||||
|
c, err := commenterGetByHex(commenterHex)
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("cannot get commenter to send email notification: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
commenterName = c.Name
|
||||||
|
commenterEmail = c.Email
|
||||||
|
}
|
||||||
|
|
||||||
|
kind := d.EmailNotificationPolicy
|
||||||
|
if state != "approved" {
|
||||||
|
kind = "pending-moderation"
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, m := range d.Moderators {
|
||||||
|
// Do not email the commenting moderator their own comment.
|
||||||
|
if commenterHex != "anonymous" && m.Email == commenterEmail {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
emailNotificationPendingIncrement(m.Email)
|
||||||
|
emailNotificationEnqueue(emailNotification{
|
||||||
|
Email: m.Email,
|
||||||
|
CommenterName: commenterName,
|
||||||
|
Domain: d.Domain,
|
||||||
|
Path: path,
|
||||||
|
Title: title,
|
||||||
|
CommentHex: commentHex,
|
||||||
|
Kind: kind,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func emailNotificationReply(d domain, path string, title string, commenterHex string, commentHex string, parentHex string, state string) {
|
||||||
|
// No reply notifications for root comments.
|
||||||
|
if parentHex == "root" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// No reply notification emails for unapproved comments.
|
||||||
|
if state != "approved" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
statement := `
|
||||||
|
SELECT commenterHex
|
||||||
|
FROM comments
|
||||||
|
WHERE commentHex = $1;
|
||||||
|
`
|
||||||
|
row := db.QueryRow(statement, parentHex)
|
||||||
|
|
||||||
|
var parentCommenterHex string
|
||||||
|
err := row.Scan(&parentCommenterHex)
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("cannot scan commenterHex and parentCommenterHex: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// No reply notification emails for anonymous users.
|
||||||
|
if parentCommenterHex == "anonymous" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// No reply notification email for self replies.
|
||||||
|
if parentCommenterHex == commenterHex {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pc, err := commenterGetByHex(parentCommenterHex)
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("cannot get commenter to send email notification: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var commenterName string
|
||||||
|
if commenterHex == "anonymous" {
|
||||||
|
commenterName = "Anonymous"
|
||||||
|
} else {
|
||||||
|
c, err := commenterGetByHex(commenterHex)
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("cannot get commenter to send email notification: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
commenterName = c.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// We'll check if they want to receive reply notifications later at the time
|
||||||
|
// of sending.
|
||||||
|
emailNotificationEnqueue(emailNotification{
|
||||||
|
Email: pc.Email,
|
||||||
|
CommenterName: commenterName,
|
||||||
|
Domain: d.Domain,
|
||||||
|
Path: path,
|
||||||
|
Title: title,
|
||||||
|
CommentHex: commentHex,
|
||||||
|
Kind: "reply",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func emailNotificationNew(d domain, path string, commenterHex string, commentHex string, parentHex string, state string) {
|
||||||
|
p, err := pageGet(d.Domain, path)
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("cannot get page to send email notification: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.Title == "" {
|
||||||
|
p.Title, err = pageTitleUpdate(d.Domain, path)
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("cannot update/get page title to send email notification: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
emailNotificationModerator(d, path, p.Title, commenterHex, commentHex, state)
|
||||||
|
emailNotificationReply(d, path, p.Title, commenterHex, commentHex, parentHex, state)
|
||||||
|
}
|
||||||
63
api/email_notification_send.go
Normal file
63
api/email_notification_send.go
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"html/template"
|
||||||
|
)
|
||||||
|
|
||||||
|
func emailNotificationSend(em string, kind string, notifications []emailNotification) {
|
||||||
|
if len(notifications) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
e, err := emailGet(em)
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("cannot get email: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
messages := []emailNotificationText{}
|
||||||
|
for _, notification := range notifications {
|
||||||
|
statement := `
|
||||||
|
SELECT html
|
||||||
|
FROM comments
|
||||||
|
WHERE commentHex = $1;
|
||||||
|
`
|
||||||
|
row := db.QueryRow(statement, notification.CommentHex)
|
||||||
|
|
||||||
|
var html string
|
||||||
|
if err = row.Scan(&html); err != nil {
|
||||||
|
// the comment was deleted?
|
||||||
|
// TODO: is this the only error?
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
messages = append(messages, emailNotificationText{
|
||||||
|
emailNotification: notification,
|
||||||
|
Html: template.HTML(html),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
statement := `
|
||||||
|
SELECT name
|
||||||
|
FROM commenters
|
||||||
|
WHERE email = $1;
|
||||||
|
`
|
||||||
|
row := db.QueryRow(statement, em)
|
||||||
|
|
||||||
|
var name string
|
||||||
|
if err := row.Scan(&name); err != nil {
|
||||||
|
// The moderator has probably not created a commenter account. Let's just
|
||||||
|
// use their email as name.
|
||||||
|
name = nameFromEmail(em)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := emailNotificationPendingReset(em); err != nil {
|
||||||
|
logger.Errorf("cannot reset after email notification: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := smtpEmailNotification(em, name, e.UnsubscribeSecretHex, messages, kind); err != nil {
|
||||||
|
logger.Errorf("cannot send email notification: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
39
api/email_update.go
Normal file
39
api/email_update.go
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func emailUpdate(e email) error {
|
||||||
|
statement := `
|
||||||
|
UPDATE emails
|
||||||
|
SET sendReplyNotifications = $3, sendModeratorNotifications = $4
|
||||||
|
WHERE email = $1 AND unsubscribeSecretHex = $2;
|
||||||
|
`
|
||||||
|
_, err := db.Exec(statement, e.Email, e.UnsubscribeSecretHex, e.SendReplyNotifications, e.SendModeratorNotifications)
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("error updating email: %v", err)
|
||||||
|
return errorInternal
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func emailUpdateHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
type request struct {
|
||||||
|
Email *email `json:"email"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var x request
|
||||||
|
if err := bodyUnmarshal(r, &x); err != nil {
|
||||||
|
bodyMarshal(w, response{"success": false, "message": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := emailUpdate(*x.Email); err != nil {
|
||||||
|
bodyMarshal(w, response{"success": true, "message": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
bodyMarshal(w, response{"success": true})
|
||||||
|
}
|
||||||
@@ -42,3 +42,5 @@ var errorInvalidConfigFile = errors.New("Invalid config file.")
|
|||||||
var errorInvalidConfigValue = errors.New("Invalid config value.")
|
var errorInvalidConfigValue = errors.New("Invalid config value.")
|
||||||
var errorNewOwnerForbidden = errors.New("New user registrations are forbidden and closed.")
|
var errorNewOwnerForbidden = errors.New("New user registrations are forbidden and closed.")
|
||||||
var errorThreadLocked = errors.New("This thread is locked. You cannot add new comments.")
|
var errorThreadLocked = errors.New("This thread is locked. You cannot add new comments.")
|
||||||
|
var errorDatabaseMigration = errors.New("Encountered error applying database migration.")
|
||||||
|
var errorNoSuchUnsubscribeSecretHex = errors.New("Invalid unsubscribe link.")
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ func main() {
|
|||||||
exitIfError(smtpTemplatesLoad())
|
exitIfError(smtpTemplatesLoad())
|
||||||
exitIfError(oauthConfigure())
|
exitIfError(oauthConfigure())
|
||||||
exitIfError(markdownRendererCreate())
|
exitIfError(markdownRendererCreate())
|
||||||
|
exitIfError(emailNotificationPendingResetAll())
|
||||||
|
exitIfError(emailNotificationBegin())
|
||||||
exitIfError(sigintCleanupSetup())
|
exitIfError(sigintCleanupSetup())
|
||||||
exitIfError(versionCheckStart())
|
exitIfError(versionCheckStart())
|
||||||
exitIfError(domainExportCleanupBegin())
|
exitIfError(domainExportCleanupBegin())
|
||||||
|
|||||||
@@ -46,3 +46,24 @@ func ownerGetByOwnerToken(ownerToken string) (owner, error) {
|
|||||||
|
|
||||||
return o, nil
|
return o, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ownerGetByOwnerHex(ownerHex string) (owner, error) {
|
||||||
|
if ownerHex == "" {
|
||||||
|
return owner{}, errorMissingField
|
||||||
|
}
|
||||||
|
|
||||||
|
statement := `
|
||||||
|
SELECT ownerHex, email, name, confirmedEmail, joinDate
|
||||||
|
FROM owners
|
||||||
|
WHERE ownerHex = $1;
|
||||||
|
`
|
||||||
|
row := db.QueryRow(statement, ownerHex)
|
||||||
|
|
||||||
|
var o owner
|
||||||
|
if err := row.Scan(&o.OwnerHex, &o.Email, &o.Name, &o.ConfirmedEmail, &o.JoinDate); err != nil {
|
||||||
|
logger.Errorf("cannot scan owner: %v\n", err)
|
||||||
|
return owner{}, errorInternal
|
||||||
|
}
|
||||||
|
|
||||||
|
return o, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ func ownerNew(email string, name string, password string) (string, error) {
|
|||||||
return "", errorEmailAlreadyExists
|
return "", errorEmailAlreadyExists
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := emailNew(email); err != nil {
|
||||||
|
return "", errorInternal
|
||||||
|
}
|
||||||
|
|
||||||
ownerHex, err := randomHex(32)
|
ownerHex, err := randomHex(32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf("cannot generate ownerHex: %v", err)
|
logger.Errorf("cannot generate ownerHex: %v", err)
|
||||||
|
|||||||
@@ -8,4 +8,5 @@ type page struct {
|
|||||||
IsLocked bool `json:"isLocked"`
|
IsLocked bool `json:"isLocked"`
|
||||||
CommentCount int `json:"commentCount"`
|
CommentCount int `json:"commentCount"`
|
||||||
StickyCommentHex string `json:"stickyCommentHex"`
|
StickyCommentHex string `json:"stickyCommentHex"`
|
||||||
|
Title string `json:"title"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,14 +11,14 @@ func pageGet(domain string, path string) (page, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
statement := `
|
statement := `
|
||||||
SELECT isLocked, commentCount, stickyCommentHex
|
SELECT isLocked, commentCount, stickyCommentHex, title
|
||||||
FROM pages
|
FROM pages
|
||||||
WHERE domain=$1 AND path=$2;
|
WHERE domain=$1 AND path=$2;
|
||||||
`
|
`
|
||||||
row := db.QueryRow(statement, domain, path)
|
row := db.QueryRow(statement, domain, path)
|
||||||
|
|
||||||
p := page{Domain: domain, Path: path}
|
p := page{Domain: domain, Path: path}
|
||||||
if err := row.Scan(&p.IsLocked, &p.CommentCount, &p.StickyCommentHex); err != nil {
|
if err := row.Scan(&p.IsLocked, &p.CommentCount, &p.StickyCommentHex, &p.Title); err != nil {
|
||||||
if err == sql.ErrNoRows {
|
if err == sql.ErrNoRows {
|
||||||
// If there haven't been any comments, there won't be a record for this
|
// If there haven't been any comments, there won't be a record for this
|
||||||
// page. The sane thing to do is return defaults.
|
// page. The sane thing to do is return defaults.
|
||||||
@@ -26,6 +26,7 @@ func pageGet(domain string, path string) (page, error) {
|
|||||||
p.IsLocked = false
|
p.IsLocked = false
|
||||||
p.CommentCount = 0
|
p.CommentCount = 0
|
||||||
p.StickyCommentHex = "none"
|
p.StickyCommentHex = "none"
|
||||||
|
p.Title = ""
|
||||||
} else {
|
} else {
|
||||||
logger.Errorf("error scanning page: %v", err)
|
logger.Errorf("error scanning page: %v", err)
|
||||||
return page{}, errorInternal
|
return page{}, errorInternal
|
||||||
|
|||||||
28
api/page_title.go
Normal file
28
api/page_title.go
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import ()
|
||||||
|
|
||||||
|
func pageTitleUpdate(domain string, path string) (string, error) {
|
||||||
|
title, err := htmlTitleGet("http://" + domain + path)
|
||||||
|
if err != nil {
|
||||||
|
// This could fail due to a variety of reasons that we can't control such
|
||||||
|
// as the user's URL 404 or something, so let's not pollute the error log
|
||||||
|
// with messages. Just use a sane title. Maybe we'll have the ability to
|
||||||
|
// retry later.
|
||||||
|
logger.Errorf("%v", err)
|
||||||
|
title = domain
|
||||||
|
}
|
||||||
|
|
||||||
|
statement := `
|
||||||
|
UPDATE pages
|
||||||
|
SET title = $3
|
||||||
|
WHERE domain = $1 AND path = $2;
|
||||||
|
`
|
||||||
|
_, err = db.Exec(statement, domain, path, title)
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("cannot update pages table with title: %v", err)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return title, nil
|
||||||
|
}
|
||||||
@@ -28,6 +28,10 @@ func apiRouterInit(router *mux.Router) error {
|
|||||||
router.HandleFunc("/api/commenter/login", commenterLoginHandler).Methods("POST")
|
router.HandleFunc("/api/commenter/login", commenterLoginHandler).Methods("POST")
|
||||||
router.HandleFunc("/api/commenter/self", commenterSelfHandler).Methods("POST")
|
router.HandleFunc("/api/commenter/self", commenterSelfHandler).Methods("POST")
|
||||||
|
|
||||||
|
router.HandleFunc("/api/email/get", emailGetHandler).Methods("POST")
|
||||||
|
router.HandleFunc("/api/email/update", emailUpdateHandler).Methods("POST")
|
||||||
|
router.HandleFunc("/api/email/moderate", emailModerateHandler).Methods("GET")
|
||||||
|
|
||||||
router.HandleFunc("/api/oauth/google/redirect", googleRedirectHandler).Methods("GET")
|
router.HandleFunc("/api/oauth/google/redirect", googleRedirectHandler).Methods("GET")
|
||||||
router.HandleFunc("/api/oauth/google/callback", googleCallbackHandler).Methods("GET")
|
router.HandleFunc("/api/oauth/google/callback", googleCallbackHandler).Methods("GET")
|
||||||
|
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ func staticRouterInit(router *mux.Router) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, dir := range []string{"/js", "/css", "/images"} {
|
for _, dir := range []string{"/js", "/css", "/images", "/fonts"} {
|
||||||
files, err := ioutil.ReadDir(os.Getenv("STATIC") + dir)
|
files, err := ioutil.ReadDir(os.Getenv("STATIC") + dir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf("cannot read directory %s%s: %v", os.Getenv("STATIC"), dir, err)
|
logger.Errorf("cannot read directory %s%s: %v", os.Getenv("STATIC"), dir, err)
|
||||||
@@ -98,6 +98,7 @@ func staticRouterInit(router *mux.Router) error {
|
|||||||
"/reset-password",
|
"/reset-password",
|
||||||
"/signup",
|
"/signup",
|
||||||
"/confirm-email",
|
"/confirm-email",
|
||||||
|
"/unsubscribe",
|
||||||
"/dashboard",
|
"/dashboard",
|
||||||
"/logout",
|
"/logout",
|
||||||
}
|
}
|
||||||
|
|||||||
86
api/smtp_email_notification.go
Normal file
86
api/smtp_email_notification.go
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
ht "html/template"
|
||||||
|
"net/smtp"
|
||||||
|
"os"
|
||||||
|
tt "text/template"
|
||||||
|
)
|
||||||
|
|
||||||
|
type emailNotificationText struct {
|
||||||
|
emailNotification
|
||||||
|
Html ht.HTML
|
||||||
|
}
|
||||||
|
|
||||||
|
type emailNotificationPlugs struct {
|
||||||
|
Origin string
|
||||||
|
Kind string
|
||||||
|
Subject string
|
||||||
|
UnsubscribeSecretHex string
|
||||||
|
Notifications []emailNotificationText
|
||||||
|
}
|
||||||
|
|
||||||
|
func smtpEmailNotification(to string, toName string, unsubscribeSecretHex string, notifications []emailNotificationText, kind string) error {
|
||||||
|
var subject string
|
||||||
|
if kind == "reply" {
|
||||||
|
var verb string
|
||||||
|
if len(notifications) > 1 {
|
||||||
|
verb = "replies"
|
||||||
|
} else {
|
||||||
|
verb = "reply"
|
||||||
|
}
|
||||||
|
subject = fmt.Sprintf("%d new comment %s", len(notifications), verb)
|
||||||
|
} else {
|
||||||
|
var verb string
|
||||||
|
if len(notifications) > 1 {
|
||||||
|
verb = "comments"
|
||||||
|
} else {
|
||||||
|
verb = "comment"
|
||||||
|
}
|
||||||
|
if kind == "pending-moderation" {
|
||||||
|
subject = fmt.Sprintf("%d new %s pending moderation", len(notifications), verb)
|
||||||
|
} else {
|
||||||
|
subject = fmt.Sprintf("%d new %s on your website", len(notifications), verb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h, err := tt.New("header").Parse(`MIME-Version: 1.0
|
||||||
|
From: Commento <{{.FromAddress}}>
|
||||||
|
To: {{.ToName}} <{{.ToAddress}}>
|
||||||
|
Content-Type: text/html; charset=UTF-8
|
||||||
|
Subject: {{.Subject}}
|
||||||
|
|
||||||
|
`)
|
||||||
|
|
||||||
|
var header bytes.Buffer
|
||||||
|
h.Execute(&header, &headerPlugs{FromAddress: os.Getenv("SMTP_FROM_ADDRESS"), ToAddress: to, ToName: toName, Subject: "[Commento] " + subject})
|
||||||
|
|
||||||
|
t, err := ht.ParseFiles(fmt.Sprintf("%s/templates/email-notification.txt", os.Getenv("STATIC")))
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("cannot parse %s/templates/email-notification.txt: %v", os.Getenv("STATIC"), err)
|
||||||
|
return errorMalformedTemplate
|
||||||
|
}
|
||||||
|
|
||||||
|
var body bytes.Buffer
|
||||||
|
err = t.Execute(&body, &emailNotificationPlugs{
|
||||||
|
Origin: os.Getenv("ORIGIN"),
|
||||||
|
Kind: kind,
|
||||||
|
Subject: subject,
|
||||||
|
UnsubscribeSecretHex: unsubscribeSecretHex,
|
||||||
|
Notifications: notifications,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("error generating templated HTML for email notification: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = smtp.SendMail(os.Getenv("SMTP_HOST")+":"+os.Getenv("SMTP_PORT"), smtpAuth, os.Getenv("SMTP_FROM_ADDRESS"), []string{to}, concat(header, body))
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("cannot send email notification: %v", err)
|
||||||
|
return errorCannotSendEmail
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -31,7 +31,12 @@ Subject: {{.Subject}}
|
|||||||
return errorMalformedTemplate
|
return errorMalformedTemplate
|
||||||
}
|
}
|
||||||
|
|
||||||
names := []string{"confirm-hex", "reset-hex", "domain-export", "domain-export-error"}
|
names := []string{
|
||||||
|
"confirm-hex",
|
||||||
|
"reset-hex",
|
||||||
|
"domain-export",
|
||||||
|
"domain-export-error",
|
||||||
|
}
|
||||||
|
|
||||||
templates = make(map[string]*template.Template)
|
templates = make(map[string]*template.Template)
|
||||||
|
|
||||||
|
|||||||
36
api/utils_html.go
Normal file
36
api/utils_html.go
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"golang.org/x/net/html"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func htmlTitleRecurse(h *html.Node) string {
|
||||||
|
if h.Type == html.ElementNode && h.Data == "title" {
|
||||||
|
return h.FirstChild.Data
|
||||||
|
}
|
||||||
|
|
||||||
|
for c := h.FirstChild; c != nil; c = c.NextSibling {
|
||||||
|
res := htmlTitleRecurse(c)
|
||||||
|
if res != "" {
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func htmlTitleGet(url string) (string, error) {
|
||||||
|
resp, err := http.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
h, err := html.Parse(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return htmlTitleRecurse(h), nil
|
||||||
|
}
|
||||||
@@ -10,6 +10,16 @@ func concat(a bytes.Buffer, b bytes.Buffer) []byte {
|
|||||||
return append(a.Bytes(), b.Bytes()...)
|
return append(a.Bytes(), b.Bytes()...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func nameFromEmail(email string) string {
|
||||||
|
for i, c := range email {
|
||||||
|
if c == '@' {
|
||||||
|
return email[:i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return email
|
||||||
|
}
|
||||||
|
|
||||||
func exitIfError(err error) {
|
func exitIfError(err error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("fatal error: %v\n", err)
|
fmt.Printf("fatal error: %v\n", err)
|
||||||
|
|||||||
38
db/20190213033530-email-notifications.sql
Normal file
38
db/20190213033530-email-notifications.sql
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
-- Email notifications
|
||||||
|
-- There are two kinds of email notifications: those sent to domain moderators
|
||||||
|
-- and those sent to commenters. Domain owners can choose to subscribe their
|
||||||
|
-- moderators to all comments, those pending moderation, or no emails. Each
|
||||||
|
-- moderator can independently opt out of these emails, of course. Commenters,
|
||||||
|
-- on the other, can choose to opt into reply notifications by email.
|
||||||
|
|
||||||
|
-- TODO: daily and weekly digests instead of just batched real-time emails?
|
||||||
|
|
||||||
|
-- TODO: more granular options to unsubscribe from emails for particular
|
||||||
|
-- domains can be provided - add unsubscribedReplyDomains []TEXT and
|
||||||
|
-- unsubscribedModeratorDomains []TEXT to emails table?
|
||||||
|
|
||||||
|
-- Each address has a cooldown period so that emails aren't sent within 10
|
||||||
|
-- minutes of each other. Why is this a separate table instead of another
|
||||||
|
-- column on commenters/owners? Because there may be some mods that haven't
|
||||||
|
-- logged in to create a row in the commenter table.
|
||||||
|
CREATE TABLE IF NOT EXISTS emails (
|
||||||
|
email TEXT NOT NULL UNIQUE PRIMARY KEY,
|
||||||
|
unsubscribeSecretHex TEXT NOT NULL UNIQUE,
|
||||||
|
lastEmailNotificationDate TIMESTAMP NOT NULL,
|
||||||
|
pendingEmails INTEGER NOT NULL DEFAULT 0,
|
||||||
|
sendReplyNotifications BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
sendModeratorNotifications BOOLEAN NOT NULL DEFAULT true
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS unsubscribeSecretHexIndex ON emails(unsubscribeSecretHex);
|
||||||
|
|
||||||
|
-- Which comments should be sent?
|
||||||
|
-- Possible values: all, pending-moderation, none
|
||||||
|
-- Default to pending-moderation because this is critical. If the user forgets
|
||||||
|
-- to moderate, some comments will never see the light of day.
|
||||||
|
ALTER TABLE domains
|
||||||
|
ADD COLUMN emailNotificationPolicy TEXT DEFAULT 'pending-moderation';
|
||||||
|
|
||||||
|
-- Each page now needs to store the title of the page.
|
||||||
|
ALTER TABLE pages
|
||||||
|
ADD COLUMN title TEXT DEFAULT '';
|
||||||
2
db/20190218173502-v1.6.0.sql
Normal file
2
db/20190218173502-v1.6.0.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
UPDATE config
|
||||||
|
SET version = 'v1.6.0';
|
||||||
0
db/20190218183556-v1.6.1.sql
Normal file
0
db/20190218183556-v1.6.1.sql
Normal file
@@ -19,8 +19,6 @@ services:
|
|||||||
POSTGRES_DB: commento
|
POSTGRES_DB: commento
|
||||||
POSTGRES_USER: postgres
|
POSTGRES_USER: postgres
|
||||||
POSTGRES_PASSWORD: postgres
|
POSTGRES_PASSWORD: postgres
|
||||||
ports:
|
|
||||||
- 5432:5432
|
|
||||||
networks:
|
networks:
|
||||||
- db_network
|
- db_network
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
@@ -160,35 +160,36 @@
|
|||||||
<ul class="tabs">
|
<ul class="tabs">
|
||||||
<li class="tab-link original current" data-tab="mod-tab-1">General</li>
|
<li class="tab-link original current" data-tab="mod-tab-1">General</li>
|
||||||
<li class="tab-link" data-tab="mod-tab-2">Add/Remove Moderators</li>
|
<li class="tab-link" data-tab="mod-tab-2">Add/Remove Moderators</li>
|
||||||
|
<li class="tab-link" data-tab="mod-tab-3">Email Settings</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<div id="mod-tab-1" class="content original current">
|
<div id="mod-tab-1" class="content original current">
|
||||||
<div class="row no-border round-check">
|
<div class="row no-border commento-round-check">
|
||||||
<input type="checkbox" class="switch" v-model="domains[cd].autoSpamFilter" id="spam-filtering">
|
<input type="checkbox" v-model="domains[cd].autoSpamFilter" id="spam-filtering">
|
||||||
<label for="spam-filtering">Automatic spam filtering</label>
|
<label for="spam-filtering">Automatic spam filtering</label>
|
||||||
<div class="pitch">
|
<div class="pitch">
|
||||||
Commento uses Akismet's advanced spam detection to automatically identify and remove spam comments. This is strongly recommended. Requires backend configuration.
|
Commento uses Akismet's advanced spam detection to automatically identify and remove spam comments. This is strongly recommended. Requires backend configuration.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row no-border round-check">
|
<div class="row no-border commento-round-check">
|
||||||
<input type="checkbox" class="switch" v-model="domains[cd].requireModeration" id="require-moderation">
|
<input type="checkbox" v-model="domains[cd].requireModeration" id="require-moderation">
|
||||||
<label for="require-moderation">Require all comments to be approved manually</label>
|
<label for="require-moderation">Require all comments to be approved manually</label>
|
||||||
<div class="pitch">
|
<div class="pitch">
|
||||||
Enabling this would require a moderator to approve all comments. This is generally recommended if your site doesn't receive too much traffic.
|
Enabling this would require a moderator to approve all comments. This is generally recommended if your site doesn't receive too much traffic.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row no-border round-check">
|
<div class="row no-border commento-round-check">
|
||||||
<input type="checkbox" class="switch" v-model="domains[cd].allowAnonymous" id="allow-anonymous">
|
<input type="checkbox" v-model="domains[cd].allowAnonymous" id="allow-anonymous">
|
||||||
<label for="allow-anonymous">Allow anonymous comments</label>
|
<label for="allow-anonymous">Allow anonymous comments</label>
|
||||||
<div class="pitch">
|
<div class="pitch">
|
||||||
Enabling this would allow your readers to comment anonymously. Disabling would require the to authenticate themselves (using their Google account, for example). Recommended.
|
Enabling this would allow your readers to comment anonymously. Disabling would require the to authenticate themselves (using their Google account, for example). Recommended.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row no-border round-check indent" v-if="domains[cd].allowAnonymous">
|
<div class="row no-border commento-round-check indent" v-if="domains[cd].allowAnonymous">
|
||||||
<input type="checkbox" class="switch" v-model="domains[cd].moderateAllAnonymous" id="moderate-all-anonymous">
|
<input type="checkbox" v-model="domains[cd].moderateAllAnonymous" id="moderate-all-anonymous">
|
||||||
<label for="moderate-all-anonymous">Require anonymous comments to be approved manually</label>
|
<label for="moderate-all-anonymous">Require anonymous comments to be approved manually</label>
|
||||||
<div class="pitch">
|
<div class="pitch">
|
||||||
Enabling this would require a moderator to approve all anonymous comments. This is recommended if most of your spam comments are from anonymous users.
|
Enabling this would require a moderator to approve all anonymous comments. This is recommended if most of your spam comments are from anonymous users.
|
||||||
@@ -201,7 +202,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="mod-tab-2" class="content">
|
<div id="mod-tab-2" class="content">
|
||||||
<div class="pitch">
|
<div class="normal-text">
|
||||||
Moderators have the power to approve/delete comments and lock threads. Once you add an user as a moderator, shiny new buttons will appear on each comment on each page when they log in.<br><br>
|
Moderators have the power to approve/delete comments and lock threads. Once you add an user as a moderator, shiny new buttons will appear on each comment on each page when they log in.<br><br>
|
||||||
|
|
||||||
You're still the only administrator and the only person who can add and remove moderators. Moderators do not have access to this dashboard. Their access is restricted to pages on your website.
|
You're still the only administrator and the only person who can add and remove moderators. Moderators do not have access to this dashboard. Their access is restricted to pages on your website.
|
||||||
@@ -222,6 +223,31 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="mod-tab-3" class="content">
|
||||||
|
<div class="normal-text">
|
||||||
|
You can enable email notifications to notify your moderators when a new comment is posted or when a comment is pending moderation. Commento tries to be smart about how often an email is sent. Emails will be delayed and batched until you go 10 minutes without one. This requires valid SMTP settings in order to send emails.<br><br>
|
||||||
|
</div>
|
||||||
|
<div class="question">
|
||||||
|
When do you want emails sent to moderators?
|
||||||
|
</div>
|
||||||
|
<div class="row no-border commento-round-check indent">
|
||||||
|
<input type="radio" id="email-all" value="all" v-model="domains[cd].emailNotificationPolicy">
|
||||||
|
<label for="email-all">Whenever a new comment is created</label>
|
||||||
|
</div>
|
||||||
|
<div class="row no-border commento-round-check indent">
|
||||||
|
<input type="radio" id="email-pending-moderation" value="pending-moderation" v-model="domains[cd].emailNotificationPolicy">
|
||||||
|
<label for="email-pending-moderation">Only for comments pending moderation</label>
|
||||||
|
</div>
|
||||||
|
<div class="row no-border commento-round-check indent">
|
||||||
|
<input type="radio" id="email-none" value="none" v-model="domains[cd].emailNotificationPolicy">
|
||||||
|
<label for="email-none">Do not email moderators</label>
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
<div class="center">
|
||||||
|
<button id="save-general-button" onclick="window.commento.generalSaveHandler()" class="button">Save Changes</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -236,8 +262,7 @@
|
|||||||
<div class="tab">
|
<div class="tab">
|
||||||
<ul class="tabs">
|
<ul class="tabs">
|
||||||
<li class="tab-link original current" data-tab="configure-tab-1">General</li>
|
<li class="tab-link original current" data-tab="configure-tab-1">General</li>
|
||||||
<!-- <li class="tab-link" data-tab="configure-tab-2">Email Settings</li> -->
|
<li class="tab-link" data-tab="configure-tab-2">Export Data</li>
|
||||||
<li class="tab-link" data-tab="configure-tab-3">Export Data</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<div id="configure-tab-1" class="content original current">
|
<div id="configure-tab-1" class="content original current">
|
||||||
@@ -252,12 +277,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!--
|
|
||||||
<div id="configure-tab-2" class="content">
|
<div id="configure-tab-2" class="content">
|
||||||
</div>
|
|
||||||
-->
|
|
||||||
|
|
||||||
<div id="configure-tab-3" class="content">
|
|
||||||
<div class="normal-text">
|
<div class="normal-text">
|
||||||
You can export an archive of this domain's data (which includes all comments and commenters) in the JSON format. To initiate and queue an archive request, click the button below. You will receive an email containing the archive once it's ready.<br><br>
|
You can export an archive of this domain's data (which includes all comments and commenters) in the JSON format. To initiate and queue an archive request, click the button below. You will receive an email containing the archive once it's ready.<br><br>
|
||||||
|
|
||||||
|
|||||||
BIN
frontend/fonts/source-sans-400-cyrillic-ext.woff2
Normal file
BIN
frontend/fonts/source-sans-400-cyrillic-ext.woff2
Normal file
Binary file not shown.
BIN
frontend/fonts/source-sans-400-cyrillic.woff2
Normal file
BIN
frontend/fonts/source-sans-400-cyrillic.woff2
Normal file
Binary file not shown.
BIN
frontend/fonts/source-sans-400-greek-ext.woff2
Normal file
BIN
frontend/fonts/source-sans-400-greek-ext.woff2
Normal file
Binary file not shown.
BIN
frontend/fonts/source-sans-400-greek.woff2
Normal file
BIN
frontend/fonts/source-sans-400-greek.woff2
Normal file
Binary file not shown.
BIN
frontend/fonts/source-sans-400-latin-ext.woff2
Normal file
BIN
frontend/fonts/source-sans-400-latin-ext.woff2
Normal file
Binary file not shown.
BIN
frontend/fonts/source-sans-400-latin.woff2
Normal file
BIN
frontend/fonts/source-sans-400-latin.woff2
Normal file
Binary file not shown.
BIN
frontend/fonts/source-sans-400-vietnamese.woff2
Normal file
BIN
frontend/fonts/source-sans-400-vietnamese.woff2
Normal file
Binary file not shown.
BIN
frontend/fonts/source-sans-700-cyrillic-ext.woff2
Normal file
BIN
frontend/fonts/source-sans-700-cyrillic-ext.woff2
Normal file
Binary file not shown.
BIN
frontend/fonts/source-sans-700-cyrillic.woff2
Normal file
BIN
frontend/fonts/source-sans-700-cyrillic.woff2
Normal file
Binary file not shown.
BIN
frontend/fonts/source-sans-700-greek-ext.woff2
Normal file
BIN
frontend/fonts/source-sans-700-greek-ext.woff2
Normal file
Binary file not shown.
BIN
frontend/fonts/source-sans-700-greek.woff2
Normal file
BIN
frontend/fonts/source-sans-700-greek.woff2
Normal file
Binary file not shown.
BIN
frontend/fonts/source-sans-700-latin-ext.woff2
Normal file
BIN
frontend/fonts/source-sans-700-latin-ext.woff2
Normal file
Binary file not shown.
BIN
frontend/fonts/source-sans-700-latin.woff2
Normal file
BIN
frontend/fonts/source-sans-700-latin.woff2
Normal file
Binary file not shown.
BIN
frontend/fonts/source-sans-700-vietnamese.woff2
Normal file
BIN
frontend/fonts/source-sans-700-vietnamese.woff2
Normal file
Binary file not shown.
@@ -14,6 +14,8 @@ const develPath = "build/devel/";
|
|||||||
const prodPath = "build/prod/";
|
const prodPath = "build/prod/";
|
||||||
const scssSrc = "./sass/*.scss";
|
const scssSrc = "./sass/*.scss";
|
||||||
const cssDir = "css/";
|
const cssDir = "css/";
|
||||||
|
const fontsDir = "fonts/";
|
||||||
|
const fontsGlob = fontsDir + "**/*";
|
||||||
const imagesDir = "images/";
|
const imagesDir = "images/";
|
||||||
const imagesGlob = imagesDir + "**/*";
|
const imagesGlob = imagesDir + "**/*";
|
||||||
const jsDir = "js/";
|
const jsDir = "js/";
|
||||||
@@ -74,6 +76,12 @@ const jsCompileMap = {
|
|||||||
"js/logout.js"
|
"js/logout.js"
|
||||||
],
|
],
|
||||||
"js/commento.js": ["js/commento.js"],
|
"js/commento.js": ["js/commento.js"],
|
||||||
|
"js/unsubscribe.js": [
|
||||||
|
"js/constants.js",
|
||||||
|
"js/utils.js",
|
||||||
|
"js/http.js",
|
||||||
|
"js/unsubscribe.js",
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
gulp.task("scss-devel", function () {
|
gulp.task("scss-devel", function () {
|
||||||
@@ -101,6 +109,14 @@ gulp.task("html-prod", function () {
|
|||||||
.pipe(gulp.dest(prodPath))
|
.pipe(gulp.dest(prodPath))
|
||||||
});
|
});
|
||||||
|
|
||||||
|
gulp.task("fonts-devel", function () {
|
||||||
|
gulp.src([fontsGlob]).pipe(gulp.dest(develPath + fontsDir));
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task("fonts-prod", function () {
|
||||||
|
gulp.src([fontsGlob]).pipe(gulp.dest(prodPath + fontsDir));
|
||||||
|
});
|
||||||
|
|
||||||
gulp.task("images-devel", function () {
|
gulp.task("images-devel", function () {
|
||||||
gulp.src([imagesGlob]).pipe(gulp.dest(develPath + imagesDir));
|
gulp.src([imagesGlob]).pipe(gulp.dest(develPath + imagesDir));
|
||||||
});
|
});
|
||||||
@@ -136,5 +152,5 @@ gulp.task("lint", function () {
|
|||||||
.pipe(eslint.failAfterError())
|
.pipe(eslint.failAfterError())
|
||||||
});
|
});
|
||||||
|
|
||||||
gulp.task("devel", ["scss-devel", "html-devel", "images-devel", "lint", "js-devel"]);
|
gulp.task("devel", ["scss-devel", "html-devel", "fonts-devel", "images-devel", "lint", "js-devel"]);
|
||||||
gulp.task("prod", ["scss-prod", "html-prod", "images-prod", "lint", "js-prod"]);
|
gulp.task("prod", ["scss-prod", "html-prod", "fonts-prod", "images-prod", "lint", "js-prod"]);
|
||||||
|
|||||||
@@ -21,10 +21,10 @@
|
|||||||
|
|
||||||
var ID_ROOT = "commento";
|
var ID_ROOT = "commento";
|
||||||
var ID_MAIN_AREA = "commento-main-area";
|
var ID_MAIN_AREA = "commento-main-area";
|
||||||
|
var ID_LOGIN = "commento-login";
|
||||||
var ID_LOGIN_BOX_CONTAINER = "commento-login-box-container";
|
var ID_LOGIN_BOX_CONTAINER = "commento-login-box-container";
|
||||||
var ID_LOGIN_BOX = "commento-login-box";
|
var ID_LOGIN_BOX = "commento-login-box";
|
||||||
var ID_LOGIN_BOX_HEADER = "commento-login-box-header";
|
var ID_LOGIN_BOX_EMAIL_SUBTITLE = "commento-login-box-email-subtitle";
|
||||||
var ID_LOGIN_BOX_SUBTITLE = "commento-login-box-subtitle";
|
|
||||||
var ID_LOGIN_BOX_EMAIL_INPUT = "commento-login-box-email-input";
|
var ID_LOGIN_BOX_EMAIL_INPUT = "commento-login-box-email-input";
|
||||||
var ID_LOGIN_BOX_PASSWORD_INPUT = "commento-login-box-password-input";
|
var ID_LOGIN_BOX_PASSWORD_INPUT = "commento-login-box-password-input";
|
||||||
var ID_LOGIN_BOX_NAME_INPUT = "commento-login-box-name-input";
|
var ID_LOGIN_BOX_NAME_INPUT = "commento-login-box-name-input";
|
||||||
@@ -35,7 +35,6 @@
|
|||||||
var ID_LOGIN_BOX_HR = "commento-login-box-hr";
|
var ID_LOGIN_BOX_HR = "commento-login-box-hr";
|
||||||
var ID_LOGIN_BOX_OAUTH_PRETEXT = "commento-login-box-oauth-pretext";
|
var ID_LOGIN_BOX_OAUTH_PRETEXT = "commento-login-box-oauth-pretext";
|
||||||
var ID_LOGIN_BOX_OAUTH_BUTTONS_CONTAINER = "commento-login-box-oauth-buttons-container";
|
var ID_LOGIN_BOX_OAUTH_BUTTONS_CONTAINER = "commento-login-box-oauth-buttons-container";
|
||||||
var ID_LOGIN_BOX_ANONYMOUS_BUTTON = "commento-login-box-anonymous-button";
|
|
||||||
var ID_MOD_TOOLS = "commento-mod-tools";
|
var ID_MOD_TOOLS = "commento-mod-tools";
|
||||||
var ID_MOD_TOOLS_LOCK_BUTTON = "commento-mod-tools-lock-button";
|
var ID_MOD_TOOLS_LOCK_BUTTON = "commento-mod-tools-lock-button";
|
||||||
var ID_ERROR = "commento-error";
|
var ID_ERROR = "commento-error";
|
||||||
@@ -44,6 +43,7 @@
|
|||||||
var ID_SUPER_CONTAINER = "commento-textarea-super-container-";
|
var ID_SUPER_CONTAINER = "commento-textarea-super-container-";
|
||||||
var ID_TEXTAREA_CONTAINER = "commento-textarea-container-";
|
var ID_TEXTAREA_CONTAINER = "commento-textarea-container-";
|
||||||
var ID_TEXTAREA = "commento-textarea-";
|
var ID_TEXTAREA = "commento-textarea-";
|
||||||
|
var ID_ANONYMOUS_CHECKBOX = "commento-anonymous-checkbox-";
|
||||||
var ID_CARD = "commento-comment-card-";
|
var ID_CARD = "commento-comment-card-";
|
||||||
var ID_BODY = "commento-comment-body-";
|
var ID_BODY = "commento-comment-body-";
|
||||||
var ID_TEXT = "commento-comment-text-";
|
var ID_TEXT = "commento-comment-text-";
|
||||||
@@ -77,13 +77,12 @@
|
|||||||
var requireIdentification = true;
|
var requireIdentification = true;
|
||||||
var isModerator = false;
|
var isModerator = false;
|
||||||
var isFrozen = false;
|
var isFrozen = false;
|
||||||
var shownSubmitButton = {"root": false};
|
|
||||||
var chosenAnonymous = false;
|
var chosenAnonymous = false;
|
||||||
var isLocked = false;
|
var isLocked = false;
|
||||||
var stickyCommentHex = "none";
|
var stickyCommentHex = "none";
|
||||||
var shownReply = {};
|
var shownReply = {};
|
||||||
var configuredOauths = [];
|
var configuredOauths = [];
|
||||||
var loginBoxType = "signup";
|
var popupBoxType = "login";
|
||||||
var oauthButtonsShown = false;
|
var oauthButtonsShown = false;
|
||||||
var selfHex = undefined;
|
var selfHex = undefined;
|
||||||
|
|
||||||
@@ -226,6 +225,57 @@
|
|||||||
refreshAll();
|
refreshAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function selfLoad(commenter) {
|
||||||
|
commenters[commenter.commenterHex] = commenter;
|
||||||
|
selfHex = commenter.commenterHex;
|
||||||
|
|
||||||
|
var loggedContainer = create("div");
|
||||||
|
var loggedInAs = create("div");
|
||||||
|
var name = create("a");
|
||||||
|
var avatar;
|
||||||
|
var logout = create("div");
|
||||||
|
var color = colorGet(commenter.commenterHex + "-" + commenter.name);
|
||||||
|
|
||||||
|
loggedContainer.id = ID_LOGGED_CONTAINER;
|
||||||
|
|
||||||
|
classAdd(loggedContainer, "logged-container");
|
||||||
|
classAdd(loggedInAs, "logged-in-as");
|
||||||
|
classAdd(name, "name");
|
||||||
|
classAdd(logout, "logout");
|
||||||
|
|
||||||
|
name.innerText = commenter.name;
|
||||||
|
logout.innerText = "Logout";
|
||||||
|
|
||||||
|
onclick(logout, global.logout);
|
||||||
|
|
||||||
|
attrSet(loggedContainer, "style", "display: none");
|
||||||
|
attrSet(name, "href", commenter.link);
|
||||||
|
if (commenter.photo === "undefined") {
|
||||||
|
avatar = create("div");
|
||||||
|
avatar.style["background"] = color;
|
||||||
|
avatar.innerHTML = commenter.name[0].toUpperCase();
|
||||||
|
classAdd(avatar, "avatar");
|
||||||
|
} else {
|
||||||
|
avatar = create("img");
|
||||||
|
if (commenter.provider === "google") {
|
||||||
|
attrSet(avatar, "src", commenter.photo + "?sz=50");
|
||||||
|
} else if (commenter.provider === "github") {
|
||||||
|
attrSet(avatar, "src", commenter.photo + "&s=50");
|
||||||
|
} else {
|
||||||
|
attrSet(avatar, "src", commenter.photo);
|
||||||
|
}
|
||||||
|
classAdd(avatar, "avatar-img");
|
||||||
|
}
|
||||||
|
|
||||||
|
append(loggedInAs, avatar);
|
||||||
|
append(loggedInAs, name);
|
||||||
|
append(loggedContainer, loggedInAs);
|
||||||
|
append(loggedContainer, logout);
|
||||||
|
prepend(root, loggedContainer);
|
||||||
|
|
||||||
|
isAuthenticated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function selfGet(callback) {
|
function selfGet(callback) {
|
||||||
var commenterToken = commenterTokenGet();
|
var commenterToken = commenterTokenGet();
|
||||||
@@ -246,54 +296,7 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
commenters[resp.commenter.commenterHex] = resp.commenter;
|
selfLoad(resp.commenter);
|
||||||
selfHex = resp.commenter.commenterHex;
|
|
||||||
|
|
||||||
var loggedContainer = create("div");
|
|
||||||
var loggedInAs = create("div");
|
|
||||||
var name = create("a");
|
|
||||||
var avatar;
|
|
||||||
var logout = create("div");
|
|
||||||
var color = colorGet(resp.commenter.commenterHex + "-" + resp.commenter.name);
|
|
||||||
|
|
||||||
loggedContainer.id = ID_LOGGED_CONTAINER;
|
|
||||||
|
|
||||||
classAdd(loggedContainer, "logged-container");
|
|
||||||
classAdd(loggedInAs, "logged-in-as");
|
|
||||||
classAdd(name, "name");
|
|
||||||
classAdd(logout, "logout");
|
|
||||||
|
|
||||||
name.innerText = resp.commenter.name;
|
|
||||||
logout.innerText = "Logout";
|
|
||||||
|
|
||||||
onclick(logout, global.logout);
|
|
||||||
|
|
||||||
attrSet(loggedContainer, "style", "display: none");
|
|
||||||
attrSet(name, "href", resp.commenter.link);
|
|
||||||
if (resp.commenter.photo === "undefined") {
|
|
||||||
avatar = create("div");
|
|
||||||
avatar.style["background"] = color;
|
|
||||||
avatar.innerHTML = resp.commenter.name[0].toUpperCase();
|
|
||||||
classAdd(avatar, "avatar");
|
|
||||||
} else {
|
|
||||||
avatar = create("img");
|
|
||||||
if (resp.commenter.provider === "google") {
|
|
||||||
attrSet(avatar, "src", resp.commenter.photo + "?sz=50");
|
|
||||||
} else if (resp.commenter.provider === "github") {
|
|
||||||
attrSet(avatar, "src", resp.commenter.photo + "&s=50");
|
|
||||||
} else {
|
|
||||||
attrSet(avatar, "src", resp.commenter.photo);
|
|
||||||
}
|
|
||||||
classAdd(avatar, "avatar-img");
|
|
||||||
}
|
|
||||||
|
|
||||||
append(loggedInAs, avatar);
|
|
||||||
append(loggedInAs, name);
|
|
||||||
append(loggedContainer, loggedInAs);
|
|
||||||
append(loggedContainer, logout);
|
|
||||||
append(root, loggedContainer);
|
|
||||||
|
|
||||||
isAuthenticated = true;
|
|
||||||
|
|
||||||
call(callback);
|
call(callback);
|
||||||
});
|
});
|
||||||
@@ -346,8 +349,8 @@
|
|||||||
function commentsGet(callback) {
|
function commentsGet(callback) {
|
||||||
var json = {
|
var json = {
|
||||||
"commenterToken": commenterTokenGet(),
|
"commenterToken": commenterTokenGet(),
|
||||||
"domain": location.host,
|
"domain": parent.location.host,
|
||||||
"path": location.pathname,
|
"path": parent.location.pathname,
|
||||||
};
|
};
|
||||||
|
|
||||||
post(origin + "/api/comment/list", json, function(resp) {
|
post(origin + "/api/comment/list", json, function(resp) {
|
||||||
@@ -407,82 +410,81 @@
|
|||||||
var textareaSuperContainer = create("div");
|
var textareaSuperContainer = create("div");
|
||||||
var textareaContainer = create("div");
|
var textareaContainer = create("div");
|
||||||
var textarea = create("textarea");
|
var textarea = create("textarea");
|
||||||
|
var anonymousCheckboxContainer = create("div");
|
||||||
|
var anonymousCheckbox = create("input");
|
||||||
|
var anonymousCheckboxLabel = create("label");
|
||||||
|
var submitButton = create("button");
|
||||||
|
|
||||||
textareaSuperContainer.id = ID_SUPER_CONTAINER + id;
|
textareaSuperContainer.id = ID_SUPER_CONTAINER + id;
|
||||||
textareaContainer.id = ID_TEXTAREA_CONTAINER + id;
|
textareaContainer.id = ID_TEXTAREA_CONTAINER + id;
|
||||||
textarea.id = ID_TEXTAREA + id;
|
textarea.id = ID_TEXTAREA + id;
|
||||||
|
anonymousCheckbox.id = ID_ANONYMOUS_CHECKBOX + id;
|
||||||
|
submitButton.id = ID_SUBMIT_BUTTON + id;
|
||||||
|
|
||||||
classAdd(textareaContainer, "textarea-container");
|
classAdd(textareaContainer, "textarea-container");
|
||||||
|
classAdd(anonymousCheckboxContainer, "round-check");
|
||||||
|
classAdd(anonymousCheckboxContainer, "anonymous-checkbox-container");
|
||||||
|
classAdd(submitButton, "button");
|
||||||
|
classAdd(submitButton, "submit-button");
|
||||||
|
classAdd(textareaSuperContainer, "button-margin");
|
||||||
|
|
||||||
if (!isAuthenticated && !chosenAnonymous) {
|
attrSet(textarea, "placeholder", "Add a comment");
|
||||||
var buttonsContainer = create("div");
|
attrSet(anonymousCheckbox, "type", "checkbox");
|
||||||
var question = create("div");
|
attrSet(anonymousCheckboxLabel, "for", ID_ANONYMOUS_CHECKBOX + id);
|
||||||
var buttons = create("div");
|
|
||||||
var createButton = create("div");
|
|
||||||
var loginButton = create("div");
|
|
||||||
var anonymousButton = create("div");
|
|
||||||
|
|
||||||
classAdd(buttonsContainer, "account-buttons-container");
|
anonymousCheckboxLabel.innerText = "Comment anonymously";
|
||||||
classAdd(question, "account-buttons-question");
|
submitButton.innerText = "Add Comment";
|
||||||
classAdd(buttons, "account-buttons");
|
|
||||||
classAdd(createButton, "button");
|
|
||||||
classAdd(createButton, "create-button");
|
|
||||||
classAdd(loginButton, "button");
|
|
||||||
classAdd(loginButton, "login-button");
|
|
||||||
classAdd(anonymousButton, "anonymous-button");
|
|
||||||
|
|
||||||
onclick(createButton, global.loginBoxShow);
|
|
||||||
onclick(loginButton, global.loginBoxShow);
|
|
||||||
onclick(loginButton, global.loginSwitch);
|
|
||||||
onclick(anonymousButton, global.anonymousChoose);
|
|
||||||
|
|
||||||
attrSet(textarea, "disabled", true);
|
|
||||||
|
|
||||||
createButton.innerText = "Create an Account";
|
|
||||||
loginButton.innerText = "Login";
|
|
||||||
anonymousButton.innerText = "Comment anonymously";
|
|
||||||
question.innerText = "Want to add to the discussion?";
|
|
||||||
|
|
||||||
append(buttons, createButton);
|
|
||||||
append(buttons, loginButton);
|
|
||||||
append(buttonsContainer, buttons);
|
|
||||||
if (!requireIdentification) {
|
|
||||||
append(buttonsContainer, anonymousButton);
|
|
||||||
}
|
|
||||||
append(textareaContainer, question);
|
|
||||||
append(textareaContainer, buttonsContainer);
|
|
||||||
} else {
|
|
||||||
onclick(textarea, global.showSubmitButton, id);
|
|
||||||
|
|
||||||
attrSet(textarea, "placeholder", "Join the discussion!");
|
|
||||||
}
|
|
||||||
|
|
||||||
textarea.oninput = autoExpander(textarea);
|
textarea.oninput = autoExpander(textarea);
|
||||||
|
onclick(submitButton, submitAccountDecide, id);
|
||||||
|
|
||||||
append(textareaContainer, textarea);
|
append(textareaContainer, textarea);
|
||||||
append(textareaSuperContainer, textareaContainer);
|
append(textareaSuperContainer, textareaContainer);
|
||||||
|
append(anonymousCheckboxContainer, anonymousCheckbox);
|
||||||
|
append(anonymousCheckboxContainer, anonymousCheckboxLabel);
|
||||||
|
append(textareaSuperContainer, submitButton);
|
||||||
|
if (!requireIdentification) {
|
||||||
|
append(textareaSuperContainer, anonymousCheckboxContainer);
|
||||||
|
}
|
||||||
|
|
||||||
return textareaSuperContainer;
|
return textareaSuperContainer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function rootCreate(callback) {
|
function rootCreate(callback) {
|
||||||
|
var login = create("div");
|
||||||
|
var loginText = create("div");
|
||||||
var mainArea = $(ID_MAIN_AREA);
|
var mainArea = $(ID_MAIN_AREA);
|
||||||
var commentsArea = create("div");
|
var commentsArea = create("div");
|
||||||
|
|
||||||
|
login.id = ID_LOGIN;
|
||||||
commentsArea.id = ID_COMMENTS_AREA;
|
commentsArea.id = ID_COMMENTS_AREA;
|
||||||
|
|
||||||
|
classAdd(login, "login");
|
||||||
|
classAdd(loginText, "login-text");
|
||||||
classAdd(commentsArea, "comments");
|
classAdd(commentsArea, "comments");
|
||||||
|
|
||||||
|
loginText.innerText = "Login";
|
||||||
commentsArea.innerHTML = "";
|
commentsArea.innerHTML = "";
|
||||||
|
|
||||||
|
onclick(loginText, global.loginBoxShow, null);
|
||||||
|
|
||||||
|
append(login, loginText);
|
||||||
|
|
||||||
if (isLocked || isFrozen) {
|
if (isLocked || isFrozen) {
|
||||||
if (isAuthenticated || chosenAnonymous) {
|
if (isAuthenticated || chosenAnonymous) {
|
||||||
append(mainArea, messageCreate("This thread is locked. You cannot add new comments."));
|
append(mainArea, messageCreate("This thread is locked. You cannot add new comments."));
|
||||||
|
remove($(ID_LOGIN));
|
||||||
} else {
|
} else {
|
||||||
|
append(mainArea, login);
|
||||||
append(mainArea, textareaCreate("root"));
|
append(mainArea, textareaCreate("root"));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
append(mainArea, login);
|
||||||
|
} else {
|
||||||
|
remove($(ID_LOGIN));
|
||||||
|
}
|
||||||
append(mainArea, textareaCreate("root"));
|
append(mainArea, textareaCreate("root"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -504,7 +506,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
global.commentNew = function(id) {
|
global.commentNew = function(id, commenterToken, callback) {
|
||||||
var textareaSuperContainer = $(ID_SUPER_CONTAINER + id);
|
var textareaSuperContainer = $(ID_SUPER_CONTAINER + id);
|
||||||
var textarea = $(ID_TEXTAREA + id);
|
var textarea = $(ID_TEXTAREA + id);
|
||||||
var replyButton = $(ID_REPLY + id);
|
var replyButton = $(ID_REPLY + id);
|
||||||
@@ -520,8 +522,8 @@
|
|||||||
|
|
||||||
var json = {
|
var json = {
|
||||||
"commenterToken": commenterTokenGet(),
|
"commenterToken": commenterTokenGet(),
|
||||||
"domain": location.host,
|
"domain": parent.location.host,
|
||||||
"path": location.pathname,
|
"path": parent.location.pathname,
|
||||||
"parentHex": id,
|
"parentHex": id,
|
||||||
"markdown": markdown,
|
"markdown": markdown,
|
||||||
};
|
};
|
||||||
@@ -540,17 +542,18 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (message !== "") {
|
if (message !== "") {
|
||||||
if (id === "root") {
|
prepend($(ID_SUPER_CONTAINER + id), messageCreate(message));
|
||||||
prepend($(ID_SUPER_CONTAINER + id), messageCreate(message));
|
}
|
||||||
} else {
|
|
||||||
append($(ID_BODY + id), messageCreate(message));
|
var commenterHex = selfHex;
|
||||||
}
|
if (commenterHex === undefined || commenterToken === "anonymous") {
|
||||||
|
commenterHex = "anonymous";
|
||||||
}
|
}
|
||||||
|
|
||||||
var newCard = commentsRecurse({
|
var newCard = commentsRecurse({
|
||||||
"root": [{
|
"root": [{
|
||||||
"commentHex": resp.commentHex,
|
"commentHex": resp.commentHex,
|
||||||
"commenterHex": selfHex,
|
"commenterHex": commenterHex,
|
||||||
"markdown": markdown,
|
"markdown": markdown,
|
||||||
"html": resp.html,
|
"html": resp.html,
|
||||||
"parentHex": "root",
|
"parentHex": "root",
|
||||||
@@ -565,7 +568,6 @@
|
|||||||
textareaSuperContainer.replaceWith(newCard);
|
textareaSuperContainer.replaceWith(newCard);
|
||||||
|
|
||||||
shownReply[id] = false;
|
shownReply[id] = false;
|
||||||
shownSubmitButton[id] = false;
|
|
||||||
|
|
||||||
classAdd(replyButton, "option-reply");
|
classAdd(replyButton, "option-reply");
|
||||||
classRemove(replyButton, "option-cancel");
|
classRemove(replyButton, "option-cancel");
|
||||||
@@ -577,6 +579,8 @@
|
|||||||
textarea.value = "";
|
textarea.value = "";
|
||||||
insertAfter(textareaSuperContainer, newCard);
|
insertAfter(textareaSuperContainer, newCard);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
call(callback);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -801,15 +805,11 @@
|
|||||||
if (isAuthenticated) {
|
if (isAuthenticated) {
|
||||||
upDownOnclickSet(upvote, downvote, comment.commentHex, comment.direction);
|
upDownOnclickSet(upvote, downvote, comment.commentHex, comment.direction);
|
||||||
} else {
|
} else {
|
||||||
onclick(upvote, global.loginBoxShow);
|
onclick(upvote, global.loginBoxShow, null);
|
||||||
onclick(downvote, global.loginBoxShow);
|
onclick(downvote, global.loginBoxShow, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isAuthenticated || chosenAnonymous) {
|
onclick(reply, global.replyShow, comment.commentHex);
|
||||||
onclick(reply, global.replyShow, comment.commentHex);
|
|
||||||
} else {
|
|
||||||
onclick(reply, global.loginBoxShow);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (commenter.link !== "undefined" && commenter.link !== "https://undefined" && commenter.link !== "") {
|
if (commenter.link !== "undefined" && commenter.link !== "https://undefined" && commenter.link !== "") {
|
||||||
attrSet(name, "href", commenter.link);
|
attrSet(name, "href", commenter.link);
|
||||||
@@ -868,8 +868,6 @@
|
|||||||
append(card, header);
|
append(card, header);
|
||||||
append(card, contents);
|
append(card, contents);
|
||||||
append(cards, card);
|
append(cards, card);
|
||||||
|
|
||||||
shownSubmitButton[comment.commentHex] = false;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return cards;
|
return cards;
|
||||||
@@ -1002,7 +1000,6 @@
|
|||||||
|
|
||||||
el.remove();
|
el.remove();
|
||||||
shownReply[id] = false;
|
shownReply[id] = false;
|
||||||
shownSubmitButton[id] = false;
|
|
||||||
|
|
||||||
classAdd(replyButton, "option-reply");
|
classAdd(replyButton, "option-reply");
|
||||||
classRemove(replyButton, "option-cancel");
|
classRemove(replyButton, "option-cancel");
|
||||||
@@ -1068,39 +1065,53 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
global.showSubmitButton = function(id) {
|
function submitAuthenticated(id) {
|
||||||
if (id in shownSubmitButton && shownSubmitButton[id]) {
|
if (isAuthenticated) {
|
||||||
|
global.commentNew(id, commenterTokenGet());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
shownSubmitButton[id] = true;
|
global.loginBoxShow(id);
|
||||||
|
return;
|
||||||
var el = $(ID_SUPER_CONTAINER + id);
|
|
||||||
|
|
||||||
var submit = create("button");
|
|
||||||
|
|
||||||
submit.id = ID_SUBMIT_BUTTON + id;
|
|
||||||
|
|
||||||
submit.innerText = "Add Comment";
|
|
||||||
|
|
||||||
classAdd(submit, "button");
|
|
||||||
classAdd(submit, "submit-button");
|
|
||||||
classAdd(el, "button-margin");
|
|
||||||
|
|
||||||
onclick(submit, global.commentNew, id);
|
|
||||||
|
|
||||||
append(el, submit);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
global.anonymousChoose = function() {
|
function submitAnonymous(id) {
|
||||||
cookieSet("commentoCommenterToken", "anonymous");
|
|
||||||
chosenAnonymous = true;
|
chosenAnonymous = true;
|
||||||
refreshAll();
|
global.commentNew(id, "anonymous");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
global.commentoAuth = function(provider) {
|
function submitAccountDecide(id) {
|
||||||
|
if (requireIdentification) {
|
||||||
|
submitAuthenticated(id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var anonymousCheckbox = $(ID_ANONYMOUS_CHECKBOX + id);
|
||||||
|
var textarea = $(ID_TEXTAREA + id);
|
||||||
|
var markdown = textarea.value;
|
||||||
|
|
||||||
|
if (markdown === "") {
|
||||||
|
classAdd(textarea, "red-border");
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
classRemove(textarea, "red-border");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!anonymousCheckbox.checked) {
|
||||||
|
submitAuthenticated(id);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
submitAnonymous(id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
global.commentoAuth = function(data) {
|
||||||
|
var provider = data.provider;
|
||||||
|
var id = data.id;
|
||||||
var popup = window.open("", "_blank");
|
var popup = window.open("", "_blank");
|
||||||
|
|
||||||
get(origin + "/api/commenter/token/new", function(resp) {
|
get(origin + "/api/commenter/token/new", function(resp) {
|
||||||
@@ -1115,8 +1126,22 @@
|
|||||||
|
|
||||||
var interval = setInterval(function() {
|
var interval = setInterval(function() {
|
||||||
if (popup.closed) {
|
if (popup.closed) {
|
||||||
refreshAll();
|
|
||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
|
selfGet(function() {
|
||||||
|
var loggedContainer = $(ID_LOGGED_CONTAINER);
|
||||||
|
if (loggedContainer) {
|
||||||
|
attrSet(loggedContainer, "style", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
remove($(ID_LOGIN));
|
||||||
|
if (id !== null) {
|
||||||
|
global.commentNew(id, resp.commenterToken, function() {
|
||||||
|
global.loginBoxClose();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
global.loginBoxClose();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, 250);
|
}, 250);
|
||||||
});
|
});
|
||||||
@@ -1125,7 +1150,6 @@
|
|||||||
|
|
||||||
function refreshAll(callback) {
|
function refreshAll(callback) {
|
||||||
$(ID_ROOT).innerHTML = "";
|
$(ID_ROOT).innerHTML = "";
|
||||||
shownSubmitButton = {"root": false};
|
|
||||||
shownReply = {};
|
shownReply = {};
|
||||||
global.main(callback);
|
global.main(callback);
|
||||||
}
|
}
|
||||||
@@ -1140,64 +1164,54 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
global.signupRender = function() {
|
global.popupRender = function(id) {
|
||||||
var loginBoxContainer = $(ID_LOGIN_BOX_CONTAINER);
|
var loginBoxContainer = $(ID_LOGIN_BOX_CONTAINER);
|
||||||
var loginBox = create("div");
|
var loginBox = create("div");
|
||||||
var header = create("div");
|
var oauthSubtitle = create("div");
|
||||||
var subtitle = create("div");
|
var oauthButtonsContainer = create("div");
|
||||||
|
var oauthButtons = create("div");
|
||||||
|
var hr = create("hr");
|
||||||
|
var emailSubtitle = create("div");
|
||||||
var emailContainer = create("div");
|
var emailContainer = create("div");
|
||||||
var email = create("div");
|
var email = create("div");
|
||||||
var emailInput = create("input");
|
var emailInput = create("input");
|
||||||
var emailButton = create("button");
|
var emailButton = create("button");
|
||||||
var loginLinkContainer = create("div");
|
var loginLinkContainer = create("div");
|
||||||
var loginLink = create("a");
|
var loginLink = create("a");
|
||||||
var hr = create("hr");
|
|
||||||
var oauthPretext = create("div");
|
|
||||||
var oauthButtonsContainer = create("div");
|
|
||||||
var oauthButtons = create("div");
|
|
||||||
var anonymousButton = create("div");
|
|
||||||
var close = create("div");
|
var close = create("div");
|
||||||
|
|
||||||
loginBox.id = ID_LOGIN_BOX;
|
loginBox.id = ID_LOGIN_BOX;
|
||||||
header.id = ID_LOGIN_BOX_HEADER;
|
emailSubtitle.id = ID_LOGIN_BOX_EMAIL_SUBTITLE;
|
||||||
subtitle.id = ID_LOGIN_BOX_SUBTITLE;
|
|
||||||
emailInput.id = ID_LOGIN_BOX_EMAIL_INPUT;
|
emailInput.id = ID_LOGIN_BOX_EMAIL_INPUT;
|
||||||
emailButton.id = ID_LOGIN_BOX_EMAIL_BUTTON;
|
emailButton.id = ID_LOGIN_BOX_EMAIL_BUTTON;
|
||||||
loginLink.id = ID_LOGIN_BOX_LOGIN_LINK;
|
loginLink.id = ID_LOGIN_BOX_LOGIN_LINK;
|
||||||
loginLinkContainer.id = ID_LOGIN_BOX_LOGIN_LINK_CONTAINER;
|
loginLinkContainer.id = ID_LOGIN_BOX_LOGIN_LINK_CONTAINER;
|
||||||
hr.id = ID_LOGIN_BOX_HR;
|
hr.id = ID_LOGIN_BOX_HR;
|
||||||
oauthPretext.id = ID_LOGIN_BOX_OAUTH_PRETEXT;
|
oauthSubtitle.id = ID_LOGIN_BOX_OAUTH_PRETEXT;
|
||||||
oauthButtonsContainer.id = ID_LOGIN_BOX_OAUTH_BUTTONS_CONTAINER;
|
oauthButtonsContainer.id = ID_LOGIN_BOX_OAUTH_BUTTONS_CONTAINER;
|
||||||
anonymousButton.id = ID_LOGIN_BOX_ANONYMOUS_BUTTON;
|
|
||||||
|
|
||||||
header.innerText = "Create an account to join the discussion!";
|
|
||||||
|
|
||||||
classAdd(loginBoxContainer, "login-box-container");
|
classAdd(loginBoxContainer, "login-box-container");
|
||||||
classAdd(loginBox, "login-box");
|
classAdd(loginBox, "login-box");
|
||||||
classAdd(header, "login-box-header");
|
classAdd(emailSubtitle, "login-box-subtitle");
|
||||||
classAdd(subtitle, "login-box-subtitle");
|
|
||||||
classAdd(emailContainer, "email-container");
|
classAdd(emailContainer, "email-container");
|
||||||
classAdd(email, "email");
|
classAdd(email, "email");
|
||||||
classAdd(emailInput, "input");
|
classAdd(emailInput, "input");
|
||||||
classAdd(emailButton, "email-button");
|
classAdd(emailButton, "email-button");
|
||||||
classAdd(loginLinkContainer, "login-link-container");
|
classAdd(loginLinkContainer, "login-link-container");
|
||||||
classAdd(loginLink, "login-link");
|
classAdd(loginLink, "login-link");
|
||||||
classAdd(oauthPretext, "login-box-subtitle");
|
classAdd(oauthSubtitle, "login-box-subtitle");
|
||||||
classAdd(oauthButtonsContainer, "oauth-buttons-container");
|
classAdd(oauthButtonsContainer, "oauth-buttons-container");
|
||||||
classAdd(oauthButtons, "oauth-buttons");
|
classAdd(oauthButtons, "oauth-buttons");
|
||||||
classAdd(anonymousButton, "anonymous-button");
|
|
||||||
classAdd(close, "login-box-close");
|
classAdd(close, "login-box-close");
|
||||||
classAdd(root, "root-min-height");
|
classAdd(root, "root-min-height");
|
||||||
|
|
||||||
|
loginLink.innerText = "Don't have an account? Sign up.";
|
||||||
|
emailSubtitle.innerText = "Login with your email address";
|
||||||
emailButton.innerText = "Continue";
|
emailButton.innerText = "Continue";
|
||||||
loginLink.innerText = "Already have an account? Log in.";
|
oauthSubtitle.innerText = "Proceed with social login";
|
||||||
subtitle.innerText = "Sign up with your email to vote and comment.";
|
|
||||||
oauthPretext.innerText = "Or proceed with social login.";
|
|
||||||
anonymousButton.innerText = "Comment anonymously";
|
|
||||||
|
|
||||||
onclick(emailButton, global.passwordAsk);
|
onclick(emailButton, global.passwordAsk, id);
|
||||||
onclick(loginLink, global.loginSwitch);
|
onclick(loginLink, global.popupSwitch);
|
||||||
onclick(anonymousButton, global.anonymousChoose);
|
|
||||||
onclick(close, global.loginBoxClose);
|
onclick(close, global.loginBoxClose);
|
||||||
|
|
||||||
attrSet(loginBoxContainer, "style", "display: none; opacity: 0;");
|
attrSet(loginBoxContainer, "style", "display: none; opacity: 0;");
|
||||||
@@ -1213,14 +1227,22 @@
|
|||||||
|
|
||||||
button.innerText = configuredOauths[i];
|
button.innerText = configuredOauths[i];
|
||||||
|
|
||||||
onclick(button, global.commentoAuth, configuredOauths[i]);
|
onclick(button, global.commentoAuth, {"provider": configuredOauths[i], "id": id});
|
||||||
|
|
||||||
append(oauthButtons, button);
|
append(oauthButtons, button);
|
||||||
}
|
}
|
||||||
|
|
||||||
append(loginBox, header);
|
if (configuredOauths.length > 0) {
|
||||||
append(loginBox, subtitle);
|
append(loginBox, oauthSubtitle);
|
||||||
|
append(oauthButtonsContainer, oauthButtons);
|
||||||
|
append(loginBox, oauthButtonsContainer);
|
||||||
|
append(loginBox, hr);
|
||||||
|
oauthButtonsShown = true;
|
||||||
|
} else {
|
||||||
|
oauthButtonsShown = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
append(loginBox, emailSubtitle);
|
||||||
append(email, emailInput);
|
append(email, emailInput);
|
||||||
append(email, emailButton);
|
append(email, emailButton);
|
||||||
append(emailContainer, email);
|
append(emailContainer, email);
|
||||||
@@ -1229,59 +1251,31 @@
|
|||||||
append(loginLinkContainer, loginLink);
|
append(loginLinkContainer, loginLink);
|
||||||
append(loginBox, loginLinkContainer);
|
append(loginBox, loginLinkContainer);
|
||||||
|
|
||||||
if (configuredOauths.length > 0) {
|
|
||||||
append(loginBox, hr);
|
|
||||||
append(loginBox, oauthPretext);
|
|
||||||
append(oauthButtonsContainer, oauthButtons);
|
|
||||||
append(loginBox, oauthButtonsContainer);
|
|
||||||
if (!requireIdentification) {
|
|
||||||
append(loginBox, anonymousButton);
|
|
||||||
}
|
|
||||||
oauthButtonsShown = true;
|
|
||||||
} else {
|
|
||||||
oauthButtonsShown = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
append(loginBox, close);
|
append(loginBox, close);
|
||||||
|
|
||||||
loginBoxType = "signup";
|
popupBoxType = "login";
|
||||||
loginBoxContainer.innerHTML = "";
|
loginBoxContainer.innerHTML = "";
|
||||||
append(loginBoxContainer, loginBox);
|
append(loginBoxContainer, loginBox);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
global.loginSwitch = function(leaveOauth) {
|
global.popupSwitch = function() {
|
||||||
var header = $(ID_LOGIN_BOX_HEADER);
|
var emailSubtitle = $(ID_LOGIN_BOX_EMAIL_SUBTITLE);
|
||||||
var subtitle = $(ID_LOGIN_BOX_SUBTITLE);
|
|
||||||
var loginLink = $(ID_LOGIN_BOX_LOGIN_LINK);
|
var loginLink = $(ID_LOGIN_BOX_LOGIN_LINK);
|
||||||
var hr = $(ID_LOGIN_BOX_HR);
|
|
||||||
var oauthButtonsContainer = $(ID_LOGIN_BOX_OAUTH_BUTTONS_CONTAINER);
|
|
||||||
var oauthPretext = $(ID_LOGIN_BOX_OAUTH_PRETEXT);
|
|
||||||
|
|
||||||
header.innerText = "Login to continue";
|
if (popupBoxType === "login") {
|
||||||
loginLink.innerText = "Don't have an account? Sign up.";
|
loginLink.innerText = "Already have an account? Log in.";
|
||||||
subtitle.innerText = "Enter your email address to start with.";
|
emailSubtitle.innerText = "Create an account";
|
||||||
|
popupBoxType = "signup";
|
||||||
onclick(loginLink, global.signupSwitch);
|
} else {
|
||||||
|
loginLink.innerText = "Don't have an account? Sign up.";
|
||||||
loginBoxType = "login";
|
emailSubtitle.innerText = "Login with your email address";
|
||||||
|
popupBoxType = "login";
|
||||||
if (ouathButtonsShown && leaveOauth !== true && configuredOauths.length > 0) {
|
|
||||||
remove(hr);
|
|
||||||
remove(oauthPretext);
|
|
||||||
remove(oauthButtonsContainer);
|
|
||||||
oauthButtonsShown = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
global.signupSwitch = function() {
|
function loginUP(username, password, id) {
|
||||||
global.loginBoxClose();
|
|
||||||
global.loginBoxShow();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function loginUP(username, password) {
|
|
||||||
var json = {
|
var json = {
|
||||||
"email": username,
|
"email": username,
|
||||||
"password": password,
|
"password": password,
|
||||||
@@ -1295,20 +1289,34 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
cookieSet("commentoCommenterToken", resp.commenterToken);
|
cookieSet("commentoCommenterToken", resp.commenterToken);
|
||||||
refreshAll();
|
|
||||||
|
selfLoad(resp.commenter);
|
||||||
|
var loggedContainer = $(ID_LOGGED_CONTAINER);
|
||||||
|
if (loggedContainer) {
|
||||||
|
attrSet(loggedContainer, "style", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
remove($(ID_LOGIN));
|
||||||
|
if (id !== null) {
|
||||||
|
global.commentNew(id, resp.commenterToken, function() {
|
||||||
|
global.loginBoxClose();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
global.loginBoxClose();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
global.login = function() {
|
global.login = function(id) {
|
||||||
var email = $(ID_LOGIN_BOX_EMAIL_INPUT);
|
var email = $(ID_LOGIN_BOX_EMAIL_INPUT);
|
||||||
var password = $(ID_LOGIN_BOX_PASSWORD_INPUT);
|
var password = $(ID_LOGIN_BOX_PASSWORD_INPUT);
|
||||||
|
|
||||||
loginUP(email.value, password.value);
|
loginUP(email.value, password.value, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
global.signup = function() {
|
global.signup = function(id) {
|
||||||
var email = $(ID_LOGIN_BOX_EMAIL_INPUT);
|
var email = $(ID_LOGIN_BOX_EMAIL_INPUT);
|
||||||
var name = $(ID_LOGIN_BOX_NAME_INPUT);
|
var name = $(ID_LOGIN_BOX_NAME_INPUT);
|
||||||
var website = $(ID_LOGIN_BOX_WEBSITE_INPUT);
|
var website = $(ID_LOGIN_BOX_WEBSITE_INPUT);
|
||||||
@@ -1328,24 +1336,19 @@
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
loginUP(email.value, password.value);
|
loginUP(email.value, password.value, id);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
global.passwordAsk = function() {
|
global.passwordAsk = function(id) {
|
||||||
var loginBox = $(ID_LOGIN_BOX);
|
var loginBox = $(ID_LOGIN_BOX);
|
||||||
var subtitle = $(ID_LOGIN_BOX_SUBTITLE);
|
var subtitle = $(ID_LOGIN_BOX_EMAIL_SUBTITLE);
|
||||||
var emailButton = $(ID_LOGIN_BOX_EMAIL_BUTTON);
|
var emailButton = $(ID_LOGIN_BOX_EMAIL_BUTTON);
|
||||||
var loginLinkContainer = $(ID_LOGIN_BOX_LOGIN_LINK_CONTAINER);
|
var loginLinkContainer = $(ID_LOGIN_BOX_LOGIN_LINK_CONTAINER);
|
||||||
var hr = $(ID_LOGIN_BOX_HR);
|
var hr = $(ID_LOGIN_BOX_HR);
|
||||||
var oauthButtonsContainer = $(ID_LOGIN_BOX_OAUTH_BUTTONS_CONTAINER);
|
var oauthButtonsContainer = $(ID_LOGIN_BOX_OAUTH_BUTTONS_CONTAINER);
|
||||||
var oauthPretext = $(ID_LOGIN_BOX_OAUTH_PRETEXT);
|
var oauthPretext = $(ID_LOGIN_BOX_OAUTH_PRETEXT);
|
||||||
var anonymousButton = null;
|
|
||||||
if (!requireIdentification){
|
|
||||||
;
|
|
||||||
}
|
|
||||||
anonymousButton = $(ID_LOGIN_BOX_ANONYMOUS_BUTTON);
|
|
||||||
|
|
||||||
remove(emailButton);
|
remove(emailButton);
|
||||||
remove(loginLinkContainer);
|
remove(loginLinkContainer);
|
||||||
@@ -1356,23 +1359,22 @@
|
|||||||
remove(oauthButtonsContainer);
|
remove(oauthButtonsContainer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
remove(anonymousButton);
|
|
||||||
|
|
||||||
var order, id, type, placeholder;
|
var order, fid, type, placeholder;
|
||||||
|
|
||||||
if (loginBoxType === "signup") {
|
if (popupBoxType === "signup") {
|
||||||
var order = ["name", "website", "password"];
|
order = ["name", "website", "password"];
|
||||||
var id = [ID_LOGIN_BOX_NAME_INPUT, ID_LOGIN_BOX_WEBSITE_INPUT, ID_LOGIN_BOX_PASSWORD_INPUT];
|
fid = [ID_LOGIN_BOX_NAME_INPUT, ID_LOGIN_BOX_WEBSITE_INPUT, ID_LOGIN_BOX_PASSWORD_INPUT];
|
||||||
var type = ["text", "text", "password"];
|
type = ["text", "text", "password"];
|
||||||
var placeholder = ["Real Name", "Website (Optional)", "Password"];
|
placeholder = ["Real Name", "Website (Optional)", "Password"];
|
||||||
} else {
|
} else {
|
||||||
var order = ["password"];
|
order = ["password"];
|
||||||
var id = [ID_LOGIN_BOX_PASSWORD_INPUT];
|
fid = [ID_LOGIN_BOX_PASSWORD_INPUT];
|
||||||
var type = ["password"];
|
type = ["password"];
|
||||||
var placeholder = ["Password"];
|
placeholder = ["Password"];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loginBoxType === "signup") {
|
if (popupBoxType === "signup") {
|
||||||
subtitle.innerText = "Finish the rest of your profile to complete."
|
subtitle.innerText = "Finish the rest of your profile to complete."
|
||||||
} else {
|
} else {
|
||||||
subtitle.innerText = "Enter your password to log in."
|
subtitle.innerText = "Enter your password to log in."
|
||||||
@@ -1383,7 +1385,7 @@
|
|||||||
var field = create("div");
|
var field = create("div");
|
||||||
var fieldInput = create("input");
|
var fieldInput = create("input");
|
||||||
|
|
||||||
fieldInput.id = id[i];
|
fieldInput.id = fid[i];
|
||||||
|
|
||||||
classAdd(fieldContainer, "email-container");
|
classAdd(fieldContainer, "email-container");
|
||||||
classAdd(field, "email");
|
classAdd(field, "email");
|
||||||
@@ -1399,12 +1401,12 @@
|
|||||||
if (order[i] === "password") {
|
if (order[i] === "password") {
|
||||||
var fieldButton = create("button");
|
var fieldButton = create("button");
|
||||||
classAdd(fieldButton, "email-button");
|
classAdd(fieldButton, "email-button");
|
||||||
fieldButton.innerText = loginBoxType;
|
fieldButton.innerText = popupBoxType;
|
||||||
|
|
||||||
if (loginBoxType === "signup") {
|
if (popupBoxType === "signup") {
|
||||||
onclick(fieldButton, global.signup);
|
onclick(fieldButton, global.signup, id);
|
||||||
} else {
|
} else {
|
||||||
onclick(fieldButton, global.login);
|
onclick(fieldButton, global.login, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
append(field, fieldButton);
|
append(field, fieldButton);
|
||||||
@@ -1413,7 +1415,7 @@
|
|||||||
append(loginBox, fieldContainer);
|
append(loginBox, fieldContainer);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loginBoxType === "signup") {
|
if (popupBoxType === "signup") {
|
||||||
$(ID_LOGIN_BOX_NAME_INPUT).focus();
|
$(ID_LOGIN_BOX_NAME_INPUT).focus();
|
||||||
} else {
|
} else {
|
||||||
$(ID_LOGIN_BOX_PASSWORD_INPUT).focus();
|
$(ID_LOGIN_BOX_PASSWORD_INPUT).focus();
|
||||||
@@ -1429,8 +1431,8 @@
|
|||||||
|
|
||||||
var json = {
|
var json = {
|
||||||
"commenterToken": commenterTokenGet(),
|
"commenterToken": commenterTokenGet(),
|
||||||
"domain": location.host,
|
"domain": parent.location.host,
|
||||||
"path": location.pathname,
|
"path": parent.location.pathname,
|
||||||
"attributes": attributes,
|
"attributes": attributes,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1549,6 +1551,7 @@
|
|||||||
attrSet(footer, "style", "");
|
attrSet(footer, "style", "");
|
||||||
|
|
||||||
nameWidthFix();
|
nameWidthFix();
|
||||||
|
loadHash();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1563,11 +1566,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
global.loginBoxShow = function() {
|
global.loginBoxShow = function(id) {
|
||||||
var mainArea = $(ID_MAIN_AREA);
|
var mainArea = $(ID_MAIN_AREA);
|
||||||
var loginBoxContainer = $(ID_LOGIN_BOX_CONTAINER);
|
var loginBoxContainer = $(ID_LOGIN_BOX_CONTAINER);
|
||||||
|
|
||||||
global.signupRender();
|
global.popupRender(id);
|
||||||
|
|
||||||
classAdd(mainArea, "blurred");
|
classAdd(mainArea, "blurred");
|
||||||
|
|
||||||
@@ -1596,6 +1599,19 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function loadHash() {
|
||||||
|
if (window.location.hash && window.location.hash.startsWith("#commento-")) {
|
||||||
|
var el = $(ID_CARD + window.location.hash.split("-")[1]);
|
||||||
|
if (el === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
classAdd(el, "highlighted-card");
|
||||||
|
el.scrollIntoView(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
global.main = function(callback) {
|
global.main = function(callback) {
|
||||||
root = $(ID_ROOT);
|
root = $(ID_ROOT);
|
||||||
if (root === null) {
|
if (root === null) {
|
||||||
|
|||||||
54
frontend/js/unsubscribe.js
Normal file
54
frontend/js/unsubscribe.js
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
(function (global, document) {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
(document);
|
||||||
|
|
||||||
|
var e;
|
||||||
|
|
||||||
|
// Update the email records.
|
||||||
|
global.emailUpdate = function() {
|
||||||
|
$(".err").text("");
|
||||||
|
$(".msg").text("");
|
||||||
|
e.sendModeratorNotifications = $("#moderator").is(":checked");
|
||||||
|
e.sendReplyNotifications = $("#reply").is(":checked");
|
||||||
|
|
||||||
|
var json = {
|
||||||
|
"email": e,
|
||||||
|
};
|
||||||
|
|
||||||
|
global.buttonDisable("#save-button");
|
||||||
|
global.post(global.origin + "/api/email/update", json, function(resp) {
|
||||||
|
global.buttonEnable("#save-button");
|
||||||
|
if (!resp.success) {
|
||||||
|
$(".err").text(resp.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$(".msg").text("Successfully updated!");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checks the unsubscribeSecretHex token to retrieve current settings.
|
||||||
|
global.emailGet = function() {
|
||||||
|
$(".err").text("");
|
||||||
|
$(".msg").text("");
|
||||||
|
var json = {
|
||||||
|
"unsubscribeSecretHex": global.paramGet("unsubscribeSecretHex"),
|
||||||
|
};
|
||||||
|
|
||||||
|
global.post(global.origin + "/api/email/get", json, function(resp) {
|
||||||
|
$(".loading").hide();
|
||||||
|
if (!resp.success) {
|
||||||
|
$(".err").text(resp.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
e = resp.email;
|
||||||
|
$("#email").text(e.email);
|
||||||
|
$("#moderator").prop("checked", e.sendModeratorNotifications);
|
||||||
|
$("#reply").prop("checked", e.sendReplyNotifications);
|
||||||
|
$(".checkboxes").attr("style", "");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
} (window.commento, document));
|
||||||
34
frontend/sass/button.scss
Normal file
34
frontend/sass/button.scss
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
@import "colors-main.scss";
|
||||||
|
@import "common-main.scss";
|
||||||
|
|
||||||
|
.button {
|
||||||
|
@extend .shadow;
|
||||||
|
height: 40px;
|
||||||
|
min-width: 110px;
|
||||||
|
background: $white;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
padding: 0px 10px 0px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: bold;
|
||||||
|
color: $blue-7;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:hover {
|
||||||
|
color: $blue-6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:disabled {
|
||||||
|
cursor: default;
|
||||||
|
color: $gray-6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.short-button {
|
||||||
|
width: 80px;
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
}
|
||||||
88
frontend/sass/checkbox.scss
Normal file
88
frontend/sass/checkbox.scss
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
@import "colors-main.scss";
|
||||||
|
|
||||||
|
.commento-round-check {
|
||||||
|
input[type="radio"],
|
||||||
|
input[type="checkbox"] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="radio"] + label,
|
||||||
|
input[type="checkbox"] + label {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
padding-left: 35px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="radio"] + label:last-child,
|
||||||
|
input[type="checkbox"] + label:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="radio"] + label:before,
|
||||||
|
input[type="checkbox"] + label:before {
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
width: 13px;
|
||||||
|
height: 13px;
|
||||||
|
margin-top: 2px;
|
||||||
|
background: $gray-0;
|
||||||
|
border: 1px solid $gray-3;
|
||||||
|
border-radius: 3px;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
transition: all .15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="radio"]:disabled + label:before,
|
||||||
|
input[type="checkbox"]:disabled + label:before {
|
||||||
|
background: $gray-0;
|
||||||
|
border: 1px solid $gray-4;
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="radio"]:checked + label:before,
|
||||||
|
input[type="checkbox"]:checked + label:before {
|
||||||
|
background: $blue-6;
|
||||||
|
border: 1px solid $blue-6;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="radio"] + label:after,
|
||||||
|
input[type="checkbox"] + label:after {
|
||||||
|
position: absolute;
|
||||||
|
left: -7px;
|
||||||
|
top: 4px;
|
||||||
|
content: '';
|
||||||
|
display: inline-block;
|
||||||
|
width: 3px;
|
||||||
|
height: 7px;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
margin-left: 12px;
|
||||||
|
margin-right: 12px;
|
||||||
|
border: solid transparent;
|
||||||
|
border-width: 0 2px 2px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="radio"]:disabled + label:after,
|
||||||
|
input[type="checkbox"]:disabled + label:after {
|
||||||
|
border: solid transparent;
|
||||||
|
border-width: 0 2px 2px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="radio"]:checked + label:after,
|
||||||
|
input[type="checkbox"]:checked + label:after {
|
||||||
|
border: solid $gray-0;
|
||||||
|
border-width: 0 2px 2px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pitch {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #a5a5a5;
|
||||||
|
line-height: 20px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
130
frontend/sass/commento-card.scss
Normal file
130
frontend/sass/commento-card.scss
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
@import "colors-main.scss";
|
||||||
|
|
||||||
|
.commento-card {
|
||||||
|
@import "commento-options.scss"; // SVG buttons to the right of each comment
|
||||||
|
|
||||||
|
padding: 12px 0px 0px 12px;
|
||||||
|
margin-top: 16px;
|
||||||
|
border-top: 1px solid #f0f0f0;
|
||||||
|
|
||||||
|
.commento-header {
|
||||||
|
padding-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commento-avatar::after {
|
||||||
|
content:"";
|
||||||
|
display:block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commento-name {
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #555;
|
||||||
|
border: none;
|
||||||
|
display: block;
|
||||||
|
z-index: 1;
|
||||||
|
margin-left: 48px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commento-flagged::after {
|
||||||
|
content: "Flagged";
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 10px;
|
||||||
|
background: $red-7;
|
||||||
|
color: white;
|
||||||
|
margin-left: 8px;
|
||||||
|
padding: 2px 6px 2px 6px;
|
||||||
|
border-radius: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commento-subtitle {
|
||||||
|
display: block;
|
||||||
|
color: #999;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-left: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commento-timeago {
|
||||||
|
display: inline;
|
||||||
|
color: #888;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commento-score {
|
||||||
|
display: inline;
|
||||||
|
color: #888;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commento-timeago::before {
|
||||||
|
content: "\00a0 \00a0 \00b7 \00a0 \00a0";
|
||||||
|
}
|
||||||
|
|
||||||
|
.commento-body {
|
||||||
|
p {
|
||||||
|
margin-top: 6px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.commento-options {
|
||||||
|
float: right;
|
||||||
|
position: relative;
|
||||||
|
height: 38px;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commento-options-mobile {
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commento-options-clearfix {
|
||||||
|
height: 38px;
|
||||||
|
width: 1px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commento-moderation {
|
||||||
|
height: 48px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.commento-dark-card {
|
||||||
|
background: $red-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commento-highlighted-card {
|
||||||
|
background: $yellow-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// do not place this inside .commento-card as this is used by logged-container
|
||||||
|
// as well
|
||||||
|
.commento-avatar {
|
||||||
|
width: 38px;
|
||||||
|
height: 38px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
color: white;
|
||||||
|
font-size: 22px;
|
||||||
|
float: left;
|
||||||
|
margin-right: 10px;
|
||||||
|
border: 0px transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commento-avatar-img {
|
||||||
|
width: 38px;
|
||||||
|
height: 38px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
float: left;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
42
frontend/sass/commento-common.scss
Normal file
42
frontend/sass/commento-common.scss
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
@import "colors-main.scss";
|
||||||
|
|
||||||
|
code {
|
||||||
|
background: $red-1;
|
||||||
|
font-family: monospace;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: $red-6;
|
||||||
|
padding: 2px;
|
||||||
|
margin: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: $blue-6;
|
||||||
|
border-bottom: 1px solid $blue-6;
|
||||||
|
outline: none;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:focus {
|
||||||
|
box-shadow: 0 0 0 1px rgba(87, 85, 217, .2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.commento-button {
|
||||||
|
display: inline-flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: bold;
|
||||||
|
line-height: 24px;
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 6px;
|
||||||
|
padding-left: 8px;
|
||||||
|
padding-right: 8px;
|
||||||
|
box-shadow: 0 4px 6px rgba(50,50,93,.11),0 1px 3px rgba(0,0,0,.08);
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: #fff;
|
||||||
|
width: 100px;
|
||||||
|
margin-left: 5px;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
@import "colors-main.scss";
|
@import "colors-main.scss";
|
||||||
|
@import "checkbox.scss";
|
||||||
|
|
||||||
textarea,
|
textarea,
|
||||||
input[type=text] {
|
input[type=text] {
|
||||||
@@ -13,7 +14,7 @@ input[type=text]::placeholder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
textarea::placeholder {
|
textarea::placeholder {
|
||||||
color: #aaa;
|
color: $gray-6;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
display: flex;
|
display: flex;
|
||||||
line-height: 110px;
|
line-height: 110px;
|
||||||
@@ -24,7 +25,7 @@ textarea::placeholder {
|
|||||||
|
|
||||||
textarea {
|
textarea {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
font-family: "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
|
font-family: 'Source Sans Pro', sans-serif;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
outline: none;
|
outline: none;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
@@ -47,93 +48,6 @@ textarea {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.commento-oauth-buttons-container {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.commento-oauth-buttons,
|
|
||||||
.commento-account-buttons,
|
|
||||||
.commento-account-buttons-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
position: absolute;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.commento-account-buttons {
|
|
||||||
position: static;
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
|
|
||||||
.commento-account-buttons-container {
|
|
||||||
top: 45px;
|
|
||||||
padding-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.commento-oauth-buttons {
|
|
||||||
display: contents;
|
|
||||||
}
|
|
||||||
|
|
||||||
.commento-account-buttons-question {
|
|
||||||
position: absolute;
|
|
||||||
top: 10px;
|
|
||||||
display: block;
|
|
||||||
text-align: center;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 14px;
|
|
||||||
padding: 6px;
|
|
||||||
color: $gray-8;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.commento-button {
|
|
||||||
display: inline-flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
text-align: center;
|
|
||||||
cursor: pointer;
|
|
||||||
font-weight: bold;
|
|
||||||
line-height: 24px;
|
|
||||||
font-size: 14px;
|
|
||||||
padding: 6px;
|
|
||||||
padding-left: 8px;
|
|
||||||
padding-right: 8px;
|
|
||||||
box-shadow: 0 4px 6px rgba(50,50,93,.11),0 1px 3px rgba(0,0,0,.08);
|
|
||||||
border: 1px solid transparent;
|
|
||||||
border-radius: 3px;
|
|
||||||
color: #fff;
|
|
||||||
width: 100px;
|
|
||||||
margin-left: 5px;
|
|
||||||
margin-right: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.commento-google-button {
|
|
||||||
background: #dd4b39;
|
|
||||||
text-transform: uppercase;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.commento-github-button {
|
|
||||||
background: #000000;
|
|
||||||
text-transform: uppercase;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.commento-anonymous-button {
|
|
||||||
display: block;
|
|
||||||
color: $blue-6;
|
|
||||||
font-weight: 700;
|
|
||||||
margin-top: 12px;
|
|
||||||
font-size: 12px;
|
|
||||||
cursor: pointer;
|
|
||||||
text-align: center;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
.commento-blurred-textarea {
|
.commento-blurred-textarea {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -144,47 +58,35 @@ textarea {
|
|||||||
will-change: transform;
|
will-change: transform;
|
||||||
}
|
}
|
||||||
|
|
||||||
.commento-approve-button,
|
|
||||||
.commento-delete-button,
|
|
||||||
.commento-submit-button {
|
|
||||||
margin-top: 10px;
|
|
||||||
font-size: 14px;
|
|
||||||
width: -moz-fit-content;
|
|
||||||
width: -webkit-fit-content;
|
|
||||||
width: -ms-fit-content;
|
|
||||||
width: -o-fit-content;
|
|
||||||
width: fit-content;
|
|
||||||
}
|
|
||||||
|
|
||||||
.commento-create-button {
|
|
||||||
width: 150px;
|
|
||||||
background: $pink-9;
|
|
||||||
text-transform: uppercase;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.commento-login-button {
|
|
||||||
width: 50px;
|
|
||||||
background: $cyan-9;
|
|
||||||
text-transform: uppercase;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.commento-submit-button {
|
.commento-submit-button {
|
||||||
float: right;
|
float: right;
|
||||||
background: $indigo-7;
|
background: $blue-8;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
margin-top: 10px;
|
||||||
|
|
||||||
.commento-approve-button {
|
|
||||||
background: $green-7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.commento-delete-button {
|
|
||||||
background: $red-7;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.commento-button-margin {
|
.commento-button-margin {
|
||||||
padding-bottom: 60px;
|
padding-bottom: 60px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.commento-anonymous-checkbox-container {
|
||||||
|
float: right;
|
||||||
|
margin: 20px 16px;
|
||||||
|
|
||||||
|
input[type="checkbox"] + label {
|
||||||
|
padding-left: 24px;
|
||||||
|
font-size: 12px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: bold;
|
||||||
|
color: $gray-7;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="checkbox"] + label:before {
|
||||||
|
margin-top: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="checkbox"] + label:after {
|
||||||
|
margin-top: -1px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,19 @@
|
|||||||
@import "colors-main.scss";
|
@import "colors-main.scss";
|
||||||
|
|
||||||
|
.commento-login {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.commento-login-text {
|
||||||
|
text-align: right;
|
||||||
|
margin-right: 16px;
|
||||||
|
height: 38px;
|
||||||
|
color: $gray-6;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.commento-logged-container {
|
.commento-logged-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
|||||||
@@ -23,25 +23,20 @@
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
transition: opacity 0.2s;
|
transition: opacity 0.2s;
|
||||||
|
|
||||||
|
@import "commento-oauth.scss";
|
||||||
|
|
||||||
hr {
|
hr {
|
||||||
border: none;
|
border: none;
|
||||||
background: $gray-2;
|
background: $gray-2;
|
||||||
height: 1px;
|
height: 1px;
|
||||||
}
|
margin: 24px 0px;
|
||||||
|
|
||||||
.commento-login-box-header {
|
|
||||||
font-size: 20px;
|
|
||||||
font-weight: 200;
|
|
||||||
text-align: center;
|
|
||||||
color: $pink-8;
|
|
||||||
margin: 16px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.commento-login-box-subtitle {
|
.commento-login-box-subtitle {
|
||||||
color: $gray-6;
|
color: $gray-6;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 15px;
|
|
||||||
margin: 12px 0px;
|
margin: 12px 0px;
|
||||||
|
font-size: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@import "email-main.scss";
|
@import "email-main.scss";
|
||||||
|
|||||||
24
frontend/sass/commento-mod-tools.scss
Normal file
24
frontend/sass/commento-mod-tools.scss
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
@import "colors-main.scss";
|
||||||
|
|
||||||
|
.commento-mod-tools {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
button {
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: $gray-7;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-left: 12px;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.commento-mod-tools::before {
|
||||||
|
content: "Moderator Tools";
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: $indigo-8;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
27
frontend/sass/commento-oauth.scss
Normal file
27
frontend/sass/commento-oauth.scss
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
@import "colors-main.scss";
|
||||||
|
|
||||||
|
.commento-oauth-buttons-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.commento-oauth-buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
display: contents;
|
||||||
|
|
||||||
|
.commento-google-button {
|
||||||
|
background: #dd4b39;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commento-github-button {
|
||||||
|
background: #000000;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
@import "colors-main.scss";
|
|
||||||
|
|
||||||
code {
|
|
||||||
background: $red-1;
|
|
||||||
font-family: monospace;
|
|
||||||
line-height: 1.5;
|
|
||||||
color: $red-6;
|
|
||||||
padding: 2px;
|
|
||||||
margin: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: $blue-6;
|
|
||||||
border-bottom: 1px solid $blue-6;
|
|
||||||
outline: none;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:focus {
|
|
||||||
box-shadow: 0 0 0 1px rgba(87, 85, 217, .2);
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
@import url('https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,700');
|
@import "source-sans.scss";
|
||||||
|
|
||||||
.commento-root-min-height {
|
.commento-root-min-height {
|
||||||
min-height: 350px;
|
min-height: 350px;
|
||||||
@@ -17,14 +17,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@import "colors-main.scss";
|
@import "colors-main.scss";
|
||||||
@import "common-main.scss";
|
@import "commento-common.scss";
|
||||||
|
@import "commento-logged.scss"; // Logged in as <name>
|
||||||
@import "commento-tags.scss";
|
@import "commento-mod-tools.scss"; // Moderator tools and buttons
|
||||||
@import "commento-logo.scss";
|
@import "commento-input.scss"; // Textarea, anonymous checkbox, submit button
|
||||||
@import "commento-input.scss";
|
@import "commento-card.scss"; // Each comment card
|
||||||
@import "commento-logged.scss";
|
@import "commento-login.scss"; // Popup box when logging in
|
||||||
@import "commento-buttons.scss";
|
@import "commento-footer.scss"; // Powered by Commento
|
||||||
@import "commento-login.scss";
|
|
||||||
|
|
||||||
.commento-hidden {
|
.commento-hidden {
|
||||||
display: none;
|
display: none;
|
||||||
@@ -47,29 +46,6 @@
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.commento-mod-tools {
|
|
||||||
margin-bottom: 16px;
|
|
||||||
|
|
||||||
button {
|
|
||||||
text-transform: uppercase;
|
|
||||||
color: $gray-7;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 700;
|
|
||||||
cursor: pointer;
|
|
||||||
margin-left: 12px;
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.commento-mod-tools::before {
|
|
||||||
content: "Moderator Tools";
|
|
||||||
text-transform: uppercase;
|
|
||||||
color: $indigo-8;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.commento-moderation-notice {
|
.commento-moderation-notice {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
@@ -79,125 +55,4 @@
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.commento-dark-card {
|
|
||||||
background: $yellow-0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.commento-avatar {
|
|
||||||
width: 38px;
|
|
||||||
height: 38px;
|
|
||||||
border-radius: 50%;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
color: white;
|
|
||||||
font-size: 22px;
|
|
||||||
float: left;
|
|
||||||
margin-right: 10px;
|
|
||||||
border: 0px transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.commento-avatar-img {
|
|
||||||
width: 38px;
|
|
||||||
height: 38px;
|
|
||||||
border-radius: 50%;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
float: left;
|
|
||||||
margin-right: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.commento-card {
|
|
||||||
padding: 12px 0px 0px 12px;
|
|
||||||
margin-top: 16px;
|
|
||||||
border-top: 1px solid #f0f0f0;
|
|
||||||
|
|
||||||
.commento-header {
|
|
||||||
padding-bottom: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.commento-avatar::after {
|
|
||||||
content:"";
|
|
||||||
display:block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.commento-name {
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 14px;
|
|
||||||
color: #555;
|
|
||||||
border: none;
|
|
||||||
display: block;
|
|
||||||
z-index: 1;
|
|
||||||
margin-left: 48px;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
width: fit-content;
|
|
||||||
}
|
|
||||||
|
|
||||||
.commento-flagged::after {
|
|
||||||
content: "Flagged";
|
|
||||||
text-transform: uppercase;
|
|
||||||
font-size: 10px;
|
|
||||||
background: $red-7;
|
|
||||||
color: white;
|
|
||||||
margin-left: 8px;
|
|
||||||
padding: 2px 6px 2px 6px;
|
|
||||||
border-radius: 100px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.commento-subtitle {
|
|
||||||
display: block;
|
|
||||||
color: #999;
|
|
||||||
font-size: 12px;
|
|
||||||
margin-left: 48px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.commento-timeago {
|
|
||||||
display: inline;
|
|
||||||
color: #888;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.commento-score {
|
|
||||||
display: inline;
|
|
||||||
color: #888;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.commento-timeago::before {
|
|
||||||
content: "\00a0 \00a0 \00b7 \00a0 \00a0";
|
|
||||||
}
|
|
||||||
|
|
||||||
.commento-body {
|
|
||||||
p {
|
|
||||||
margin-top: 6px;
|
|
||||||
margin-bottom: 6px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.commento-options {
|
|
||||||
float: right;
|
|
||||||
position: relative;
|
|
||||||
height: 38px;
|
|
||||||
z-index: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.commento-options-mobile {
|
|
||||||
margin-right: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.commento-options-clearfix {
|
|
||||||
height: 38px;
|
|
||||||
width: 1px;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.commento-moderation {
|
|
||||||
height: 48px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
@import "colors-main.scss";
|
@import "colors-main.scss";
|
||||||
@import "common-main.scss";
|
@import "common-main.scss";
|
||||||
|
@import "checkbox.scss";
|
||||||
|
@import "button.scss";
|
||||||
|
|
||||||
.subscription-nag {
|
.subscription-nag {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -156,57 +158,6 @@ body {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.round-check {
|
|
||||||
input[type="checkbox"] {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="checkbox"] + label {
|
|
||||||
display: block;
|
|
||||||
position: relative;
|
|
||||||
padding-left: 35px;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
cursor: pointer;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
-moz-user-select: none;
|
|
||||||
-ms-user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="checkbox"] + label:last-child { margin-bottom: 0; }
|
|
||||||
|
|
||||||
input[type="checkbox"] + label:before {
|
|
||||||
content: '';
|
|
||||||
display: block;
|
|
||||||
width: 13px;
|
|
||||||
height: 13px;
|
|
||||||
margin-top: 2px;
|
|
||||||
background: $gray-0;
|
|
||||||
border: 1px solid $gray-3;
|
|
||||||
border-radius: 3px;
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
transition: all .15s;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="checkbox"]:disabled + label:before {
|
|
||||||
background: $gray-0;
|
|
||||||
border: 1px solid $gray-4;
|
|
||||||
opacity: 0.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="checkbox"]:checked + label:before {
|
|
||||||
background: $blue-6;
|
|
||||||
border: 1px solid $blue-6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pitch {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #a5a5a5;
|
|
||||||
line-height: 20px !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard-container {
|
.dashboard-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
top: 72px;
|
top: 72px;
|
||||||
@@ -376,9 +327,10 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.normal-text {
|
.normal-text {
|
||||||
font-size: 14px;
|
font-size: 15px;
|
||||||
color: $gray-7;
|
color: $gray-7;
|
||||||
line-height: 22px;
|
line-height: 22px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: $blue-6;
|
color: $blue-6;
|
||||||
@@ -425,6 +377,12 @@ body {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.question {
|
||||||
|
font-size: 15px;
|
||||||
|
color: $gray-7;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.float-right {
|
.float-right {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 10px;
|
right: 10px;
|
||||||
@@ -630,38 +588,6 @@ body {
|
|||||||
padding-left: 32px;
|
padding-left: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button {
|
|
||||||
@extend .shadow;
|
|
||||||
height: 40px;
|
|
||||||
min-width: 110px;
|
|
||||||
background: $white;
|
|
||||||
border: none;
|
|
||||||
outline: none;
|
|
||||||
padding: 0px 10px 0px 10px;
|
|
||||||
font-size: 12px;
|
|
||||||
text-transform: uppercase;
|
|
||||||
text-align: center;
|
|
||||||
font-weight: bold;
|
|
||||||
color: $blue-7;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button:hover {
|
|
||||||
color: $blue-6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button:disabled {
|
|
||||||
cursor: default;
|
|
||||||
color: $gray-6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.short-button {
|
|
||||||
width: 80px;
|
|
||||||
position: absolute;
|
|
||||||
top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat {
|
.stat {
|
||||||
-webkit-box-shadow: 0 7px 14px 0 rgba(50,50,93,.1), 0 3px 6px 0 rgba(0,0,0,.07);
|
-webkit-box-shadow: 0 7px 14px 0 rgba(50,50,93,.1), 0 3px 6px 0 rgba(0,0,0,.07);
|
||||||
-moz-box-shadow: 0 7px 14px 0 rgba(50,50,93,.1), 0 3px 6px 0 rgba(0,0,0,.07);
|
-moz-box-shadow: 0 7px 14px 0 rgba(50,50,93,.1), 0 3px 6px 0 rgba(0,0,0,.07);
|
||||||
|
|||||||
112
frontend/sass/source-sans.scss
Normal file
112
frontend/sass/source-sans.scss
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
/* cyrillic-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Source Sans Pro';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
src: local('Source Sans Pro Regular'), local('SourceSansPro-Regular'), url([[[.CdnPrefix]]]/fonts/source-sans-400-cryllic-ext.woff2) format('woff2');
|
||||||
|
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||||
|
}
|
||||||
|
/* cyrillic */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Source Sans Pro';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
src: local('Source Sans Pro Regular'), local('SourceSansPro-Regular'), url([[[.CdnPrefix]]]/fonts/source-sans-400-cryllic.woff2) format('woff2');
|
||||||
|
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||||
|
}
|
||||||
|
/* greek-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Source Sans Pro';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
src: local('Source Sans Pro Regular'), local('SourceSansPro-Regular'), url([[[.CdnPrefix]]]/fonts/source-sans-400-greek-ext.woff2) format('woff2');
|
||||||
|
unicode-range: U+1F00-1FFF;
|
||||||
|
}
|
||||||
|
/* greek */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Source Sans Pro';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
src: local('Source Sans Pro Regular'), local('SourceSansPro-Regular'), url([[[.CdnPrefix]]]/fonts/source-sans-400-greek.woff2) format('woff2');
|
||||||
|
unicode-range: U+0370-03FF;
|
||||||
|
}
|
||||||
|
/* vietnamese */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Source Sans Pro';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
src: local('Source Sans Pro Regular'), local('SourceSansPro-Regular'), url([[[.CdnPrefix]]]/fonts/source-sans-400-vietnamese.woff2) format('woff2');
|
||||||
|
unicode-range: U+0102-0103, U+0110-0111, U+1EA0-1EF9, U+20AB;
|
||||||
|
}
|
||||||
|
/* latin-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Source Sans Pro';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
src: local('Source Sans Pro Regular'), local('SourceSansPro-Regular'), url([[[.CdnPrefix]]]/fonts/source-sans-400-latin-ext.woff2) format('woff2');
|
||||||
|
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
|
}
|
||||||
|
/* latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Source Sans Pro';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
src: local('Source Sans Pro Regular'), local('SourceSansPro-Regular'), url([[[.CdnPrefix]]]/fonts/source-sans-400-latin.woff2) format('woff2');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
/* cyrillic-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Source Sans Pro';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
src: local('Source Sans Pro Bold'), local('SourceSansPro-Bold'), url([[[.CdnPrefix]]]/fonts/source-sans-700-cryllic-ext.woff2) format('woff2');
|
||||||
|
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||||
|
}
|
||||||
|
/* cyrillic */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Source Sans Pro';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
src: local('Source Sans Pro Bold'), local('SourceSansPro-Bold'), url([[[.CdnPrefix]]]/fonts/source-sans-700-cryllic.woff2) format('woff2');
|
||||||
|
unicode-range: U+0700-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||||
|
}
|
||||||
|
/* greek-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Source Sans Pro';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
src: local('Source Sans Pro Bold'), local('SourceSansPro-Bold'), url([[[.CdnPrefix]]]/fonts/source-sans-700-greek-ext.woff2) format('woff2');
|
||||||
|
unicode-range: U+1F00-1FFF;
|
||||||
|
}
|
||||||
|
/* greek */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Source Sans Pro';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
src: local('Source Sans Pro Bold'), local('SourceSansPro-Bold'), url([[[.CdnPrefix]]]/fonts/source-sans-700-greek.woff2) format('woff2');
|
||||||
|
unicode-range: U+0370-03FF;
|
||||||
|
}
|
||||||
|
/* vietnamese */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Source Sans Pro';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
src: local('Source Sans Pro Bold'), local('SourceSansPro-Bold'), url([[[.CdnPrefix]]]/fonts/source-sans-700-vietnamese.woff2) format('woff2');
|
||||||
|
unicode-range: U+0102-0103, U+0110-0111, U+1EA0-1EF9, U+20AB;
|
||||||
|
}
|
||||||
|
/* latin-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Source Sans Pro';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
src: local('Source Sans Pro Bold'), local('SourceSansPro-Bold'), url([[[.CdnPrefix]]]/fonts/source-sans-700-latin-ext.woff2) format('woff2');
|
||||||
|
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
|
}
|
||||||
|
/* latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Source Sans Pro';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
src: local('Source Sans Pro Bold'), local('SourceSansPro-Bold'), url([[[.CdnPrefix]]]/fonts/source-sans-700-latin.woff2) format('woff2');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
49
frontend/sass/unsubscribe-main.scss
Normal file
49
frontend/sass/unsubscribe-main.scss
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
@import "checkbox.scss";
|
||||||
|
@import "button.scss";
|
||||||
|
|
||||||
|
.unsubscribe-container {
|
||||||
|
margin-top: 72px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.unsubscribe {
|
||||||
|
max-width: 600px;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.err {
|
||||||
|
color: $red-6;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msg {
|
||||||
|
color: $green-6;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkboxes {
|
||||||
|
.email {
|
||||||
|
margin-bottom: 32px;
|
||||||
|
font-size: 21px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email::before {
|
||||||
|
content: "Changing email notifications for ";
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
4
frontend/sass/unsubscribe.scss
Normal file
4
frontend/sass/unsubscribe.scss
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
@import "common-main.scss";
|
||||||
|
@import "navbar-main.scss";
|
||||||
|
@import "unsubscribe-main.scss";
|
||||||
|
@import "tomorrow.scss";
|
||||||
49
frontend/unsubscribe.html
Normal file
49
frontend/unsubscribe.html
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="user-scalable=no, initial-scale=1.0">
|
||||||
|
<script src="[[[.CdnPrefix]]]/js/jquery.js"></script>
|
||||||
|
<script src="[[[.CdnPrefix]]]/js/unsubscribe.js"></script>
|
||||||
|
<link rel="icon" href="[[[.CdnPrefix]]]/images/120x120.png">
|
||||||
|
<link rel="stylesheet" href="[[[.CdnPrefix]]]/css/unsubscribe.css">
|
||||||
|
<link href="https://fonts.googleapis.com/css?family=Source+Code+Pro:400,700|Source+Sans+Pro:200,300,400,700" rel="stylesheet">
|
||||||
|
<title>Commento: Unsubscribe</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<div class="navbar">
|
||||||
|
<a href="[[[.Origin]]]/" class="navbar-item navbar-logo-text"><img src="[[[.CdnPrefix]]]/images/logo.svg" class="navbar-logo">Commento</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
window.onload = function() {
|
||||||
|
window.commento.emailGet();
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="unsubscribe-container">
|
||||||
|
<div class="unsubscribe">
|
||||||
|
<div class="err"></div>
|
||||||
|
<div class="loading">Loading...</div>
|
||||||
|
<div class="checkboxes" style="display: none">
|
||||||
|
<div class="email" id="email"></div>
|
||||||
|
<div class="row no-border commento-round-check indent" id="mod-checkbox">
|
||||||
|
<input type="checkbox" id="moderator" value="moderator">
|
||||||
|
<label for="moderator">Email me moderator-related notifications</label>
|
||||||
|
<div class="pitch">
|
||||||
|
If you're a moderator of any domain, depending on each domain's moderator email policy, you may receive emails when a new comment is posted or when a comment is pending moderation. Unchecking this override those settings for you.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row no-border commento-round-check indent" id="reply-checkbox">
|
||||||
|
<input type="checkbox" id="reply" value="reply">
|
||||||
|
<label for="reply">Email me when someone replies to my comment</label>
|
||||||
|
<div class="pitch">
|
||||||
|
When someone replies to your comment, you can choose to receive a notification. These emails will be batched and delayed (by 10 minutes) so that your inbox won't get overwhelmed.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="msg"></div>
|
||||||
|
<button id="save-button" class="button" onclick="window.commento.emailUpdate()">Update</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
[[[.Footer]]]
|
||||||
|
</html>
|
||||||
104
templates/email-notification.txt
Normal file
104
templates/email-notification.txt
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="user-scalable=no,initial-scale=1">
|
||||||
|
<title>You have {{ .Subject }}</title>
|
||||||
|
<style type="text/css">
|
||||||
|
@media only screen and (max-width:600px) {
|
||||||
|
.options {
|
||||||
|
float: none;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.options::before {
|
||||||
|
content: "Options:";
|
||||||
|
float: left;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: #495057;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.option {
|
||||||
|
padding: 6px 8px;
|
||||||
|
margin: 10px 5px;
|
||||||
|
color: white;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
.green {
|
||||||
|
background: #2f9e44;
|
||||||
|
}
|
||||||
|
.red {
|
||||||
|
background: #f03e3e;
|
||||||
|
}
|
||||||
|
.blue {
|
||||||
|
background: #1c7ed6;
|
||||||
|
}
|
||||||
|
.gray {
|
||||||
|
background: #495057;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
padding-right: 0px;
|
||||||
|
}
|
||||||
|
.logo {
|
||||||
|
display: block;
|
||||||
|
float: none;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0px 8px 16px 0px;
|
||||||
|
}
|
||||||
|
.unsubscribe {
|
||||||
|
max-width: 100%;
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="content" style="font-size:14px;background:white;font-family:sans-serif;padding:0px;margin:0px;">
|
||||||
|
<div class="h1" style="font-weight:bold;text-align:center;margin-top:12px;padding:8px;font-size:24px;">You have {{ .Subject }}</div>
|
||||||
|
<div class="comments-container" style="display:flex;justify-content:center;">
|
||||||
|
<div class="comments" style="max-width:600px;width:calc(100% - 20px);margin-top:16px;border-top:1px solid #eee;">
|
||||||
|
{{ with .Notifications }}
|
||||||
|
{{ range . }}
|
||||||
|
|
||||||
|
<div class="comment" style="border-radius:2px;width:calc(100% - 32px);padding:16px;margin:8px 0px 8px 0px;border-bottom:1px solid #eee;">
|
||||||
|
<div class="options" style="float:right;">
|
||||||
|
{{ if eq .Kind "pending-moderation" }}
|
||||||
|
<a href="{{ $.Origin }}/api/email/moderate?commentHex={{ .CommentHex }}&action=approve&unsubscribeSecretHex={{ $.UnsubscribeSecretHex }}" target="_black" class="option green" style="padding-right:5px;text-transform:uppercase;font-size:12px;font-weight:bold;text-decoration:none;color:#2f9e44;">Approve</a>
|
||||||
|
{{ end }}
|
||||||
|
{{ if ne .Kind "reply" }}
|
||||||
|
<a href="{{ $.Origin }}/api/email/moderate?commentHex={{ .CommentHex }}&action=delete&unsubscribeSecretHex={{ $.UnsubscribeSecretHex }}" target="_black" class="option red" style="padding-right:5px;text-transform:uppercase;font-size:12px;font-weight:bold;text-decoration:none;color:#f03e3e;">Delete</a>
|
||||||
|
{{ end }}
|
||||||
|
<a href="http://{{ .Domain }}{{ .Path }}#commento-{{ .CommentHex }}" class="option gray" style="padding-right:5px;text-transform:uppercase;font-size:12px;font-weight:bold;text-decoration:none;color:#495057;">Context</a>
|
||||||
|
</div>
|
||||||
|
<div class="header" style="white-space:nowrap;overflow:hidden;text-overflow:ellipsis;padding-right:10px;">
|
||||||
|
<div class="name" style="display:inline;font-size:14px;font-weight:bold;color:#1e2127;">{{ .CommenterName }}</div>
|
||||||
|
on
|
||||||
|
<a href="http://{{ .Domain }}{{ .Path }}" class="page" style="margin-bottom:10px;text-decoration:none;color:#228be6;">"{{ .Title }}"</a>
|
||||||
|
</div>
|
||||||
|
<div class="text" style="line-height:20px;padding:10px;">
|
||||||
|
{{ .Html }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
<div class="footer" style="width:100%;margin-top:16px;">
|
||||||
|
<a href="https://commento.io" class="logo" style="float:right;font-weight:bold;color:#868e96;font-size:13px;text-decoration:none;">Powered by Commento</a>
|
||||||
|
<div class="unsubscribe" style="color:#868e96;font-size:13px;text-align:left;max-width:300px;margin-bottom:16px;">
|
||||||
|
{{ if eq .Kind "reply" }}
|
||||||
|
You've received this email because you opted in to receive email notifications for comment replies. To unsubscribe, <a href="{{ .Origin }}/unsubscribe?unsubscribeSecretHex={{ .UnsubscribeSecretHex }}" style="color:#868e96;font-weight:bold;text-decoration:none;">click here</a>.
|
||||||
|
{{ end }}
|
||||||
|
{{ if eq .Kind "pending-moderation" }}
|
||||||
|
You've received this email because the domain owner chose to notify moderators of comments pending moderation by email. To unsubscribe, <a href="{{ .Origin }}/unsubscribe?unsubscribeSecretHex={{ .UnsubscribeSecretHex }}" style="color:#868e96;font-weight:bold;text-decoration:none;">click here</a>.
|
||||||
|
{{ end }}
|
||||||
|
{{ if eq .Kind "all" }}
|
||||||
|
You've received this email because the domain owner chose to notify moderators for all new comments by email. To unsubscribe, <a href="{{ .Origin }}/unsubscribe?unsubscribeSecretHex={{ .UnsubscribeSecretHex }}" style="color:#868e96;font-weight:bold;text-decoration:none;">click here</a>.
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user