Comment Tree paging (#726)

* Updating translations.

* Forgot to add comment-sort-select

* Upgrading deps
This commit is contained in:
Dessalines 2022-07-30 09:28:08 -04:00 committed by GitHub
parent 49ceb00dc8
commit 69b623b8fb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 652 additions and 443 deletions

@ -1 +1 @@
Subproject commit 7c1b691af63845a2fe2f8219b4620b8db3c9c3ba Subproject commit 7c3945745dcd07774b19453803f7f14ab80ab3d3

View file

@ -77,7 +77,7 @@
"eslint-plugin-prettier": "^4.2.1", "eslint-plugin-prettier": "^4.2.1",
"husky": "^8.0.1", "husky": "^8.0.1",
"import-sort-style-module": "^6.0.0", "import-sort-style-module": "^6.0.0",
"lemmy-js-client": "0.17.0-rc.38", "lemmy-js-client": "0.17.0-rc.39",
"lint-staged": "^13.0.3", "lint-staged": "^13.0.3",
"mini-css-extract-plugin": "^2.6.1", "mini-css-extract-plugin": "^2.6.1",
"node-fetch": "^2.6.1", "node-fetch": "^2.6.1",

View file

@ -3,6 +3,7 @@ import { Component } from "inferno";
import { T } from "inferno-i18next-dess"; import { T } from "inferno-i18next-dess";
import { Link } from "inferno-router"; import { Link } from "inferno-router";
import { import {
CommentNode as CommentNodeI,
CommentResponse, CommentResponse,
CreateComment, CreateComment,
EditComment, EditComment,
@ -12,7 +13,6 @@ import {
} from "lemmy-js-client"; } from "lemmy-js-client";
import { Subscription } from "rxjs"; import { Subscription } from "rxjs";
import { i18n } from "../../i18next"; import { i18n } from "../../i18next";
import { CommentNode as CommentNodeI } from "../../interfaces";
import { UserService, WebSocketService } from "../../services"; import { UserService, WebSocketService } from "../../services";
import { import {
auth, auth,

View file

@ -8,12 +8,16 @@ import {
BanFromCommunity, BanFromCommunity,
BanPerson, BanPerson,
BlockPerson, BlockPerson,
CommentNode as CommentNodeI,
CommentReplyView,
CommentView, CommentView,
CommunityModeratorView, CommunityModeratorView,
CreateCommentLike, CreateCommentLike,
CreateCommentReport, CreateCommentReport,
DeleteComment, DeleteComment,
MarkCommentAsRead, GetComments,
ListingType,
MarkCommentReplyAsRead,
MarkPersonMentionAsRead, MarkPersonMentionAsRead,
PersonMentionView, PersonMentionView,
PersonViewSafe, PersonViewSafe,
@ -26,11 +30,7 @@ import {
} from "lemmy-js-client"; } from "lemmy-js-client";
import moment from "moment"; import moment from "moment";
import { i18n } from "../../i18next"; import { i18n } from "../../i18next";
import { import { BanType, CommentViewType, PurgeType } from "../../interfaces";
BanType,
CommentNode as CommentNodeI,
PurgeType,
} from "../../interfaces";
import { UserService, WebSocketService } from "../../services"; import { UserService, WebSocketService } from "../../services";
import { import {
amCommunityCreator, amCommunityCreator,
@ -38,6 +38,7 @@ import {
canAdmin, canAdmin,
canMod, canMod,
colorList, colorList,
commentTreeMaxDepth,
futureDaysToUnixTime, futureDaysToUnixTime,
isAdmin, isAdmin,
isBanned, isBanned,
@ -82,7 +83,6 @@ interface CommentNodeState {
score: number; score: number;
upvotes: number; upvotes: number;
downvotes: number; downvotes: number;
borderColor: string;
readLoading: boolean; readLoading: boolean;
saveLoading: boolean; saveLoading: boolean;
} }
@ -99,6 +99,7 @@ interface CommentNodeProps {
showContext?: boolean; showContext?: boolean;
showCommunity?: boolean; showCommunity?: boolean;
enableDownvotes: boolean; enableDownvotes: boolean;
viewType: CommentViewType;
} }
export class CommentNode extends Component<CommentNodeProps, CommentNodeState> { export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
@ -129,9 +130,6 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
score: this.props.node.comment_view.counts.score, score: this.props.node.comment_view.counts.score,
upvotes: this.props.node.comment_view.counts.upvotes, upvotes: this.props.node.comment_view.counts.upvotes,
downvotes: this.props.node.comment_view.counts.downvotes, downvotes: this.props.node.comment_view.counts.downvotes,
borderColor: this.props.node.depth
? colorList[this.props.node.depth % colorList.length]
: colorList[0],
readLoading: false, readLoading: false,
saveLoading: false, saveLoading: false,
}; };
@ -181,10 +179,23 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
cv.creator.id cv.creator.id
); );
let borderColor = this.props.node.depth
? colorList[(this.props.node.depth - 1) % colorList.length]
: colorList[0];
let moreRepliesBorderColor = this.props.node.depth
? colorList[this.props.node.depth % colorList.length]
: colorList[0];
let showMoreChildren =
this.props.viewType == CommentViewType.Tree &&
!this.state.collapsed &&
node.children.length == 0 &&
node.comment_view.counts.child_count > 0;
return ( return (
<div <div
className={`comment ${ className={`comment ${
cv.comment.parent_id.isSome() && !this.props.noIndent ? "ml-1" : "" this.props.node.depth && !this.props.noIndent ? "ml-1" : ""
}`} }`}
> >
<div <div
@ -194,14 +205,12 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
} ${this.isCommentNew ? "mark" : ""}`} } ${this.isCommentNew ? "mark" : ""}`}
style={ style={
!this.props.noIndent && !this.props.noIndent &&
cv.comment.parent_id.isSome() && this.props.node.depth &&
`border-left: 2px ${this.state.borderColor} solid !important` `border-left: 2px ${borderColor} solid !important`
} }
> >
<div <div
class={`${ class={`${!this.props.noIndent && this.props.node.depth && "ml-2"}`}
!this.props.noIndent && cv.comment.parent_id.isSome() && "ml-2"
}`}
> >
<div class="d-flex flex-wrap align-items-center text-muted small"> <div class="d-flex flex-wrap align-items-center text-muted small">
<span class="mr-2"> <span class="mr-2">
@ -262,7 +271,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
<> <>
<a <a
className={`unselectable pointer ${this.scoreColor}`} className={`unselectable pointer ${this.scoreColor}`}
onClick={linkEvent(node, this.handleCommentUpvote)} onClick={this.handleCommentUpvote}
data-tippy-content={this.pointsTippy} data-tippy-content={this.pointsTippy}
> >
<span <span
@ -314,12 +323,12 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
class="btn btn-link btn-animate text-muted" class="btn btn-link btn-animate text-muted"
onClick={linkEvent(this, this.handleMarkRead)} onClick={linkEvent(this, this.handleMarkRead)}
data-tippy-content={ data-tippy-content={
this.commentOrMentionRead this.commentReplyOrMentionRead
? i18n.t("mark_as_unread") ? i18n.t("mark_as_unread")
: i18n.t("mark_as_read") : i18n.t("mark_as_read")
} }
aria-label={ aria-label={
this.commentOrMentionRead this.commentReplyOrMentionRead
? i18n.t("mark_as_unread") ? i18n.t("mark_as_unread")
: i18n.t("mark_as_read") : i18n.t("mark_as_read")
} }
@ -330,7 +339,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
<Icon <Icon
icon="check" icon="check"
classes={`icon-inline ${ classes={`icon-inline ${
this.commentOrMentionRead && "text-success" this.commentReplyOrMentionRead && "text-success"
}`} }`}
/> />
)} )}
@ -345,7 +354,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
? "text-info" ? "text-info"
: "text-muted" : "text-muted"
}`} }`}
onClick={linkEvent(node, this.handleCommentUpvote)} onClick={this.handleCommentUpvote}
data-tippy-content={i18n.t("upvote")} data-tippy-content={i18n.t("upvote")}
aria-label={i18n.t("upvote")} aria-label={i18n.t("upvote")}
> >
@ -364,10 +373,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
? "text-danger" ? "text-danger"
: "text-muted" : "text-muted"
}`} }`}
onClick={linkEvent( onClick={this.handleCommentDownvote}
node,
this.handleCommentDownvote
)}
data-tippy-content={i18n.t("downvote")} data-tippy-content={i18n.t("downvote")}
aria-label={i18n.t("downvote")} aria-label={i18n.t("downvote")}
> >
@ -772,6 +778,25 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
)} )}
</div> </div>
</div> </div>
{showMoreChildren && (
<div
className={`details ml-1 comment-node py-2 ${
!this.props.noBorder ? "border-top border-light" : ""
}`}
style={`border-left: 2px ${moreRepliesBorderColor} solid !important`}
>
<button
class="btn btn-link text-muted"
onClick={linkEvent(this, this.handleFetchChildren)}
>
{i18n.t("x_more_replies", {
count: node.comment_view.counts.child_count,
formattedCount: numToSI(node.comment_view.counts.child_count),
})}{" "}
</button>
</div>
)}
{/* end of details */} {/* end of details */}
{this.state.showRemoveDialog && ( {this.state.showRemoveDialog && (
<form <form
@ -931,7 +956,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
focus focus
/> />
)} )}
{!this.state.collapsed && node.children && ( {!this.state.collapsed && node.children.length > 0 && (
<CommentNodes <CommentNodes
nodes={node.children} nodes={node.children}
locked={this.props.locked} locked={this.props.locked}
@ -939,6 +964,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
admins={this.props.admins} admins={this.props.admins}
maxCommentsShown={None} maxCommentsShown={None}
enableDownvotes={this.props.enableDownvotes} enableDownvotes={this.props.enableDownvotes}
viewType={this.props.viewType}
/> />
)} )}
{/* A collapsed clearfix */} {/* A collapsed clearfix */}
@ -947,11 +973,16 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
); );
} }
get commentOrMentionRead() { get commentReplyOrMentionRead(): boolean {
let cv = this.props.node.comment_view; let cv = this.props.node.comment_view;
return this.isPersonMentionType(cv)
? cv.person_mention.read if (this.isPersonMentionType(cv)) {
: cv.comment.read; return cv.person_mention.read;
} else if (this.isCommentReplyType(cv)) {
return cv.comment_reply.read;
} else {
return false;
}
} }
linkBtn(small = false) { linkBtn(small = false) {
@ -968,7 +999,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
<> <>
<Link <Link
className={classnames} className={classnames}
to={`/post/${cv.post.id}/comment/${cv.comment.id}`} to={`/comment/${cv.comment.id}`}
title={title} title={title}
> >
<Icon icon="link" classes="icon-inline" /> <Icon icon="link" classes="icon-inline" />
@ -1061,7 +1092,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
this.setState(this.state); this.setState(this.state);
} }
handleCommentUpvote(i: CommentNodeI, event: any) { handleCommentUpvote(event: any) {
event.preventDefault(); event.preventDefault();
let myVote = this.state.my_vote.unwrapOr(0); let myVote = this.state.my_vote.unwrapOr(0);
let newVote = myVote == 1 ? 0 : 1; let newVote = myVote == 1 ? 0 : 1;
@ -1081,17 +1112,16 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
this.state.my_vote = Some(newVote); this.state.my_vote = Some(newVote);
let form = new CreateCommentLike({ let form = new CreateCommentLike({
comment_id: i.comment_view.comment.id, comment_id: this.props.node.comment_view.comment.id,
score: newVote, score: newVote,
auth: auth().unwrap(), auth: auth().unwrap(),
}); });
WebSocketService.Instance.send(wsClient.likeComment(form)); WebSocketService.Instance.send(wsClient.likeComment(form));
this.setState(this.state); this.setState(this.state);
setupTippy(); setupTippy();
} }
handleCommentDownvote(i: CommentNodeI, event: any) { handleCommentDownvote(event: any) {
event.preventDefault(); event.preventDefault();
let myVote = this.state.my_vote.unwrapOr(0); let myVote = this.state.my_vote.unwrapOr(0);
let newVote = myVote == -1 ? 0 : -1; let newVote = myVote == -1 ? 0 : -1;
@ -1111,7 +1141,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
this.state.my_vote = Some(newVote); this.state.my_vote = Some(newVote);
let form = new CreateCommentLike({ let form = new CreateCommentLike({
comment_id: i.comment_view.comment.id, comment_id: this.props.node.comment_view.comment.id,
score: newVote, score: newVote,
auth: auth().unwrap(), auth: auth().unwrap(),
}); });
@ -1175,11 +1205,17 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
} }
isPersonMentionType( isPersonMentionType(
item: CommentView | PersonMentionView item: CommentView | PersonMentionView | CommentReplyView
): item is PersonMentionView { ): item is PersonMentionView {
return (item as PersonMentionView).person_mention?.id !== undefined; return (item as PersonMentionView).person_mention?.id !== undefined;
} }
isCommentReplyType(
item: CommentView | PersonMentionView | CommentReplyView
): item is CommentReplyView {
return (item as CommentReplyView).comment_reply?.id !== undefined;
}
handleMarkRead(i: CommentNode) { handleMarkRead(i: CommentNode) {
if (i.isPersonMentionType(i.props.node.comment_view)) { if (i.isPersonMentionType(i.props.node.comment_view)) {
let form = new MarkPersonMentionAsRead({ let form = new MarkPersonMentionAsRead({
@ -1188,13 +1224,13 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
auth: auth().unwrap(), auth: auth().unwrap(),
}); });
WebSocketService.Instance.send(wsClient.markPersonMentionAsRead(form)); WebSocketService.Instance.send(wsClient.markPersonMentionAsRead(form));
} else { } else if (i.isCommentReplyType(i.props.node.comment_view)) {
let form = new MarkCommentAsRead({ let form = new MarkCommentReplyAsRead({
comment_id: i.props.node.comment_view.comment.id, comment_reply_id: i.props.node.comment_view.comment_reply.id,
read: !i.props.node.comment_view.comment.read, read: !i.props.node.comment_view.comment_reply.read,
auth: auth().unwrap(), auth: auth().unwrap(),
}); });
WebSocketService.Instance.send(wsClient.markCommentAsRead(form)); WebSocketService.Instance.send(wsClient.markCommentReplyAsRead(form));
} }
i.state.readLoading = true; i.state.readLoading = true;
@ -1419,6 +1455,24 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
setupTippy(); setupTippy();
} }
handleFetchChildren(i: CommentNode) {
let form = new GetComments({
post_id: Some(i.props.node.comment_view.post.id),
parent_id: Some(i.props.node.comment_view.comment.id),
max_depth: Some(commentTreeMaxDepth),
page: None,
sort: None,
limit: Some(999),
type_: Some(ListingType.All),
community_name: None,
community_id: None,
saved_only: Some(false),
auth: auth(false).ok(),
});
WebSocketService.Instance.send(wsClient.getComments(form));
}
get scoreColor() { get scoreColor() {
if (this.state.my_vote.unwrapOr(0) == 1) { if (this.state.my_vote.unwrapOr(0) == 1) {
return "text-info"; return "text-info";

View file

@ -1,7 +1,11 @@
import { Option } from "@sniptt/monads"; import { Option } from "@sniptt/monads";
import { Component } from "inferno"; import { Component } from "inferno";
import { CommunityModeratorView, PersonViewSafe } from "lemmy-js-client"; import {
import { CommentNode as CommentNodeI } from "../../interfaces"; CommentNode as CommentNodeI,
CommunityModeratorView,
PersonViewSafe,
} from "lemmy-js-client";
import { CommentViewType } from "../../interfaces";
import { CommentNode } from "./comment-node"; import { CommentNode } from "./comment-node";
interface CommentNodesProps { interface CommentNodesProps {
@ -17,6 +21,7 @@ interface CommentNodesProps {
showContext?: boolean; showContext?: boolean;
showCommunity?: boolean; showCommunity?: boolean;
enableDownvotes?: boolean; enableDownvotes?: boolean;
viewType: CommentViewType;
} }
export class CommentNodes extends Component<CommentNodesProps, any> { export class CommentNodes extends Component<CommentNodesProps, any> {
@ -45,6 +50,7 @@ export class CommentNodes extends Component<CommentNodesProps, any> {
showContext={this.props.showContext} showContext={this.props.showContext}
showCommunity={this.props.showCommunity} showCommunity={this.props.showCommunity}
enableDownvotes={this.props.enableDownvotes} enableDownvotes={this.props.enableDownvotes}
viewType={this.props.viewType}
/> />
))} ))}
</div> </div>

View file

@ -2,13 +2,14 @@ import { None } from "@sniptt/monads";
import { Component, linkEvent } from "inferno"; import { Component, linkEvent } from "inferno";
import { T } from "inferno-i18next-dess"; import { T } from "inferno-i18next-dess";
import { import {
CommentNode as CommentNodeI,
CommentReportView, CommentReportView,
CommentView, CommentView,
ResolveCommentReport, ResolveCommentReport,
SubscribedType, SubscribedType,
} from "lemmy-js-client"; } from "lemmy-js-client";
import { i18n } from "../../i18next"; import { i18n } from "../../i18next";
import { CommentNode as CommentNodeI } from "../../interfaces"; import { CommentViewType } from "../../interfaces";
import { WebSocketService } from "../../services"; import { WebSocketService } from "../../services";
import { auth, wsClient } from "../../utils"; import { auth, wsClient } from "../../utils";
import { Icon } from "../common/icon"; import { Icon } from "../common/icon";
@ -44,18 +45,20 @@ export class CommentReport extends Component<CommentReportProps, any> {
subscribed: SubscribedType.NotSubscribed, subscribed: SubscribedType.NotSubscribed,
saved: false, saved: false,
creator_blocked: false, creator_blocked: false,
recipient: None,
my_vote: r.my_vote, my_vote: r.my_vote,
}; };
let node: CommentNodeI = { let node: CommentNodeI = {
comment_view, comment_view,
children: [],
depth: 0,
}; };
return ( return (
<div> <div>
<CommentNode <CommentNode
node={node} node={node}
viewType={CommentViewType.Flat}
moderators={None} moderators={None}
admins={None} admins={None}
enableDownvotes={true} enableDownvotes={true}

View file

@ -0,0 +1,70 @@
import { Component, linkEvent } from "inferno";
import { CommentSortType } from "lemmy-js-client";
import { i18n } from "../../i18next";
import { randomStr, relTags, sortingHelpUrl } from "../../utils";
import { Icon } from "./icon";
interface CommentSortSelectProps {
sort: CommentSortType;
onChange?(val: CommentSortType): any;
}
interface CommentSortSelectState {
sort: CommentSortType;
}
export class CommentSortSelect extends Component<
CommentSortSelectProps,
CommentSortSelectState
> {
private id = `sort-select-${randomStr()}`;
private emptyState: CommentSortSelectState = {
sort: this.props.sort,
};
constructor(props: any, context: any) {
super(props, context);
this.state = this.emptyState;
}
static getDerivedStateFromProps(props: any): CommentSortSelectState {
return {
sort: props.sort,
};
}
render() {
return (
<>
<select
id={this.id}
name={this.id}
value={this.state.sort}
onChange={linkEvent(this, this.handleSortChange)}
class="custom-select w-auto mr-2 mb-2"
aria-label={i18n.t("sort_type")}
>
<option disabled aria-hidden="true">
{i18n.t("sort_type")}
</option>
<option value={CommentSortType.Hot}>{i18n.t("hot")}</option>,
<option value={CommentSortType.Top}>{i18n.t("top")}</option>,
<option value={CommentSortType.New}>{i18n.t("new")}</option>
<option value={CommentSortType.Old}>{i18n.t("old")}</option>
</select>
<a
className="text-muted"
href={sortingHelpUrl}
rel={relTags}
title={i18n.t("sorting_help")}
>
<Icon icon="help-circle" classes="icon-inline" />
</a>
</>
);
}
handleSortChange(i: CommentSortSelect, event: any) {
i.props.onChange(event.target.value);
}
}

View file

@ -51,6 +51,7 @@ export class SortSelect extends Component<SortSelectProps, SortSelectState> {
<option value={SortType.Active}>{i18n.t("active")}</option>, <option value={SortType.Active}>{i18n.t("active")}</option>,
]} ]}
<option value={SortType.New}>{i18n.t("new")}</option> <option value={SortType.New}>{i18n.t("new")}</option>
<option value={SortType.Old}>{i18n.t("old")}</option>
{!this.props.hideMostComments && [ {!this.props.hideMostComments && [
<option value={SortType.MostComments}> <option value={SortType.MostComments}>
{i18n.t("most_comments")} {i18n.t("most_comments")}

View file

@ -29,7 +29,11 @@ import {
} from "lemmy-js-client"; } from "lemmy-js-client";
import { Subscription } from "rxjs"; import { Subscription } from "rxjs";
import { i18n } from "../../i18next"; import { i18n } from "../../i18next";
import { DataType, InitialFetchRequest } from "../../interfaces"; import {
CommentViewType,
DataType,
InitialFetchRequest,
} from "../../interfaces";
import { UserService, WebSocketService } from "../../services"; import { UserService, WebSocketService } from "../../services";
import { import {
auth, auth,
@ -46,6 +50,7 @@ import {
getPageFromProps, getPageFromProps,
getSortTypeFromProps, getSortTypeFromProps,
notifyPost, notifyPost,
postToCommentSortType,
relTags, relTags,
restoreScrollPosition, restoreScrollPosition,
saveCommentRes, saveCommentRes,
@ -233,9 +238,12 @@ export class Community extends Component<any, State> {
community_id: None, community_id: None,
page, page,
limit: Some(fetchLimit), limit: Some(fetchLimit),
sort, max_depth: None,
sort: sort.map(postToCommentSortType),
type_: Some(ListingType.All), type_: Some(ListingType.All),
saved_only: Some(false), saved_only: Some(false),
post_id: None,
parent_id: None,
auth: req.auth, auth: req.auth,
}); });
promises.push(Promise.resolve()); promises.push(Promise.resolve());
@ -389,6 +397,7 @@ export class Community extends Component<any, State> {
) : ( ) : (
<CommentNodes <CommentNodes
nodes={commentsToFlatNodes(this.state.comments)} nodes={commentsToFlatNodes(this.state.comments)}
viewType={CommentViewType.Flat}
noIndent noIndent
showContext showContext
enableDownvotes={enableDownvotes(this.state.siteRes)} enableDownvotes={enableDownvotes(this.state.siteRes)}
@ -499,11 +508,14 @@ export class Community extends Component<any, State> {
let form = new GetComments({ let form = new GetComments({
page: Some(this.state.page), page: Some(this.state.page),
limit: Some(fetchLimit), limit: Some(fetchLimit),
sort: Some(this.state.sort), max_depth: None,
sort: Some(postToCommentSortType(this.state.sort)),
type_: Some(ListingType.All), type_: Some(ListingType.All),
community_name: Some(this.state.communityName), community_name: Some(this.state.communityName),
community_id: None, community_id: None,
saved_only: Some(false), saved_only: Some(false),
post_id: None,
parent_id: None,
auth: auth(false).ok(), auth: auth(false).ok(),
}); });
WebSocketService.Instance.send(wsClient.getComments(form)); WebSocketService.Instance.send(wsClient.getComments(form));

View file

@ -30,7 +30,11 @@ import {
} from "lemmy-js-client"; } from "lemmy-js-client";
import { Subscription } from "rxjs"; import { Subscription } from "rxjs";
import { i18n } from "../../i18next"; import { i18n } from "../../i18next";
import { DataType, InitialFetchRequest } from "../../interfaces"; import {
CommentViewType,
DataType,
InitialFetchRequest,
} from "../../interfaces";
import { UserService, WebSocketService } from "../../services"; import { UserService, WebSocketService } from "../../services";
import { import {
auth, auth,
@ -48,6 +52,7 @@ import {
getSortTypeFromProps, getSortTypeFromProps,
isBrowser, isBrowser,
notifyPost, notifyPost,
postToCommentSortType,
relTags, relTags,
restoreScrollPosition, restoreScrollPosition,
saveCommentRes, saveCommentRes,
@ -263,9 +268,12 @@ export class Home extends Component<any, HomeState> {
community_name: None, community_name: None,
page, page,
limit: Some(fetchLimit), limit: Some(fetchLimit),
sort, max_depth: None,
sort: sort.map(postToCommentSortType),
type_, type_,
saved_only: Some(false), saved_only: Some(false),
post_id: None,
parent_id: None,
auth: req.auth, auth: req.auth,
}); });
promises.push(Promise.resolve()); promises.push(Promise.resolve());
@ -565,6 +573,7 @@ export class Home extends Component<any, HomeState> {
) : ( ) : (
<CommentNodes <CommentNodes
nodes={commentsToFlatNodes(this.state.comments)} nodes={commentsToFlatNodes(this.state.comments)}
viewType={CommentViewType.Flat}
moderators={None} moderators={None}
admins={None} admins={None}
maxCommentsShown={None} maxCommentsShown={None}
@ -694,8 +703,11 @@ export class Home extends Component<any, HomeState> {
community_name: None, community_name: None,
page: Some(this.state.page), page: Some(this.state.page),
limit: Some(fetchLimit), limit: Some(fetchLimit),
sort: Some(this.state.sort), max_depth: None,
sort: Some(postToCommentSortType(this.state.sort)),
saved_only: Some(false), saved_only: Some(false),
post_id: None,
parent_id: None,
auth: auth(false).ok(), auth: auth(false).ok(),
type_: Some(this.state.listingType), type_: Some(this.state.listingType),
}); });

View file

@ -2,8 +2,11 @@ import { None, Some } from "@sniptt/monads";
import { Component, linkEvent } from "inferno"; import { Component, linkEvent } from "inferno";
import { import {
BlockPersonResponse, BlockPersonResponse,
CommentReplyResponse,
CommentReplyView,
CommentReportResponse, CommentReportResponse,
CommentResponse, CommentResponse,
CommentSortType,
CommentView, CommentView,
GetPersonMentions, GetPersonMentions,
GetPersonMentionsResponse, GetPersonMentionsResponse,
@ -17,14 +20,13 @@ import {
PrivateMessageResponse, PrivateMessageResponse,
PrivateMessagesResponse, PrivateMessagesResponse,
PrivateMessageView, PrivateMessageView,
SortType,
UserOperation, UserOperation,
wsJsonToRes, wsJsonToRes,
wsUserOp, wsUserOp,
} from "lemmy-js-client"; } from "lemmy-js-client";
import { Subscription } from "rxjs"; import { Subscription } from "rxjs";
import { i18n } from "../../i18next"; import { i18n } from "../../i18next";
import { InitialFetchRequest } from "../../interfaces"; import { CommentViewType, InitialFetchRequest } from "../../interfaces";
import { UserService, WebSocketService } from "../../services"; import { UserService, WebSocketService } from "../../services";
import { import {
auth, auth,
@ -44,10 +46,10 @@ import {
wsSubscribe, wsSubscribe,
} from "../../utils"; } from "../../utils";
import { CommentNodes } from "../comment/comment-nodes"; import { CommentNodes } from "../comment/comment-nodes";
import { CommentSortSelect } from "../common/comment-sort-select";
import { HtmlTags } from "../common/html-tags"; import { HtmlTags } from "../common/html-tags";
import { Icon, Spinner } from "../common/icon"; import { Icon, Spinner } from "../common/icon";
import { Paginator } from "../common/paginator"; import { Paginator } from "../common/paginator";
import { SortSelect } from "../common/sort-select";
import { PrivateMessage } from "../private_message/private-message"; import { PrivateMessage } from "../private_message/private-message";
enum UnreadOrAll { enum UnreadOrAll {
@ -70,18 +72,18 @@ enum ReplyEnum {
type ReplyType = { type ReplyType = {
id: number; id: number;
type_: ReplyEnum; type_: ReplyEnum;
view: CommentView | PrivateMessageView | PersonMentionView; view: CommentView | PrivateMessageView | PersonMentionView | CommentReplyView;
published: string; published: string;
}; };
interface InboxState { interface InboxState {
unreadOrAll: UnreadOrAll; unreadOrAll: UnreadOrAll;
messageType: MessageType; messageType: MessageType;
replies: CommentView[]; replies: CommentReplyView[];
mentions: PersonMentionView[]; mentions: PersonMentionView[];
messages: PrivateMessageView[]; messages: PrivateMessageView[];
combined: ReplyType[]; combined: ReplyType[];
sort: SortType; sort: CommentSortType;
page: number; page: number;
siteRes: GetSiteResponse; siteRes: GetSiteResponse;
loading: boolean; loading: boolean;
@ -102,7 +104,7 @@ export class Inbox extends Component<any, InboxState> {
mentions: [], mentions: [],
messages: [], messages: [],
combined: [], combined: [],
sort: SortType.New, sort: CommentSortType.New,
page: 1, page: 1,
siteRes: this.isoData.site_res, siteRes: this.isoData.site_res,
loading: true, loading: true,
@ -323,19 +325,17 @@ export class Inbox extends Component<any, InboxState> {
<div className="mb-2"> <div className="mb-2">
<span class="mr-3">{this.unreadOrAllRadios()}</span> <span class="mr-3">{this.unreadOrAllRadios()}</span>
<span class="mr-3">{this.messageTypeRadios()}</span> <span class="mr-3">{this.messageTypeRadios()}</span>
<SortSelect <CommentSortSelect
sort={this.state.sort} sort={this.state.sort}
onChange={this.handleSortChange} onChange={this.handleSortChange}
hideHot
hideMostComments
/> />
</div> </div>
); );
} }
replyToReplyType(r: CommentView): ReplyType { replyToReplyType(r: CommentReplyView): ReplyType {
return { return {
id: r.comment.id, id: r.comment_reply.id,
type_: ReplyEnum.Reply, type_: ReplyEnum.Reply,
view: r, view: r,
published: r.comment.published, published: r.comment.published,
@ -382,7 +382,10 @@ export class Inbox extends Component<any, InboxState> {
return ( return (
<CommentNodes <CommentNodes
key={i.id} key={i.id}
nodes={[{ comment_view: i.view as CommentView }]} nodes={[
{ comment_view: i.view as CommentView, children: [], depth: 0 },
]}
viewType={CommentViewType.Flat}
moderators={None} moderators={None}
admins={None} admins={None}
maxCommentsShown={None} maxCommentsShown={None}
@ -397,7 +400,14 @@ export class Inbox extends Component<any, InboxState> {
return ( return (
<CommentNodes <CommentNodes
key={i.id} key={i.id}
nodes={[{ comment_view: i.view as PersonMentionView }]} nodes={[
{
comment_view: i.view as PersonMentionView,
children: [],
depth: 0,
},
]}
viewType={CommentViewType.Flat}
moderators={None} moderators={None}
admins={None} admins={None}
maxCommentsShown={None} maxCommentsShown={None}
@ -429,6 +439,7 @@ export class Inbox extends Component<any, InboxState> {
<div> <div>
<CommentNodes <CommentNodes
nodes={commentsToFlatNodes(this.state.replies)} nodes={commentsToFlatNodes(this.state.replies)}
viewType={CommentViewType.Flat}
moderators={None} moderators={None}
admins={None} admins={None}
maxCommentsShown={None} maxCommentsShown={None}
@ -448,7 +459,8 @@ export class Inbox extends Component<any, InboxState> {
{this.state.mentions.map(umv => ( {this.state.mentions.map(umv => (
<CommentNodes <CommentNodes
key={umv.person_mention.id} key={umv.person_mention.id}
nodes={[{ comment_view: umv }]} nodes={[{ comment_view: umv, children: [], depth: 0 }]}
viewType={CommentViewType.Flat}
moderators={None} moderators={None}
admins={None} admins={None}
maxCommentsShown={None} maxCommentsShown={None}
@ -498,9 +510,11 @@ export class Inbox extends Component<any, InboxState> {
static fetchInitialData(req: InitialFetchRequest): Promise<any>[] { static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
let promises: Promise<any>[] = []; let promises: Promise<any>[] = [];
let sort = Some(CommentSortType.New);
// It can be /u/me, or /username/1 // It can be /u/me, or /username/1
let repliesForm = new GetReplies({ let repliesForm = new GetReplies({
sort: Some(SortType.New), sort,
unread_only: Some(true), unread_only: Some(true),
page: Some(1), page: Some(1),
limit: Some(fetchLimit), limit: Some(fetchLimit),
@ -509,7 +523,7 @@ export class Inbox extends Component<any, InboxState> {
promises.push(req.client.getReplies(repliesForm)); promises.push(req.client.getReplies(repliesForm));
let personMentionsForm = new GetPersonMentions({ let personMentionsForm = new GetPersonMentions({
sort: Some(SortType.New), sort,
unread_only: Some(true), unread_only: Some(true),
page: Some(1), page: Some(1),
limit: Some(fetchLimit), limit: Some(fetchLimit),
@ -565,7 +579,7 @@ export class Inbox extends Component<any, InboxState> {
); );
} }
handleSortChange(val: SortType) { handleSortChange(val: CommentSortType) {
this.state.sort = val; this.state.sort = val;
this.state.page = 1; this.state.page = 1;
this.setState(this.state); this.setState(this.state);
@ -581,6 +595,7 @@ export class Inbox extends Component<any, InboxState> {
i.state.replies = []; i.state.replies = [];
i.state.mentions = []; i.state.mentions = [];
i.state.messages = []; i.state.messages = [];
i.state.combined = i.buildCombined();
UserService.Instance.unreadInboxCountSub.next(0); UserService.Instance.unreadInboxCountSub.next(0);
window.scrollTo(0, 0); window.scrollTo(0, 0);
i.setState(i.state); i.setState(i.state);
@ -716,34 +731,51 @@ export class Inbox extends Component<any, InboxState> {
let data = wsJsonToRes<CommentResponse>(msg, CommentResponse); let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
editCommentRes(data.comment_view, this.state.replies); editCommentRes(data.comment_view, this.state.replies);
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.MarkCommentAsRead) { } else if (op == UserOperation.MarkCommentReplyAsRead) {
let data = wsJsonToRes<CommentResponse>(msg, CommentResponse); let data = wsJsonToRes<CommentReplyResponse>(msg, CommentReplyResponse);
console.log(data);
// If youre in the unread view, just remove it from the list let found = this.state.replies.find(
if ( c => c.comment_reply.id == data.comment_reply_view.comment_reply.id
this.state.unreadOrAll == UnreadOrAll.Unread && );
data.comment_view.comment.read
) { if (found) {
this.state.replies = this.state.replies.filter(
r => r.comment.id !== data.comment_view.comment.id
);
this.state.combined = this.state.combined.filter(
r => r.id !== data.comment_view.comment.id
);
} else {
let found = this.state.replies.find(
c => c.comment.id == data.comment_view.comment.id
);
let combinedView = this.state.combined.find( let combinedView = this.state.combined.find(
i => i.id == data.comment_view.comment.id i => i.id == data.comment_reply_view.comment_reply.id
).view as CommentView; ).view as CommentReplyView;
found.comment.read = combinedView.comment.read = found.comment.content = combinedView.comment.content =
data.comment_view.comment.read; data.comment_reply_view.comment.content;
} found.comment.updated = combinedView.comment.updated =
data.comment_reply_view.comment.updated;
found.comment.removed = combinedView.comment.removed =
data.comment_reply_view.comment.removed;
found.comment.deleted = combinedView.comment.deleted =
data.comment_reply_view.comment.deleted;
found.counts.upvotes = combinedView.counts.upvotes =
data.comment_reply_view.counts.upvotes;
found.counts.downvotes = combinedView.counts.downvotes =
data.comment_reply_view.counts.downvotes;
found.counts.score = combinedView.counts.score =
data.comment_reply_view.counts.score;
this.sendUnreadCount(data.comment_view.comment.read); // If youre in the unread view, just remove it from the list
if (
this.state.unreadOrAll == UnreadOrAll.Unread &&
data.comment_reply_view.comment_reply.read
) {
this.state.replies = this.state.replies.filter(
r => r.comment_reply.id !== data.comment_reply_view.comment_reply.id
);
this.state.combined = this.state.combined.filter(
r => r.id !== data.comment_reply_view.comment_reply.id
);
} else {
found.comment_reply.read = combinedView.comment_reply.read =
data.comment_reply_view.comment_reply.read;
}
}
this.sendUnreadCount(data.comment_reply_view.comment_reply.read);
this.setState(this.state); this.setState(this.state);
setupTippy();
} else if (op == UserOperation.MarkPersonMentionAsRead) { } else if (op == UserOperation.MarkPersonMentionAsRead) {
let data = wsJsonToRes<PersonMentionResponse>(msg, PersonMentionResponse); let data = wsJsonToRes<PersonMentionResponse>(msg, PersonMentionResponse);
@ -791,71 +823,6 @@ export class Inbox extends Component<any, InboxState> {
} }
this.sendUnreadCount(data.person_mention_view.person_mention.read); this.sendUnreadCount(data.person_mention_view.person_mention.read);
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.CreateComment) {
let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
UserService.Instance.myUserInfo.match({
some: mui => {
if (data.recipient_ids.includes(mui.local_user_view.local_user.id)) {
this.state.replies.unshift(data.comment_view);
this.state.combined.unshift(
this.replyToReplyType(data.comment_view)
);
this.setState(this.state);
} else if (
data.comment_view.creator.id == mui.local_user_view.person.id
) {
// If youre in the unread view, just remove it from the list
if (this.state.unreadOrAll == UnreadOrAll.Unread) {
this.state.replies = this.state.replies.filter(
r =>
r.comment.id !==
data.comment_view.comment.parent_id.unwrapOr(0)
);
this.state.mentions = this.state.mentions.filter(
m =>
m.comment.id !==
data.comment_view.comment.parent_id.unwrapOr(0)
);
this.state.combined = this.state.combined.filter(r => {
if (this.isMention(r.view))
return (
r.view.comment.id !==
data.comment_view.comment.parent_id.unwrapOr(0)
);
else
return (
r.id !== data.comment_view.comment.parent_id.unwrapOr(0)
);
});
} else {
let mention_found = this.state.mentions.find(
i =>
i.comment.id ==
data.comment_view.comment.parent_id.unwrapOr(0)
);
if (mention_found) {
mention_found.person_mention.read = true;
}
let reply_found = this.state.replies.find(
i =>
i.comment.id ==
data.comment_view.comment.parent_id.unwrapOr(0)
);
if (reply_found) {
reply_found.comment.read = true;
}
this.state.combined = this.buildCombined();
}
this.sendUnreadCount(true);
this.setState(this.state);
setupTippy();
// TODO this seems wrong, you should be using form_id
toast(i18n.t("reply_sent"));
}
},
none: void 0,
});
} else if (op == UserOperation.CreatePrivateMessage) { } else if (op == UserOperation.CreatePrivateMessage) {
let data = wsJsonToRes<PrivateMessageResponse>( let data = wsJsonToRes<PrivateMessageResponse>(
msg, msg,
@ -904,4 +871,8 @@ export class Inbox extends Component<any, InboxState> {
isMention(view: any): view is PersonMentionView { isMention(view: any): view is PersonMentionView {
return (view as PersonMentionView).person_mention !== undefined; return (view as PersonMentionView).person_mention !== undefined;
} }
isReply(view: any): view is CommentReplyView {
return (view as CommentReplyView).comment_reply !== undefined;
}
} }

View file

@ -7,7 +7,7 @@ import {
PostView, PostView,
SortType, SortType,
} from "lemmy-js-client"; } from "lemmy-js-client";
import { PersonDetailsView } from "../../interfaces"; import { CommentViewType, PersonDetailsView } from "../../interfaces";
import { commentsToFlatNodes, setupTippy } from "../../utils"; import { commentsToFlatNodes, setupTippy } from "../../utils";
import { CommentNodes } from "../comment/comment-nodes"; import { CommentNodes } from "../comment/comment-nodes";
import { Paginator } from "../common/paginator"; import { Paginator } from "../common/paginator";
@ -89,7 +89,8 @@ export class PersonDetails extends Component<PersonDetailsProps, any> {
return ( return (
<CommentNodes <CommentNodes
key={i.id} key={i.id}
nodes={[{ comment_view: c }]} nodes={[{ comment_view: c, children: [], depth: 0 }]}
viewType={CommentViewType.Flat}
admins={Some(this.props.admins)} admins={Some(this.props.admins)}
moderators={None} moderators={None}
maxCommentsShown={None} maxCommentsShown={None}
@ -159,6 +160,7 @@ export class PersonDetails extends Component<PersonDetailsProps, any> {
<div> <div>
<CommentNodes <CommentNodes
nodes={commentsToFlatNodes(this.props.personRes.comments)} nodes={commentsToFlatNodes(this.props.personRes.comments)}
viewType={CommentViewType.Flat}
admins={Some(this.props.admins)} admins={Some(this.props.admins)}
moderators={None} moderators={None}
maxCommentsShown={None} maxCommentsShown={None}

View file

@ -408,7 +408,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
className={`btn-animate btn btn-link p-0 ${ className={`btn-animate btn btn-link p-0 ${
this.state.my_vote.unwrapOr(0) == 1 ? "text-info" : "text-muted" this.state.my_vote.unwrapOr(0) == 1 ? "text-info" : "text-muted"
}`} }`}
onClick={linkEvent(this, this.handlePostLike)} onClick={this.handlePostLike}
data-tippy-content={i18n.t("upvote")} data-tippy-content={i18n.t("upvote")}
aria-label={i18n.t("upvote")} aria-label={i18n.t("upvote")}
> >
@ -431,7 +431,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
? "text-danger" ? "text-danger"
: "text-muted" : "text-muted"
}`} }`}
onClick={linkEvent(this, this.handlePostDisLike)} onClick={this.handlePostDisLike}
data-tippy-content={i18n.t("downvote")} data-tippy-content={i18n.t("downvote")}
aria-label={i18n.t("downvote")} aria-label={i18n.t("downvote")}
> >
@ -647,7 +647,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
this.state.my_vote.unwrapOr(0) == 1 ? "text-info" : "text-muted" this.state.my_vote.unwrapOr(0) == 1 ? "text-info" : "text-muted"
}`} }`}
{...tippy} {...tippy}
onClick={linkEvent(this, this.handlePostLike)} onClick={this.handlePostLike}
aria-label={i18n.t("upvote")} aria-label={i18n.t("upvote")}
> >
<Icon icon="arrow-up1" classes="icon-inline small" /> <Icon icon="arrow-up1" classes="icon-inline small" />
@ -662,7 +662,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
? "text-danger" ? "text-danger"
: "text-muted" : "text-muted"
}`} }`}
onClick={linkEvent(this, this.handlePostDisLike)} onClick={this.handlePostDisLike}
{...tippy} {...tippy}
aria-label={i18n.t("downvote")} aria-label={i18n.t("downvote")}
> >
@ -1250,7 +1250,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
}); });
} }
handlePostLike(i: PostListing, event: any) { handlePostLike(event: any) {
event.preventDefault(); event.preventDefault();
if (UserService.Instance.myUserInfo.isNone()) { if (UserService.Instance.myUserInfo.isNone()) {
this.context.router.history.push(`/login`); this.context.router.history.push(`/login`);
@ -1260,31 +1260,31 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
let newVote = myVote == 1 ? 0 : 1; let newVote = myVote == 1 ? 0 : 1;
if (myVote == 1) { if (myVote == 1) {
i.state.score--; this.state.score--;
i.state.upvotes--; this.state.upvotes--;
} else if (myVote == -1) { } else if (myVote == -1) {
i.state.downvotes--; this.state.downvotes--;
i.state.upvotes++; this.state.upvotes++;
i.state.score += 2; this.state.score += 2;
} else { } else {
i.state.upvotes++; this.state.upvotes++;
i.state.score++; this.state.score++;
} }
i.state.my_vote = Some(newVote); this.state.my_vote = Some(newVote);
let form = new CreatePostLike({ let form = new CreatePostLike({
post_id: i.props.post_view.post.id, post_id: this.props.post_view.post.id,
score: newVote, score: newVote,
auth: auth().unwrap(), auth: auth().unwrap(),
}); });
WebSocketService.Instance.send(wsClient.likePost(form)); WebSocketService.Instance.send(wsClient.likePost(form));
i.setState(i.state); this.setState(this.state);
setupTippy(); setupTippy();
} }
handlePostDisLike(i: PostListing, event: any) { handlePostDisLike(event: any) {
event.preventDefault(); event.preventDefault();
if (UserService.Instance.myUserInfo.isNone()) { if (UserService.Instance.myUserInfo.isNone()) {
this.context.router.history.push(`/login`); this.context.router.history.push(`/login`);
@ -1294,27 +1294,27 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
let newVote = myVote == -1 ? 0 : -1; let newVote = myVote == -1 ? 0 : -1;
if (myVote == 1) { if (myVote == 1) {
i.state.score -= 2; this.state.score -= 2;
i.state.upvotes--; this.state.upvotes--;
i.state.downvotes++; this.state.downvotes++;
} else if (myVote == -1) { } else if (myVote == -1) {
i.state.downvotes--; this.state.downvotes--;
i.state.score++; this.state.score++;
} else { } else {
i.state.downvotes++; this.state.downvotes++;
i.state.score--; this.state.score--;
} }
i.state.my_vote = Some(newVote); this.state.my_vote = Some(newVote);
let form = new CreatePostLike({ let form = new CreatePostLike({
post_id: i.props.post_view.post.id, post_id: this.props.post_view.post.id,
score: newVote, score: newVote,
auth: auth().unwrap(), auth: auth().unwrap(),
}); });
WebSocketService.Instance.send(wsClient.likePost(form)); WebSocketService.Instance.send(wsClient.likePost(form));
i.setState(i.state); this.setState(this.state);
setupTippy(); setupTippy();
} }

View file

@ -7,15 +7,18 @@ import {
BanFromCommunityResponse, BanFromCommunityResponse,
BanPersonResponse, BanPersonResponse,
BlockPersonResponse, BlockPersonResponse,
CommentNode as CommentNodeI,
CommentReportResponse, CommentReportResponse,
CommentResponse, CommentResponse,
CommentSortType,
CommunityResponse, CommunityResponse,
GetComments,
GetCommentsResponse,
GetCommunityResponse, GetCommunityResponse,
GetPost, GetPost,
GetPostResponse, GetPostResponse,
GetSiteResponse, GetSiteResponse,
ListingType, ListingType,
MarkCommentAsRead,
PostReportResponse, PostReportResponse,
PostResponse, PostResponse,
PostView, PostView,
@ -30,17 +33,13 @@ import {
} from "lemmy-js-client"; } from "lemmy-js-client";
import { Subscription } from "rxjs"; import { Subscription } from "rxjs";
import { i18n } from "../../i18next"; import { i18n } from "../../i18next";
import { import { CommentViewType, InitialFetchRequest } from "../../interfaces";
CommentNode as CommentNodeI,
CommentSortType,
CommentViewType,
InitialFetchRequest,
} from "../../interfaces";
import { UserService, WebSocketService } from "../../services"; import { UserService, WebSocketService } from "../../services";
import { import {
auth, auth,
buildCommentsTree, buildCommentsTree,
commentsToFlatNodes, commentsToFlatNodes,
commentTreeMaxDepth,
createCommentLikeRes, createCommentLikeRes,
createPostLikeRes, createPostLikeRes,
debounce, debounce,
@ -48,6 +47,8 @@ import {
enableDownvotes, enableDownvotes,
enableNsfw, enableNsfw,
getCommentIdFromProps, getCommentIdFromProps,
getCommentParentId,
getDepthFromComment,
getIdFromProps, getIdFromProps,
insertCommentIntoTree, insertCommentIntoTree,
isBrowser, isBrowser,
@ -73,10 +74,11 @@ import { PostListing } from "./post-listing";
const commentsShownInterval = 15; const commentsShownInterval = 15;
interface PostState { interface PostState {
postId: Option<number>;
commentId: Option<number>;
postRes: Option<GetPostResponse>; postRes: Option<GetPostResponse>;
postId: number; commentsRes: Option<GetCommentsResponse>;
commentTree: CommentNodeI[]; commentTree: CommentNodeI[];
commentId?: number;
commentSort: CommentSortType; commentSort: CommentSortType;
commentViewType: CommentViewType; commentViewType: CommentViewType;
scrolled?: boolean; scrolled?: boolean;
@ -90,14 +92,19 @@ interface PostState {
export class Post extends Component<any, PostState> { export class Post extends Component<any, PostState> {
private subscription: Subscription; private subscription: Subscription;
private isoData = setIsoData(this.context, GetPostResponse); private isoData = setIsoData(
this.context,
GetPostResponse,
GetCommentsResponse
);
private commentScrollDebounced: () => void; private commentScrollDebounced: () => void;
private emptyState: PostState = { private emptyState: PostState = {
postRes: None, postRes: None,
commentsRes: None,
postId: getIdFromProps(this.props), postId: getIdFromProps(this.props),
commentTree: [],
commentId: getCommentIdFromProps(this.props), commentId: getCommentIdFromProps(this.props),
commentSort: CommentSortType.Hot, commentTree: [],
commentSort: CommentSortType[CommentSortType.Hot],
commentViewType: CommentViewType.Tree, commentViewType: CommentViewType.Tree,
scrolled: false, scrolled: false,
loading: true, loading: true,
@ -120,10 +127,19 @@ export class Post extends Component<any, PostState> {
// Only fetch the data if coming from another route // Only fetch the data if coming from another route
if (this.isoData.path == this.context.router.route.match.url) { if (this.isoData.path == this.context.router.route.match.url) {
this.state.postRes = Some(this.isoData.routeData[0] as GetPostResponse); this.state.postRes = Some(this.isoData.routeData[0] as GetPostResponse);
this.state.commentTree = buildCommentsTree( this.state.commentsRes = Some(
this.state.postRes.unwrap().comments, this.isoData.routeData[1] as GetCommentsResponse
this.state.commentSort
); );
this.state.commentsRes.match({
some: res => {
this.state.commentTree = buildCommentsTree(
res.comments,
this.state.commentId.isSome()
);
},
none: void 0,
});
this.state.loading = false; this.state.loading = false;
if (isBrowser()) { if (isBrowser()) {
@ -133,14 +149,14 @@ export class Post extends Component<any, PostState> {
this.state.postRes.unwrap().community_view.community.id, this.state.postRes.unwrap().community_view.community.id,
}) })
); );
WebSocketService.Instance.send(
wsClient.postJoin({ post_id: this.state.postId }) this.state.postId.match({
); some: post_id =>
WebSocketService.Instance.send(wsClient.postJoin({ post_id })),
none: void 0,
});
this.fetchCrossPosts(); this.fetchCrossPosts();
if (this.state.commentId) {
this.scrollCommentIntoView();
}
if (this.checkScrollIntoCommentsParam) { if (this.checkScrollIntoCommentsParam) {
this.scrollIntoCommentSection(); this.scrollIntoCommentSection();
@ -152,11 +168,28 @@ export class Post extends Component<any, PostState> {
} }
fetchPost() { fetchPost() {
let form = new GetPost({ this.setState({ commentsRes: None });
let postForm = new GetPost({
id: this.state.postId, id: this.state.postId,
comment_id: this.state.commentId,
auth: auth(false).ok(), auth: auth(false).ok(),
}); });
WebSocketService.Instance.send(wsClient.getPost(form)); WebSocketService.Instance.send(wsClient.getPost(postForm));
let commentsForm = new GetComments({
post_id: this.state.postId,
parent_id: this.state.commentId,
max_depth: Some(commentTreeMaxDepth),
page: None,
limit: None,
sort: Some(this.state.commentSort),
type_: Some(ListingType.All),
community_name: None,
community_id: None,
saved_only: Some(false),
auth: auth(false).ok(),
});
WebSocketService.Instance.send(wsClient.getComments(commentsForm));
} }
fetchCrossPosts() { fetchCrossPosts() {
@ -184,15 +217,44 @@ export class Post extends Component<any, PostState> {
static fetchInitialData(req: InitialFetchRequest): Promise<any>[] { static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
let pathSplit = req.path.split("/"); let pathSplit = req.path.split("/");
let promises: Promise<any>[] = [];
let pathType = pathSplit[1];
let id = Number(pathSplit[2]); let id = Number(pathSplit[2]);
let postForm = new GetPost({ let postForm = new GetPost({
id, id: None,
comment_id: None,
auth: req.auth, auth: req.auth,
}); });
return [req.client.getPost(postForm)]; let commentsForm = new GetComments({
post_id: None,
parent_id: None,
max_depth: Some(commentTreeMaxDepth),
page: None,
limit: None,
sort: Some(CommentSortType.Hot),
type_: Some(ListingType.All),
community_name: None,
community_id: None,
saved_only: Some(false),
auth: req.auth,
});
// Set the correct id based on the path type
if (pathType == "post") {
postForm.id = Some(id);
commentsForm.post_id = Some(id);
} else {
postForm.comment_id = Some(id);
commentsForm.parent_id = Some(id);
}
promises.push(req.client.getPost(postForm));
promises.push(req.client.getComments(commentsForm));
return promises;
} }
componentWillUnmount() { componentWillUnmount() {
@ -222,18 +284,6 @@ export class Post extends Component<any, PostState> {
} }
} }
scrollCommentIntoView() {
let commentElement = document.getElementById(
`comment-${this.state.commentId}`
);
if (commentElement) {
commentElement.scrollIntoView();
commentElement.classList.add("mark");
this.state.scrolled = true;
this.markScrolledAsRead(this.state.commentId);
}
}
get checkScrollIntoCommentsParam() { get checkScrollIntoCommentsParam() {
return Boolean( return Boolean(
new URLSearchParams(this.props.location.search).get("scrollToComments") new URLSearchParams(this.props.location.search).get("scrollToComments")
@ -244,39 +294,6 @@ export class Post extends Component<any, PostState> {
this.state.commentSectionRef.current?.scrollIntoView(); this.state.commentSectionRef.current?.scrollIntoView();
} }
// TODO this needs some re-work
markScrolledAsRead(commentId: number) {
this.state.postRes.match({
some: res => {
let found = res.comments.find(c => c.comment.id == commentId);
let parent = res.comments.find(
c => found.comment.parent_id.unwrapOr(0) == c.comment.id
);
let parent_person_id = parent
? parent.creator.id
: res.post_view.creator.id;
UserService.Instance.myUserInfo.match({
some: mui => {
if (mui.local_user_view.person.id == parent_person_id) {
let form = new MarkCommentAsRead({
comment_id: found.comment.id,
read: true,
auth: auth().unwrap(),
});
WebSocketService.Instance.send(wsClient.markCommentAsRead(form));
UserService.Instance.unreadInboxCountSub.next(
UserService.Instance.unreadInboxCountSub.value - 1
);
}
},
none: void 0,
});
},
none: void 0,
});
}
isBottom(el: Element): boolean { isBottom(el: Element): boolean {
return el?.getBoundingClientRect().bottom <= window.innerHeight; return el?.getBoundingClientRect().bottom <= window.innerHeight;
} }
@ -351,7 +368,7 @@ export class Post extends Component<any, PostState> {
/> />
<div ref={this.state.commentSectionRef} className="mb-2" /> <div ref={this.state.commentSectionRef} className="mb-2" />
<CommentForm <CommentForm
node={Right(this.state.postId)} node={Right(res.post_view.post.id)}
disabled={res.post_view.post.locked} disabled={res.post_view.post.locked}
/> />
<div class="d-block d-md-none"> <div class="d-block d-md-none">
@ -371,10 +388,10 @@ export class Post extends Component<any, PostState> {
</button> </button>
{this.state.showSidebarMobile && this.sidebar()} {this.state.showSidebarMobile && this.sidebar()}
</div> </div>
{res.comments.length > 0 && this.sortRadios()} {this.sortRadios()}
{this.state.commentViewType == CommentViewType.Tree && {this.state.commentViewType == CommentViewType.Tree &&
this.commentsTree()} this.commentsTree()}
{this.state.commentViewType == CommentViewType.Chat && {this.state.commentViewType == CommentViewType.Flat &&
this.commentsFlat()} this.commentsFlat()}
</div> </div>
<div class="d-none d-md-block col-md-4">{this.sidebar()}</div> <div class="d-none d-md-block col-md-4">{this.sidebar()}</div>
@ -393,7 +410,8 @@ export class Post extends Component<any, PostState> {
<div class="btn-group btn-group-toggle flex-wrap mr-3 mb-2"> <div class="btn-group btn-group-toggle flex-wrap mr-3 mb-2">
<label <label
className={`btn btn-outline-secondary pointer ${ className={`btn btn-outline-secondary pointer ${
this.state.commentSort === CommentSortType.Hot && "active" CommentSortType[this.state.commentSort] === CommentSortType.Hot &&
"active"
}`} }`}
> >
{i18n.t("hot")} {i18n.t("hot")}
@ -406,7 +424,8 @@ export class Post extends Component<any, PostState> {
</label> </label>
<label <label
className={`btn btn-outline-secondary pointer ${ className={`btn btn-outline-secondary pointer ${
this.state.commentSort === CommentSortType.Top && "active" CommentSortType[this.state.commentSort] === CommentSortType.Top &&
"active"
}`} }`}
> >
{i18n.t("top")} {i18n.t("top")}
@ -419,7 +438,8 @@ export class Post extends Component<any, PostState> {
</label> </label>
<label <label
className={`btn btn-outline-secondary pointer ${ className={`btn btn-outline-secondary pointer ${
this.state.commentSort === CommentSortType.New && "active" CommentSortType[this.state.commentSort] === CommentSortType.New &&
"active"
}`} }`}
> >
{i18n.t("new")} {i18n.t("new")}
@ -432,7 +452,8 @@ export class Post extends Component<any, PostState> {
</label> </label>
<label <label
className={`btn btn-outline-secondary pointer ${ className={`btn btn-outline-secondary pointer ${
this.state.commentSort === CommentSortType.Old && "active" CommentSortType[this.state.commentSort] === CommentSortType.Old &&
"active"
}`} }`}
> >
{i18n.t("old")} {i18n.t("old")}
@ -447,14 +468,14 @@ export class Post extends Component<any, PostState> {
<div class="btn-group btn-group-toggle flex-wrap mb-2"> <div class="btn-group btn-group-toggle flex-wrap mb-2">
<label <label
className={`btn btn-outline-secondary pointer ${ className={`btn btn-outline-secondary pointer ${
this.state.commentViewType === CommentViewType.Chat && "active" this.state.commentViewType === CommentViewType.Flat && "active"
}`} }`}
> >
{i18n.t("chat")} {i18n.t("chat")}
<input <input
type="radio" type="radio"
value={CommentViewType.Chat} value={CommentViewType.Flat}
checked={this.state.commentViewType === CommentViewType.Chat} checked={this.state.commentViewType === CommentViewType.Flat}
onChange={linkEvent(this, this.handleCommentViewTypeChange)} onChange={linkEvent(this, this.handleCommentViewTypeChange)}
/> />
</label> </label>
@ -465,21 +486,26 @@ export class Post extends Component<any, PostState> {
commentsFlat() { commentsFlat() {
// These are already sorted by new // These are already sorted by new
return this.state.postRes.match({ return this.state.commentsRes.match({
some: res => ( some: commentsRes =>
<div> this.state.postRes.match({
<CommentNodes some: postRes => (
nodes={commentsToFlatNodes(res.comments)} <div>
maxCommentsShown={Some(this.state.maxCommentsShown)} <CommentNodes
noIndent nodes={commentsToFlatNodes(commentsRes.comments)}
locked={res.post_view.post.locked} viewType={this.state.commentViewType}
moderators={Some(res.moderators)} maxCommentsShown={Some(this.state.maxCommentsShown)}
admins={Some(this.state.siteRes.admins)} noIndent
enableDownvotes={enableDownvotes(this.state.siteRes)} locked={postRes.post_view.post.locked}
showContext moderators={Some(postRes.moderators)}
/> admins={Some(this.state.siteRes.admins)}
</div> enableDownvotes={enableDownvotes(this.state.siteRes)}
), showContext
/>
</div>
),
none: <></>,
}),
none: <></>, none: <></>,
}); });
} }
@ -503,21 +529,18 @@ export class Post extends Component<any, PostState> {
} }
handleCommentSortChange(i: Post, event: any) { handleCommentSortChange(i: Post, event: any) {
i.state.commentSort = Number(event.target.value); i.state.commentSort = CommentSortType[event.target.value];
i.state.commentViewType = CommentViewType.Tree; i.state.commentViewType = CommentViewType.Tree;
i.state.commentTree = buildCommentsTree(
i.state.postRes.map(r => r.comments).unwrapOr([]),
i.state.commentSort
);
i.setState(i.state); i.setState(i.state);
i.fetchPost();
} }
handleCommentViewTypeChange(i: Post, event: any) { handleCommentViewTypeChange(i: Post, event: any) {
i.state.commentViewType = Number(event.target.value); i.state.commentViewType = Number(event.target.value);
i.state.commentSort = CommentSortType.New; i.state.commentSort = CommentSortType.New;
i.state.commentTree = buildCommentsTree( i.state.commentTree = buildCommentsTree(
i.state.postRes.map(r => r.comments).unwrapOr([]), i.state.commentsRes.map(r => r.comments).unwrapOr([]),
i.state.commentSort i.state.commentId.isSome()
); );
i.setState(i.state); i.setState(i.state);
} }
@ -527,12 +550,52 @@ export class Post extends Component<any, PostState> {
i.setState(i.state); i.setState(i.state);
} }
handleViewPost(i: Post) {
i.state.postRes.match({
some: res =>
i.context.router.history.push(`/post/${res.post_view.post.id}`),
none: void 0,
});
}
handleViewContext(i: Post) {
i.state.commentsRes.match({
some: res =>
i.context.router.history.push(
`/comment/${getCommentParentId(res.comments[0].comment).unwrap()}`
),
none: void 0,
});
}
commentsTree() { commentsTree() {
let showContextButton =
getDepthFromComment(this.state.commentTree[0].comment_view.comment) > 0;
return this.state.postRes.match({ return this.state.postRes.match({
some: res => ( some: res => (
<div> <div>
{this.state.commentId.isSome() && (
<>
<button
class="pl-0 d-block btn btn-link text-muted"
onClick={linkEvent(this, this.handleViewPost)}
>
{i18n.t("view_all_comments")}
</button>
{showContextButton && (
<button
class="pl-0 d-block btn btn-link text-muted"
onClick={linkEvent(this, this.handleViewContext)}
>
{i18n.t("show_context")}
</button>
)}
</>
)}
<CommentNodes <CommentNodes
nodes={this.state.commentTree} nodes={this.state.commentTree}
viewType={this.state.commentViewType}
maxCommentsShown={Some(this.state.maxCommentsShown)} maxCommentsShown={Some(this.state.maxCommentsShown)}
locked={res.post_view.post.locked} locked={res.post_view.post.locked}
moderators={Some(res.moderators)} moderators={Some(res.moderators)}
@ -552,27 +615,29 @@ export class Post extends Component<any, PostState> {
toast(i18n.t(msg.error), "danger"); toast(i18n.t(msg.error), "danger");
return; return;
} else if (msg.reconnect) { } else if (msg.reconnect) {
let postId = Number(this.props.match.params.id); this.state.postRes.match({
WebSocketService.Instance.send(wsClient.postJoin({ post_id: postId })); some: res => {
WebSocketService.Instance.send( let postId = res.post_view.post.id;
wsClient.getPost({ WebSocketService.Instance.send(
id: postId, wsClient.postJoin({ post_id: postId })
auth: auth(false).ok(), );
}) WebSocketService.Instance.send(
); wsClient.getPost({
id: Some(postId),
comment_id: None,
auth: auth(false).ok(),
})
);
},
none: void 0,
});
} else if (op == UserOperation.GetPost) { } else if (op == UserOperation.GetPost) {
let data = wsJsonToRes<GetPostResponse>(msg, GetPostResponse); let data = wsJsonToRes<GetPostResponse>(msg, GetPostResponse);
this.state.postRes = Some(data); this.state.postRes = Some(data);
this.state.commentTree = buildCommentsTree(
this.state.postRes.map(r => r.comments).unwrapOr([]),
this.state.commentSort
);
this.state.loading = false;
// join the rooms // join the rooms
WebSocketService.Instance.send( WebSocketService.Instance.send(
wsClient.postJoin({ post_id: this.state.postId }) wsClient.postJoin({ post_id: data.post_view.post.id })
); );
WebSocketService.Instance.send( WebSocketService.Instance.send(
wsClient.communityJoin({ wsClient.communityJoin({
@ -581,18 +646,36 @@ export class Post extends Component<any, PostState> {
); );
// Get cross-posts // Get cross-posts
// TODO move this into initial fetch and refetch
this.fetchCrossPosts(); this.fetchCrossPosts();
this.setState(this.state); this.setState(this.state);
setupTippy(); setupTippy();
if (!this.state.commentId) restoreScrollPosition(this.context); if (this.state.commentId.isNone()) restoreScrollPosition(this.context);
if (this.checkScrollIntoCommentsParam) { if (this.checkScrollIntoCommentsParam) {
this.scrollIntoCommentSection(); this.scrollIntoCommentSection();
} }
} else if (op == UserOperation.GetComments) {
if (this.state.commentId && !this.state.scrolled) { let data = wsJsonToRes<GetCommentsResponse>(msg, GetCommentsResponse);
this.scrollCommentIntoView(); // You might need to append here, since this could be building more comments from a tree fetch
} this.state.commentsRes.match({
some: res => {
// Remove the first comment, since it is the parent
let newComments = data.comments;
newComments.shift();
res.comments.push(...newComments);
},
none: () => {
this.state.commentsRes = Some(data);
},
});
// this.state.commentsRes = Some(data);
this.state.commentTree = buildCommentsTree(
this.state.commentsRes.map(r => r.comments).unwrapOr([]),
this.state.commentId.isSome()
);
this.state.loading = false;
this.setState(this.state);
} else if (op == UserOperation.CreateComment) { } else if (op == UserOperation.CreateComment) {
let data = wsJsonToRes<CommentResponse>(msg, CommentResponse); let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
@ -606,11 +689,19 @@ export class Post extends Component<any, PostState> {
// Necessary since it might be a user reply, which has the recipients, to avoid double // Necessary since it might be a user reply, which has the recipients, to avoid double
if (data.recipient_ids.length == 0 && !creatorBlocked) { if (data.recipient_ids.length == 0 && !creatorBlocked) {
this.state.postRes.match({ this.state.postRes.match({
some: res => { some: postRes =>
res.comments.unshift(data.comment_view); this.state.commentsRes.match({
insertCommentIntoTree(this.state.commentTree, data.comment_view); some: commentsRes => {
res.post_view.counts.comments++; commentsRes.comments.unshift(data.comment_view);
}, insertCommentIntoTree(
this.state.commentTree,
data.comment_view,
this.state.commentId.isSome()
);
postRes.post_view.counts.comments++;
},
none: void 0,
}),
none: void 0, none: void 0,
}); });
this.setState(this.state); this.setState(this.state);
@ -624,14 +715,14 @@ export class Post extends Component<any, PostState> {
let data = wsJsonToRes<CommentResponse>(msg, CommentResponse); let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
editCommentRes( editCommentRes(
data.comment_view, data.comment_view,
this.state.postRes.map(r => r.comments).unwrapOr([]) this.state.commentsRes.map(r => r.comments).unwrapOr([])
); );
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.SaveComment) { } else if (op == UserOperation.SaveComment) {
let data = wsJsonToRes<CommentResponse>(msg, CommentResponse); let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
saveCommentRes( saveCommentRes(
data.comment_view, data.comment_view,
this.state.postRes.map(r => r.comments).unwrapOr([]) this.state.commentsRes.map(r => r.comments).unwrapOr([])
); );
this.setState(this.state); this.setState(this.state);
setupTippy(); setupTippy();
@ -639,7 +730,7 @@ export class Post extends Component<any, PostState> {
let data = wsJsonToRes<CommentResponse>(msg, CommentResponse); let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
createCommentLikeRes( createCommentLikeRes(
data.comment_view, data.comment_view,
this.state.postRes.map(r => r.comments).unwrapOr([]) this.state.commentsRes.map(r => r.comments).unwrapOr([])
); );
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.CreatePostLike) { } else if (op == UserOperation.CreatePostLike) {
@ -685,15 +776,19 @@ export class Post extends Component<any, PostState> {
BanFromCommunityResponse BanFromCommunityResponse
); );
this.state.postRes.match({ this.state.postRes.match({
some: res => { some: postRes =>
res.comments this.state.commentsRes.match({
.filter(c => c.creator.id == data.person_view.person.id) some: commentsRes => {
.forEach(c => (c.creator_banned_from_community = data.banned)); commentsRes.comments
if (res.post_view.creator.id == data.person_view.person.id) { .filter(c => c.creator.id == data.person_view.person.id)
res.post_view.creator_banned_from_community = data.banned; .forEach(c => (c.creator_banned_from_community = data.banned));
} if (postRes.post_view.creator.id == data.person_view.person.id) {
this.setState(this.state); postRes.post_view.creator_banned_from_community = data.banned;
}, }
this.setState(this.state);
},
none: void 0,
}),
none: void 0, none: void 0,
}); });
} else if (op == UserOperation.AddModToCommunity) { } else if (op == UserOperation.AddModToCommunity) {
@ -711,15 +806,19 @@ export class Post extends Component<any, PostState> {
} else if (op == UserOperation.BanPerson) { } else if (op == UserOperation.BanPerson) {
let data = wsJsonToRes<BanPersonResponse>(msg, BanPersonResponse); let data = wsJsonToRes<BanPersonResponse>(msg, BanPersonResponse);
this.state.postRes.match({ this.state.postRes.match({
some: res => { some: postRes =>
res.comments this.state.commentsRes.match({
.filter(c => c.creator.id == data.person_view.person.id) some: commentsRes => {
.forEach(c => (c.creator.banned = data.banned)); commentsRes.comments
if (res.post_view.creator.id == data.person_view.person.id) { .filter(c => c.creator.id == data.person_view.person.id)
res.post_view.creator.banned = data.banned; .forEach(c => (c.creator.banned = data.banned));
} if (postRes.post_view.creator.id == data.person_view.person.id) {
this.setState(this.state); postRes.post_view.creator.banned = data.banned;
}, }
this.setState(this.state);
},
none: void 0,
}),
none: void 0, none: void 0,
}); });
} else if (op == UserOperation.AddAdmin) { } else if (op == UserOperation.AddAdmin) {

View file

@ -26,8 +26,8 @@ import {
wsUserOp, wsUserOp,
} from "lemmy-js-client"; } from "lemmy-js-client";
import { Subscription } from "rxjs"; import { Subscription } from "rxjs";
import { InitialFetchRequest } from "shared/interfaces";
import { i18n } from "../i18next"; import { i18n } from "../i18next";
import { CommentViewType, InitialFetchRequest } from "../interfaces";
import { WebSocketService } from "../services"; import { WebSocketService } from "../services";
import { import {
auth, auth,
@ -594,7 +594,14 @@ export class Search extends Component<any, SearchState> {
{i.type_ == "comments" && ( {i.type_ == "comments" && (
<CommentNodes <CommentNodes
key={(i.data as CommentView).comment.id} key={(i.data as CommentView).comment.id}
nodes={[{ comment_view: i.data as CommentView }]} nodes={[
{
comment_view: i.data as CommentView,
children: [],
depth: 0,
},
]}
viewType={CommentViewType.Flat}
moderators={None} moderators={None}
admins={None} admins={None}
maxCommentsShown={None} maxCommentsShown={None}
@ -631,6 +638,7 @@ export class Search extends Component<any, SearchState> {
return ( return (
<CommentNodes <CommentNodes
nodes={commentsToFlatNodes(comments)} nodes={commentsToFlatNodes(comments)}
viewType={CommentViewType.Flat}
locked locked
noIndent noIndent
moderators={None} moderators={None}

View file

@ -1,10 +1,5 @@
import { Either, Option } from "@sniptt/monads"; import { Either, Option } from "@sniptt/monads";
import { import { GetSiteResponse, LemmyHttp } from "lemmy-js-client";
CommentView,
GetSiteResponse,
LemmyHttp,
PersonMentionView,
} from "lemmy-js-client";
/** /**
* This contains serialized data, it needs to be deserialized before use. * This contains serialized data, it needs to be deserialized before use.
@ -32,12 +27,6 @@ export interface InitialFetchRequest {
path: string; path: string;
} }
export interface CommentNode {
comment_view: CommentView | PersonMentionView;
children?: CommentNode[];
depth?: number;
}
export interface PostFormParams { export interface PostFormParams {
name: Option<string>; name: Option<string>;
url: Option<string>; url: Option<string>;
@ -45,16 +34,9 @@ export interface PostFormParams {
nameOrId: Option<Either<string, number>>; nameOrId: Option<Either<string, number>>;
} }
export enum CommentSortType {
Hot,
Top,
New,
Old,
}
export enum CommentViewType { export enum CommentViewType {
Tree, Tree,
Chat, Flat,
} }
export enum DataType { export enum DataType {

View file

@ -72,12 +72,12 @@ export const routes: IRoutePropsWithFetch[] = [
fetchInitialData: req => Communities.fetchInitialData(req), fetchInitialData: req => Communities.fetchInitialData(req),
}, },
{ {
path: `/post/:id/comment/:comment_id`, path: `/post/:post_id`,
component: Post, component: Post,
fetchInitialData: req => Post.fetchInitialData(req), fetchInitialData: req => Post.fetchInitialData(req),
}, },
{ {
path: `/post/:id`, path: `/comment/:comment_id`,
component: Post, component: Post,
fetchInitialData: req => Post.fetchInitialData(req), fetchInitialData: req => Post.fetchInitialData(req),
}, },

View file

@ -4,7 +4,10 @@ import emojiShortName from "emoji-short-name";
import { import {
BlockCommunityResponse, BlockCommunityResponse,
BlockPersonResponse, BlockPersonResponse,
Comment as CommentI,
CommentNode as CommentNodeI,
CommentReportView, CommentReportView,
CommentSortType,
CommentView, CommentView,
CommunityBlockView, CommunityBlockView,
CommunityModeratorView, CommunityModeratorView,
@ -39,12 +42,7 @@ import tippy from "tippy.js";
import Toastify from "toastify-js"; import Toastify from "toastify-js";
import { httpBase } from "./env"; import { httpBase } from "./env";
import { i18n, languages } from "./i18next"; import { i18n, languages } from "./i18next";
import { import { DataType, IsoData } from "./interfaces";
CommentNode as CommentNodeI,
CommentSortType,
DataType,
IsoData,
} from "./interfaces";
import { UserService, WebSocketService } from "./services"; import { UserService, WebSocketService } from "./services";
var Tribute: any; var Tribute: any;
@ -74,6 +72,7 @@ export const postRefetchSeconds: number = 60 * 1000;
export const fetchLimit = 20; export const fetchLimit = 20;
export const trendingFetchLimit = 6; export const trendingFetchLimit = 6;
export const mentionDropdownFetchLimit = 10; export const mentionDropdownFetchLimit = 10;
export const commentTreeMaxDepth = 8;
export const relTags = "noopener nofollow"; export const relTags = "noopener nofollow";
@ -611,7 +610,7 @@ export function notifyComment(comment_view: CommentView, router: any) {
let info: NotifyInfo = { let info: NotifyInfo = {
name: comment_view.creator.name, name: comment_view.creator.name,
icon: comment_view.creator.avatar, icon: comment_view.creator.avatar,
link: `/post/${comment_view.post.id}/comment/${comment_view.comment.id}`, link: `/comment/${comment_view.comment.id}`,
body: comment_view.comment.content, body: comment_view.comment.content,
}; };
notify(info, router); notify(info, router);
@ -813,12 +812,14 @@ export function getRecipientIdFromProps(props: any): number {
: 1; : 1;
} }
export function getIdFromProps(props: any): number { export function getIdFromProps(props: any): Option<number> {
return Number(props.match.params.id); let id: string = props.match.params.post_id;
return id ? Some(Number(id)) : None;
} }
export function getCommentIdFromProps(props: any): number { export function getCommentIdFromProps(props: any): Option<number> {
return Number(props.match.params.comment_id); let id: string = props.match.params.comment_id;
return id ? Some(Number(id)) : None;
} }
export function getUsernameFromProps(props: any): string { export function getUsernameFromProps(props: any): string {
@ -985,61 +986,12 @@ export function updateRegistrationApplicationRes(
export function commentsToFlatNodes(comments: CommentView[]): CommentNodeI[] { export function commentsToFlatNodes(comments: CommentView[]): CommentNodeI[] {
let nodes: CommentNodeI[] = []; let nodes: CommentNodeI[] = [];
for (let comment of comments) { for (let comment of comments) {
nodes.push({ comment_view: comment }); nodes.push({ comment_view: comment, children: [], depth: 0 });
} }
return nodes; return nodes;
} }
function commentSort(tree: CommentNodeI[], sort: CommentSortType) { export function convertCommentSortType(sort: SortType): CommentSortType {
// First, put removed and deleted comments at the bottom, then do your other sorts
if (sort == CommentSortType.Top) {
tree.sort(
(a, b) =>
+a.comment_view.comment.removed - +b.comment_view.comment.removed ||
+a.comment_view.comment.deleted - +b.comment_view.comment.deleted ||
b.comment_view.counts.score - a.comment_view.counts.score
);
} else if (sort == CommentSortType.New) {
tree.sort(
(a, b) =>
+a.comment_view.comment.removed - +b.comment_view.comment.removed ||
+a.comment_view.comment.deleted - +b.comment_view.comment.deleted ||
b.comment_view.comment.published.localeCompare(
a.comment_view.comment.published
)
);
} else if (sort == CommentSortType.Old) {
tree.sort(
(a, b) =>
+a.comment_view.comment.removed - +b.comment_view.comment.removed ||
+a.comment_view.comment.deleted - +b.comment_view.comment.deleted ||
a.comment_view.comment.published.localeCompare(
b.comment_view.comment.published
)
);
} else if (sort == CommentSortType.Hot) {
tree.sort(
(a, b) =>
+a.comment_view.comment.removed - +b.comment_view.comment.removed ||
+a.comment_view.comment.deleted - +b.comment_view.comment.deleted ||
hotRankComment(b.comment_view as CommentView) -
hotRankComment(a.comment_view as CommentView)
);
}
// Go through the children recursively
for (let node of tree) {
if (node.children) {
commentSort(node.children, sort);
}
}
}
export function commentSortSortType(tree: CommentNodeI[], sort: SortType) {
commentSort(tree, convertCommentSortType(sort));
}
function convertCommentSortType(sort: SortType): CommentSortType {
if ( if (
sort == SortType.TopAll || sort == SortType.TopAll ||
sort == SortType.TopDay || sort == SortType.TopDay ||
@ -1059,21 +1011,32 @@ function convertCommentSortType(sort: SortType): CommentSortType {
export function buildCommentsTree( export function buildCommentsTree(
comments: CommentView[], comments: CommentView[],
commentSortType: CommentSortType parentComment: boolean
): CommentNodeI[] { ): CommentNodeI[] {
let map = new Map<number, CommentNodeI>(); let map = new Map<number, CommentNodeI>();
let depthOffset = !parentComment
? 0
: getDepthFromComment(comments[0].comment);
for (let comment_view of comments) { for (let comment_view of comments) {
let node: CommentNodeI = { let node: CommentNodeI = {
comment_view: comment_view, comment_view: comment_view,
children: [], children: [],
depth: 0, depth: getDepthFromComment(comment_view.comment) - depthOffset,
}; };
map.set(comment_view.comment.id, { ...node }); map.set(comment_view.comment.id, { ...node });
} }
let tree: CommentNodeI[] = []; let tree: CommentNodeI[] = [];
// if its a parent comment fetch, then push the first comment to the top node.
if (parentComment) {
tree.push(map.get(comments[0].comment.id));
}
for (let comment_view of comments) { for (let comment_view of comments) {
let child = map.get(comment_view.comment.id); let child = map.get(comment_view.comment.id);
let parent_id = comment_view.comment.parent_id; let parent_id = getCommentParentId(comment_view.comment);
parent_id.match({ parent_id.match({
some: parentId => { some: parentId => {
let parent = map.get(parentId); let parent = map.get(parentId);
@ -1083,26 +1046,37 @@ export function buildCommentsTree(
} }
}, },
none: () => { none: () => {
tree.push(child); if (!parentComment) {
tree.push(child);
}
}, },
}); });
setDepth(child);
} }
commentSort(tree, commentSortType);
return tree; return tree;
} }
function setDepth(node: CommentNodeI, i = 0) { export function getCommentParentId(comment: CommentI): Option<number> {
for (let child of node.children) { let split = comment.path.split(".");
child.depth = i; // remove the 0
setDepth(child, i + 1); split.shift();
if (split.length > 1) {
return Some(Number(split[split.length - 2]));
} else {
return None;
} }
} }
export function insertCommentIntoTree(tree: CommentNodeI[], cv: CommentView) { export function getDepthFromComment(comment: CommentI): number {
return comment.path.split(".").length - 2;
}
export function insertCommentIntoTree(
tree: CommentNodeI[],
cv: CommentView,
parentComment: boolean
) {
// Building a fake node to be used for later // Building a fake node to be used for later
let node: CommentNodeI = { let node: CommentNodeI = {
comment_view: cv, comment_view: cv,
@ -1110,7 +1084,7 @@ export function insertCommentIntoTree(tree: CommentNodeI[], cv: CommentView) {
depth: 0, depth: 0,
}; };
cv.comment.parent_id.match({ getCommentParentId(cv.comment).match({
some: parentId => { some: parentId => {
let parentComment = searchCommentTree(tree, parentId); let parentComment = searchCommentTree(tree, parentId);
parentComment.match({ parentComment.match({
@ -1122,7 +1096,9 @@ export function insertCommentIntoTree(tree: CommentNodeI[], cv: CommentView) {
}); });
}, },
none: () => { none: () => {
tree.unshift(node); if (!parentComment) {
tree.unshift(node);
}
}, },
}); });
} }
@ -1149,6 +1125,7 @@ export function searchCommentTree(
export const colorList: string[] = [ export const colorList: string[] = [
hsl(0), hsl(0),
hsl(50),
hsl(100), hsl(100),
hsl(150), hsl(150),
hsl(200), hsl(200),
@ -1439,3 +1416,15 @@ export function enableDownvotes(siteRes: GetSiteResponse): boolean {
export function enableNsfw(siteRes: GetSiteResponse): boolean { export function enableNsfw(siteRes: GetSiteResponse): boolean {
return siteRes.site_view.map(s => s.site.enable_nsfw).unwrapOr(false); return siteRes.site_view.map(s => s.site.enable_nsfw).unwrapOr(false);
} }
export function postToCommentSortType(sort: SortType): CommentSortType {
if ([SortType.Active, SortType.Hot].includes(sort)) {
return CommentSortType.Hot;
} else if ([SortType.New, SortType.NewComments].includes(sort)) {
return CommentSortType.New;
} else if (sort == SortType.Old) {
return CommentSortType.Old;
} else {
return CommentSortType.Top;
}
}

View file

@ -5056,10 +5056,10 @@ lcid@^1.0.0:
dependencies: dependencies:
invert-kv "^1.0.0" invert-kv "^1.0.0"
lemmy-js-client@0.17.0-rc.38: lemmy-js-client@0.17.0-rc.39:
version "0.17.0-rc.38" version "0.17.0-rc.39"
resolved "https://registry.yarnpkg.com/lemmy-js-client/-/lemmy-js-client-0.17.0-rc.38.tgz#bbe0667ad44bbd0c1e516813d3c81b65c7f6a329" resolved "https://registry.yarnpkg.com/lemmy-js-client/-/lemmy-js-client-0.17.0-rc.39.tgz#b17c5c0d9a0f36c90c17be99845a5703091ab306"
integrity sha512-uDC19s3+Eva+Hu3LhIPkT5j0Ngh7F84TA4VnfxMVJN6BQZFLZUmTvAErwJcqyj5vz3sNnw4jsEeTSGPODSXfeg== integrity sha512-MsKavo5xOob6DgfjyhbmXyFvXwdW4iwftStJ7Bz3ArlHXy6zGBp+2uy2rU2c5ujivNDX72ol3TupTHBtSXLs4w==
levn@^0.4.1: levn@^0.4.1:
version "0.4.1" version "0.4.1"