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