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",
"husky": "^8.0.1",
"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",
"mini-css-extract-plugin": "^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 { Link } from "inferno-router";
import {
CommentNode as CommentNodeI,
CommentResponse,
CreateComment,
EditComment,
@ -12,7 +13,6 @@ import {
} from "lemmy-js-client";
import { Subscription } from "rxjs";
import { i18n } from "../../i18next";
import { CommentNode as CommentNodeI } from "../../interfaces";
import { UserService, WebSocketService } from "../../services";
import {
auth,

View file

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

View file

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

View file

@ -2,13 +2,14 @@ import { None } from "@sniptt/monads";
import { Component, linkEvent } from "inferno";
import { T } from "inferno-i18next-dess";
import {
CommentNode as CommentNodeI,
CommentReportView,
CommentView,
ResolveCommentReport,
SubscribedType,
} from "lemmy-js-client";
import { i18n } from "../../i18next";
import { CommentNode as CommentNodeI } from "../../interfaces";
import { CommentViewType } from "../../interfaces";
import { WebSocketService } from "../../services";
import { auth, wsClient } from "../../utils";
import { Icon } from "../common/icon";
@ -44,18 +45,20 @@ export class CommentReport extends Component<CommentReportProps, any> {
subscribed: SubscribedType.NotSubscribed,
saved: false,
creator_blocked: false,
recipient: None,
my_vote: r.my_vote,
};
let node: CommentNodeI = {
comment_view,
children: [],
depth: 0,
};
return (
<div>
<CommentNode
node={node}
viewType={CommentViewType.Flat}
moderators={None}
admins={None}
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.New}>{i18n.t("new")}</option>
<option value={SortType.Old}>{i18n.t("old")}</option>
{!this.props.hideMostComments && [
<option value={SortType.MostComments}>
{i18n.t("most_comments")}

View file

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

View file

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

View file

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

View file

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

View file

@ -7,15 +7,18 @@ import {
BanFromCommunityResponse,
BanPersonResponse,
BlockPersonResponse,
CommentNode as CommentNodeI,
CommentReportResponse,
CommentResponse,
CommentSortType,
CommunityResponse,
GetComments,
GetCommentsResponse,
GetCommunityResponse,
GetPost,
GetPostResponse,
GetSiteResponse,
ListingType,
MarkCommentAsRead,
PostReportResponse,
PostResponse,
PostView,
@ -30,17 +33,13 @@ import {
} from "lemmy-js-client";
import { Subscription } from "rxjs";
import { i18n } from "../../i18next";
import {
CommentNode as CommentNodeI,
CommentSortType,
CommentViewType,
InitialFetchRequest,
} from "../../interfaces";
import { CommentViewType, InitialFetchRequest } from "../../interfaces";
import { UserService, WebSocketService } from "../../services";
import {
auth,
buildCommentsTree,
commentsToFlatNodes,
commentTreeMaxDepth,
createCommentLikeRes,
createPostLikeRes,
debounce,
@ -48,6 +47,8 @@ import {
enableDownvotes,
enableNsfw,
getCommentIdFromProps,
getCommentParentId,
getDepthFromComment,
getIdFromProps,
insertCommentIntoTree,
isBrowser,
@ -73,10 +74,11 @@ import { PostListing } from "./post-listing";
const commentsShownInterval = 15;
interface PostState {
postId: Option<number>;
commentId: Option<number>;
postRes: Option<GetPostResponse>;
postId: number;
commentsRes: Option<GetCommentsResponse>;
commentTree: CommentNodeI[];
commentId?: number;
commentSort: CommentSortType;
commentViewType: CommentViewType;
scrolled?: boolean;
@ -90,14 +92,19 @@ interface PostState {
export class Post extends Component<any, PostState> {
private subscription: Subscription;
private isoData = setIsoData(this.context, GetPostResponse);
private isoData = setIsoData(
this.context,
GetPostResponse,
GetCommentsResponse
);
private commentScrollDebounced: () => void;
private emptyState: PostState = {
postRes: None,
commentsRes: None,
postId: getIdFromProps(this.props),
commentTree: [],
commentId: getCommentIdFromProps(this.props),
commentSort: CommentSortType.Hot,
commentTree: [],
commentSort: CommentSortType[CommentSortType.Hot],
commentViewType: CommentViewType.Tree,
scrolled: false,
loading: true,
@ -120,10 +127,19 @@ export class Post extends Component<any, PostState> {
// Only fetch the data if coming from another route
if (this.isoData.path == this.context.router.route.match.url) {
this.state.postRes = Some(this.isoData.routeData[0] as GetPostResponse);
this.state.commentTree = buildCommentsTree(
this.state.postRes.unwrap().comments,
this.state.commentSort
this.state.commentsRes = Some(
this.isoData.routeData[1] as GetCommentsResponse
);
this.state.commentsRes.match({
some: res => {
this.state.commentTree = buildCommentsTree(
res.comments,
this.state.commentId.isSome()
);
},
none: void 0,
});
this.state.loading = false;
if (isBrowser()) {
@ -133,14 +149,14 @@ export class Post extends Component<any, PostState> {
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();
if (this.state.commentId) {
this.scrollCommentIntoView();
}
if (this.checkScrollIntoCommentsParam) {
this.scrollIntoCommentSection();
@ -152,11 +168,28 @@ export class Post extends Component<any, PostState> {
}
fetchPost() {
let form = new GetPost({
this.setState({ commentsRes: None });
let postForm = new GetPost({
id: this.state.postId,
comment_id: this.state.commentId,
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() {
@ -184,15 +217,44 @@ export class Post extends Component<any, PostState> {
static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
let pathSplit = req.path.split("/");
let promises: Promise<any>[] = [];
let pathType = pathSplit[1];
let id = Number(pathSplit[2]);
let postForm = new GetPost({
id,
id: None,
comment_id: None,
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() {
@ -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() {
return Boolean(
new URLSearchParams(this.props.location.search).get("scrollToComments")
@ -244,39 +294,6 @@ export class Post extends Component<any, PostState> {
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 {
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" />
<CommentForm
node={Right(this.state.postId)}
node={Right(res.post_view.post.id)}
disabled={res.post_view.post.locked}
/>
<div class="d-block d-md-none">
@ -371,10 +388,10 @@ export class Post extends Component<any, PostState> {
</button>
{this.state.showSidebarMobile && this.sidebar()}
</div>
{res.comments.length > 0 && this.sortRadios()}
{this.sortRadios()}
{this.state.commentViewType == CommentViewType.Tree &&
this.commentsTree()}
{this.state.commentViewType == CommentViewType.Chat &&
{this.state.commentViewType == CommentViewType.Flat &&
this.commentsFlat()}
</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">
<label
className={`btn btn-outline-secondary pointer ${
this.state.commentSort === CommentSortType.Hot && "active"
CommentSortType[this.state.commentSort] === CommentSortType.Hot &&
"active"
}`}
>
{i18n.t("hot")}
@ -406,7 +424,8 @@ export class Post extends Component<any, PostState> {
</label>
<label
className={`btn btn-outline-secondary pointer ${
this.state.commentSort === CommentSortType.Top && "active"
CommentSortType[this.state.commentSort] === CommentSortType.Top &&
"active"
}`}
>
{i18n.t("top")}
@ -419,7 +438,8 @@ export class Post extends Component<any, PostState> {
</label>
<label
className={`btn btn-outline-secondary pointer ${
this.state.commentSort === CommentSortType.New && "active"
CommentSortType[this.state.commentSort] === CommentSortType.New &&
"active"
}`}
>
{i18n.t("new")}
@ -432,7 +452,8 @@ export class Post extends Component<any, PostState> {
</label>
<label
className={`btn btn-outline-secondary pointer ${
this.state.commentSort === CommentSortType.Old && "active"
CommentSortType[this.state.commentSort] === CommentSortType.Old &&
"active"
}`}
>
{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">
<label
className={`btn btn-outline-secondary pointer ${
this.state.commentViewType === CommentViewType.Chat && "active"
this.state.commentViewType === CommentViewType.Flat && "active"
}`}
>
{i18n.t("chat")}
<input
type="radio"
value={CommentViewType.Chat}
checked={this.state.commentViewType === CommentViewType.Chat}
value={CommentViewType.Flat}
checked={this.state.commentViewType === CommentViewType.Flat}
onChange={linkEvent(this, this.handleCommentViewTypeChange)}
/>
</label>
@ -465,21 +486,26 @@ export class Post extends Component<any, PostState> {
commentsFlat() {
// These are already sorted by new
return this.state.postRes.match({
some: res => (
<div>
<CommentNodes
nodes={commentsToFlatNodes(res.comments)}
maxCommentsShown={Some(this.state.maxCommentsShown)}
noIndent
locked={res.post_view.post.locked}
moderators={Some(res.moderators)}
admins={Some(this.state.siteRes.admins)}
enableDownvotes={enableDownvotes(this.state.siteRes)}
showContext
/>
</div>
),
return this.state.commentsRes.match({
some: commentsRes =>
this.state.postRes.match({
some: postRes => (
<div>
<CommentNodes
nodes={commentsToFlatNodes(commentsRes.comments)}
viewType={this.state.commentViewType}
maxCommentsShown={Some(this.state.maxCommentsShown)}
noIndent
locked={postRes.post_view.post.locked}
moderators={Some(postRes.moderators)}
admins={Some(this.state.siteRes.admins)}
enableDownvotes={enableDownvotes(this.state.siteRes)}
showContext
/>
</div>
),
none: <></>,
}),
none: <></>,
});
}
@ -503,21 +529,18 @@ export class Post extends Component<any, PostState> {
}
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.commentTree = buildCommentsTree(
i.state.postRes.map(r => r.comments).unwrapOr([]),
i.state.commentSort
);
i.setState(i.state);
i.fetchPost();
}
handleCommentViewTypeChange(i: Post, event: any) {
i.state.commentViewType = Number(event.target.value);
i.state.commentSort = CommentSortType.New;
i.state.commentTree = buildCommentsTree(
i.state.postRes.map(r => r.comments).unwrapOr([]),
i.state.commentSort
i.state.commentsRes.map(r => r.comments).unwrapOr([]),
i.state.commentId.isSome()
);
i.setState(i.state);
}
@ -527,12 +550,52 @@ export class Post extends Component<any, PostState> {
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() {
let showContextButton =
getDepthFromComment(this.state.commentTree[0].comment_view.comment) > 0;
return this.state.postRes.match({
some: res => (
<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
nodes={this.state.commentTree}
viewType={this.state.commentViewType}
maxCommentsShown={Some(this.state.maxCommentsShown)}
locked={res.post_view.post.locked}
moderators={Some(res.moderators)}
@ -552,27 +615,29 @@ export class Post extends Component<any, PostState> {
toast(i18n.t(msg.error), "danger");
return;
} else if (msg.reconnect) {
let postId = Number(this.props.match.params.id);
WebSocketService.Instance.send(wsClient.postJoin({ post_id: postId }));
WebSocketService.Instance.send(
wsClient.getPost({
id: postId,
auth: auth(false).ok(),
})
);
this.state.postRes.match({
some: res => {
let postId = res.post_view.post.id;
WebSocketService.Instance.send(
wsClient.postJoin({ post_id: postId })
);
WebSocketService.Instance.send(
wsClient.getPost({
id: Some(postId),
comment_id: None,
auth: auth(false).ok(),
})
);
},
none: void 0,
});
} else if (op == UserOperation.GetPost) {
let data = wsJsonToRes<GetPostResponse>(msg, GetPostResponse);
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
WebSocketService.Instance.send(
wsClient.postJoin({ post_id: this.state.postId })
wsClient.postJoin({ post_id: data.post_view.post.id })
);
WebSocketService.Instance.send(
wsClient.communityJoin({
@ -581,18 +646,36 @@ export class Post extends Component<any, PostState> {
);
// Get cross-posts
// TODO move this into initial fetch and refetch
this.fetchCrossPosts();
this.setState(this.state);
setupTippy();
if (!this.state.commentId) restoreScrollPosition(this.context);
if (this.state.commentId.isNone()) restoreScrollPosition(this.context);
if (this.checkScrollIntoCommentsParam) {
this.scrollIntoCommentSection();
}
if (this.state.commentId && !this.state.scrolled) {
this.scrollCommentIntoView();
}
} else if (op == UserOperation.GetComments) {
let data = wsJsonToRes<GetCommentsResponse>(msg, GetCommentsResponse);
// 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) {
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
if (data.recipient_ids.length == 0 && !creatorBlocked) {
this.state.postRes.match({
some: res => {
res.comments.unshift(data.comment_view);
insertCommentIntoTree(this.state.commentTree, data.comment_view);
res.post_view.counts.comments++;
},
some: postRes =>
this.state.commentsRes.match({
some: commentsRes => {
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,
});
this.setState(this.state);
@ -624,14 +715,14 @@ export class Post extends Component<any, PostState> {
let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
editCommentRes(
data.comment_view,
this.state.postRes.map(r => r.comments).unwrapOr([])
this.state.commentsRes.map(r => r.comments).unwrapOr([])
);
this.setState(this.state);
} else if (op == UserOperation.SaveComment) {
let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
saveCommentRes(
data.comment_view,
this.state.postRes.map(r => r.comments).unwrapOr([])
this.state.commentsRes.map(r => r.comments).unwrapOr([])
);
this.setState(this.state);
setupTippy();
@ -639,7 +730,7 @@ export class Post extends Component<any, PostState> {
let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
createCommentLikeRes(
data.comment_view,
this.state.postRes.map(r => r.comments).unwrapOr([])
this.state.commentsRes.map(r => r.comments).unwrapOr([])
);
this.setState(this.state);
} else if (op == UserOperation.CreatePostLike) {
@ -685,15 +776,19 @@ export class Post extends Component<any, PostState> {
BanFromCommunityResponse
);
this.state.postRes.match({
some: res => {
res.comments
.filter(c => c.creator.id == data.person_view.person.id)
.forEach(c => (c.creator_banned_from_community = data.banned));
if (res.post_view.creator.id == data.person_view.person.id) {
res.post_view.creator_banned_from_community = data.banned;
}
this.setState(this.state);
},
some: postRes =>
this.state.commentsRes.match({
some: commentsRes => {
commentsRes.comments
.filter(c => c.creator.id == data.person_view.person.id)
.forEach(c => (c.creator_banned_from_community = data.banned));
if (postRes.post_view.creator.id == data.person_view.person.id) {
postRes.post_view.creator_banned_from_community = data.banned;
}
this.setState(this.state);
},
none: void 0,
}),
none: void 0,
});
} else if (op == UserOperation.AddModToCommunity) {
@ -711,15 +806,19 @@ export class Post extends Component<any, PostState> {
} else if (op == UserOperation.BanPerson) {
let data = wsJsonToRes<BanPersonResponse>(msg, BanPersonResponse);
this.state.postRes.match({
some: res => {
res.comments
.filter(c => c.creator.id == data.person_view.person.id)
.forEach(c => (c.creator.banned = data.banned));
if (res.post_view.creator.id == data.person_view.person.id) {
res.post_view.creator.banned = data.banned;
}
this.setState(this.state);
},
some: postRes =>
this.state.commentsRes.match({
some: commentsRes => {
commentsRes.comments
.filter(c => c.creator.id == data.person_view.person.id)
.forEach(c => (c.creator.banned = data.banned));
if (postRes.post_view.creator.id == data.person_view.person.id) {
postRes.post_view.creator.banned = data.banned;
}
this.setState(this.state);
},
none: void 0,
}),
none: void 0,
});
} else if (op == UserOperation.AddAdmin) {

View file

@ -26,8 +26,8 @@ import {
wsUserOp,
} from "lemmy-js-client";
import { Subscription } from "rxjs";
import { InitialFetchRequest } from "shared/interfaces";
import { i18n } from "../i18next";
import { CommentViewType, InitialFetchRequest } from "../interfaces";
import { WebSocketService } from "../services";
import {
auth,
@ -594,7 +594,14 @@ export class Search extends Component<any, SearchState> {
{i.type_ == "comments" && (
<CommentNodes
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}
admins={None}
maxCommentsShown={None}
@ -631,6 +638,7 @@ export class Search extends Component<any, SearchState> {
return (
<CommentNodes
nodes={commentsToFlatNodes(comments)}
viewType={CommentViewType.Flat}
locked
noIndent
moderators={None}

View file

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

View file

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

View file

@ -4,7 +4,10 @@ import emojiShortName from "emoji-short-name";
import {
BlockCommunityResponse,
BlockPersonResponse,
Comment as CommentI,
CommentNode as CommentNodeI,
CommentReportView,
CommentSortType,
CommentView,
CommunityBlockView,
CommunityModeratorView,
@ -39,12 +42,7 @@ import tippy from "tippy.js";
import Toastify from "toastify-js";
import { httpBase } from "./env";
import { i18n, languages } from "./i18next";
import {
CommentNode as CommentNodeI,
CommentSortType,
DataType,
IsoData,
} from "./interfaces";
import { DataType, IsoData } from "./interfaces";
import { UserService, WebSocketService } from "./services";
var Tribute: any;
@ -74,6 +72,7 @@ export const postRefetchSeconds: number = 60 * 1000;
export const fetchLimit = 20;
export const trendingFetchLimit = 6;
export const mentionDropdownFetchLimit = 10;
export const commentTreeMaxDepth = 8;
export const relTags = "noopener nofollow";
@ -611,7 +610,7 @@ export function notifyComment(comment_view: CommentView, router: any) {
let info: NotifyInfo = {
name: comment_view.creator.name,
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,
};
notify(info, router);
@ -813,12 +812,14 @@ export function getRecipientIdFromProps(props: any): number {
: 1;
}
export function getIdFromProps(props: any): number {
return Number(props.match.params.id);
export function getIdFromProps(props: any): Option<number> {
let id: string = props.match.params.post_id;
return id ? Some(Number(id)) : None;
}
export function getCommentIdFromProps(props: any): number {
return Number(props.match.params.comment_id);
export function getCommentIdFromProps(props: any): Option<number> {
let id: string = props.match.params.comment_id;
return id ? Some(Number(id)) : None;
}
export function getUsernameFromProps(props: any): string {
@ -985,61 +986,12 @@ export function updateRegistrationApplicationRes(
export function commentsToFlatNodes(comments: CommentView[]): CommentNodeI[] {
let nodes: CommentNodeI[] = [];
for (let comment of comments) {
nodes.push({ comment_view: comment });
nodes.push({ comment_view: comment, children: [], depth: 0 });
}
return nodes;
}
function commentSort(tree: CommentNodeI[], sort: 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 {
export function convertCommentSortType(sort: SortType): CommentSortType {
if (
sort == SortType.TopAll ||
sort == SortType.TopDay ||
@ -1059,21 +1011,32 @@ function convertCommentSortType(sort: SortType): CommentSortType {
export function buildCommentsTree(
comments: CommentView[],
commentSortType: CommentSortType
parentComment: boolean
): CommentNodeI[] {
let map = new Map<number, CommentNodeI>();
let depthOffset = !parentComment
? 0
: getDepthFromComment(comments[0].comment);
for (let comment_view of comments) {
let node: CommentNodeI = {
comment_view: comment_view,
children: [],
depth: 0,
depth: getDepthFromComment(comment_view.comment) - depthOffset,
};
map.set(comment_view.comment.id, { ...node });
}
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) {
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({
some: parentId => {
let parent = map.get(parentId);
@ -1083,26 +1046,37 @@ export function buildCommentsTree(
}
},
none: () => {
tree.push(child);
if (!parentComment) {
tree.push(child);
}
},
});
setDepth(child);
}
commentSort(tree, commentSortType);
return tree;
}
function setDepth(node: CommentNodeI, i = 0) {
for (let child of node.children) {
child.depth = i;
setDepth(child, i + 1);
export function getCommentParentId(comment: CommentI): Option<number> {
let split = comment.path.split(".");
// remove the 0
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
let node: CommentNodeI = {
comment_view: cv,
@ -1110,7 +1084,7 @@ export function insertCommentIntoTree(tree: CommentNodeI[], cv: CommentView) {
depth: 0,
};
cv.comment.parent_id.match({
getCommentParentId(cv.comment).match({
some: parentId => {
let parentComment = searchCommentTree(tree, parentId);
parentComment.match({
@ -1122,7 +1096,9 @@ export function insertCommentIntoTree(tree: CommentNodeI[], cv: CommentView) {
});
},
none: () => {
tree.unshift(node);
if (!parentComment) {
tree.unshift(node);
}
},
});
}
@ -1149,6 +1125,7 @@ export function searchCommentTree(
export const colorList: string[] = [
hsl(0),
hsl(50),
hsl(100),
hsl(150),
hsl(200),
@ -1439,3 +1416,15 @@ export function enableDownvotes(siteRes: GetSiteResponse): boolean {
export function enableNsfw(siteRes: GetSiteResponse): boolean {
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:
invert-kv "^1.0.0"
lemmy-js-client@0.17.0-rc.38:
version "0.17.0-rc.38"
resolved "https://registry.yarnpkg.com/lemmy-js-client/-/lemmy-js-client-0.17.0-rc.38.tgz#bbe0667ad44bbd0c1e516813d3c81b65c7f6a329"
integrity sha512-uDC19s3+Eva+Hu3LhIPkT5j0Ngh7F84TA4VnfxMVJN6BQZFLZUmTvAErwJcqyj5vz3sNnw4jsEeTSGPODSXfeg==
lemmy-js-client@0.17.0-rc.39:
version "0.17.0-rc.39"
resolved "https://registry.yarnpkg.com/lemmy-js-client/-/lemmy-js-client-0.17.0-rc.39.tgz#b17c5c0d9a0f36c90c17be99845a5703091ab306"
integrity sha512-MsKavo5xOob6DgfjyhbmXyFvXwdW4iwftStJ7Bz3ArlHXy6zGBp+2uy2rU2c5ujivNDX72ol3TupTHBtSXLs4w==
levn@^0.4.1:
version "0.4.1"