diff options
author | Dessalines <tyhou13@gmx.com> | 2020-01-22 20:37:08 -0500 |
---|---|---|
committer | Dessalines <tyhou13@gmx.com> | 2020-01-22 20:37:08 -0500 |
commit | 0d2b4e6f18b1212f95914b3371a42a0f4d699d34 (patch) | |
tree | a261c287aab36f22b66bfceb2ddb16d97cfda772 /ui/src | |
parent | f1b5b50dc8531e08be9b055c709c3381ec540852 (diff) | |
parent | 253bc3e0afb6adf64b79f334a8bc1f972aa45eba (diff) |
Merge branch 'private_messaging' into dev
Diffstat (limited to 'ui/src')
-rw-r--r-- | ui/src/components/create-private-message.tsx | 52 | ||||
-rw-r--r-- | ui/src/components/inbox.tsx | 117 | ||||
-rw-r--r-- | ui/src/components/navbar.tsx | 41 | ||||
-rw-r--r-- | ui/src/components/private-message-form.tsx | 291 | ||||
-rw-r--r-- | ui/src/components/private-message.tsx | 249 | ||||
-rw-r--r-- | ui/src/components/user.tsx | 51 | ||||
-rw-r--r-- | ui/src/index.tsx | 5 | ||||
-rw-r--r-- | ui/src/interfaces.ts | 55 | ||||
-rw-r--r-- | ui/src/services/WebSocketService.ts | 26 | ||||
-rw-r--r-- | ui/src/translations/en.ts | 13 | ||||
-rw-r--r-- | ui/src/utils.ts | 5 |
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> - |