summaryrefslogtreecommitdiffstats
path: root/ui
diff options
context:
space:
mode:
authorDessalines <tyhou13@gmx.com>2020-01-22 16:35:29 -0500
committerDessalines <tyhou13@gmx.com>2020-01-22 16:38:16 -0500
commit253bc3e0afb6adf64b79f334a8bc1f972aa45eba (patch)
treebdbbb36ee59ea4331c7bf7cf89e8ef554eb2d63e /ui
parenta964b4ce21cc19eb42ae4da1a1aef8bfc0a1df5c (diff)
Adding private messaging, and matrix user ids.
- Fixes #244
Diffstat (limited to 'ui')
-rw-r--r--ui/src/components/create-private-message.tsx52
-rw-r--r--ui/src/components/inbox.tsx117
-rw-r--r--ui/src/components/navbar.tsx41
-rw-r--r--ui/src/components/private-message-form.tsx291
-rw-r--r--ui/src/components/private-message.tsx249
-rw-r--r--ui/src/components/user.tsx51
-rw-r--r--ui/src/index.tsx5
-rw-r--r--ui/src/interfaces.ts55
-rw-r--r--ui/src/services/WebSocketService.ts26
-rw-r--r--ui/src/translations/en.ts13
-rw-r--r--ui/src/utils.ts5
11 files changed, 870 insertions, 35 deletions
diff --git a/ui/src/components/create-private-message.tsx b/ui/src/components/create-private-message.tsx
new file mode 100644
index 00000000..f74d5e9f
--- /dev/null
+++ b/ui/src/components/create-private-message.tsx
@@ -0,0 +1,52 @@
+import { Component } from 'inferno';
+import { PrivateMessageForm } from './private-message-form';
+import { WebSocketService } from '../services';
+import { PrivateMessageFormParams } from '../interfaces';
+import { i18n } from '../i18next';
+
+export class CreatePrivateMessage extends Component<any, any> {
+ constructor(props: any, context: any) {
+ super(props, context);
+ this.handlePrivateMessageCreate = this.handlePrivateMessageCreate.bind(
+ this
+ );
+ }
+
+ componentDidMount() {
+ document.title = `${i18n.t('create_private_message')} - ${
+ WebSocketService.Instance.site.name
+ }`;
+ }
+
+ render() {
+ return (
+ <div class="container">
+ <div class="row">
+ <div class="col-12 col-lg-6 offset-lg-3 mb-4">
+ <h5>{i18n.t('create_private_message')}</h5>
+ <PrivateMessageForm
+ onCreate={this.handlePrivateMessageCreate}
+ params={this.params}
+ />
+ </div>
+ </div>
+ </div>
+ );
+ }
+
+ get params(): PrivateMessageFormParams {
+ let urlParams = new URLSearchParams(this.props.location.search);
+ let params: PrivateMessageFormParams = {
+ recipient_id: Number(urlParams.get('recipient_id')),
+ };
+
+ return params;
+ }
+
+ handlePrivateMessageCreate() {
+ alert(i18n.t('message_sent'));
+
+ // Navigate to the front
+ this.props.history.push(`/`);
+ }
+}
diff --git a/ui/src/components/inbox.tsx b/ui/src/components/inbox.tsx
index a302b834..6a426bcc 100644
--- a/ui/src/components/inbox.tsx
+++ b/ui/src/components/inbox.tsx
@@ -12,10 +12,15 @@ import {
GetUserMentionsResponse,
UserMentionResponse,
CommentResponse,
+ PrivateMessage as PrivateMessageI,
+ GetPrivateMessagesForm,
+ PrivateMessagesResponse,
+ PrivateMessageResponse,
} from '../interfaces';
import { WebSocketService, UserService } from '../services';
-import { msgOp, fetchLimit } from '../utils';
+import { msgOp, fetchLimit, isCommentType } from '../utils';
import { CommentNodes } from './comment-nodes';
+import { PrivateMessage } from './private-message';
import { SortSelect } from './sort-select';
import { i18n } from '../i18next';
import { T } from 'inferno-i18next';
@@ -26,9 +31,10 @@ enum UnreadOrAll {
}
enum UnreadType {
- Both,
+ All,
Replies,
Mentions,
+ Messages,
}
interface InboxState {
@@ -36,6 +42,7 @@ interface InboxState {
unreadType: UnreadType;
replies: Array<Comment>;
mentions: Array<Comment>;
+ messages: Array<PrivateMessageI>;
sort: SortType;
page: number;
}
@@ -44,9 +51,10 @@ export class Inbox extends Component<any, InboxState> {
private subscription: Subscription;
private emptyState: InboxState = {
unreadOrAll: UnreadOrAll.Unread,
- unreadType: UnreadType.Both,
+ unreadType: UnreadType.All,
replies: [],
mentions: [],
+ messages: [],
sort: SortType.New,
page: 1,
};
@@ -103,7 +111,10 @@ export class Inbox extends Component<any, InboxState> {
</a>
</small>
</h5>
- {this.state.replies.length + this.state.mentions.length > 0 &&
+ {this.state.replies.length +
+ this.state.mentions.length +
+ this.state.messages.length >
+ 0 &&
this.state.unreadOrAll == UnreadOrAll.Unread && (
<ul class="list-inline mb-1 text-muted small font-weight-bold">
<li className="list-inline-item">
@@ -114,9 +125,10 @@ export class Inbox extends Component<any, InboxState> {
</ul>
)}
{this.selects()}
- {this.state.unreadType == UnreadType.Both && this.both()}
+ {this.state.unreadType == UnreadType.All && this.all()}
{this.state.unreadType == UnreadType.Replies && this.replies()}
{this.state.unreadType == UnreadType.Mentions && this.mentions()}
+ {this.state.unreadType == UnreadType.Messages && this.messages()}
{this.paginator()}
</div>
</div>
@@ -150,8 +162,8 @@ export class Inbox extends Component<any, InboxState> {
<option disabled>
<T i18nKey="type">#</T>
</option>
- <option value={UnreadType.Both}>
- <T i18nKey="both">#</T>
+ <option value={UnreadType.All}>
+ <T i18nKey="all">#</T>
</option>
<option value={UnreadType.Replies}>
<T i18nKey="replies">#</T>
@@ -159,6 +171,9 @@ export class Inbox extends Component<any, InboxState> {
<option value={UnreadType.Mentions}>
<T i18nKey="mentions">#</T>
</option>
+ <option value={UnreadType.Messages}>
+ <T i18nKey="messages">#</T>
+ </option>
</select>
<SortSelect
sort={this.state.sort}
@@ -169,33 +184,29 @@ 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 };
- });
+ all() {
+ let combined: Array<Comment | PrivateMessageI> = [];
- combined.push(...replies);
- combined.push(...mentions);
+ combined.push(...this.state.replies);
+ combined.push(...this.state.mentions);
+ combined.push(...this.state.messages);
// 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);
- }
+ combined.sort((a, b) => b.published.localeCompare(a.published));
return (
<div>
- {combined.map(i => (
- <CommentNodes nodes={[{ comment: i.data }]} noIndent markable />
- ))}
+ {combined.map(i =>
+ isCommentType(i) ? (
+ <CommentNodes
+ nodes={[{ comment: i }]}
+ noIndent
+ markable
+ />
+ ) : (
+ <PrivateMessage privateMessage={i} />
+ )
+ )}
</div>
);
}
@@ -220,6 +231,16 @@ export class Inbox extends Component<any, InboxState> {
);
}
+ messages() {
+ return (
+ <div>
+ {this.state.messages.map(message => (
+ <PrivateMessage privateMessage={message} />
+ ))}
+ </div>
+ );
+ }
+
paginator() {
return (
<div class="mt-2">
@@ -283,6 +304,13 @@ export class Inbox extends Component<any, InboxState> {
limit: fetchLimit,
};
WebSocketService.Instance.getUserMentions(userMentionsForm);
+
+ let privateMessagesForm: GetPrivateMessagesForm = {
+ unread_only: this.state.unreadOrAll == UnreadOrAll.Unread,
+ page: this.state.page,
+ limit: fetchLimit,
+ };
+ WebSocketService.Instance.getPrivateMessages(privateMessagesForm);
}
handleSortChange(val: SortType) {
@@ -314,9 +342,37 @@ export class Inbox extends Component<any, InboxState> {
this.sendUnreadCount();
window.scrollTo(0, 0);
this.setState(this.state);
+ } else if (op == UserOperation.GetPrivateMessages) {
+ let res: PrivateMessagesResponse = msg;
+ this.state.messages = res.messages;
+ this.sendUnreadCount();
+ window.scrollTo(0, 0);
+ this.setState(this.state);
+ } else if (op == UserOperation.EditPrivateMessage) {
+ let res: PrivateMessageResponse = msg;
+ let found: PrivateMessageI = this.state.messages.find(
+ m => m.id === res.message.id
+ );
+ found.content = res.message.content;
+ found.updated = res.message.updated;
+ found.deleted = res.message.deleted;
+ // If youre in the unread view, just remove it from the list
+ if (this.state.unreadOrAll == UnreadOrAll.Unread && res.message.read) {
+ this.state.messages = this.state.messages.filter(
+ r => r.id !== res.message.id
+ );
+ } else {
+ let found = this.state.messages.find(c => c.id == res.message.id);
+ found.read = res.message.read;
+ }
+ this.sendUnreadCount();
+ window.scrollTo(0, 0);
+ this.setState(this.state);
} else if (op == UserOperation.MarkAllAsRead) {
this.state.replies = [];
this.state.mentions = [];
+ this.state.messages = [];
+ this.sendUnreadCount();
window.scrollTo(0, 0);
this.setState(this.state);
} else if (op == UserOperation.EditComment) {
@@ -391,7 +447,10 @@ export class Inbox extends Component<any, InboxState> {
sendUnreadCount() {
let count =
this.state.replies.filter(r => !r.read).length +
- this.state.mentions.filter(r => !r.read).length;
+ this.state.mentions.filter(r => !r.read).length +
+ this.state.messages.filter(
+ r => !r.read && r.creator_id !== UserService.Instance.user.id
+ ).length;
UserService.Instance.sub.next({
user: UserService.Instance.user,
unreadCount: count,
diff --git a/ui/src/components/navbar.tsx b/ui/src/components/navbar.tsx
index b1dcf096..85a54987 100644
--- a/ui/src/components/navbar.tsx
+++ b/ui/src/components/navbar.tsx
@@ -9,15 +9,19 @@ import {
GetRepliesResponse,
GetUserMentionsForm,
GetUserMentionsResponse,
+ GetPrivateMessagesForm,
+ PrivateMessagesResponse,
SortType,
GetSiteResponse,
Comment,
+ PrivateMessage,
} from '../interfaces';
import {
msgOp,
pictshareAvatarThumbnail,
showAvatars,
fetchLimit,
+ isCommentType,
} from '../utils';
import { version } from '../version';
import { i18n } from '../i18next';
@@ -28,6 +32,7 @@ interface NavbarState {
expanded: boolean;
replies: Array<Comment>;
mentions: Array<Comment>;
+ messages: Array<PrivateMessage>;
fetchCount: number;
unreadCount: number;
siteName: string;
@@ -42,6 +47,7 @@ export class Navbar extends Component<any, NavbarState> {
fetchCount: 0,
replies: [],
mentions: [],
+ messages: [],
expanded: false,
siteName: undefined,
};
@@ -228,6 +234,20 @@ export class Navbar extends Component<any, NavbarState> {
this.state.mentions = unreadMentions;
this.setState(this.state);
this.sendUnreadCount();
+ } else if (op == UserOperation.GetPrivateMessages) {
+ let res: PrivateMessagesResponse = msg;
+ let unreadMessages = res.messages.filter(r => !r.read);
+ if (
+ unreadMessages.length > 0 &&
+ this.state.fetchCount > 1 &&
+ JSON.stringify(this.state.messages) !== JSON.stringify(unreadMessages)
+ ) {
+ this.notify(unreadMessages);
+ }
+
+ this.state.messages = unreadMessages;
+ this.setState(this.state);
+ this.sendUnreadCount();
} else if (op == UserOperation.GetSite) {
let res: GetSiteResponse = msg;
@@ -259,9 +279,17 @@ export class Navbar extends Component<any, NavbarState> {
page: 1,
limit: fetchLimit,
};
+
+ let privateMessagesForm: GetPrivateMessagesForm = {
+ unread_only: true,
+ page: 1,
+ limit: fetchLimit,
+ };
+
if (this.currentLocation !== '/inbox') {
WebSocketService.Instance.getReplies(repliesForm);
WebSocketService.Instance.getUserMentions(userMentionsForm);
+ WebSocketService.Instance.getPrivateMessages(privateMessagesForm);
this.state.fetchCount++;
}
}
@@ -281,7 +309,8 @@ export class Navbar extends Component<any, NavbarState> {
get unreadCount() {
return (
this.state.replies.filter(r => !r.read).length +
- this.state.mentions.filter(r => !r.read).length
+ this.state.mentions.filter(r => !r.read).length +
+ this.state.messages.filter(r => !r.read).length
);
}
@@ -299,21 +328,25 @@ export class Navbar extends Component<any, NavbarState> {
}
}
- notify(replies: Array<Comment>) {
+ notify(replies: Array<Comment | PrivateMessage>) {
let recentReply = replies[0];
if (Notification.permission !== 'granted') Notification.requestPermission();
else {
var notification = new Notification(
`${replies.length} ${i18n.t('unread_messages')}`,
{
- icon: `${window.location.protocol}//${window.location.host}/static/assets/apple-touch-icon.png`,
+ icon: recentReply.creator_avatar
+ ? recentReply.creator_avatar
+ : `${window.location.protocol}//${window.location.host}/static/assets/apple-touch-icon.png`,
body: `${recentReply.creator_name}: ${recentReply.content}`,
}
);
notification.onclick = () => {
this.context.router.history.push(
- `/post/${recentReply.post_id}/comment/${recentReply.id}`
+ isCommentType(recentReply)
+ ? `/post/${recentReply.post_id}/comment/${recentReply.id}`
+ : `/inbox`
);
};
}
diff --git a/ui/src/components/private-message-form.tsx b/ui/src/components/private-message-form.tsx
new file mode 100644
index 00000000..c628bf71
--- /dev/null
+++ b/ui/src/components/private-message-form.tsx
@@ -0,0 +1,291 @@
+import { Component, linkEvent } from 'inferno';
+import { Link } from 'inferno-router';
+import { Subscription } from 'rxjs';
+import { retryWhen, delay, take } from 'rxjs/operators';
+import {
+ PrivateMessageForm as PrivateMessageFormI,
+ EditPrivateMessageForm,
+ PrivateMessageFormParams,
+ PrivateMessage,
+ PrivateMessageResponse,
+ UserView,
+ UserOperation,
+ UserDetailsResponse,
+ GetUserDetailsForm,
+ SortType,
+} from '../interfaces';
+import { WebSocketService } from '../services';
+import {
+ msgOp,
+ capitalizeFirstLetter,
+ markdownHelpUrl,
+ mdToHtml,
+ showAvatars,
+ pictshareAvatarThumbnail,
+} from '../utils';
+import autosize from 'autosize';
+import { i18n } from '../i18next';
+import { T } from 'inferno-i18next';
+
+interface PrivateMessageFormProps {
+ privateMessage?: PrivateMessage; // If a pm is given, that means this is an edit
+ params?: PrivateMessageFormParams;
+ onCancel?(): any;
+ onCreate?(message: PrivateMessage): any;
+ onEdit?(message: PrivateMessage): any;
+}
+
+interface PrivateMessageFormState {
+ privateMessageForm: PrivateMessageFormI;
+ recipient: UserView;
+ loading: boolean;
+ previewMode: boolean;
+ showDisclaimer: boolean;
+}
+
+export class PrivateMessageForm extends Component<
+ PrivateMessageFormProps,
+ PrivateMessageFormState
+> {
+ private subscription: Subscription;
+ private emptyState: PrivateMessageFormState = {
+ privateMessageForm: {
+ content: null,
+ recipient_id: null,
+ },
+ recipient: null,
+ loading: false,
+ previewMode: false,
+ showDisclaimer: false,
+ };
+
+ constructor(props: any, context: any) {
+ super(props, context);
+
+ this.state = this.emptyState;
+
+ if (this.props.privateMessage) {
+ this.state.privateMessageForm = {
+ content: this.props.privateMessage.content,
+ recipient_id: this.props.privateMessage.recipient_id,
+ };
+ }
+
+ if (this.props.params) {
+ this.state.privateMessageForm.recipient_id = this.props.params.recipient_id;
+ let form: GetUserDetailsForm = {
+ user_id: this.state.privateMessageForm.recipient_id,
+ sort: SortType[SortType.New],
+ saved_only: false,
+ };
+ WebSocketService.Instance.getUserDetails(form);
+ }
+
+ this.subscription = WebSocketService.Instance.subject
+ .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
+ .subscribe(
+ msg => this.parseMessage(msg),
+ err => console.error(err),
+ () => console.log('complete')
+ );
+ }
+
+ componentDidMount() {
+ autosize(document.querySelectorAll('textarea'));
+ }
+
+ componentWillUnmount() {
+ this.subscription.unsubscribe();
+ }
+
+ render() {
+ return (
+ <div>
+ <form onSubmit={linkEvent(this, this.handlePrivateMessageSubmit)}>
+ {!this.props.privateMessage && (
+ <div class="form-group row">
+ <label class="col-sm-2 col-form-label">
+ {capitalizeFirstLetter(i18n.t('to'))}
+ </label>
+
+ {this.state.recipient && (
+ <div class="col-sm-10 form-control-plaintext">
+ <Link
+ className="text-info"
+ to={`/u/${this.state.recipient.name}`}
+ >
+ {this.state.recipient.avatar && showAvatars() && (
+ <img
+ height="32"
+ width="32"
+ src={pictshareAvatarThumbnail(
+ this.state.recipient.avatar
+ )}
+ class="rounded-circle mr-1"
+ />
+ )}
+ <span>{this.state.recipient.name}</span>
+ </Link>
+ </div>
+ )}
+ </div>
+ )}
+ <div class="form-group row">
+ <label class="col-sm-2 col-form-label">{i18n.t('message')}</label>
+ <div class="col-sm-10">
+ <textarea
+ value={this.state.privateMessageForm.content}
+ onInput={linkEvent(this, this.handleContentChange)}
+ className={`form-control ${this.state.previewMode && 'd-none'}`}
+ rows={4}
+ maxLength={10000}
+ />
+ {this.state.previewMode && (
+ <div
+ className="md-div"
+ dangerouslySetInnerHTML={mdToHtml(
+ this.state.privateMessageForm.content
+ )}
+ />
+ )}
+
+ {this.state.privateMessageForm.content && (
+ <button
+ className={`mt-1 mr-2 btn btn-sm btn-secondary ${this.state
+ .previewMode && 'active'}`}
+ onClick={linkEvent(this, this.handlePreviewToggle)}
+ >
+ {i18n.t('preview')}
+ </button>
+ )}
+ <ul class="float-right list-inline mb-1 text-muted small font-weight-bold">
+ <li class="list-inline-item">
+ <span
+ onClick={linkEvent(this, this.handleShowDisclaimer)}
+ class="pointer"
+ >
+ {i18n.t('disclaimer')}
+ </span>
+ </li>
+ <li class="list-inline-item">
+ <a href={markdownHelpUrl} target="_blank" class="text-muted">
+ {i18n.t('formatting_help')}
+ </a>
+ </li>
+ </ul>
+ </div>
+ </div>
+
+ {this.state.showDisclaimer && (
+ <div class="form-group row">
+ <div class="col-sm-10">
+ <div class="alert alert-danger" role="alert">
+ <T i18nKey="private_message_disclaimer">
+ #
+ <a
+ class="alert-link"
+ target="_blank"
+ href="https://about.riot.im/"
+ >
+ #
+ </a>
+ </T>
+ </div>
+ </div>
+ </div>
+ )}
+ <div class="form-group row">
+ <div class="col-sm-10">
+ <button type="submit" class="btn btn-secondary mr-2">
+ {this.state.loading ? (
+ <svg class="icon icon-spinner spin">
+ <use xlinkHref="#icon-spinner"></use>
+ </svg>
+ ) : this.props.privateMessage ? (
+ capitalizeFirstLetter(i18n.t('save'))
+ ) : (
+ capitalizeFirstLetter(i18n.t('send_message'))
+ )}
+ </button>
+ {this.props.privateMessage && (
+ <button
+ type="button"
+ class="btn btn-secondary"
+ onClick={linkEvent(this, this.handleCancel)}
+ >
+ {i18n.t('cancel')}
+ </button>
+ )}
+ </div>
+ </div>
+ </form>
+ </div>
+ );
+ }
+
+ handlePrivateMessageSubmit(i: PrivateMessageForm, event: any) {
+ event.preventDefault();
+ if (i.props.privateMessage) {
+ let editForm: EditPrivateMessageForm = {
+ edit_id: i.props.privateMessage.id,
+ content: i.state.privateMessageForm.content,
+ };
+ WebSocketService.Instance.editPrivateMessage(editForm);
+ } else {
+ WebSocketService.Instance.createPrivateMessage(
+ i.state.privateMessageForm
+ );
+ }
+ i.state.loading = true;
+ i.setState(i.state);
+ }
+
+ handleRecipientChange(i: PrivateMessageForm, event: any) {
+ i.state.recipient = event.target.value;
+ i.setState(i.state);
+ }
+
+ handleContentChange(i: PrivateMessageForm, event: any) {
+ i.state.privateMessageForm.content = event.target.value;
+ i.setState(i.state);
+ }
+
+ handleCancel(i: PrivateMessageForm) {
+ i.props.onCancel();
+ }
+
+ handlePreviewToggle(i: PrivateMessageForm, event: any) {
+ event.preventDefault();
+ i.state.previewMode = !i.state.previewMode;
+ i.setState(i.state);
+ }
+
+ handleShowDisclaimer(i: PrivateMessageForm) {
+ i.state.showDisclaimer = !i.state.showDisclaimer;
+ i.setState(i.state);
+ }
+
+ parseMessage(msg: any) {
+ let op: UserOperation = msgOp(msg);
+ if (msg.error) {
+ alert(i18n.t(msg.error));
+ this.state.loading = false;
+ this.setState(this.state);
+ return;
+ } else if (op == UserOperation.EditPrivateMessage) {
+ this.state.loading = false;
+ let res: PrivateMessageResponse = msg;
+ this.props.onEdit(res.message);
+ } else if (op == UserOperation.GetUserDetails) {
+ let res: UserDetailsResponse = msg;
+ this.state.recipient = res.user;
+ this.state.privateMessageForm.recipient_id = res.user.id;
+ this.setState(this.state);
+ } else if (op == UserOperation.CreatePrivateMessage) {
+ this.state.loading = false;
+ let res: PrivateMessageResponse = msg;
+ this.props.onCreate(res.message);
+ this.setState(this.state);
+ }
+ }
+}
diff --git a/ui/src/components/private-message.tsx b/ui/src/components/private-message.tsx
new file mode 100644
index 00000000..524b1a9d
--- /dev/null
+++ b/ui/src/components/private-message.tsx
@@ -0,0 +1,249 @@
+import { Component, linkEvent } from 'inferno';
+import { Link } from 'inferno-router';
+import {
+ PrivateMessage as PrivateMessageI,
+ EditPrivateMessageForm,
+} from '../interfaces';
+import { WebSocketService, UserService } from '../services';
+import { mdToHtml, pictshareAvatarThumbnail, showAvatars } from '../utils';
+import { MomentTime } from './moment-time';
+import { PrivateMessageForm } from './private-message-form';
+import { i18n } from '../i18next';
+import { T } from 'inferno-i18next';
+
+interface PrivateMessageState {
+ showReply: boolean;
+ showEdit: boolean;
+ collapsed: boolean;
+ viewSource: boolean;
+}
+
+interface PrivateMessageProps {
+ privateMessage: PrivateMessageI;
+}
+
+export class PrivateMessage extends Component<
+ PrivateMessageProps,
+ PrivateMessageState
+> {
+ private emptyState: PrivateMessageState = {
+ showReply: false,
+ showEdit: false,
+ collapsed: false,
+ viewSource: false,
+ };
+
+ constructor(props: any, context: any) {
+ super(props, context);
+
+ this.state = this.emptyState;
+ this.handleReplyCancel = this.handleReplyCancel.bind(this);
+ this.handlePrivateMessageCreate = this.handlePrivateMessageCreate.bind(
+ this
+ );
+ this.handlePrivateMessageEdit = this.handlePrivateMessageEdit.bind(this);
+ }
+
+ get mine(): boolean {
+ return UserService.Instance.user.id == this.props.privateMessage.creator_id;
+ }
+
+ render() {
+ let message = this.props.privateMessage;
+ return (
+ <div class="mb-2">
+ <div>
+ <ul class="list-inline mb-0 text-muted small">
+ <li className="list-inline-item">
+ {this.mine ? i18n.t('to') : i18n.t('from')}
+ </li>
+ <li className="list-inline-item">
+ <Link
+ className="text-info"
+ to={
+ this.mine
+ ? `/u/${message.recipient_name}`
+ : `/u/${message.creator_name}`
+ }
+ >
+ {(this.mine
+ ? message.recipient_avatar
+ : message.creator_avatar) &&
+ showAvatars() && (
+ <img
+ height="32"
+ width="32"
+ src={pictshareAvatarThumbnail(
+ this.mine
+ ? message.recipient_avatar
+ : message.creator_avatar
+ )}
+ class="rounded-circle mr-1"
+ />
+ )}
+ <span>
+ {this.mine ? message.recipient_name : message.creator_name}
+ </span>
+ </Link>
+ </li>
+ <li className="list-inline-item">
+ <span>
+ <MomentTime data={message} />
+ </span>
+ </li>
+ <li className="list-inline-item">
+ <div
+ className="pointer text-monospace"
+ onClick={linkEvent(this, this.handleMessageCollapse)}
+ >
+ {this.state.collapsed ? '[+]' : '[-]'}
+ </div>
+ </li>
+ </ul>
+ {this.state.showEdit && (
+ <PrivateMessageForm
+ privateMessage={message}
+ onEdit={this.handlePrivateMessageEdit}
+ onCancel={this.handleReplyCancel}
+ />
+ )}
+ {!this.state.showEdit && !this.state.collapsed && (
+ <div>
+ {this.state.viewSource ? (
+ <pre>{this.messageUnlessRemoved}</pre>
+ ) : (
+ <div
+ className="md-div"
+ dangerouslySetInnerHTML={mdToHtml(this.messageUnlessRemoved)}
+ />
+ )}
+ <ul class="list-inline mb-1 text-muted small font-weight-bold">
+ {!this.mine && (
+ <>
+ <li className="list-inline-item">
+ <span
+ class="pointer"
+ onClick={linkEvent(this, this.handleMarkRead)}
+ >
+ {message.read
+ ? i18n.t('mark_as_unread')
+ : i18n.t('mark_as_read')}
+ </span>
+ </li>
+ <li className="list-inline-item">
+ <span
+ class="pointer"
+ onClick={linkEvent(this, this.handleReplyClick)}
+ >
+ <T i18nKey="reply">#</T>
+ </span>
+ </li>
+ </>
+ )}
+ {this.mine && (
+ <>
+ <li className="list-inline-item">
+ <span
+ class="pointer"
+ onClick={linkEvent(this, this.handleEditClick)}
+ >
+ <T i18nKey="edit">#</T>
+ </span>
+ </li>
+ <li className="list-inline-item">
+ <span
+ class="pointer"
+ onClick={linkEvent(this, this.handleDeleteClick)}
+ >
+ {!message.deleted
+ ? i18n.t('delete')
+ : i18n.t('restore')}
+ </span>
+ </li>
+ </>
+ )}
+ <li className="list-inline-item">•</li>
+ <li className="list-inline-item">
+ <span
+ className="pointer"
+ onClick={linkEvent(this, this.handleViewSource)}
+ >
+ <T i18nKey="view_source">#</T>
+ </span>
+ </li>
+ </ul>
+ </div>
+ )}
+ </div>
+ {this.state.showReply && (
+ <PrivateMessageForm
+ params={{
+ recipient_id: this.props.privateMessage.creator_id,
+ }}
+ onCreate={this.handlePrivateMessageCreate}
+ />
+ )}
+ {/* A collapsed clearfix */}
+ {this.state.collapsed && <div class="row col-12"></div>}
+ </div>
+ );
+ }
+
+ get messageUnlessRemoved(): string {
+ let message = this.props.privateMessage;
+ return message.deleted ? `*${i18n.t('deleted')}*` : message.content;
+ }
+
+ handleReplyClick(i: PrivateMessage) {
+ i.state.showReply = true;
+ i.setState(i.state);
+ }
+
+ handleEditClick(i: PrivateMessage) {
+ i.state.showEdit = true;
+ i.setState(i.state);
+ }
+
+ handleDeleteClick(i: PrivateMessage) {
+ let form: EditPrivateMessageForm = {
+ edit_id: i.props.privateMessage.id,
+ deleted: !i.props.privateMessage.deleted,
+ };
+ WebSocketService.Instance.editPrivateMessage(form);
+ }
+
+ handleReplyCancel() {
+ this.state.showReply = false;
+ this.state.showEdit = false;
+ this.setState(this.state);
+ }
+
+ handleMarkRead(i: PrivateMessage) {
+ let form: EditPrivateMessageForm = {
+ edit_id: i.props.privateMessage.id,
+ read: !i.props.privateMessage.read,
+ };
+ WebSocketService.Instance.editPrivateMessage(form);
+ }
+
+ handleMessageCollapse(i: PrivateMessage) {
+ i.state.collapsed = !i.state.collapsed;
+ i.setState(i.state);
+ }
+
+ handleViewSource(i: PrivateMessage) {
+ i.state.viewSource = !i.state.viewSource;
+ i.setState(i.state);
+ }
+
+ handlePrivateMessageEdit() {
+ this.state.showEdit = false;
+ this.setState(this.state);
+ }
+
+ handlePrivateMessageCreate() {
+ this.state.showReply = false;
+ this.setState(this.state);
+ alert(i18n.t('message_sent'));
+ }
+}
diff --git a/ui/src/components/user.tsx b/ui/src/components/user.tsx
index 206fb8ff..19bd5fb9 100644
--- a/ui/src/components/user.tsx
+++ b/ui/src/components/user.tsx
@@ -405,13 +405,30 @@ export class User extends Component<any, UserState> {
</tr>
</table>
</div>
- {this.isCurrentUser && (
+ {this.isCurrentUser ? (
<button
class="btn btn-block btn-secondary mt-3"
onClick={linkEvent(this, this.handleLogoutClick)}
>
<T i18nKey="logout">#</T>