Resolve conflicts
This commit is contained in:
@@ -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://<siteud>.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 <disqus-export-name>.xml.gz`.
|
||||
3. Run import command - `docker-compose run remark /srv/import-disqus.sh <disqus-export-name>.xml <your site id>`
|
||||
3. Run import command - `docker-compose exec remark /srv/import-disqus.sh <disqus-export-name>.xml <your site id>`
|
||||
|
||||
### Frontend
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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, "<p><strong>test</strong> <em>123</em> http://radio-t.com</p>\n", comment.Text)
|
||||
assert.Equal(t, `<p><strong>test</strong> <em>123</em> <a href="http://radio-t.com" rel="nofollow">http://radio-t.com</a></p>`+"\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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 == "<nil>" {
|
||||
userInfo.Name = data.value("login")
|
||||
}
|
||||
if userInfo.Name == "" {
|
||||
userInfo.Name = userInfo.ID
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
.auth-panel__column {
|
||||
&:nth-child(1) {
|
||||
overflow: hidden;
|
||||
font-weight: 700;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
.auth-panel_logged-in {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
|
||||
.auth-panel__column {
|
||||
&:first-child {
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,21 +90,17 @@ export default class AuthPanel extends Component {
|
||||
|
||||
{user.admin && ' • '}
|
||||
|
||||
{
|
||||
!!user.id && (
|
||||
<span className="auth-panel__sort">
|
||||
Sort by
|
||||
{' '}
|
||||
<select className="auth-panel__select" onChange={this.onSortChange}>
|
||||
{
|
||||
sortArray.map(sort => (
|
||||
<option value={sort.value} selected={sort.selected}>{sort.label}</option>
|
||||
))
|
||||
}
|
||||
</select>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
<span className="auth-panel__sort">
|
||||
Sort by
|
||||
{' '}
|
||||
<select className="auth-panel__select" onChange={this.onSortChange}>
|
||||
{
|
||||
sortArray.map(sort => (
|
||||
<option value={sort.value} selected={sort.selected}>{sort.label}</option>
|
||||
))
|
||||
}
|
||||
</select>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -2,6 +2,5 @@
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
+26
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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: '•';
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
.comment__text {
|
||||
margin-bottom: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
.comment__vote_selected {
|
||||
background-image: url('comment__vote_selected.svg');
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg width="14" height="18" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="#cc0606" d="M4.426 17.116V7.72H.394L7 .988l6.606 6.732H9.574v9.396z" fill-rule="evenodd"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 176 B |
@@ -1,3 +1,9 @@
|
||||
.comment__vote_type_up {
|
||||
margin-right: 4px;
|
||||
|
||||
&.comment__vote_selected {
|
||||
&, &:hover {
|
||||
background-image: url('comment__vote_type_up.svg');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg width="14" height="18" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="#259e06" d="M4.426 17.116V7.72H.394L7 .988l6.606 6.732H9.574v9.396z" fill-rule="evenodd"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 176 B |
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<div className={b('comment', props, defaultMods)} id={`${COMMENT_NODE_CLASSNAME_PREFIX}${o.id}`} role="listitem article" aria-level={mods.level}>
|
||||
<div className={b('comment', props, defaultMods)} role="listitem article" aria-level={mods.level}>
|
||||
<div className="comment__body">
|
||||
<div className="comment__info">
|
||||
<a href={`${o.locator.url}#${COMMENT_NODE_CLASSNAME_PREFIX}${o.id}`} className="comment__username">{o.user.name}</a>
|
||||
@@ -328,7 +335,7 @@ export default class Comment extends Component {
|
||||
|
||||
<div className="comment__actions">
|
||||
{
|
||||
!mods.disabled && !isGuest && (
|
||||
!deleted && !mods.disabled && !isGuest && (
|
||||
<span
|
||||
className="comment__action"
|
||||
role="button"
|
||||
@@ -339,8 +346,16 @@ export default class Comment extends Component {
|
||||
}
|
||||
|
||||
{
|
||||
isAdmin &&
|
||||
(
|
||||
!mods.disabled && mods.collapsible && (
|
||||
<span
|
||||
className={b('comment__action', {}, { type: 'collapse', selected: mods.collapsed })}
|
||||
onClick={this.toggleCollapse}
|
||||
>{mods.collapsed ? '+' : '−'}</span>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
!deleted && isAdmin && (
|
||||
<span className="comment__controls">
|
||||
{
|
||||
!pinned && (
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
.input__preview-wrapper {
|
||||
overflow: hidden;
|
||||
background: #eee;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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 && (
|
||||
<div
|
||||
className={b('input__preview', { mix: 'raw-content' })}
|
||||
dangerouslySetInnerHTML={{ __html: preview }}
|
||||
/>
|
||||
<div className="input__preview-wrapper">
|
||||
<div
|
||||
className={b('input__preview', { mix: 'raw-content' })}
|
||||
dangerouslySetInnerHTML={{ __html: preview }}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</form>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<div className={b('thread', props)} role="list" >
|
||||
<Comment
|
||||
data={comment}
|
||||
mods={{ level: mods.level }}
|
||||
mods={{ level: mods.level, collapsed, collapsible: !!replies.length }}
|
||||
onReply={props.onReply}
|
||||
onReplyClick={onReplyClick}
|
||||
onCollapseToggle={this.onCollapseToggle}
|
||||
/>
|
||||
|
||||
{
|
||||
!!replies.length && replies.map(thread => (
|
||||
!collapsed && !!replies.length && replies.map(thread => (
|
||||
<Thread
|
||||
data={thread}
|
||||
mods={{ level: mods.level < 5 ? mods.level + 1 : mods.level }}
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@
|
||||
|
||||
<script>
|
||||
var remark_config = {
|
||||
ё site_id: 'remark',
|
||||
site_id: 'remark',
|
||||
url: 'https://remark42.com/demo/',
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user