]> Untitled Git - lemmy.git/blob - ui/src/components/private-message-form.tsx
Merge branch 'html_titles'
[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   componentWillUnmount() {
114     this.subscription.unsubscribe();
115   }
116
117   render() {
118     return (
119       <div>
120         <Prompt
121           when={!this.state.loading && this.state.privateMessageForm.content}
122           message={i18n.t('block_leaving')}
123         />
124         <form onSubmit={linkEvent(this, this.handlePrivateMessageSubmit)}>
125           {!this.props.privateMessage && (
126             <div class="form-group row">
127               <label class="col-sm-2 col-form-label">
128                 {capitalizeFirstLetter(i18n.t('to'))}
129               </label>
130
131               {this.state.recipient && (
132                 <div class="col-sm-10 form-control-plaintext">
133                   <UserListing
134                     user={{
135                       name: this.state.recipient.name,
136                       avatar: this.state.recipient.avatar,
137                       id: this.state.recipient.id,
138                       local: this.state.recipient.local,
139                       actor_id: this.state.recipient.actor_id,
140                     }}
141                   />
142                 </div>
143               )}
144             </div>
145           )}
146           <div class="form-group row">
147             <label class="col-sm-2 col-form-label">{i18n.t('message')}</label>
148             <div class="col-sm-10">
149               <textarea
150                 id={this.id}
151                 value={this.state.privateMessageForm.content}
152                 onInput={linkEvent(this, this.handleContentChange)}
153                 className={`form-control ${this.state.previewMode && 'd-none'}`}
154                 rows={4}
155                 maxLength={10000}
156               />
157               {this.state.previewMode && (
158                 <div
159                   className="card card-body md-div"
160                   dangerouslySetInnerHTML={mdToHtml(
161                     this.state.privateMessageForm.content
162                   )}
163                 />
164               )}
165             </div>
166           </div>
167
168           {this.state.showDisclaimer && (
169             <div class="form-group row">
170               <div class="offset-sm-2 col-sm-10">
171                 <div class="alert alert-danger" role="alert">
172                   <T i18nKey="private_message_disclaimer">
173                     #
174                     <a
175                       class="alert-link"
176                       target="_blank"
177                       rel="noopener"
178                       href="https://about.riot.im/"
179                     >
180                       #
181                     </a>
182                   </T>
183                 </div>
184               </div>
185             </div>
186           )}
187           <div class="form-group row">
188             <div class="offset-sm-2 col-sm-10">
189               <button
190                 type="submit"
191                 class="btn btn-secondary mr-2"
192                 disabled={this.state.loading}
193               >
194                 {this.state.loading ? (
195                   <svg class="icon icon-spinner spin">
196                     <use xlinkHref="#icon-spinner"></use>
197                   </svg>
198                 ) : this.props.privateMessage ? (
199                   capitalizeFirstLetter(i18n.t('save'))
200                 ) : (
201                   capitalizeFirstLetter(i18n.t('send_message'))
202                 )}
203               </button>
204               {this.state.privateMessageForm.content && (
205                 <button
206                   className={`btn btn-secondary mr-2 ${
207                     this.state.previewMode && 'active'
208                   }`}
209                   onClick={linkEvent(this, this.handlePreviewToggle)}
210                 >
211                   {i18n.t('preview')}
212                 </button>
213               )}
214               {this.props.privateMessage && (
215                 <button
216                   type="button"
217                   class="btn btn-secondary"
218                   onClick={linkEvent(this, this.handleCancel)}
219                 >
220                   {i18n.t('cancel')}
221                 </button>
222               )}
223               <ul class="d-inline-block float-right list-inline mb-1 text-muted font-weight-bold">
224                 <li class="list-inline-item">
225                   <span
226                     onClick={linkEvent(this, this.handleShowDisclaimer)}
227                     class="pointer"
228                     data-tippy-content={i18n.t('disclaimer')}
229                   >
230                     <svg class={`icon icon-inline`}>
231                       <use xlinkHref="#icon-alert-triangle"></use>
232                     </svg>
233                   </span>
234                 </li>
235                 <li class="list-inline-item">
236                   <a
237                     href={markdownHelpUrl}
238                     target="_blank"
239                     rel="noopener"
240                     class="text-muted"
241                     title={i18n.t('formatting_help')}
242                   >
243                     <svg class="icon icon-inline">
244                       <use xlinkHref="#icon-help-circle"></use>
245                     </svg>
246                   </a>
247                 </li>
248               </ul>
249             </div>
250           </div>
251         </form>
252       </div>
253     );
254   }
255
256   handlePrivateMessageSubmit(i: PrivateMessageForm, event: any) {
257     event.preventDefault();
258     if (i.props.privateMessage) {
259       let editForm: EditPrivateMessageForm = {
260         edit_id: i.props.privateMessage.id,
261         content: i.state.privateMessageForm.content,
262       };
263       WebSocketService.Instance.editPrivateMessage(editForm);
264     } else {
265       WebSocketService.Instance.createPrivateMessage(
266         i.state.privateMessageForm
267       );
268     }
269     i.state.loading = true;
270     i.setState(i.state);
271   }
272
273   handleRecipientChange(i: PrivateMessageForm, event: any) {
274     i.state.recipient = event.target.value;
275     i.setState(i.state);
276   }
277
278   handleContentChange(i: PrivateMessageForm, event: any) {
279     i.state.privateMessageForm.content = event.target.value;
280     i.setState(i.state);
281   }
282
283   handleCancel(i: PrivateMessageForm) {
284     i.props.onCancel();
285   }
286
287   handlePreviewToggle(i: PrivateMessageForm, event: any) {
288     event.preventDefault();
289     i.state.previewMode = !i.state.previewMode;
290     i.setState(i.state);
291   }
292
293   handleShowDisclaimer(i: PrivateMessageForm) {
294     i.state.showDisclaimer = !i.state.showDisclaimer;
295     i.setState(i.state);
296   }
297
298   parseMessage(msg: WebSocketJsonResponse) {
299     let res = wsJsonToRes(msg);
300     if (msg.error) {
301       toast(i18n.t(msg.error), 'danger');
302       this.state.loading = false;
303       this.setState(this.state);
304       return;
305     } else if (res.op == UserOperation.EditPrivateMessage) {
306       let data = res.data as PrivateMessageResponse;
307       this.state.loading = false;
308       this.props.onEdit(data.message);
309     } else if (res.op == UserOperation.GetUserDetails) {
310       let data = res.data as UserDetailsResponse;
311       this.state.recipient = data.user;
312       this.state.privateMessageForm.recipient_id = data.user.id;
313       this.setState(this.state);
314     } else if (res.op == UserOperation.CreatePrivateMessage) {
315       let data = res.data as PrivateMessageResponse;
316       this.state.loading = false;
317       this.props.onCreate(data.message);
318       this.setState(this.state);
319     }
320   }
321 }