Compare commits

...
13 Commits
Author SHA1 Message Date
Pavel MineevandUmputun 6fe373d540 fix: update fe contributing documentation and get dev commands in order 2022-11-30 01:19:51 -06:00
Pavel MineevandUmputun ba19bcc729 fix: update iframe size on mount 2022-11-29 18:12:14 -06:00
Dmitry VerkhoturovandUmputun f1b65db2c7 prefer unused PreferServerCipherSuites param
This is not used since 1.17,
https://github.com/golang/go/commit/9d0819b27ca248f9949e7cf6bf7cb9fe7cf574e8
2022-11-13 12:02:40 -06:00
Dmitry VerkhoturovandUmputun f5f287ef06 bump actions/setup-go to v3 to resolve node12 deprecation note 2022-11-13 12:00:54 -06:00
Igor KlipachandUmputun f161e6033c Removed duplicated linter 'megacheck' 2022-11-13 11:54:26 -06:00
Dmitry VerkhoturovandUmputun c86bff8811 remove duplicate type definitions from function signatures
I haven't found a linter for these, so I had to catch these manually.
I found #757 to fix one of these, and I thought it would be good
to fix everything at once.
2022-11-13 11:51:51 -06:00
Dmitry VerkhoturovandUmputun 596b1045bd allow dots in site id during email user validation 2022-11-03 16:31:00 -05:00
Dmitry VerkhoturovandUmputun 907ca2b590 remove the only generic logger usage
github.com/go-pkgz/lgr should be used instead for consistency
with the rest of the code
2022-11-03 16:30:10 -05:00
Dmitry VerkhoturovandUmputun f1b5469b83 bump github actions versions
Old ones produce warnings due to deprecation of NodeJS 12
2022-11-03 15:20:30 -05:00
Denis HananeinandUmputun 984fbde540 Support no_footer option 2022-10-25 21:42:08 -05:00
Dmitry VerkhoturovandUmputun 2bdc05dd47 remove unused entrypoint.sh
It should be removed after changes in 141c75401.
2022-10-23 12:55:34 -05:00
Dmitry VerkhoturovandUmputun d2ea572abf fix "it's" used in place of "its" 2022-10-20 14:18:21 -05:00
Dmitry VerkhoturovandUmputun 8d5c4cd578 make docker build work on tag again 2022-10-20 14:16:38 -05:00
26 changed files with 72 additions and 71 deletions
+1 -1
View File
@@ -30,7 +30,7 @@ jobs:
DEBUG: ${{secrets.DEBUG}} DEBUG: ${{secrets.DEBUG}}
- name: install go - name: install go
uses: actions/setup-go@v2 uses: actions/setup-go@v3
with: with:
go-version: 1.17 go-version: 1.17
+2 -3
View File
@@ -3,7 +3,6 @@ name: build
on: on:
push: push:
branches: branches:
- master
tags: tags:
paths: paths:
- ".github/workflows/ci-build.yml" - ".github/workflows/ci-build.yml"
@@ -33,11 +32,11 @@ jobs:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: set up QEMU - name: set up QEMU
uses: docker/setup-qemu-action@v1 uses: docker/setup-qemu-action@v2
- name: set up Docker Buildx - name: set up Docker Buildx
id: buildx id: buildx
uses: docker/setup-buildx-action@v1 uses: docker/setup-buildx-action@v2
- name: available platforms - name: available platforms
run: echo ${{ steps.buildx.outputs.platforms }} run: echo ${{ steps.buildx.outputs.platforms }}
+2 -2
View File
@@ -23,11 +23,11 @@ jobs:
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: set up QEMU - name: set up QEMU
uses: docker/setup-qemu-action@v1 uses: docker/setup-qemu-action@v2
- name: set up Docker Buildx - name: set up Docker Buildx
id: buildx id: buildx
uses: docker/setup-buildx-action@v1 uses: docker/setup-buildx-action@v2
- name: available platforms - name: available platforms
run: echo ${{ steps.buildx.outputs.platforms }} run: echo ${{ steps.buildx.outputs.platforms }}
+2 -4
View File
@@ -96,20 +96,18 @@ LABEL org.opencontainers.image.authors="Umputun <umputun@gmail.com>" \
WORKDIR /srv WORKDIR /srv
ADD docker-init.sh /entrypoint.sh COPY docker-init.sh /srv/init.sh
ADD backend/scripts/backup.sh /usr/local/bin/backup ADD backend/scripts/backup.sh /usr/local/bin/backup
ADD backend/scripts/restore.sh /usr/local/bin/restore ADD backend/scripts/restore.sh /usr/local/bin/restore
ADD backend/scripts/import.sh /usr/local/bin/import ADD backend/scripts/import.sh /usr/local/bin/import
RUN chmod +x /entrypoint.sh /usr/local/bin/backup /usr/local/bin/restore /usr/local/bin/import RUN chmod +x /srv/init.sh /usr/local/bin/backup /usr/local/bin/restore /usr/local/bin/import
COPY --from=build-backend /build/backend/remark42 /srv/remark42 COPY --from=build-backend /build/backend/remark42 /srv/remark42
COPY --from=build-frontend /srv/frontend/apps/remark42/public/ /srv/web/ COPY --from=build-frontend /srv/frontend/apps/remark42/public/ /srv/web/
COPY docker-init.sh /srv/init.sh
RUN chown -R app:app /srv RUN chown -R app:app /srv
RUN ln -s /srv/remark42 /usr/bin/remark42 RUN ln -s /srv/remark42 /usr/bin/remark42
EXPOSE 8080 EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s CMD curl --fail http://localhost:8080/ping || exit 1 HEALTHCHECK --interval=30s --timeout=3s CMD curl --fail http://localhost:8080/ping || exit 1
RUN chmod +x /srv/init.sh
CMD ["/srv/remark42", "server"] CMD ["/srv/remark42", "server"]
-1
View File
@@ -35,7 +35,6 @@ linters:
- revive - revive
- govet - govet
- unconvert - unconvert
- megacheck
- gas - gas
- gocyclo - gocyclo
- dupl - dupl
@@ -8,11 +8,11 @@ package accessor
import ( import (
"fmt" "fmt"
"log"
"sort" "sort"
"sync" "sync"
"time" "time"
log "github.com/go-pkgz/lgr"
"github.com/umputun/remark42/backend/app/store" "github.com/umputun/remark42/backend/app/store"
"github.com/umputun/remark42/backend/app/store/engine" "github.com/umputun/remark42/backend/app/store/engine"
) )
@@ -267,7 +267,7 @@ func (m *MemData) ListFlags(req engine.FlagRequest) (res []interface{}, err erro
return res, nil return res, nil
case engine.Blocked: case engine.Blocked:
log.Printf("%+v", m.metaUsers) log.Printf("[INFO] metaUsers: %+v", m.metaUsers)
for _, u := range m.metaUsers { for _, u := range m.metaUsers {
if u.SiteID == req.Locator.SiteID && u.Blocked && u.BlockedUntil.After(time.Now()) { if u.SiteID == req.Locator.SiteID && u.Blocked && u.BlockedUntil.After(time.Now()) {
res = append(res, store.BlockedUser{ID: u.UserID, Until: u.BlockedUntil}) res = append(res, store.BlockedUser{ID: u.UserID, Until: u.BlockedUntil})
+1 -1
View File
@@ -83,7 +83,7 @@ func (ab AutoBackup) removeOldBackupFiles() {
backFiles = append(backFiles, info) backFiles = append(backFiles, info)
} }
} }
sort.Slice(backFiles, func(i int, j int) bool { return backFiles[i].Name() < backFiles[j].Name() }) sort.Slice(backFiles, func(i, j int) bool { return backFiles[i].Name() < backFiles[j].Name() })
if len(backFiles) > ab.KeepMax { if len(backFiles) > ab.KeepMax {
for i := 0; i < len(backFiles)-ab.KeepMax; i++ { for i := 0; i < len(backFiles)-ab.KeepMax; i++ {
+1 -1
View File
@@ -38,7 +38,7 @@ type MapperMaker func(reader io.Reader) (Mapper, error)
type Store interface { type Store interface {
Create(comment store.Comment) (commentID string, err error) Create(comment store.Comment) (commentID string, err error)
Find(locator store.Locator, sort string, user store.User) ([]store.Comment, error) Find(locator store.Locator, sort string, user store.User) ([]store.Comment, error)
List(siteID string, limit int, skip int) ([]store.PostInfo, error) List(siteID string, limit, skip int) ([]store.PostInfo, error)
DeleteAll(siteID string) error DeleteAll(siteID string) error
Metas(siteID string) (umetas []service.UserMetaData, pmetas []service.PostMetaData, err error) Metas(siteID string) (umetas []service.UserMetaData, pmetas []service.PostMetaData, err error)
SetMetas(siteID string, umetas []service.UserMetaData, pmetas []service.PostMetaData) error SetMetas(siteID string, umetas []service.UserMetaData, pmetas []service.PostMetaData) error
+2 -2
View File
@@ -34,8 +34,8 @@ type Destination interface {
// Store defines the minimal interface accessing stored comments used by notifier // Store defines the minimal interface accessing stored comments used by notifier
type Store interface { type Store interface {
Get(locator store.Locator, id string, user store.User) (store.Comment, error) Get(locator store.Locator, id string, user store.User) (store.Comment, error)
GetUserEmail(siteID string, userID string) (string, error) GetUserEmail(siteID, userID string) (string, error)
GetUserTelegram(siteID string, userID string) (string, error) GetUserTelegram(siteID, userID string) (string, error)
} }
// used for email and telegram retrieval from user details // used for email and telegram retrieval from user details
+5 -5
View File
@@ -29,15 +29,15 @@ type admin struct {
type adminStore interface { type adminStore interface {
Delete(locator store.Locator, commentID string, mode store.DeleteMode) error Delete(locator store.Locator, commentID string, mode store.DeleteMode) error
DeleteUser(siteID string, userID string, mode store.DeleteMode) error DeleteUser(siteID, userID string, mode store.DeleteMode) error
DeleteUserDetail(siteID string, userID string, detail engine.UserDetail) error DeleteUserDetail(siteID, userID string, detail engine.UserDetail) error
User(siteID, userID string, limit, skip int, user store.User) ([]store.Comment, error) User(siteID, userID string, limit, skip int, user store.User) ([]store.Comment, error)
IsBlocked(siteID string, userID string) bool IsBlocked(siteID, userID string) bool
SetBlock(siteID string, userID string, status bool, ttl time.Duration) error SetBlock(siteID, userID string, status bool, ttl time.Duration) error
BlockedUsers(siteID string) ([]store.BlockedUser, error) BlockedUsers(siteID string) ([]store.BlockedUser, error)
Info(locator store.Locator, readonlyAge int) (store.PostInfo, error) Info(locator store.Locator, readonlyAge int) (store.PostInfo, error)
SetTitle(locator store.Locator, commentID string) (comment store.Comment, err error) SetTitle(locator store.Locator, commentID string) (comment store.Comment, err error)
SetVerified(siteID string, userID string, status bool) error SetVerified(siteID, userID string, status bool) error
SetReadOnly(locator store.Locator, status bool) error SetReadOnly(locator store.Locator, status bool) error
SetPin(locator store.Locator, commentID string, status bool) error SetPin(locator store.Locator, commentID string, status bool) error
} }
+1 -1
View File
@@ -651,7 +651,7 @@ func subscribersOnly(enable bool) func(http.Handler) http.Handler {
func validEmailAuth() func(http.Handler) http.Handler { func validEmailAuth() func(http.Handler) http.Handler {
reUser := regexp.MustCompile(`^[\p{L}\d\s_]{4,64}$`) // matches ui side validation, adding min/max limitation reUser := regexp.MustCompile(`^[\p{L}\d\s_]{4,64}$`) // matches ui side validation, adding min/max limitation
reSite := regexp.MustCompile(`^[a-zA-Z\d\s_-]{1,64}$`) reSite := regexp.MustCompile(`^[a-zA-Z\d\s_.-]{1,64}$`)
return func(h http.Handler) http.Handler { return func(h http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) { fn := func(w http.ResponseWriter, r *http.Request) {
+7 -7
View File
@@ -58,15 +58,15 @@ type privStore interface {
Vote(req service.VoteReq) (comment store.Comment, err error) Vote(req service.VoteReq) (comment store.Comment, err error)
Get(locator store.Locator, commentID string, user store.User) (store.Comment, error) Get(locator store.Locator, commentID string, user store.User) (store.Comment, error)
User(siteID, userID string, limit, skip int, user store.User) ([]store.Comment, error) User(siteID, userID string, limit, skip int, user store.User) ([]store.Comment, error)
GetUserEmail(siteID string, userID string) (string, error) GetUserEmail(siteID, userID string) (string, error)
SetUserEmail(siteID string, userID string, value string) (string, error) SetUserEmail(siteID, userID, value string) (string, error)
GetUserTelegram(siteID string, userID string) (string, error) GetUserTelegram(siteID, userID string) (string, error)
SetUserTelegram(siteID string, userID string, value string) (string, error) SetUserTelegram(siteID, userID, value string) (string, error)
DeleteUserDetail(siteID string, userID string, detail engine.UserDetail) error DeleteUserDetail(siteID, userID string, detail engine.UserDetail) error
ValidateComment(c *store.Comment) error ValidateComment(c *store.Comment) error
IsVerified(siteID string, userID string) bool IsVerified(siteID, userID string) bool
IsReadOnly(locator store.Locator) bool IsReadOnly(locator store.Locator) bool
IsBlocked(siteID string, userID string) bool IsBlocked(siteID, userID string) bool
Info(locator store.Locator, readonlyAge int) (store.PostInfo, error) Info(locator store.Locator, readonlyAge int) (store.PostInfo, error)
} }
+1 -1
View File
@@ -40,7 +40,7 @@ type pubStore interface {
User(siteID, userID string, limit, skip int, user store.User) ([]store.Comment, error) User(siteID, userID string, limit, skip int, user store.User) ([]store.Comment, error)
UserCount(siteID, userID string) (int, error) UserCount(siteID, userID string) (int, error)
Count(locator store.Locator) (int, error) Count(locator store.Locator) (int, error)
List(siteID string, limit int, skip int) ([]store.PostInfo, error) List(siteID string, limit, skip int) ([]store.PostInfo, error)
Info(locator store.Locator, readonlyAge int) (store.PostInfo, error) Info(locator store.Locator, readonlyAge int) (store.PostInfo, error)
ValidateComment(c *store.Comment) error ValidateComment(c *store.Comment) error
+1 -1
View File
@@ -378,7 +378,7 @@ func Test_validEmailAuth(t *testing.T) {
status int status int
}{ }{
{"/auth/email/login?site=remark42&address=umputun%example.com&user=someone", http.StatusOK}, {"/auth/email/login?site=remark42&address=umputun%example.com&user=someone", http.StatusOK},
{"/auth/email/login?site=site-with-dash_and_underscore&address=umputun%example.com&user=someone", http.StatusOK}, {"/auth/email/login?site=site-with-dash_and_underscore-and.dot&address=umputun%example.com&user=someone", http.StatusOK},
{"/auth/email/login?site=remark42&address=umputun%example.com&user=someone+blah", http.StatusOK}, {"/auth/email/login?site=remark42&address=umputun%example.com&user=someone+blah", http.StatusOK},
{"/auth/email/login?site=remark42&address=umputun%example.com&user=Евгений+Умпутун", http.StatusOK}, {"/auth/email/login?site=remark42&address=umputun%example.com&user=Евгений+Умпутун", http.StatusOK},
{"/auth/email/login?site=remark42&address=umputun%example.com&user=12", http.StatusForbidden}, {"/auth/email/login?site=remark42&address=umputun%example.com&user=12", http.StatusForbidden},
-4
View File
@@ -111,15 +111,11 @@ func (s *Rest) getRemarkHost() string {
func (s *Rest) makeTLSConfig() *tls.Config { func (s *Rest) makeTLSConfig() *tls.Config {
return &tls.Config{ return &tls.Config{
PreferServerCipherSuites: true,
CipherSuites: []uint16{ CipherSuites: []uint16{
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
// tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
// tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
// tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
}, },
MinVersion: tls.VersionTLS12, MinVersion: tls.VersionTLS12,
CurvePreferences: []tls.CurveID{ CurvePreferences: []tls.CurveID{
+1 -1
View File
@@ -78,5 +78,5 @@ func (s *StaticStore) Enabled(site string) (ok bool, err error) {
return false, nil return false, nil
} }
// OnEvent doesn nothing for StaticStore // OnEvent does nothing for StaticStore
func (s *StaticStore) OnEvent(_ string, _ EventType) error { return nil } func (s *StaticStore) OnEvent(_ string, _ EventType) error { return nil }
+10 -10
View File
@@ -16,15 +16,15 @@ import (
// BoltDB implements store.Interface, represents multiple sites with multiplexing to different bolt dbs. Thread safe. // BoltDB implements store.Interface, represents multiple sites with multiplexing to different bolt dbs. Thread safe.
// there are 6 types of top-level buckets: // there are 6 types of top-level buckets:
// - comments for post in "posts" top-level bucket. Each url (post) makes its own bucket and each k:v pair is commentID:comment // - comments for post in "posts" top-level bucket. Each url (post) makes its own bucket and each k:v pair is commentID:comment
// - history of all comments. They all in a single "last" bucket (per site) and key is defined by ref struct as ts+commentID // - history of all comments. They all in a single "last" bucket (per site) and key is defined by ref struct as ts+commentID
// value is not full comment but a reference combined from post-url+commentID // value is not full comment but a reference combined from post-url+commentID
// - user to comment references in "users" bucket. It used to get comments for user. Key is userID and value // - user to comment references in "users" bucket. It used to get comments for user. Key is userID and value
// is a nested bucket named userID with kv as ts:reference // is a nested bucket named userID with kv as ts:reference
// - users details in "user_details" bucket. Key is userID, value - UserDetailEntry // - users details in "user_details" bucket. Key is userID, value - UserDetailEntry
// - blocking info sits in "block" bucket. Key is userID, value - ts // - blocking info sits in "block" bucket. Key is userID, value - ts
// - counts per post to keep number of comments. Key is post url, value - count // - counts per post to keep number of comments. Key is post url, value - count
// - readonly per post to keep status of manually set RO posts. Key is post url, value - ts // - readonly per post to keep status of manually set RO posts. Key is post url, value - ts
type BoltDB struct { type BoltDB struct {
dbs map[string]*bolt.DB dbs map[string]*bolt.DB
} }
@@ -780,7 +780,7 @@ func (b *BoltDB) deleteUserDetail(bdb *bolt.DB, userID string, userDetail UserDe
} }
return bdb.Update(func(tx *bolt.Tx) error { return bdb.Update(func(tx *bolt.Tx) error {
// updated entry is not empty and we need to store it's updated copy // updated entry is not empty and we need to store its updated copy
err := b.save(tx.Bucket([]byte(userDetailsBucketName)), userID, entry) err := b.save(tx.Bucket([]byte(userDetailsBucketName)), userID, entry)
if err != nil { if err != nil {
return fmt.Errorf("failed to update detail %s for %s: %w", userDetail, userID, err) return fmt.Errorf("failed to update detail %s for %s: %w", userDetail, userID, err)
+2 -2
View File
@@ -180,7 +180,7 @@ func TestFsStore_Cleanup(t *testing.T) {
svc, teardown := prepareImageTest(t) svc, teardown := prepareImageTest(t)
defer teardown() defer teardown()
save := func(file string, user string) (filePath string) { save := func(file, user string) (filePath string) {
id := path.Join(user, file) id := path.Join(user, file)
err := svc.Save(id, gopherPNGBytes()) err := svc.Save(id, gopherPNGBytes())
require.NoError(t, err) require.NoError(t, err)
@@ -227,7 +227,7 @@ func TestFsStore_Cleanup(t *testing.T) {
_, err = os.Stat(img2) _, err = os.Stat(img2)
assert.Error(t, err, "no file on staging anymore") assert.Error(t, err, "no file on staging anymore")
_, err = os.Stat(img3) _, err = os.Stat(img3)
assert.NoError(t, err, "third image is still on staging because it's cleanup timer was reset") assert.NoError(t, err, "third image is still on staging because its cleanup timer was reset")
err = svc.ResetCleanupTimer("unknown_image.png") err = svc.ResetCleanupTimer("unknown_image.png")
assert.Error(t, err) assert.Error(t, err)
+3 -3
View File
@@ -496,7 +496,7 @@ func (s *DataStore) EditComment(locator store.Locator, commentID string, req Edi
if e := s.AdminStore.OnEvent(comment.Locator.SiteID, admin.EvDelete); e != nil { if e := s.AdminStore.OnEvent(comment.Locator.SiteID, admin.EvDelete); e != nil {
log.Printf("[WARN] failed to send delete event, %s", e) log.Printf("[WARN] failed to send delete event, %s", e)
} }
// clean up the comment and it's parent from cache, so that // clean up the comment and its parent from cache, so that
// after cleaning up the child, parent won't be stuck non-deletable till cache expires // after cleaning up the child, parent won't be stuck non-deletable till cache expires
if s.repliesCache.LoadingCache != nil { if s.repliesCache.LoadingCache != nil {
s.repliesCache.Delete(commentID) s.repliesCache.Delete(commentID)
@@ -745,12 +745,12 @@ func (s *DataStore) Delete(locator store.Locator, commentID string, mode store.D
if e := s.AdminStore.OnEvent(locator.SiteID, admin.EvDelete); e != nil { if e := s.AdminStore.OnEvent(locator.SiteID, admin.EvDelete); e != nil {
log.Printf("[WARN] failed to send delete event, %s", e) log.Printf("[WARN] failed to send delete event, %s", e)
} }
// get comment to learn it's parent ID // get comment to learn its parent ID
comment, err := s.Engine.Get(engine.GetRequest{Locator: locator, CommentID: commentID}) comment, err := s.Engine.Get(engine.GetRequest{Locator: locator, CommentID: commentID})
if err != nil { if err != nil {
return err return err
} }
// clean up the comment and it's parent from cache, so that // clean up the comment and its parent from cache, so that
// after cleaning up the child, parent won't be stuck non-deletable till cache expires // after cleaning up the child, parent won't be stuck non-deletable till cache expires
if s.repliesCache.LoadingCache != nil { if s.repliesCache.LoadingCache != nil {
s.repliesCache.Delete(commentID) s.repliesCache.Delete(commentID)
+2 -2
View File
@@ -14,7 +14,7 @@ import (
func TestMakeTree(t *testing.T) { func TestMakeTree(t *testing.T) {
loc := store.Locator{URL: "url", SiteID: "site"} loc := store.Locator{URL: "url", SiteID: "site"}
ts := func(min int, sec int) time.Time { return time.Date(2017, 12, 25, 19, min, sec, 0, time.UTC) } ts := func(min, sec int) time.Time { return time.Date(2017, 12, 25, 19, min, sec, 0, time.UTC) }
// unsorted by purpose // unsorted by purpose
comments := []store.Comment{ comments := []store.Comment{
@@ -54,7 +54,7 @@ func TestMakeTree(t *testing.T) {
func TestMakeEmptySubtree(t *testing.T) { func TestMakeEmptySubtree(t *testing.T) {
loc := store.Locator{URL: "url", SiteID: "site"} loc := store.Locator{URL: "url", SiteID: "site"}
ts := func(min int, sec int) time.Time { return time.Date(2017, 12, 25, 19, min, sec, 0, time.UTC) } ts := func(min, sec int) time.Time { return time.Date(2017, 12, 25, 19, min, sec, 0, time.UTC) }
// unsorted by purpose // unsorted by purpose
comments := []store.Comment{ comments := []store.Comment{
@@ -305,10 +305,9 @@ function Comments({ isLoading, topComments, commentsShown, showMore }: CommentsP
return; return;
} }
updateIframeHeight();
// TODO: throttle updates // TODO: throttle updates
const observer = new MutationObserver(() => { const observer = new MutationObserver(() => {
updateIframeHeight();
// a hacky way to force iframe height update when new image is rendered and loaded // a hacky way to force iframe height update when new image is rendered and loaded
rootRef.current?.querySelectorAll('img').forEach((img) => { rootRef.current?.querySelectorAll('img').forEach((img) => {
img.addEventListener('load', updateIframeHeight); img.addEventListener('load', updateIframeHeight);
@@ -351,22 +350,28 @@ const CopyrightLink = (title: string) => (
</a> </a>
); );
const Copyright = () => (
<p className="root__copyright" role="contentinfo">
<FormattedMessage id="root.powered-by" defaultMessage="Powered by <a>Remark42</a>" values={{ a: CopyrightLink }} />
</p>
);
/** Root component connected to redux */ /** Root component connected to redux */
export function ConnectedRoot() { export function ConnectedRoot() {
const intl = useIntl(); const intl = useIntl();
const props = useSelector(mapStateToProps); const props = useSelector(mapStateToProps);
const actions = useActions(boundActions); const actions = useActions(boundActions);
if (!window.remark_config) {
throw new Error('Remark42: Config object is undefined');
}
const { no_footer } = window.remark_config;
return ( return (
<div className={clsx(b('root', {}, { theme: props.theme }), props.theme)}> <div className={clsx(b('root', {}, { theme: props.theme }), props.theme)}>
<Root {...props} {...actions} intl={intl} /> <Root {...props} {...actions} intl={intl} />
<p className="root__copyright" role="contentinfo"> {!no_footer && <Copyright />}
<FormattedMessage
id="root.powered-by"
defaultMessage="Powered by <a>Remark42</a>"
values={{ a: CopyrightLink }}
/>
</p>
</div> </div>
); );
} }
+1
View File
@@ -13,6 +13,7 @@ type RemarkConfig = {
max_last_comments?: number; max_last_comments?: number;
__colors__?: Record<string, string>; __colors__?: Record<string, string>;
simple_view?: boolean; simple_view?: boolean;
no_footer?: boolean;
}; };
declare global { declare global {
+3 -2
View File
@@ -3,8 +3,9 @@
"version": "0.16.0", "version": "0.16.0",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"dev": "cross-env REMARK_URL=http://127.0.0.1:8080 webpack serve --mode development", "dev": "cross-env REMARK_URL=http://127.0.0.1:8080 pnpm dev:custom",
"start": "cross-env REMARK_API_BASE_URL=https://demo.remark42.com webpack serve --mode development", "dev:demo": "cross-env REMARK_API_BASE_URL=https://demo.remark42.com pnpm dev:custom",
"dev:custom": "webpack serve --mode development",
"build": "webpack --mode production", "build": "webpack --mode production",
"build:analyze": "webpack --mode production --analyze", "build:analyze": "webpack --mode production --analyze",
"lint": "run-p lint:*", "lint": "run-p lint:*",
+1 -1
View File
@@ -11,7 +11,7 @@
"scripts": { "scripts": {
"prepare": "if [ -z \"$CI\" ]; then cd .. && husky install frontend/.husky; else echo \"Skip Husky Hooks\"; fi", "prepare": "if [ -z \"$CI\" ]; then cd .. && husky install frontend/.husky; else echo \"Skip Husky Hooks\"; fi",
"lint-staged": "lint-staged", "lint-staged": "lint-staged",
"dev:remark42": "turbo run dev --filter=@remark42/app", "dev:app": "turbo run dev --filter=@remark42/app",
"test:api": "turbo run test --filter=@remark42/api", "test:api": "turbo run test --filter=@remark42/api",
"coverage:api": "turbo run coverage --filter=@remark42/api", "coverage:api": "turbo run coverage --filter=@remark42/api",
"type-check:api": "turbo run type-check --filter=@remark42/api", "type-check:api": "turbo run type-check --filter=@remark42/api",
@@ -20,6 +20,7 @@ title: Frontend Configuration
- **`show_email_subscription`**`: boolean` (optional, `true` by default) enables email subscription feature in interface when enable it from backend side, if you set this param in `false` you will get notifications email notifications as admin but your users won't have interface for subscription - **`show_email_subscription`**`: boolean` (optional, `true` by default) enables email subscription feature in interface when enable it from backend side, if you set this param in `false` you will get notifications email notifications as admin but your users won't have interface for subscription
- **`show_rss_subscription`**`: boolean` (optional, `true` by default) enables RSS subscription feature in interface - **`show_rss_subscription`**`: boolean` (optional, `true` by default) enables RSS subscription feature in interface
- **`simple_view`**`: boolean` (optional, `false` by default) overrides the parameter from the backend minimized UI with basic info only - **`simple_view`**`: boolean` (optional, `false` by default) overrides the parameter from the backend minimized UI with basic info only
- **`no_footer`**`: boolean` (optional, `false` by default) hides footer with signatue and links to remark42
Example with all of the params: Example with all of the params:
@@ -34,7 +35,8 @@ Example with all of the params:
page_title: 'My custom title for a page', page_title: 'My custom title for a page',
locale: 'es', locale: 'es',
show_email_subscription: false, show_email_subscription: false,
simple_view: true simple_view: true,
no_footer: false
} }
</script> </script>
``` ```
+4 -4
View File
@@ -12,7 +12,7 @@ TLDR: Preact replicates React API and compatible with its libraries.
::: :::
In order to inject Remark42 widgets into websites we use `iframe` and `postMessage` for communication between a site and the widget. In order to inject Remark42 widgets into websites we use `iframe` and `postMessage` for communication between a site and the widget.
Simple widgets like [counter widget](https://remark42.com/docs/configuration/frontend/#counter-widget) can be injected as a script because it doesn't have it's own interface. Simple widgets like [counter widget](https://remark42.com/docs/configuration/frontend/#counter-widget) can be injected as a script because it doesn't have its own interface.
While development we setup environment which imitates real world example. We serve the page which uses Remark42 config and inject all of the widgets on it. You can check it on our [demo site](https://demo.remark42.com/web/). After successful installation you should have the same page running locally. While development we setup environment which imitates real world example. We serve the page which uses Remark42 config and inject all of the widgets on it. You can check it on our [demo site](https://demo.remark42.com/web/). After successful installation you should have the same page running locally.
@@ -36,12 +36,12 @@ Please use `127.0.0.1` and not `localhost` to access the server; otherwise, CORS
You can run frontend against demo instance of Remark42. This method of running Remark42 frontend code is preferred when you make a translation or visual adjustments that are easy to see without extensive testing. For this method we use our demo instance of Remark42 served on https://demo.remark42.com You can run frontend against demo instance of Remark42. This method of running Remark42 frontend code is preferred when you make a translation or visual adjustments that are easy to see without extensive testing. For this method we use our demo instance of Remark42 served on https://demo.remark42.com
For local development mode with Hot Reloading, use `pnpm start:app`. In this case, `webpack` will serve files using `webpack-dev-server` on `127.0.0.1:9000`. By visiting <http://127.0.0.1:9000/web/>, you will get a page with the main comments' widget communicating with a demo server backend running on `https://demo.remark42.com`. But you will not be able to log in with any OAuth providers due to security reasons. For local development mode with Hot Reloading, use `pnpm dev:app`. In this case, `webpack` will serve files using `webpack-dev-server` on `127.0.0.1:9000`. By visiting <http://127.0.0.1:9000/web/>, you will get a page with the main comments' widget communicating with a demo server backend running on `https://demo.remark42.com`. But you will not be able to log in with any OAuth providers due to security reasons.
You can attach the frontend to the locally running backend by providing the `REMARK_URL` environment variable. You can attach the frontend to the locally running backend from `frontend/apps/remark42` folder and providing the `REMARK_URL` environment variable.
```shell ```shell
npx cross-env REMARK_URL=http://127.0.0.1:8080 pnpm dev:app npx cross-env REMARK_URL=http://127.0.0.1:8080 pnpm dev:custom
``` ```
::: note ::: note