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