Compare commits
16
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d95baa70f | ||
|
|
050bce163a | ||
|
|
68b84683d0 | ||
|
|
6fe373d540 | ||
|
|
ba19bcc729 | ||
|
|
f1b65db2c7 | ||
|
|
f5f287ef06 | ||
|
|
f161e6033c | ||
|
|
c86bff8811 | ||
|
|
596b1045bd | ||
|
|
907ca2b590 | ||
|
|
f1b5469b83 | ||
|
|
984fbde540 | ||
|
|
2bdc05dd47 | ||
|
|
d2ea572abf | ||
|
|
8d5c4cd578 |
@@ -30,7 +30,7 @@ jobs:
|
||||
DEBUG: ${{secrets.DEBUG}}
|
||||
|
||||
- name: install go
|
||||
uses: actions/setup-go@v2
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: 1.17
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ name: build
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
tags:
|
||||
paths:
|
||||
- ".github/workflows/ci-build.yml"
|
||||
@@ -33,11 +32,11 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
- name: set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: available platforms
|
||||
run: echo ${{ steps.buildx.outputs.platforms }}
|
||||
|
||||
@@ -23,11 +23,11 @@ jobs:
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
- name: set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: available platforms
|
||||
run: echo ${{ steps.buildx.outputs.platforms }}
|
||||
|
||||
+2
-4
@@ -96,20 +96,18 @@ LABEL org.opencontainers.image.authors="Umputun <umputun@gmail.com>" \
|
||||
|
||||
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/restore.sh /usr/local/bin/restore
|
||||
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-frontend /srv/frontend/apps/remark42/public/ /srv/web/
|
||||
COPY docker-init.sh /srv/init.sh
|
||||
RUN chown -R app:app /srv
|
||||
RUN ln -s /srv/remark42 /usr/bin/remark42
|
||||
|
||||
EXPOSE 8080
|
||||
HEALTHCHECK --interval=30s --timeout=3s CMD curl --fail http://localhost:8080/ping || exit 1
|
||||
|
||||
RUN chmod +x /srv/init.sh
|
||||
CMD ["/srv/remark42", "server"]
|
||||
|
||||
@@ -35,7 +35,6 @@ linters:
|
||||
- revive
|
||||
- govet
|
||||
- unconvert
|
||||
- megacheck
|
||||
- gas
|
||||
- gocyclo
|
||||
- dupl
|
||||
|
||||
@@ -8,11 +8,11 @@ package accessor
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
log "github.com/go-pkgz/lgr"
|
||||
"github.com/umputun/remark42/backend/app/store"
|
||||
"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
|
||||
|
||||
case engine.Blocked:
|
||||
log.Printf("%+v", m.metaUsers)
|
||||
log.Printf("[INFO] metaUsers: %+v", m.metaUsers)
|
||||
for _, u := range m.metaUsers {
|
||||
if u.SiteID == req.Locator.SiteID && u.Blocked && u.BlockedUntil.After(time.Now()) {
|
||||
res = append(res, store.BlockedUser{ID: u.UserID, Until: u.BlockedUntil})
|
||||
|
||||
@@ -83,7 +83,7 @@ func (ab AutoBackup) removeOldBackupFiles() {
|
||||
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 {
|
||||
for i := 0; i < len(backFiles)-ab.KeepMax; i++ {
|
||||
|
||||
@@ -38,7 +38,7 @@ type MapperMaker func(reader io.Reader) (Mapper, error)
|
||||
type Store interface {
|
||||
Create(comment store.Comment) (commentID string, err 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
|
||||
Metas(siteID string) (umetas []service.UserMetaData, pmetas []service.PostMetaData, err error)
|
||||
SetMetas(siteID string, umetas []service.UserMetaData, pmetas []service.PostMetaData) error
|
||||
|
||||
@@ -34,8 +34,8 @@ type Destination interface {
|
||||
// Store defines the minimal interface accessing stored comments used by notifier
|
||||
type Store interface {
|
||||
Get(locator store.Locator, id string, user store.User) (store.Comment, error)
|
||||
GetUserEmail(siteID string, userID string) (string, error)
|
||||
GetUserTelegram(siteID string, userID string) (string, error)
|
||||
GetUserEmail(siteID, userID string) (string, error)
|
||||
GetUserTelegram(siteID, userID string) (string, error)
|
||||
}
|
||||
|
||||
// used for email and telegram retrieval from user details
|
||||
|
||||
@@ -29,15 +29,15 @@ type admin struct {
|
||||
|
||||
type adminStore interface {
|
||||
Delete(locator store.Locator, commentID string, mode store.DeleteMode) error
|
||||
DeleteUser(siteID string, userID string, mode store.DeleteMode) error
|
||||
DeleteUserDetail(siteID string, userID string, detail engine.UserDetail) error
|
||||
DeleteUser(siteID, userID string, mode store.DeleteMode) error
|
||||
DeleteUserDetail(siteID, userID string, detail engine.UserDetail) error
|
||||
User(siteID, userID string, limit, skip int, user store.User) ([]store.Comment, error)
|
||||
IsBlocked(siteID string, userID string) bool
|
||||
SetBlock(siteID string, userID string, status bool, ttl time.Duration) error
|
||||
IsBlocked(siteID, userID string) bool
|
||||
SetBlock(siteID, userID string, status bool, ttl time.Duration) error
|
||||
BlockedUsers(siteID string) ([]store.BlockedUser, error)
|
||||
Info(locator store.Locator, readonlyAge int) (store.PostInfo, 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
|
||||
SetPin(locator store.Locator, commentID string, status bool) error
|
||||
}
|
||||
|
||||
@@ -651,7 +651,7 @@ func subscribersOnly(enable bool) 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
|
||||
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 {
|
||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -58,15 +58,15 @@ type privStore interface {
|
||||
Vote(req service.VoteReq) (comment store.Comment, err 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)
|
||||
GetUserEmail(siteID string, userID string) (string, error)
|
||||
SetUserEmail(siteID string, userID string, value string) (string, error)
|
||||
GetUserTelegram(siteID string, userID string) (string, error)
|
||||
SetUserTelegram(siteID string, userID string, value string) (string, error)
|
||||
DeleteUserDetail(siteID string, userID string, detail engine.UserDetail) error
|
||||
GetUserEmail(siteID, userID string) (string, error)
|
||||
SetUserEmail(siteID, userID, value string) (string, error)
|
||||
GetUserTelegram(siteID, userID string) (string, error)
|
||||
SetUserTelegram(siteID, userID, value string) (string, error)
|
||||
DeleteUserDetail(siteID, userID string, detail engine.UserDetail) error
|
||||
ValidateComment(c *store.Comment) error
|
||||
IsVerified(siteID string, userID string) bool
|
||||
IsVerified(siteID, userID string) 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)
|
||||
}
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ type pubStore interface {
|
||||
User(siteID, userID string, limit, skip int, user store.User) ([]store.Comment, error)
|
||||
UserCount(siteID, userID string) (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)
|
||||
|
||||
ValidateComment(c *store.Comment) error
|
||||
|
||||
@@ -378,7 +378,7 @@ func Test_validEmailAuth(t *testing.T) {
|
||||
status int
|
||||
}{
|
||||
{"/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=Евгений+Умпутун", http.StatusOK},
|
||||
{"/auth/email/login?site=remark42&address=umputun%example.com&user=12", http.StatusForbidden},
|
||||
|
||||
@@ -111,15 +111,11 @@ func (s *Rest) getRemarkHost() string {
|
||||
|
||||
func (s *Rest) makeTLSConfig() *tls.Config {
|
||||
return &tls.Config{
|
||||
PreferServerCipherSuites: true,
|
||||
CipherSuites: []uint16{
|
||||
tls.TLS_ECDHE_ECDSA_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_RSA_WITH_AES_128_GCM_SHA256,
|
||||
// tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
|
||||
},
|
||||
MinVersion: tls.VersionTLS12,
|
||||
CurvePreferences: []tls.CurveID{
|
||||
|
||||
@@ -78,5 +78,5 @@ func (s *StaticStore) Enabled(site string) (ok bool, err error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// OnEvent doesn nothing for StaticStore
|
||||
// OnEvent does nothing for StaticStore
|
||||
func (s *StaticStore) OnEvent(_ string, _ EventType) error { return nil }
|
||||
|
||||
@@ -16,15 +16,15 @@ import (
|
||||
|
||||
// BoltDB implements store.Interface, represents multiple sites with multiplexing to different bolt dbs. Thread safe.
|
||||
// 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
|
||||
// - 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
|
||||
// - 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
|
||||
// - users details in "user_details" bucket. Key is userID, value - UserDetailEntry
|
||||
// - 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
|
||||
// - readonly per post to keep status of manually set RO posts. Key is post url, value - ts
|
||||
// - 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
|
||||
// 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
|
||||
// is a nested bucket named userID with kv as ts:reference
|
||||
// - users details in "user_details" bucket. Key is userID, value - UserDetailEntry
|
||||
// - 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
|
||||
// - readonly per post to keep status of manually set RO posts. Key is post url, value - ts
|
||||
type BoltDB struct {
|
||||
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 {
|
||||
// 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)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update detail %s for %s: %w", userDetail, userID, err)
|
||||
|
||||
@@ -180,7 +180,7 @@ func TestFsStore_Cleanup(t *testing.T) {
|
||||
svc, teardown := prepareImageTest(t)
|
||||
defer teardown()
|
||||
|
||||
save := func(file string, user string) (filePath string) {
|
||||
save := func(file, user string) (filePath string) {
|
||||
id := path.Join(user, file)
|
||||
err := svc.Save(id, gopherPNGBytes())
|
||||
require.NoError(t, err)
|
||||
@@ -227,7 +227,7 @@ func TestFsStore_Cleanup(t *testing.T) {
|
||||
_, err = os.Stat(img2)
|
||||
assert.Error(t, err, "no file on staging anymore")
|
||||
_, 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")
|
||||
assert.Error(t, err)
|
||||
|
||||
@@ -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 {
|
||||
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
|
||||
if s.repliesCache.LoadingCache != nil {
|
||||
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 {
|
||||
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})
|
||||
if err != nil {
|
||||
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
|
||||
if s.repliesCache.LoadingCache != nil {
|
||||
s.repliesCache.Delete(commentID)
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
|
||||
func TestMakeTree(t *testing.T) {
|
||||
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
|
||||
comments := []store.Comment{
|
||||
@@ -54,7 +54,7 @@ func TestMakeTree(t *testing.T) {
|
||||
|
||||
func TestMakeEmptySubtree(t *testing.T) {
|
||||
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
|
||||
comments := []store.Comment{
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
global.ResizeObserver = class ResizeObserver {
|
||||
observe() {
|
||||
// do nothing
|
||||
}
|
||||
unobserve() {
|
||||
// do nothing
|
||||
}
|
||||
disconnect() {
|
||||
// do nothing
|
||||
}
|
||||
};
|
||||
@@ -76,11 +76,11 @@ export function useDropdown(disableClosing?: boolean) {
|
||||
|
||||
handleChangeIframeSize(dropdownElement);
|
||||
|
||||
const observer = new MutationObserver(() => {
|
||||
const observer = new ResizeObserver(() => {
|
||||
handleChangeIframeSize(dropdownElement);
|
||||
});
|
||||
|
||||
observer.observe(dropdownElement, { attributes: true, childList: true, subtree: true });
|
||||
observer.observe(dropdownElement);
|
||||
|
||||
return () => {
|
||||
document.body.style.removeProperty('min-height');
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
line-height: 1.4;
|
||||
border: 0;
|
||||
resize: none;
|
||||
overflow: hidden; /* prevent scrollbar from appearing */
|
||||
backface-visibility: hidden; /* let's try to fix blinking in Safari */
|
||||
transform: translateZ(0); /* let's try to fix blinking in Safari, again */
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { h, Component, Fragment } from 'preact';
|
||||
import { useEffect, useRef } from 'preact/hooks';
|
||||
import { useEffect } from 'preact/hooks';
|
||||
import { useSelector } from 'react-redux';
|
||||
import b from 'bem-react-helper';
|
||||
import { IntlShape, useIntl, FormattedMessage, defineMessages } from 'react-intl';
|
||||
@@ -42,6 +42,13 @@ import { setCollapse } from 'store/thread/actions';
|
||||
|
||||
import styles from './root.module.css';
|
||||
|
||||
/**
|
||||
* Sends size of the iframe to parent window
|
||||
*/
|
||||
export function updateIframeHeight() {
|
||||
postMessageToParent({ height: document.body.offsetHeight });
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: StoreState) => ({
|
||||
sort: state.comments.sort,
|
||||
isCommentsLoading: state.comments.isFetching,
|
||||
@@ -287,10 +294,6 @@ export class Root extends Component<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
function updateIframeHeight() {
|
||||
postMessageToParent({ height: document.body.offsetHeight });
|
||||
}
|
||||
|
||||
interface CommentsProps {
|
||||
isLoading: boolean;
|
||||
topComments: string[];
|
||||
@@ -298,34 +301,12 @@ interface CommentsProps {
|
||||
showMore(): void;
|
||||
}
|
||||
function Comments({ isLoading, topComments, commentsShown, showMore }: CommentsProps) {
|
||||
const rootRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!rootRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: throttle updates
|
||||
const observer = new MutationObserver(() => {
|
||||
updateIframeHeight();
|
||||
|
||||
// a hacky way to force iframe height update when new image is rendered and loaded
|
||||
rootRef.current?.querySelectorAll('img').forEach((img) => {
|
||||
img.addEventListener('load', updateIframeHeight);
|
||||
});
|
||||
});
|
||||
|
||||
observer.observe(rootRef.current, { attributes: true, childList: true, subtree: true });
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
const renderComments =
|
||||
IS_MOBILE && commentsShown < topComments.length ? topComments.slice(0, commentsShown) : topComments;
|
||||
const isShowMoreButtonVisible = IS_MOBILE && commentsShown < topComments.length;
|
||||
|
||||
return (
|
||||
<div className="root__threads" role="list" ref={rootRef}>
|
||||
<div className="root__threads" role="list">
|
||||
{isLoading ? (
|
||||
<Preloader className="root__preloader" />
|
||||
) : (
|
||||
@@ -351,22 +332,36 @@ const CopyrightLink = (title: string) => (
|
||||
</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 */
|
||||
export function ConnectedRoot() {
|
||||
const intl = useIntl();
|
||||
const props = useSelector(mapStateToProps);
|
||||
const actions = useActions(boundActions);
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new ResizeObserver(updateIframeHeight);
|
||||
|
||||
updateIframeHeight();
|
||||
observer.observe(document.body);
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
if (!window.remark_config) {
|
||||
throw new Error('Remark42: Config object is undefined');
|
||||
}
|
||||
|
||||
const { no_footer } = window.remark_config;
|
||||
|
||||
return (
|
||||
<div className={clsx(b('root', {}, { theme: props.theme }), props.theme)}>
|
||||
<Root {...props} {...actions} intl={intl} />
|
||||
<p className="root__copyright" role="contentinfo">
|
||||
<FormattedMessage
|
||||
id="root.powered-by"
|
||||
defaultMessage="Powered by <a>Remark42</a>"
|
||||
values={{ a: CopyrightLink }}
|
||||
/>
|
||||
</p>
|
||||
{!no_footer && <Copyright />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,13 +2,13 @@ import { h, JSX } from 'preact';
|
||||
import { forwardRef } from 'preact/compat';
|
||||
import { useEffect, useRef } from 'preact/hooks';
|
||||
|
||||
function autoResize(textarea: HTMLTextAreaElement) {
|
||||
function autoResize(textarea: HTMLTextAreaElement, onResize?: () => void) {
|
||||
textarea.style.height = '';
|
||||
textarea.style.height = `${textarea.scrollHeight}px`;
|
||||
}
|
||||
|
||||
type Props = Omit<JSX.HTMLAttributes<HTMLTextAreaElement>, 'onInput'> & {
|
||||
onInput?: (evt: JSX.TargetedEvent<HTMLTextAreaElement, Event>) => void;
|
||||
onInput?(evt: JSX.TargetedEvent<HTMLTextAreaElement, Event>): void;
|
||||
};
|
||||
|
||||
export const TextareaAutosize = forwardRef<HTMLTextAreaElement, Props>(({ onInput, value, ...props }, externalRef) => {
|
||||
|
||||
@@ -13,6 +13,7 @@ type RemarkConfig = {
|
||||
max_last_comments?: number;
|
||||
__colors__?: Record<string, string>;
|
||||
simple_view?: boolean;
|
||||
no_footer?: boolean;
|
||||
};
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -33,6 +33,7 @@ const config: Config = {
|
||||
setupFilesAfterEnv: [
|
||||
'<rootDir>/app/__mocks__/fetch.ts',
|
||||
'<rootDir>/app/__mocks__/localstorage.ts',
|
||||
'<rootDir>/app/__mocks__/resize-observer.ts',
|
||||
'<rootDir>/app/__stubs__/remark-config.ts',
|
||||
'<rootDir>/app/__stubs__/static-config.ts',
|
||||
],
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
"version": "0.16.0",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"dev": "cross-env REMARK_URL=http://127.0.0.1:8080 webpack serve --mode development",
|
||||
"start": "cross-env REMARK_API_BASE_URL=https://demo.remark42.com webpack serve --mode development",
|
||||
"dev": "cross-env REMARK_URL=http://127.0.0.1:8080 pnpm dev:custom",
|
||||
"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:analyze": "webpack --mode production --analyze",
|
||||
"lint": "run-p lint:*",
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"scripts": {
|
||||
"prepare": "if [ -z \"$CI\" ]; then cd .. && husky install frontend/.husky; else echo \"Skip Husky Hooks\"; fi",
|
||||
"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",
|
||||
"coverage:api": "turbo run coverage --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_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
|
||||
- **`no_footer`**`: boolean` (optional, `false` by default) – hides footer with signatue and links to remark42
|
||||
|
||||
Example with all of the params:
|
||||
|
||||
@@ -34,7 +35,8 @@ Example with all of the params:
|
||||
page_title: 'My custom title for a page',
|
||||
locale: 'es',
|
||||
show_email_subscription: false,
|
||||
simple_view: true
|
||||
simple_view: true,
|
||||
no_footer: false
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -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.
|
||||
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.
|
||||
|
||||
@@ -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
|
||||
|
||||
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
|
||||
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 ℹ️
|
||||
|
||||
Reference in New Issue
Block a user