summaryrefslogtreecommitdiffstats
path: root/ui/src
diff options
context:
space:
mode:
authorDessalines <tyhou13@gmx.com>2019-10-19 17:46:29 -0700
committerDessalines <tyhou13@gmx.com>2019-10-19 17:46:29 -0700
commit02dd9ac32a491e7ee41a012b11ff90666455066b (patch)
tree446a4d3acff2b3b7f17e6ce3a9555143a6f79d53 /ui/src
parent5547ecdeafe09be78c76236fbf01e73c638addf2 (diff)
Adding username mentions / tagging from comments.
- Fixes #293
Diffstat (limited to 'ui/src')
-rw-r--r--ui/src/components/comment-node.tsx30
-rw-r--r--ui/src/components/inbox.tsx168
-rw-r--r--ui/src/components/navbar.tsx50
-rw-r--r--ui/src/components/search.tsx16
-rw-r--r--ui/src/i18next.ts3
-rw-r--r--ui/src/interfaces.ts30
-rw-r--r--ui/src/services/WebSocketService.ts12
-rw-r--r--ui/src/translations/en.ts2
8 files changed, 261 insertions, 50 deletions
diff --git a/ui/src/components/comment-node.tsx b/ui/src/components/comment-node.tsx
index 7eb5d9d4..e3d82196 100644
--- a/ui/src/components/comment-node.tsx
+++ b/ui/src/components/comment-node.tsx
@@ -4,6 +4,7 @@ import {
CommentNode as CommentNodeI,
CommentLikeForm,
CommentForm as CommentFormI,
+ EditUserMentionForm,
SaveCommentForm,
BanFromCommunityForm,
BanUserForm,
@@ -686,16 +687,25 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
}
handleMarkRead(i: CommentNode) {
- let form: CommentFormI = {
- content: i.props.node.comment.content,
- edit_id: i.props.node.comment.id,
- creator_id: i.props.node.comment.creator_id,
- post_id: i.props.node.comment.post_id,
- parent_id: i.props.node.comment.parent_id,
- read: !i.props.node.comment.read,
- auth: null,
- };
- WebSocketService.Instance.editComment(form);
+ // if it has a user_mention_id field, then its a mention
+ if (i.props.node.comment.user_mention_id) {
+ let form: EditUserMentionForm = {
+ user_mention_id: i.props.node.comment.user_mention_id,
+ read: !i.props.node.comment.read,
+ };
+ WebSocketService.Instance.editUserMention(form);
+ } else {
+ let form: CommentFormI = {
+ content: i.props.node.comment.content,
+ edit_id: i.props.node.comment.id,
+ creator_id: i.props.node.comment.creator_id,
+ post_id: i.props.node.comment.post_id,
+ parent_id: i.props.node.comment.parent_id,
+ read: !i.props.node.comment.read,
+ auth: null,
+ };
+ WebSocketService.Instance.editComment(form);
+ }
}
handleModBanFromCommunityShow(i: CommentNode) {
diff --git a/ui/src/components/inbox.tsx b/ui/src/components/inbox.tsx
index 9d548f8d..6e961b17 100644
--- a/ui/src/components/inbox.tsx
+++ b/ui/src/components/inbox.tsx
@@ -8,6 +8,9 @@ import {
SortType,
GetRepliesForm,
GetRepliesResponse,
+ GetUserMentionsForm,
+ GetUserMentionsResponse,
+ UserMentionResponse,
CommentResponse,
} from '../interfaces';
import { WebSocketService, UserService } from '../services';
@@ -16,14 +19,22 @@ import { CommentNodes } from './comment-nodes';
import { i18n } from '../i18next';
import { T } from 'inferno-i18next';
-enum UnreadType {
+enum UnreadOrAll {
Unread,
All,
}
+enum UnreadType {
+ Both,
+ Replies,
+ Mentions,
+}
+
interface InboxState {
+ unreadOrAll: UnreadOrAll;
unreadType: UnreadType;
replies: Array<Comment>;
+ mentions: Array<Comment>;
sort: SortType;
page: number;
}
@@ -31,8 +42,10 @@ interface InboxState {
export class Inbox extends Component<any, InboxState> {
private subscription: Subscription;
private emptyState: InboxState = {
- unreadType: UnreadType.Unread,
+ unreadOrAll: UnreadOrAll.Unread,
+ unreadType: UnreadType.Both,
replies: [],
+ mentions: [],
sort: SortType.New,
page: 1,
};
@@ -83,8 +96,8 @@ export class Inbox extends Component<any, InboxState> {
</T>
</span>
</h5>
- {this.state.replies.length > 0 &&
- this.state.unreadType == UnreadType.Unread && (
+ {this.state.replies.length + this.state.mentions.length > 0 &&
+ this.state.unreadOrAll == UnreadOrAll.Unread && (
<ul class="list-inline mb-1 text-muted small font-weight-bold">
<li className="list-inline-item">
<span class="pointer" onClick={this.markAllAsRead}>
@@ -94,7 +107,9 @@ export class Inbox extends Component<any, InboxState> {
</ul>
)}
{this.selects()}
- {this.replies()}
+ {this.state.unreadType == UnreadType.Both && this.both()}
+ {this.state.unreadType == UnreadType.Replies && this.replies()}
+ {this.state.unreadType == UnreadType.Mentions && this.mentions()}
{this.paginator()}
</div>
</div>
@@ -106,24 +121,42 @@ export class Inbox extends Component<any, InboxState> {
return (
<div className="mb-2">
<select
- value={this.state.unreadType}
- onChange={linkEvent(this, this.handleUnreadTypeChange)}
- class="custom-select custom-select-sm w-auto"
+ value={this.state.unreadOrAll}
+ onChange={linkEvent(this, this.handleUnreadOrAllChange)}
+ class="custom-select custom-select-sm w-auto mr-2"
>
<option disabled>
<T i18nKey="type">#</T>
</option>
- <option value={UnreadType.Unread}>
+ <option value={UnreadOrAll.Unread}>
<T i18nKey="unread">#</T>
</option>
- <option value={UnreadType.All}>
+ <option value={UnreadOrAll.All}>
<T i18nKey="all">#</T>
</option>
</select>
<select
+ value={this.state.unreadType}
+ onChange={linkEvent(this, this.handleUnreadTypeChange)}
+ class="custom-select custom-select-sm w-auto mr-2"
+ >
+ <option disabled>
+ <T i18nKey="type">#</T>
+ </option>
+ <option value={UnreadType.Both}>
+ <T i18nKey="both">#</T>
+ </option>
+ <option value={UnreadType.Replies}>
+ <T i18nKey="replies">#</T>
+ </option>
+ <option value={UnreadType.Mentions}>
+ <T i18nKey="mentions">#</T>
+ </option>
+ </select>
+ <select
value={this.state.sort}
onChange={linkEvent(this, this.handleSortChange)}
- class="custom-select custom-select-sm w-auto ml-2"
+ class="custom-select custom-select-sm w-auto"
>
<option disabled>
<T i18nKey="sort_type">#</T>
@@ -151,6 +184,37 @@ export class Inbox extends Component<any, InboxState> {
);
}
+ both() {
+ let combined: Array<{
+ type_: string;
+ data: Comment;
+ }> = [];
+ let replies = this.state.replies.map(e => {
+ return { type_: 'replies', data: e };
+ });
+ let mentions = this.state.mentions.map(e => {
+ return { type_: 'mentions', data: e };
+ });
+
+ combined.push(...replies);
+ combined.push(...mentions);
+
+ // Sort it
+ if (this.state.sort == SortType.New) {
+ combined.sort((a, b) => b.data.published.localeCompare(a.data.published));
+ } else {
+ combined.sort((a, b) => b.data.score - a.data.score);
+ }
+
+ return (
+ <div>
+ {combined.map(i => (
+ <CommentNodes nodes={[{ comment: i.data }]} noIndent markable />
+ ))}
+ </div>
+ );
+ }
+
replies() {
return (
<div>
@@ -161,6 +225,16 @@ export class Inbox extends Component<any, InboxState> {
);
}
+ mentions() {
+ return (
+ <div>
+ {this.state.mentions.map(mention => (
+ <CommentNodes nodes={[{ comment: mention }]} noIndent markable />
+ ))}
+ </div>
+ );
+ }
+
paginator() {
return (
<div class="mt-2">
@@ -194,6 +268,13 @@ export class Inbox extends Component<any, InboxState> {
i.refetch();
}
+ handleUnreadOrAllChange(i: Inbox, event: any) {
+ i.state.unreadOrAll = Number(event.target.value);
+ i.state.page = 1;
+ i.setState(i.state);
+ i.refetch();
+ }
+
handleUnreadTypeChange(i: Inbox, event: any) {
i.state.unreadType = Number(event.target.value);
i.state.page = 1;
@@ -202,13 +283,21 @@ export class Inbox extends Component<any, InboxState> {
}
refetch() {
- let form: GetRepliesForm = {
+ let repliesForm: GetRepliesForm = {
sort: SortType[this.state.sort],
- unread_only: this.state.unreadType == UnreadType.Unread,
+ unread_only: this.state.unreadOrAll == UnreadOrAll.Unread,
page: this.state.page,
limit: 9999,
};
- WebSocketService.Instance.getReplies(form);
+ WebSocketService.Instance.getReplies(repliesForm);
+
+ let userMentionsForm: GetUserMentionsForm = {
+ sort: SortType[this.state.sort],
+ unread_only: this.state.unreadOrAll == UnreadOrAll.Unread,
+ page: this.state.page,
+ limit: 9999,
+ };
+ WebSocketService.Instance.getUserMentions(userMentionsForm);
}
handleSortChange(i: Inbox, event: any) {
@@ -228,13 +317,21 @@ export class Inbox extends Component<any, InboxState> {
if (msg.error) {
alert(i18n.t(msg.error));
return;
- } else if (
- op == UserOperation.GetReplies ||
- op == UserOperation.MarkAllAsRead
- ) {
+ } else if (op == UserOperation.GetReplies) {
let res: GetRepliesResponse = msg;
this.state.replies = res.replies;
- this.sendRepliesCount();
+ this.sendUnreadCount();
+ window.scrollTo(0, 0);
+ this.setState(this.state);
+ } else if (op == UserOperation.GetUserMentions) {
+ let res: GetUserMentionsResponse = msg;
+ this.state.mentions = res.mentions;
+ this.sendUnreadCount();
+ window.scrollTo(0, 0);
+ this.setState(this.state);
+ } else if (op == UserOperation.MarkAllAsRead) {
+ this.state.replies = [];
+ this.state.mentions = [];
window.scrollTo(0, 0);
this.setState(this.state);
} else if (op == UserOperation.EditComment) {
@@ -250,7 +347,7 @@ export class Inbox extends Component<any, InboxState> {
found.score = res.comment.score;
// If youre in the unread view, just remove it from the list
- if (this.state.unreadType == UnreadType.Unread && res.comment.read) {
+ if (this.state.unreadOrAll == UnreadOrAll.Unread && res.comment.read) {
this.state.replies = this.state.replies.filter(
r => r.id !== res.comment.id
);
@@ -258,8 +355,30 @@ export class Inbox extends Component<any, InboxState> {
let found = this.state.replies.find(c => c.id == res.comment.id);
found.read = res.comment.read;
}
- this.sendRepliesCount();
+ this.sendUnreadCount();
+ this.setState(this.state);
+ } else if (op == UserOperation.EditUserMention) {
+ let res: UserMentionResponse = msg;
+ let found = this.state.mentions.find(c => c.id == res.mention.id);
+ found.content = res.mention.content;
+ found.updated = res.mention.updated;
+ found.removed = res.mention.removed;
+ found.deleted = res.mention.deleted;
+ found.upvotes = res.mention.upvotes;
+ found.downvotes = res.mention.downvotes;
+ found.score = res.mention.score;
+
+ // If youre in the unread view, just remove it from the list
+ if (this.state.unreadOrAll == UnreadOrAll.Unread && res.mention.read) {
+ this.state.mentions = this.state.mentions.filter(
+ r => r.id !== res.mention.id
+ );
+ } else {
+ let found = this.state.mentions.find(c => c.id == res.mention.id);
+ found.read = res.mention.read;
+ }
+ this.sendUnreadCount();
this.setState(this.state);
} else if (op == UserOperation.CreateComment) {
// let res: CommentResponse = msg;
@@ -284,10 +403,13 @@ export class Inbox extends Component<any, InboxState> {
}
}
- sendRepliesCount() {
+ sendUnreadCount() {
+ let count =
+ this.state.replies.filter(r => !r.read).length +
+ this.state.mentions.filter(r => !r.read).length;
UserService.Instance.sub.next({
user: UserService.Instance.user,
- unreadCount: this.state.replies.filter(r => !r.read).length,
+ unreadCount: count,
});
}
}
diff --git a/ui/src/components/navbar.tsx b/ui/src/components/navbar.tsx
index ba0dead2..151559df 100644
--- a/ui/src/components/navbar.tsx
+++ b/ui/src/components/navbar.tsx
@@ -7,6 +7,8 @@ import {
UserOperation,
GetRepliesForm,
GetRepliesResponse,
+ GetUserMentionsForm,
+ GetUserMentionsResponse,
SortType,
GetSiteResponse,
Comment,
@@ -21,6 +23,7 @@ interface NavbarState {
expanded: boolean;
expandUserDropdown: boolean;
replies: Array<Comment>;
+ mentions: Array<Comment>;
fetchCount: number;
unreadCount: number;
siteName: string;
@@ -34,6 +37,7 @@ export class Navbar extends Component<any, NavbarState> {
unreadCount: 0,
fetchCount: 0,
replies: [],
+ mentions: [],
expanded: false,
expandUserDropdown: false,
siteName: undefined,
@@ -44,7 +48,7 @@ export class Navbar extends Component<any, NavbarState> {
this.state = this.emptyState;
this.handleOverviewClick = this.handleOverviewClick.bind(this);
- this.keepFetchingReplies();
+ this.keepFetchingUnreads();
// Subscribe to user changes
this.userSub = UserService.Instance.sub.subscribe(user => {
@@ -233,7 +237,22 @@ export class Navbar extends Component<any, NavbarState> {
}
this.state.replies = unreadReplies;
- this.sendRepliesCount(res);
+ this.setState(this.state);
+ this.sendUnreadCount();
+ } else if (op == UserOperation.GetUserMentions) {
+ let res: GetUserMentionsResponse = msg;
+ let unreadMentions = res.mentions.filter(r => !r.read);
+ if (
+ unreadMentions.length > 0 &&
+ this.state.fetchCount > 1 &&
+ JSON.stringify(this.state.mentions) !== JSON.stringify(unreadMentions)
+ ) {
+ this.notify(unreadMentions);
+ }
+
+ this.state.mentions = unreadMentions;
+ this.setState(this.state);
+ this.sendUnreadCount();
} else if (op == UserOperation.GetSite) {
let res: GetSiteResponse = msg;
@@ -245,12 +264,12 @@ export class Navbar extends Component<any, NavbarState> {
}
}
- keepFetchingReplies() {
- this.fetchReplies();
- setInterval(() => this.fetchReplies(), 15000);
+ keepFetchingUnreads() {
+ this.fetchUnreads();
+ setInterval(() => this.fetchUnreads(), 15000);
}
- fetchReplies() {
+ fetchUnreads() {
if (this.state.isLoggedIn) {
let repliesForm: GetRepliesForm = {
sort: SortType[SortType.New],
@@ -258,8 +277,16 @@ export class Navbar extends Component<any, NavbarState> {
page: 1,
limit: 9999,
};
+
+ let userMentionsForm: GetUserMentionsForm = {
+ sort: SortType[SortType.New],
+ unread_only: true,
+ page: 1,
+ limit: 9999,
+ };
if (this.currentLocation !== '/inbox') {
WebSocketService.Instance.getReplies(repliesForm);
+ WebSocketService.Instance.getUserMentions(userMentionsForm);
this.state.fetchCount++;
}
}
@@ -269,13 +296,20 @@ export class Navbar extends Component<any, NavbarState> {
return this.context.router.history.location.pathname;
}
- sendRepliesCount(res: GetRepliesResponse) {
+ sendUnreadCount() {
UserService.Instance.sub.next({
user: UserService.Instance.user,
- unreadCount: res.replies.filter(r => !r.read).length,
+ unreadCount: this.unreadCount,
});
}
+ get unreadCount() {
+ return (
+ this.state.replies.filter(r => !r.read).length +
+ this.state.mentions.filter(r => !r.read).length
+ );
+ }
+
requestNotificationPermission() {
if (UserService.Instance.user) {
document.addEventListener('DOMContentLoaded', function() {
diff --git a/ui/src/components/search.tsx b/ui/src/components/search.tsx
index daf9aa2d..68b4ee88 100644
--- a/ui/src/components/search.tsx
+++ b/ui/src/components/search.tsx
@@ -396,11 +396,16 @@ export class Search extends Component<any, SearchState> {
let res = this.state.searchResponse;
return (
<div>
- {res && res.op && res.posts.length == 0 && res.comments.length == 0 && (
- <span>
- <T i18nKey="no_results">#</T>
- </span>
- )}
+ {res &&
+ res.op &&
+ res.posts.length == 0 &&
+ res.comments.length == 0 &&
+ res.communities.length == 0 &&
+ res.users.length == 0 && (
+ <span>
+ <T i18nKey="no_results">#</T>
+ </span>
+ )}
</div>
);
}
@@ -420,7 +425,6 @@ export class Search extends Component<any, SearchState> {
}
search() {
- // TODO community
let form: SearchForm = {
q: this.state.q,
type_: SearchType[this.state.type_],
diff --git a/ui/src/i18next.ts b/ui/src/i18next.ts
index 083cc7f6..bfe720ff 100644
--- a/ui/src/i18next.ts
+++ b/ui/src/i18next.ts
@@ -11,7 +11,6 @@ import { zh } from './translations/zh';
import { nl } from './translations/nl';
// https://github.com/nimbusec-oss/inferno-i18next/blob/master/tests/T.test.js#L66
-// TODO don't forget to add moment locales for new languages.
const resources = {
en,
eo,
@@ -30,7 +29,7 @@ function format(value: any, format: any, lng: any) {
}
i18n.init({
- debug: true,
+ debug: false,
// load: 'languageOnly',
// initImmediate: false,
diff --git a/ui/src/interfaces.ts b/ui/src/interfaces.ts
index fa14b238..4056e05d 100644
--- a/ui/src/interfaces.ts
+++ b/ui/src/interfaces.ts
@@ -20,6 +20,8 @@ export enum UserOperation {
GetFollowedCommunities,
GetUserDetails,
GetReplies,
+ GetUserMentions,
+ EditUserMention,
GetModlog,
BanFromCommunity,
AddModToCommunity,
@@ -171,6 +173,8 @@ export interface Comment {
user_id?: number;
my_vote?: number;
saved?: boolean;
+ user_mention_id?: number; // For mention type
+ recipient_id?: number;
}
export interface Category {
@@ -229,7 +233,7 @@ export interface UserDetailsResponse {
}
export interface GetRepliesForm {
- sort: string; // TODO figure this one out
+ sort: string;
page?: number;
limit?: number;
unread_only: boolean;
@@ -241,6 +245,30 @@ export interface GetRepliesResponse {
replies: Array<Comment>;
}
+export interface GetUserMentionsForm {
+ sort: string;
+ page?: number;
+ limit?: number;
+ unread_only: boolean;
+ auth?: string;
+}
+
+export interface GetUserMentionsResponse {
+ op: string;
+ mentions: Array<Comment>;
+}
+
+export interface EditUserMentionForm {
+ user_mention_id: number;
+ read?: boolean;
+ auth?: string;
+}
+
+export interface UserMentionResponse {
+ op: string;
+ mention: Comment;
+}
+
export interface BanFromCommunityForm {
community_id: number;
user_id: number;
diff --git a/ui/src/services/WebSocketService.ts b/ui/src/services/WebSocketService.ts
index f5d5b513..ac465b22 100644
--- a/ui/src/services/WebSocketService.ts
+++ b/ui/src/services/WebSocketService.ts
@@ -25,6 +25,8 @@ import {
Site,
UserView,
GetRepliesForm,
+ GetUserMentionsForm,
+ EditUserMentionForm,
SearchForm,
UserSettingsForm,
DeleteAccountForm,
@@ -222,6 +224,16 @@ export class WebSocketService {
this.subject.next(this.wsSendWrapper(UserOperation.GetReplies, form));
}
+ public getUserMentions(form: GetUserMentionsForm) {
+ this.setAuth(form);
+ this.subject.next(this.wsSendWrapper(UserOperation.GetUserMentions, form));
+ }
+
+ public editUserMention(form: EditUserMentionForm) {
+ this.setAuth(form);
+ this.subject.next(this.wsSendWrapper(UserOperation.EditUserMention, form));
+ }
+
public getModlog(form: GetModlogForm) {
this.subject.next(this.wsSendWrapper(UserOperation.GetModlog, form));
}
diff --git a/ui/src/translations/en.ts b/ui/src/translations/en.ts
index 551c7ef8..a971ae6e 100644
--- a/ui/src/translations/en.ts
+++ b/ui/src/translations/en.ts
@@ -101,6 +101,8 @@ export const en = {
mark_all_as_read: 'mark all as read',
type: 'Type',
unread: 'Unread',
+ replies: 'Replies',
+ mentions: 'Mentions',
reply_sent: 'Reply sent',
search: 'Search',
overview: 'Overview',