]> Untitled Git - lemmy.git/blob - ui/src/components/private-message-form.tsx
Merge branch 'master' into federation
[lemmy.git] / ui / src / components / private-message-form.tsx
1 import { Component, linkEvent } from 'inferno';
2 import { Prompt } from 'inferno-router';
3 import { Link } from 'inferno-router';
4 import { Subscription } from 'rxjs';
5 import { retryWhen, delay, take } from 'rxjs/operators';
6 import {
7   PrivateMessageForm as PrivateMessageFormI,
8   EditPrivateMessageForm,
9   PrivateMessageFormParams,
10   PrivateMessage,
11   PrivateMessageResponse,
12   UserView,
13   UserOperation,
14   UserDetailsResponse,
15   GetUserDetailsForm,
16   SortType,
17   WebSocketJsonResponse,
18 } from '../interfaces';
19 import { WebSocketService } from '../services';
20 import {
21   capitalizeFirstLetter,
22   markdownHelpUrl,
23   mdToHtml,
24   wsJsonToRes,
25   toast,
26   randomStr,
27   setupTribute,
28   setupTippy,
29 } from '../utils';
30 import { UserListing } from './user-listing';
31 import Tribute from 'tributejs/src/Tribute.js';
32 import autosize from 'autosize';
33 import { i18n } from '../i18next';
34 import { T } from 'inferno-i18next';
35
36 interface PrivateMessageFormProps {
37   privateMessage?: PrivateMessage; // If a pm is given, that means this is an edit
38   params?: PrivateMessageFormParams;
39   onCancel?(): any;
40   onCreate?(message: PrivateMessage): any;
41   onEdit?(message: PrivateMessage): any;
42 }
43
44 interface PrivateMessageFormState {
45   privateMessageForm: PrivateMessageFormI;
46   recipient: UserView;
47   loading: boolean;
48   previewMode: boolean;
49   showDisclaimer: boolean;
50 }
51
52 export class PrivateMessageForm extends Component<
53   PrivateMessageFormProps,
54   PrivateMessageFormState
55 > {
56   private id = `message-form-${randomStr()}`;
57   private tribute: Tribute;
58   private subscription: Subscription;
59   private emptyState: PrivateMessageFormState = {
60     privateMessageForm: {
61       content: null,
62       recipient_id: null,
63     },
64     recipient: null,
65     loading: false,
66     previewMode: false,
67     showDisclaimer: false,
68   };
69
70   constructor(props: any, context: any) {
71     super(props, context);
72
73     this.tribute = setupTribute();
74     this.state = this.emptyState;
75
76     if (this.props.privateMessage) {
77       this.state.privateMessageForm = {
78         content: this.props.privateMessage.content,
79         recipient_id: this.props.privateMessage.recipient_id,
80       };
81     }
82
83     if (this.props.params) {
84       this.state.privateMessageForm.recipient_id = this.props.params.recipient_id;
85       let form: GetUserDetailsForm = {
86         user_id: this.state.privateMessageForm.recipient_id,
87         sort: SortType[SortType.New],
88         saved_only: false,
89       };
90       WebSocketService.Instance.getUserDetails(form);
91     }
92
93     this.subscription = WebSocketService.Instance.subject
94       .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
95       .subscribe(
96         msg => this.parseMessage(msg),
97         err => console.error(err),
98         () => console.log('complete')
99       );
100   }
101
102   componentDidMount() {
103     var textarea: any = document.getElementById(this.id);
104     autosize(textarea);
105     this.tribute.attach(textarea);
106     textarea.addEventListener('tribute-replaced', () => {
107       this.state.privateMessageForm.content = textarea.value;
108       this.setState(this.state);
109       autosize.update(textarea);
110     });
111     setupTippy();
112   }
113
114   componentWillUnmount() {
115     this.subscription.unsubscribe();
116   }
117
118   render() {
119     return (
120       <div>
121         <Prompt
122           when={!this.state.loading && this.state.privateMessageForm.content}
123           message={i18n.t('block_leaving')}
124         />
125         <form onSubmit={linkEvent(this, this.handlePrivateMessageSubmit)}>
126           {!this.props.privateMessage && (
127             <div class="form-group row">
128               <label class="col-sm-2 col-form-label">
129                 {capitalizeFirstLetter(i18n.t('to'))}
130               </label>
131
132               {this.state.recipient && (
133                 <div class="col-sm-10 form-control-plaintext">
134                   <UserListing
135                     user={{
136                       name: this.state.recipient.name,
137                       avatar: this.state.recipient.avatar,
138                       id: this.state.recipient.id,
139                       local: this.state.recipient.local,
140                       actor_id: this.state.recipient.actor_id,
141                     }}
142                   />
143                 </div>
144               )}
145             </div>
146           )}
147           <div class="form-group row">
148             <label class="col-sm-2 col-form-label">{i18n.t('message')}</label>
149             <div class="col-sm-10">
150               <textarea
151                 id={this.id}
152                 value={this.state.privateMessageForm.content}
153                 onInput={linkEvent(this, this.handleContentChange)}
154                 className={`form-control ${this.state.previewMode && 'd-none'}`}
155                 rows={4}
156                 maxLength={10000}
157               />
158               {this.state.previewMode && (
159                 <div
160                   className="card card-body md-div"
161                   dangerouslySetInnerHTML={mdToHtml(
162                     this.state.privateMessageForm.content
163                   )}
164                 />
165               )}
166             </div>
167           </div>
168
169           {this.state.showDisclaimer && (
170             <div class="form-group row">
171               <div class="offset-sm-2 col-sm-10">
172                 <div class="alert alert-danger" role="alert">
173                   <T i18nKey="private_message_disclaimer">
174                     #
175                     <a
176                       class="alert-link"
177                       target="_blank"
178                       rel="noopener"
179                       href="https://about.riot.im/"
180                     >
181                       #
182                     </a>
183                   </T>
184                 </div>
185               </div>
186             </div>
187           )}
188           <div class="form-group row">
189             <div class="offset-sm-2 col-sm-10">
190               <button
191                 type="submit"
192                 class="btn btn-secondary mr-2"
193                 disabled={this.state.loading}
194               >
195                 {this.state.loading ? (
196                   <svg class="icon icon-spinner spin">
197                     <use xlinkHref="#icon-spinner"></use>
198                   </svg>
199                 ) : this.props.privateMessage ? (
200                   capitalizeFirstLetter(i18n.t('save'))
201                 ) : (
202                   capitalizeFirstLetter(i18n.t('send_message'))
203                 )}
204               </button>
205               {this.state.privateMessageForm.content && (
206                 <button
207                   className={`btn btn-secondary mr-2 ${
208                     this.state.previewMode && 'active'
209                   }`}
210                   onClick={linkEvent(this, this.handlePreviewToggle)}
211                 >
212                   {i18n.t('preview')}
213                 </button>
214               )}
215               {this.props.privateMessage && (
216                 <button
217                   type="button"
218                   class="btn btn-secondary"
219                   onClick={linkEvent(this, this.handleCancel)}
220                 >
221                   {i18n.t('cancel')}
222                 </button>
223               )}
224               <ul class="d-inline-block float-right list-inline mb-1 text-muted font-weight-bold">
225                 <li class="list-inline-item">
226                   <span
227                     onClick={linkEvent(this, this.handleShowDisclaimer)}
228                     class="pointer"
229                     data-tippy-content={i18n.t('disclaimer')}
230                   >
231                     <svg class={`icon icon-inline`}>
232                       <use xlinkHref="#icon-alert-triangle"></use>
233                     </svg>
234                   </span>
235                 </li>
236                 <li class="list-inline-item">
237                   <a
238                     href={markdownHelpUrl}
239                     target="_blank"
240                     rel="noopener"
241                     class="text-muted"
242                     title={i18n.t('formatting_help')}
243                   >
244                     <svg class="icon icon-inline">
245                       <use xlinkHref="#icon-help-circle"></use>
246                     </svg>
247                   </a>
248                 </li>
249               </ul>
250             </div>
251           </div>
252         </form>
253       </div>
254     );
255   }
256
257   handlePrivateMessageSubmit(i: PrivateMessageForm, event: any) {
258     event.preventDefault();
259     if (i.props.privateMessage) {
260       let editForm: EditPrivateMessageForm = {
261         edit_id: i.props.privateMessage.id,
262         content: i.state.privateMessageForm.content,
263       };
264       WebSocketService.Instance.editPrivateMessage(editForm);
265     } else {
266       WebSocketService.Instance.createPrivateMessage(
267         i.state.privateMessageForm
268       );
269     }
270     i.state.loading = true;
271     i.setState(i.state);
272   }
273
274   handleRecipientChange(i: PrivateMessageForm, event: any) {
275     i.state.recipient = event.target.value;
276     i.setState(i.state);
277   }
278
279   handleContentChange(i: PrivateMessageForm, event: any) {
280     i.state.privateMessageForm.content = event.target.value;
281     i.setState(i.state);
282   }
283
284   handleCancel(i: PrivateMessageForm) {
285     i.props.onCancel();
286   }
287
288   handlePreviewToggle(i: PrivateMessageForm, event: any) {
289     event.preventDefault();
290     i.state.previewMode = !i.state.previewMode;
291     i.setState(i.state);
292   }
293
294   handleShowDisclaimer(i: PrivateMessageForm) {
295     i.state.showDisclaimer = !i.state.showDisclaimer;
296     i.setState(i.state);
297   }
298
299   parseMessage(msg: WebSocketJsonResponse) {
300     let res = wsJsonToRes(msg);
301     if (msg.error) {
302       toast(i18n.t(msg.error), 'danger');
303       this.state.loading = false;
304       this.setState(this.state);
305       return;
306     } else if (res.op == UserOperation.EditPrivateMessage) {
307       let data = res.data as PrivateMessageResponse;
308       this.state.loading = false;
309       this.props.onEdit(data.message);
310     } else if (res.op == UserOperation.GetUserDetails) {
311       let data = res.data as UserDetailsResponse;
312       this.state.recipient = data.user;
313       this.state.privateMessageForm.recipient_id = data.user.id;
314       this.setState(this.state);
315     } else if (res.op == UserOperation.CreatePrivateMessage) {
316       let data = res.data as PrivateMessageResponse;
317       this.state.loading = false;
318       this.props.onCreate(data.message);
319       this.setState(this.state);
320     }
321   }
322 }