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