diff --git a/README.md b/README.md index 934f5bf6..0f1caec8 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,7 @@ _instructions for google oauth2 setup borrowed from [oauth2_proxy](https://githu 1. Disqus provides an export of all comments on your site in a g-zipped file. This is found in your Moderation panel at Disqus Admin > Setup > Export. The export will be sent into a queue and then emailed to the address associated with your account once it's ready. Direct link to export will be something like `https://.disqus.com/admin/discussions/export/`. See [importing-exporting](https://help.disqus.com/customer/portal/articles/1104797-importing-exporting) for more details. 2. Move this file to your remark42 host within `.var` and unzip, i.e. `gunzip .xml.gz`. -3. Run import command - `docker-compose run remark /srv/import-disqus.sh .xml ` +3. Run import command - `docker-compose exec remark /srv/import-disqus.sh .xml ` ### Frontend diff --git a/app/rest/api/admin_test.go b/app/rest/api/admin_test.go index 3ee4fb18..e874a3a7 100644 --- a/app/rest/api/admin_test.go +++ b/app/rest/api/admin_test.go @@ -105,13 +105,13 @@ func TestAdmin_Block(t *testing.T) { block := func(val int) (code int, body []byte) { client := http.Client{} - req, err := http.NewRequest(http.MethodPut, + req, e := http.NewRequest(http.MethodPut, fmt.Sprintf("http://dev:password@127.0.0.1:%d/api/v1/admin/user/%s?site=radio-t&block=%d", port, "user1", val), nil) - assert.Nil(t, err) - resp, err := client.Do(req) - require.Nil(t, err) - body, err = ioutil.ReadAll(resp.Body) - assert.Nil(t, err) + assert.Nil(t, e) + resp, e := client.Do(req) + require.Nil(t, e) + body, e = ioutil.ReadAll(resp.Body) + assert.Nil(t, e) resp.Body.Close() return resp.StatusCode, body } diff --git a/app/rest/api/rest.go b/app/rest/api/rest.go index 70684735..9142bbc3 100644 --- a/app/rest/api/rest.go +++ b/app/rest/api/rest.go @@ -44,7 +44,7 @@ const hardBodyLimit = 1024 * 64 // limit size of body var mdExt = blackfriday.NoIntraEmphasis | blackfriday.Tables | blackfriday.FencedCode | blackfriday.Strikethrough | blackfriday.SpaceHeadings | blackfriday.HardLineBreak | - blackfriday.BackslashLineBreak + blackfriday.BackslashLineBreak | blackfriday.Autolink // Run the lister and request's router, activate rest server func (s *Rest) Run(port int) { @@ -137,9 +137,14 @@ func (s *Rest) createCommentCtrl(w http.ResponseWriter, r *http.Request) { return } - comment.PrepareUntrusted() // clean all fields user not suppoed to set + comment.PrepareUntrusted() // clean all fields user not supposed to set comment.User = user comment.User.IP = strings.Split(r.RemoteAddr, ":")[0] + if err = s.DataService.ValidateComment(&comment); err != nil { + rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "invalid comment") + return + } + comment.Text = string(blackfriday.Run([]byte(comment.Text), blackfriday.WithExtensions(mdExt))) log.Printf("[DEBUG] create comment %+v", comment) diff --git a/app/rest/api/rest_test.go b/app/rest/api/rest_test.go index 493a3d73..38d2d939 100644 --- a/app/rest/api/rest_test.go +++ b/app/rest/api/rest_test.go @@ -62,19 +62,19 @@ func TestServer_CreateTooBig(t *testing.T) { require.NotNil(t, srv) defer cleanup(srv) - longComment := fmt.Sprintf(`{"text": "%6000s", "locator":{"url": "https://radio-t.com/blah1", "site": "radio-t"}}`, "blah") + longComment := fmt.Sprintf(`{"text": "%4001s", "locator":{"url": "https://radio-t.com/blah1", "site": "radio-t"}}`, "Щ") r := strings.NewReader(longComment) resp, err := http.Post(fmt.Sprintf("http://dev:password@127.0.0.1:%d/api/v1/comment", port), "application/json", r) assert.Nil(t, err) - assert.Equal(t, http.StatusInternalServerError, resp.StatusCode) + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) b, err := ioutil.ReadAll(resp.Body) assert.Nil(t, err) c := JSON{} err = json.Unmarshal(b, &c) assert.Nil(t, err) - assert.Equal(t, "comment text exceeded max allowed size", c["error"]) - assert.Equal(t, "can't save comment", c["details"]) + assert.Equal(t, "comment text exceeded max allowed size 4000 (4001)", c["error"]) + assert.Equal(t, "invalid comment", c["details"]) } func TestServer_Preview(t *testing.T) { @@ -84,6 +84,7 @@ func TestServer_Preview(t *testing.T) { r := strings.NewReader(`{"text": "test 123", "locator":{"url": "https://radio-t.com/blah1", "site": "radio-t"}}`) resp, err := http.Post(fmt.Sprintf("http://dev:password@127.0.0.1:%d/api/v1/preview", port), "application/json", r) + assert.Nil(t, err) assert.Equal(t, http.StatusOK, resp.StatusCode) b, err := ioutil.ReadAll(resp.Body) assert.Nil(t, err) @@ -111,6 +112,7 @@ BKT t.Log(j) r := strings.NewReader(j) resp, err := http.Post(fmt.Sprintf("http://dev:password@127.0.0.1:%d/api/v1/preview", port), "application/json", r) + assert.Nil(t, err) assert.Equal(t, http.StatusOK, resp.StatusCode) b, err := ioutil.ReadAll(resp.Body) assert.Nil(t, err) @@ -141,7 +143,7 @@ func TestServer_CreateAndGet(t *testing.T) { comment := store.Comment{} err = json.Unmarshal([]byte(res), &comment) assert.Nil(t, err) - assert.Equal(t, "

test 123 http://radio-t.com

\n", comment.Text) + assert.Equal(t, `

test 123 http://radio-t.com

`+"\n", comment.Text) assert.Equal(t, store.User{Name: "developer one", ID: "dev", Picture: "/api/v1/avatar/remark.image", Admin: true, Blocked: false, IP: "ea64bfc178468d943ca5b836e2e700c335404973"}, comment.User) @@ -442,6 +444,7 @@ func TestServer_Config(t *testing.T) { assert.Nil(t, err) assert.Equal(t, 300., j["edit_duration"]) assert.EqualValues(t, []interface{}([]interface{}{"a1", "a2"}), j["admins"]) + assert.Equal(t, 4000., j["max_comment_size"]) t.Logf("%+v", j) } diff --git a/app/rest/auth/avatar.go b/app/rest/auth/avatar.go index fdaf8f35..67323660 100644 --- a/app/rest/auth/avatar.go +++ b/app/rest/auth/avatar.go @@ -8,6 +8,7 @@ import ( "net/http" "os" "path" + "strconv" "strings" "sync" "time" @@ -87,7 +88,20 @@ func (p *AvatarProxy) Routes() (string, chi.Router) { // GET /123456789.image router.Get("/{avatar}", func(w http.ResponseWriter, r *http.Request) { + avatar := chi.URLParam(r, "avatar") + + // client-side caching + etag := `"` + avatar + `"` + w.Header().Set("Etag", etag) + w.Header().Set("Cache-Control", "max-age=2592000") // 30 days + if match := r.Header.Get("If-None-Match"); match != "" { + if strings.Contains(match, etag) { + w.WriteHeader(http.StatusNotModified) + return + } + } + location := p.location(strings.TrimSuffix(avatar, imgSfx)) avFile := path.Join(location, avatar) fh, err := os.Open(avFile) @@ -103,6 +117,11 @@ func (p *AvatarProxy) Routes() (string, chi.Router) { }() w.Header().Set("Content-Type", "image/*") + if fi, e := fh.Stat(); e == nil { + w.Header().Set("Content-Length", strconv.Itoa(int(fi.Size()))) + } + + // write all headers if status, ok := r.Context().Value(render.StatusCtxKey).(int); ok { w.WriteHeader(status) } diff --git a/app/rest/auth/avatar_test.go b/app/rest/auth/avatar_test.go index 71741cd8..9b22790c 100644 --- a/app/rest/auth/avatar_test.go +++ b/app/rest/auth/avatar_test.go @@ -85,7 +85,11 @@ func TestRoutes(t *testing.T) { handler.ServeHTTP(rr, req) assert.Equal(t, http.StatusOK, rr.Code) - assert.Equal(t, http.Header{"Content-Type": []string{"image/*"}}, rr.HeaderMap) + + assert.Equal(t, []string{"image/*"}, rr.HeaderMap["Content-Type"]) + assert.Equal(t, []string{"21"}, rr.HeaderMap["Content-Length"]) + assert.NotNil(t, rr.HeaderMap["Etag"]) + bb := bytes.Buffer{} sz, err := io.Copy(&bb, rr.Body) assert.NoError(t, err) diff --git a/app/rest/auth/provider_test.go b/app/rest/auth/provider_test.go index f7ac15d7..b5c522b7 100644 --- a/app/rest/auth/provider_test.go +++ b/app/rest/auth/provider_test.go @@ -30,6 +30,7 @@ func TestLogin(t *testing.T) { assert.Nil(t, err) assert.Equal(t, 200, resp.StatusCode) body, err := ioutil.ReadAll(resp.Body) + assert.Nil(t, err) t.Logf("resp %s", string(body)) u := store.User{} err = json.Unmarshal(body, &u) diff --git a/app/rest/auth/providers.go b/app/rest/auth/providers.go index 5e0d2c6e..f92489d5 100644 --- a/app/rest/auth/providers.go +++ b/app/rest/auth/providers.go @@ -51,6 +51,10 @@ func NewGithub(p Params) Provider { Name: data.value("name"), Picture: data.value("avatar_url"), } + // github may have no user name, use login in this case + if userInfo.Name == "" { + userInfo.Name = data.value("login") + } if userInfo.Name == "" { userInfo.Name = userInfo.ID } diff --git a/app/store/service.go b/app/store/service.go index c373f5ef..98564305 100644 --- a/app/store/service.go +++ b/app/store/service.go @@ -31,9 +31,6 @@ func (s *Service) Create(comment Comment) (commentID string, err error) { comment.Votes = make(map[string]bool) } - if err = s.ValidateComment(&comment); err != nil { - return "", err - } comment.Sanitize() // clear potentially dangerous js from all parts of comment comment.User.hashIP(s.Secret) // replace ip by hash @@ -111,10 +108,6 @@ func (s *Service) EditComment(locator Locator, commentID string, text string, ed comment.Edit = &edit comment.Edit.Timestamp = time.Now() - if err = s.ValidateComment(&comment); err != nil { - return comment, err - } - comment.Sanitize() err = s.Put(locator, comment) return comment, err @@ -140,8 +133,8 @@ func (s *Service) ValidateComment(c *Comment) error { if c.Text == "" { return errors.New("empty comment text") } - if len(c.Text) > maxSize { - return errors.New("comment text exceeded max allowed size") + if len([]rune(c.Text)) > maxSize { + return errors.Errorf("comment text exceeded max allowed size %d (%d)", maxSize, len([]rune(c.Text))) } if c.User.ID == "" || c.User.Name == "" { return errors.Errorf("empty user info") diff --git a/app/store/service_test.go b/app/store/service_test.go index 84ec4fb6..414c977b 100644 --- a/app/store/service_test.go +++ b/app/store/service_test.go @@ -85,7 +85,7 @@ func TestService_Vote(t *testing.T) { assert.Equal(t, map[string]bool{"user1": true}, c.Votes, "user voted +") c, err = b.Vote(Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID, "user", true) - assert.NotNil(t, "self-voting not allowed") + assert.NotNil(t, err, "self-voting not allowed") _, err = b.Vote(Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID, "user1", true) assert.NotNil(t, err, "double-voting rejected") @@ -178,7 +178,7 @@ func TestService_EditCommentDurationFailed(t *testing.T) { time.Sleep(time.Second) - comment, err = b.EditComment(Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID, "xxx", + _, err = b.EditComment(Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID, "xxx", Edit{Summary: "my edit"}) assert.NotNil(t, err) } @@ -195,7 +195,7 @@ func TestService_ValidateComment(t *testing.T) { {inp: Comment{}, err: errors.New("empty comment text")}, {inp: Comment{Text: "something blah", User: User{ID: "myid", Name: "name"}}, err: nil}, {inp: Comment{Text: "something blah", User: User{ID: "myid"}}, err: errors.New("empty user info")}, - {inp: Comment{Text: longText, User: User{ID: "myid", Name: "name"}}, err: errors.New("comment text exceeded max allowed size")}, + {inp: Comment{Text: longText, User: User{ID: "myid", Name: "name"}}, err: errors.New("comment text exceeded max allowed size 2000 (4000)")}, } for n, tt := range tbl { diff --git a/web/app/common/constants.js b/web/app/common/constants.js index cc22d65d..514873a3 100644 --- a/web/app/common/constants.js +++ b/web/app/common/constants.js @@ -6,6 +6,8 @@ const COMMENT_NODE_CLASSNAME_PREFIX = 'remark42__comment-'; const LAST_COMMENTS_NODE_CLASSNAME = 'remark42__last-comments'; const DEFAULT_LAST_COMMENTS_MAX = 15; const DEFAULT_MAX_COMMENT_SIZE = 1000; +const DEFAULT_SORT = '-score'; +const USELESS_COMMENT_SCORE = -10; const PROVIDER_NAMES = { google: 'Google', facebook: 'Facebook', @@ -22,4 +24,6 @@ module.exports = { DEFAULT_LAST_COMMENTS_MAX, DEFAULT_MAX_COMMENT_SIZE, PROVIDER_NAMES, + DEFAULT_SORT, + USELESS_COMMENT_SCORE, }; diff --git a/web/app/components/auth-panel/__column/auth-panel__column.scss b/web/app/components/auth-panel/__column/auth-panel__column.scss index 3dadbf14..fa430522 100644 --- a/web/app/components/auth-panel/__column/auth-panel__column.scss +++ b/web/app/components/auth-panel/__column/auth-panel__column.scss @@ -1,6 +1,7 @@ .auth-panel__column { &:nth-child(1) { overflow: hidden; + font-weight: 700; text-overflow: ellipsis; } diff --git a/web/app/components/auth-panel/_logged-in/auth-panel_logged-in.scss b/web/app/components/auth-panel/_logged-in/auth-panel_logged-in.scss index a47051c6..758c7d36 100644 --- a/web/app/components/auth-panel/_logged-in/auth-panel_logged-in.scss +++ b/web/app/components/auth-panel/_logged-in/auth-panel_logged-in.scss @@ -1,4 +1,9 @@ .auth-panel_logged-in { font-size: 12px; - font-weight: 400; + + .auth-panel__column { + &:first-child { + font-weight: 400; + } + } } diff --git a/web/app/components/auth-panel/auth-panel.jsx b/web/app/components/auth-panel/auth-panel.jsx index f39030b2..346d1754 100644 --- a/web/app/components/auth-panel/auth-panel.jsx +++ b/web/app/components/auth-panel/auth-panel.jsx @@ -90,21 +90,17 @@ export default class AuthPanel extends Component { {user.admin && ' • '} - { - !!user.id && ( - - Sort by - {' '} - - - ) - } + + Sort by + {' '} + + ); diff --git a/web/app/components/auth-panel/auth-panel.scss b/web/app/components/auth-panel/auth-panel.scss index a5749217..03c6138a 100644 --- a/web/app/components/auth-panel/auth-panel.scss +++ b/web/app/components/auth-panel/auth-panel.scss @@ -2,6 +2,5 @@ display: flex; justify-content: space-between; font-size: 14px; - font-weight: 700; line-height: 16px; } diff --git a/web/app/components/comment/__action/_type/_collapse/comment__action_type_collapse.scss b/web/app/components/comment/__action/_type/_collapse/comment__action_type_collapse.scss new file mode 100644 index 00000000..54360eeb --- /dev/null +++ b/web/app/components/comment/__action/_type/_collapse/comment__action_type_collapse.scss @@ -0,0 +1,26 @@ +.comment__action_type_collapse { + display: inline-block; + box-sizing: border-box; + width: 12px; + height: 12px; + font-size: 12px; + line-height: 10px; + text-align: center; + border: 1px solid #ddd; + border-radius: 2px; + color: #ddd; + + &:hover { + border-color: #0aa; + color: #0aa; + } + + &.comment__action_selected { + background: #ddd; + color: #fff; + + &:hover { + background: #0aa; + } + } +} diff --git a/web/app/components/comment/__action/comment__action.scss b/web/app/components/comment/__action/comment__action.scss index a324230c..8c8d91c3 100644 --- a/web/app/components/comment/__action/comment__action.scss +++ b/web/app/components/comment/__action/comment__action.scss @@ -1,5 +1,6 @@ .comment__action { font-size: 14px; + vertical-align: middle; cursor: pointer; user-select: none; color: #259C9A; @@ -12,6 +13,10 @@ outline: none; } + + .comment__action { + margin-left: 8px; + } + + .comment__controls { &::before { content: '•'; diff --git a/web/app/components/comment/__text/comment__text.scss b/web/app/components/comment/__text/comment__text.scss index 0859c323..a91f733a 100644 --- a/web/app/components/comment/__text/comment__text.scss +++ b/web/app/components/comment/__text/comment__text.scss @@ -1,3 +1,4 @@ .comment__text { margin-bottom: 4px; + overflow: hidden; } diff --git a/web/app/components/comment/__vote/_selected/comment__vote_selected.scss b/web/app/components/comment/__vote/_selected/comment__vote_selected.scss index 4303f422..28016146 100644 --- a/web/app/components/comment/__vote/_selected/comment__vote_selected.scss +++ b/web/app/components/comment/__vote/_selected/comment__vote_selected.scss @@ -1,4 +1,3 @@ .comment__vote_selected { - background-image: url('comment__vote_selected.svg'); cursor: default; } diff --git a/web/app/components/comment/__vote/_type/_down/comment__vote_type_down.scss b/web/app/components/comment/__vote/_type/_down/comment__vote_type_down.scss index a89a58f5..7f09db3f 100644 --- a/web/app/components/comment/__vote/_type/_down/comment__vote_type_down.scss +++ b/web/app/components/comment/__vote/_type/_down/comment__vote_type_down.scss @@ -1,4 +1,10 @@ .comment__vote_type_down { transform: scale(1, -1); margin-left: 4px; + + &.comment__vote_selected { + &, &:hover { + background-image: url('comment__vote_type_down.svg'); + } + } } diff --git a/web/app/components/comment/__vote/_type/_down/comment__vote_type_down.svg b/web/app/components/comment/__vote/_type/_down/comment__vote_type_down.svg new file mode 100644 index 00000000..d29d4a65 --- /dev/null +++ b/web/app/components/comment/__vote/_type/_down/comment__vote_type_down.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/app/components/comment/__vote/_type/_up/comment__vote_type_up.scss b/web/app/components/comment/__vote/_type/_up/comment__vote_type_up.scss index ac2bb6eb..9ecc2c13 100644 --- a/web/app/components/comment/__vote/_type/_up/comment__vote_type_up.scss +++ b/web/app/components/comment/__vote/_type/_up/comment__vote_type_up.scss @@ -1,3 +1,9 @@ .comment__vote_type_up { margin-right: 4px; + + &.comment__vote_selected { + &, &:hover { + background-image: url('comment__vote_type_up.svg'); + } + } } diff --git a/web/app/components/comment/__vote/_type/_up/comment__vote_type_up.svg b/web/app/components/comment/__vote/_type/_up/comment__vote_type_up.svg new file mode 100644 index 00000000..463ab2cd --- /dev/null +++ b/web/app/components/comment/__vote/_type/_up/comment__vote_type_up.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/app/components/comment/_level/comment_level.scss b/web/app/components/comment/_level/comment_level.scss index 8ba7a197..db3cdb60 100644 --- a/web/app/components/comment/_level/comment_level.scss +++ b/web/app/components/comment/_level/comment_level.scss @@ -7,7 +7,7 @@ $step: 24px; &.comment_replying { .comment__input { - @media (hover: none) and (max-width: 768px) { + @media (-moz-touch-enabled: 1) and (max-width: 768px), (pointer:coarse) and (max-width: 768px) { margin-left: -$i * $step; } } diff --git a/web/app/components/comment/_replying/comment_replying.scss b/web/app/components/comment/_replying/comment_replying.scss index 1235a983..2980cf57 100644 --- a/web/app/components/comment/_replying/comment_replying.scss +++ b/web/app/components/comment/_replying/comment_replying.scss @@ -9,7 +9,7 @@ } // it isn't mobile first, but it's fine here - @media (hover: none) and (max-width: 768px) { + @media (-moz-touch-enabled: 1) and (max-width: 768px), (pointer:coarse) and (max-width: 768px) { border: 8px solid #eee; padding-bottom: 0; diff --git a/web/app/components/comment/comment.jsx b/web/app/components/comment/comment.jsx index 15e8130b..74202c4f 100644 --- a/web/app/components/comment/comment.jsx +++ b/web/app/components/comment/comment.jsx @@ -1,7 +1,7 @@ import { h, Component } from 'preact'; import api from 'common/api'; -import { API_BASE, BASE_URL, COMMENT_NODE_CLASSNAME_PREFIX } from 'common/constants'; +import { API_BASE, BASE_URL, COMMENT_NODE_CLASSNAME_PREFIX, USELESS_COMMENT_SCORE } from 'common/constants'; import { url } from 'common/settings'; import store from 'common/store'; @@ -21,6 +21,7 @@ export default class Comment extends Component { this.decreaseScore = this.decreaseScore.bind(this); this.increaseScore = this.increaseScore.bind(this); this.toggleInputVisibility = this.toggleInputVisibility.bind(this); + this.toggleCollapse = this.toggleCollapse.bind(this); this.toggleUserIdVisibility = this.toggleUserIdVisibility.bind(this); this.scrollToParent = this.scrollToParent.bind(this); this.onReply = this.onReply.bind(this); @@ -194,8 +195,14 @@ export default class Comment extends Component { } } + toggleCollapse() { + if (this.props.onCollapseToggle) { + this.props.onCollapseToggle(); + } + } + render(props, { guest, isUserIdVisible, userBlocked, pinned, score, scoreIncreased, scoreDecreased, deleted, isInputVisible }) { - const { data, mix, mods = {} } = props; + const { data, mods = {} } = props; const isAdmin = !guest && store.get('user').admin; const isGuest = guest || !Object.keys(store.get('user')).length; const isCurrentUser = (data.user && data.user.id) === (store.get('user') && store.get('user').id); @@ -229,7 +236,7 @@ export default class Comment extends Component { const defaultMods = { pinned, - useless: userBlocked || deleted, + useless: userBlocked || deleted || (score <= USELESS_COMMENT_SCORE && !mods.pinned && !mods.disabled), // TODO: add default view mod or don't? view: o.user.admin ? 'admin' : null, replying: isInputVisible, @@ -237,7 +244,7 @@ export default class Comment extends Component { if (mods.view === 'preview') { return ( -
+
{o.user.name} @@ -328,7 +335,7 @@ export default class Comment extends Component {
{ - !mods.disabled && !isGuest && ( + !deleted && !mods.disabled && !isGuest && ( {mods.collapsed ? '+' : '−'} + ) + } + + { + !deleted && isAdmin && ( { !pinned && ( diff --git a/web/app/components/comment/index.js b/web/app/components/comment/index.js index 09b4fb07..6538f931 100644 --- a/web/app/components/comment/index.js +++ b/web/app/components/comment/index.js @@ -5,6 +5,7 @@ export { default } from './comment'; require('./comment.scss'); require('./__action/comment__action.scss'); +require('./__action/_type/_collapse/comment__action_type_collapse.scss'); require('./__avatar/comment__avatar.scss'); require('./__avatar/_default/comment__avatar_default.scss'); diff --git a/web/app/components/input/__preview-wrapper/input__preview-wrapper.scss b/web/app/components/input/__preview-wrapper/input__preview-wrapper.scss new file mode 100644 index 00000000..78e523bd --- /dev/null +++ b/web/app/components/input/__preview-wrapper/input__preview-wrapper.scss @@ -0,0 +1,4 @@ +.input__preview-wrapper { + overflow: hidden; + background: #eee; +} diff --git a/web/app/components/input/__preview/input__preview.scss b/web/app/components/input/__preview/input__preview.scss index 29ad640c..7e2b5dec 100644 --- a/web/app/components/input/__preview/input__preview.scss +++ b/web/app/components/input/__preview/input__preview.scss @@ -1,6 +1,7 @@ .input__preview { margin-top: 8px; padding: 7px 11px; + overflow: hidden; font-size: 16px; line-height: 1.2; border: 1px dashed #eee; diff --git a/web/app/components/input/index.js b/web/app/components/input/index.js index b5b8d05e..e530c84b 100644 --- a/web/app/components/input/index.js +++ b/web/app/components/input/index.js @@ -14,3 +14,4 @@ require('./__error/input__error.scss'); require('./__field/input__field.scss'); require('./__field-wrapper/input__field-wrapper.scss'); require('./__preview/input__preview.scss'); +require('./__preview-wrapper/input__preview-wrapper.scss'); diff --git a/web/app/components/input/input.jsx b/web/app/components/input/input.jsx index 3d37e897..8f0d511f 100644 --- a/web/app/components/input/input.jsx +++ b/web/app/components/input/input.jsx @@ -149,10 +149,12 @@ export default class Input extends Component { // TODO: it can be more elegant; // for example it can render full comment component here (or above textarea on mobile) !!preview && ( -
+
+
+
) } diff --git a/web/app/components/raw-content/raw-content.scss b/web/app/components/raw-content/raw-content.scss index a5781a36..d787be95 100644 --- a/web/app/components/raw-content/raw-content.scss +++ b/web/app/components/raw-content/raw-content.scss @@ -58,5 +58,19 @@ pre { overflow-x: auto; tab-size: 2; + + &:first-child { + margin-top: 0; + } + + &:last-child { + margin-bottom: 0; + } + } + + sup, sub { + sup, sub { + vertical-align: middle; // to prevent some visual cheats + } } } diff --git a/web/app/components/root/root.jsx b/web/app/components/root/root.jsx index 0875d839..6398d136 100644 --- a/web/app/components/root/root.jsx +++ b/web/app/components/root/root.jsx @@ -1,7 +1,7 @@ import { h, Component } from 'preact'; import api from 'common/api'; -import { BASE_URL, NODE_ID, COMMENT_NODE_CLASSNAME_PREFIX } from 'common/constants'; +import { BASE_URL, NODE_ID, COMMENT_NODE_CLASSNAME_PREFIX, DEFAULT_SORT } from 'common/constants'; import { url } from 'common/settings'; import store from 'common/store'; @@ -21,9 +21,9 @@ export default class Root extends Component { let sort; try { - sort = localStorage.getItem(LS_SORT_KEY); + sort = localStorage.getItem(LS_SORT_KEY) || DEFAULT_SORT; } catch(e) { - sort = '-score'; + sort = DEFAULT_SORT; } this.state = { @@ -100,7 +100,7 @@ export default class Root extends Component { const newWindow = window.open(`${BASE_URL}/auth/${provider}/login?from=${encodeURIComponent(location.href)}`); let secondsPass = 0; - const checkMsDelay = 200; + const checkMsDelay = 100; const checkInterval = setInterval(() => { secondsPass += checkMsDelay; diff --git a/web/app/components/thread/thread.jsx b/web/app/components/thread/thread.jsx index 2460b197..fa7d80f7 100644 --- a/web/app/components/thread/thread.jsx +++ b/web/app/components/thread/thread.jsx @@ -3,20 +3,36 @@ import { h, Component } from 'preact'; import Comment from 'components/comment'; export default class Thread extends Component { - render(props) { + constructor(props) { + super(props); + + this.state = { + collapsed: false, + }; + + this.onCollapseToggle = this.onCollapseToggle.bind(this); + } + + + onCollapseToggle() { + this.setState({ collapsed: !this.state.collapsed }); + } + + render(props, { collapsed }) { const { data: { comment, replies = [] }, mix, mods = {}, onReplyClick } = props; return (
{ - !!replies.length && replies.map(thread => ( + !collapsed && !!replies.length && replies.map(thread => ( var remark_config = { -ё site_id: 'remark', + site_id: 'remark', url: 'https://remark42.com/demo/', };