unoffical wafrn mirror
wafrn.net
atproto
social-network
activitypub
1@let showBlueskyOptions = postService.enableBluesky && (!data?.post || data?.post?.bskyUri) && privacy === 0;
2<mat-card appearance="outlined" class="pb-3 mb-4 lg:mx-4 wafrn-container wafrn-post-editor">
3 <div class="pt-3 px-3 below-editor-toolbar">
4 <div class="below-editor-toolbar-leftside">
5 <button mat-icon-button class="close-post-btn" (click)="closeEditor()">
6 <fa-icon size="lg" [icon]="closeIcon"></fa-icon>
7 </button>
8 <div class="below-editor-toolbar-divider"></div>
9 <button
10 mat-icon-button
11 [matMenuTriggerFor]="menu"
12 attr.aria-label="{{ 'editor.ariaLabelWootPrivacy' | translate }}"
13 class="input-height-btn"
14 [matTooltip]="getPrivacyIconName()"
15 [disabled]="editing"
16 >
17 <fa-icon size="lg" [icon]="getPrivacyIcon()"></fa-icon>
18 </button>
19 <button
20 attr.aria-label="{{ 'editor.ariaLabelQuoteSetter' | translate }}"
21 mat-icon-button
22 (click)="quoteOpen = !quoteOpen"
23 [disabled]="data?.quote"
24 class="input-height-btn"
25 matTooltip="{{ 'editor.quoteButton' | translate }}"
26 >
27 <fa-icon size="lg" [icon]="quoteIcon"></fa-icon>
28 </button>
29 @if (pollQuestions.length === 0) {
30 <div matTooltip="{{ 'editor.uploadMediaTooltip' | translate }}">
31 <app-file-upload (fileUpload)="uploadImage($event)" (uploadCanceled)="uploadCanceled()"></app-file-upload>
32 </div>
33 }
34 <button
35 class="input-height-btn"
36 mat-icon-button
37 (click)="showContentWarning = !showContentWarning"
38 matTooltip="{{ 'editor.contentWarningTooltip' | translate }}"
39 >
40 @if (contentWarning.includes('meta')) {
41 <fa-icon size="lg" [icon]="skull"></fa-icon>
42 } @else {
43 <fa-icon size="lg" [icon]="contentWarningIcon"></fa-icon>
44 }
45 </button>
46 <button
47 class="input-height-btn"
48 mat-icon-button
49 (click)="openEmojiSelection()"
50 matTooltip="{{ 'editor.insertEmojiTooltip' | translate }}"
51 >
52 <svg width="20px" height="20px" viewBox="0 0 1000 1000" fill="currentColor" class="vertical-align-middle">
53 <path
54 d="M958 104h-62V43q0-17-12-29T855 2h-87q-17 0-29 12t-12 29v61h-62q-17 0-29 12t-12 29v4q-81-34-169-34-116 0-217 59-97 57-154 154-58 100-58 216.5T84 761q57 98 154 155 101 58 217.5 58T672 916q97-57 154-155 59-100 59-216 0-88-34-168h4q17 0 29-12t12-29v-62h62q17 0 29-12t12-29v-88q0-17-12-29t-29-12zM644 348q29 0 49.5 20.5t20.5 49-20.5 49T644 487t-49.5-20.5-20.5-49 20.5-49T644 348zm-377 0q29 0 49.5 20.5t20.5 49-20.5 49T267 487t-49.5-20.5-20.5-49 20.5-49T267 348zm473 255q-10 70-50.5 126.5t-102 88.5-132 32-132-32-102-88.5T171 603q-2-16 8.5-27.5T206 564h499q16 0 26.5 11.5T740 603zm218-370H855v103h-87V233H665v-88h103V43h87v102h103v88z"
55 />
56 </svg>
57 </button>
58 <!--
59 <button
60 [matBadge]="mentionedUsers.length"
61 [matBadgeHidden]="mentionedUsers.length == 0"
62 class="input-height-btn"
63 mat-icon-button
64 (click)="showMentionedUsersList = !showMentionedUsersList"
65 matTooltip="{{ 'editor.hiddenMentionsTooltip' | translate }}"
66 >
67 <fa-icon size="lg" [icon]="atIcon"></fa-icon>
68 </button>
69 --></div>
70 <div class="below-editor-toolbar-rightside">
71 <button
72 mat-flat-button
73 class="post-btn"
74 (click)="submitPost()"
75 [disabled]="
76 !allDescriptionsFilled() ||
77 postBeingSubmitted ||
78 (postCreatorForm.value.content === initialContent && tags.length === 0 && uploadedMedias.length === 0)
79 "
80 matTooltip="{{ 'editor.publishWoot' | translate }}"
81 >
82 <fa-icon size="lg" [icon]="postIcon"></fa-icon>
83 </button>
84 </div>
85 </div>
86
87 @if (showBlueskyOptions) {
88 <mat-progress-bar
89 mode="determinate"
90 class="mb-2 bsky-progress-bar"
91 [class.length-overflow]="calculateBskyPostLengthPercent() > 1"
92 [value]="calculateBskyPostLengthPercent() * 100"
93 >
94 </mat-progress-bar>
95 } @else {
96 <hr class="mt-0 mb-2 editor-separator" />
97 }
98
99 <div class="post-editor-data">
100 @if (editing) {
101 <section class="px-2 flex gap-2 justify-content-center align-items-center editing-label-section">
102 <div class="label-horizontal-bar"></div>
103 <fa-icon size="sm" [fixedWidth]="true" [icon]="editingIcon"></fa-icon>
104 <div class="editing-label">{{ 'editor.editingLabel' | translate }}</div>
105 <fa-icon size="sm" [fixedWidth]="true" [icon]="editingIcon"></fa-icon>
106 <div class="label-horizontal-bar"></div>
107 </section>
108 }
109 @if (data && data.ask) {
110 <section class="px-2">
111 <div class="flex gap-2 justify-content-center align-items-center ask-label-section">
112 <div class="label-horizontal-bar"></div>
113 <fa-icon size="sm" [fixedWidth]="true" [icon]="replyAskIcon"></fa-icon>
114 <div class="ask-label">{{ 'editor.askReplyLabel' | translate }}</div>
115 <fa-icon size="sm" [fixedWidth]="true" [icon]="replyAskIcon"></fa-icon>
116 <div class="label-horizontal-bar"></div>
117 </div>
118 <div class="my-2">
119 <app-single-ask [ask]="data.ask"></app-single-ask>
120 </div>
121 </section>
122 }
123
124 @if (privacy === 10) {
125 <app-info-card [type]="'caution'" addClass="mb-3">
126 {{ 'editor.directMessageWarning' | translate }}
127 </app-info-card>
128 @if (data?.quote) {
129 <app-info-card [type]="'caution'" addClass="mb-3">
130 {{ 'editor.directMessageWithQuoteWarning' | translate }}
131 </app-info-card>
132 }
133 }
134 @if (privacy === 3) {
135 <app-info-card type="info" addClass="mb-3">
136 {{ 'editor.unlistedWarning' | translate }}
137 </app-info-card>
138 }
139 @if (showContentWarning || contentWarning) {
140 <section class="px-3 my-2">
141 <mat-form-field class="w-full transition-size mat-form-field-no-padding" appearance="outline">
142 <mat-label>Content warning (optional)</mat-label>
143 <input
144 [(ngModel)]="contentWarning"
145 placeholder="{{ 'editor.sensitivePlaceholder' | translate }}"
146 matNativeControl
147 />
148 </mat-form-field>
149 </section>
150 <hr class="mb-2 menu-hr" />
151 }
152
153 @if (showMentionedUsersList) {
154 <section class="px-2">
155 @if (mentionedUsers.length >= 1) {
156 <mat-chip-grid
157 #chipGrid
158 aria-label="Mentioned users"
159 class="mb-2 mention-chip-grid"
160 >
161 @for (user of mentionedUsers; track $index) {
162 <mat-chip-row [editable]="true" (removed)="removeMention($index)">
163 {{ user.url }}
164 <button matChipRemove [attr.aria-label]="'remove ' + user.url">x</button>
165 </mat-chip-row>
166 }
167 <input [hidden]="true" placeholder="Add mention (FEATURE NOT DONE YET)" [matChipInputFor]="chipGrid" />
168 </mat-chip-grid>
169 }
170 </section>
171 }
172
173 <form class="relative" [formGroup]="postCreatorForm">
174 <mat-form-field
175 class="mb-3 w-full mat-form-field-no-background mat-form-field-no-padding woot-textfield"
176 appearance="fill"
177 floatLabel="always"
178 >
179 <mat-label>{{ 'editor.wootTextLabel' | translate }}</mat-label>
180 <textarea
181 #postContent
182 id="postCreatorContent"
183 formControlName="content"
184 class="w-full woot-textarea"
185 (blur)="editorFocusedOut()"
186 (focus)="editorFocusedIn()"
187 (input)="updateMentionsPanelPosition()"
188 (paste)="handlePaste($event)"
189 (drop)="handleDrop($event)"
190 (dragenter)="handleDrag($event)"
191 (dragleave)="handleDrag($event)"
192 rows="4"
193 placeholder="{{ 'editor.wootTextPlaceholder' | translate }}"
194 matNativeControl
195 cdkTextareaAutosize
196 cdkAutosizeMinRows="5"
197 autofocus
198 ></textarea>
199 </mat-form-field>
200 @if (draggingOverTextarea) {
201 <div class="media-drop-indicator">{{ 'editor.uploadMediaIndicator' | translate }}</div>
202 }
203 </form>
204
205 @if (false && uploadedMedias.length === 0) {
206 <!-- Polls are not available yet :( -->
207 <section class="mt-3" id="pollControls">
208 <button class="w-full" mat-button (click)="quoteOpen = true" mat-flat-button>add poll</button>
209 </section>
210 }
211
212 @if (uploadedMedias.length > 0) {
213 <section class="px-3" id="uploaded-media" [class.mb-3]="allDescriptionsFilled()">
214 <div class="grid gap-3">
215 @for (media of uploadedMedias; track media; let i = $index) {
216 <div class="col-12 md:col relative">
217 <app-media-preview [media]="media"></app-media-preview>
218 <mat-form-field class="w-full mat-form-field-no-padding mb-1" appearance="outline">
219 @if (mediaIsVideo(media)) {
220 <mat-label>{{ 'editor.altTextFieldLabelVideo' | translate }}</mat-label>
221 } @else {
222 <mat-label>{{ 'editor.altTextFieldLabel' | translate }}</mat-label>
223 }
224 <textarea
225 placeholder="{{ 'editor.altTextFieldPlaceholder' | translate }}"
226 [(ngModel)]="media.description"
227 matNativeControl
228 cdkTextareaAutosize
229 required
230 class="w-full"
231 ></textarea>
232 </mat-form-field>
233 <div>
234 <!-- Yknow, I don't think translating "NSFW" is needed but YOU NEVER KNOW -->
235 <mat-checkbox [(ngModel)]="media.NSFW" class="w-full">{{
236 'editor.isNSFWToggle' | translate
237 }}</mat-checkbox>
238
239 <button mat-flat-button class="delete-btn mat-circle-button" (click)="deleteImage(i)">
240 <fa-icon size="lg" [icon]="closeIcon"></fa-icon>
241 </button>
242 </div>
243 </div>
244 }
245 </div>
246 @if (uploadedMedias.length >= 4) {
247 <p class="my-2">
248 {{ 'editor.mediaCountMastodonWarning' | translate }}
249 </p>
250 }
251 @if (!allDescriptionsFilled()) {
252 <p class="my-2">
253 {{ 'editor.altTextWarning' | translate }}
254 </p>
255 }
256 </section>
257 }
258
259 <section id="tags" class="w-full">
260 <mat-form-field
261 class="w-full mat-form-field-no-outline mat-form-field-no-padding woot-tagfield"
262 appearance="outline"
263 >
264 <mat-label>{{ 'editor.tagFieldLabel' | translate }}</mat-label>
265 <input [(ngModel)]="tags" placeholder="{{ 'common.commaSeparation' | translate }}" matNativeControl />
266 </mat-form-field>
267 @if (tags) {
268 <div class="mt-2 mx-2 tag-list">
269 @for (tag of tags.split(','); track $index) {
270 @if (tag && tag !== '' && tag.trim() !== '') {
271 <span class="tag" [attr.data-tag]="tagMap(tag)"
272 ><span class="tag-text">#{{ tagMap(tag) }}</span></span
273 >
274 }
275 }
276 </div>
277 }
278 </section>
279
280 @if (data && data.post && !editing) {
281 <section class="px-3 pt-2">
282 <p class="mb-2">{{ 'editor.inReplyTo' | translate }}</p>
283 <div class="quoted-post">
284 @if (data.post) {
285 <app-post-header [fragment]="data.post" [disableLink]="true"></app-post-header>
286 }
287 <app-post-fragment [fragment]="data.post"></app-post-fragment>
288 </div>
289 </section>
290 }
291
292 @if (quoteOpen && !data?.quote && !quoteLoading) {
293 <section class="px-3 mt-3 flex gap-3 align-items-center">
294 <mat-form-field class="w-full mat-form-field-no-padding add-quote-input" appearance="outline">
295 <mat-label> {{ 'editor.wootQuoteBoxLabel' | translate }} </mat-label>
296 <input
297 [(ngModel)]="urlPostToQuote"
298 placeholder="{{ 'editor.wootQuoteBoxPlaceholder' | translate }}"
299 matNativeControl
300 />
301 </mat-form-field>
302 <button
303 (click)="loadQuote()"
304 mat-stroked-button
305 color="primary"
306 class="mat-circle-button"
307 [disabled]="urlPostToQuote === ''"
308 >
309 <fa-icon [icon]="addIcon"></fa-icon>
310 </button>
311 </section>
312 }
313 @if (quoteLoading || data?.quote) {
314 <section class="px-3 pt-2" id="quote">
315 <p class="mb-1 quote-label">{{ 'editor.quoteTitle' | translate }}</p>
316 <div class="quoted-post">
317 @if (quoteLoading) {
318 <div class="flex align-items-center justify-content-center">
319 <mat-spinner class="my-4" color="accent" diameter="32"></mat-spinner>
320 </div>
321 }
322 @if (data && data.quote) {
323 <button
324 mat-flat-button
325 class="mat-circle-button delete-btn"
326 color="warn"
327 (click)="data ? (data.quote = undefined) : null; quoteOpen = false"
328 >
329 <fa-icon size="lg" [icon]="closeIcon"></fa-icon>
330 </button>
331 <div class="mr-6">
332 <app-post-header [fragment]="data.quote"></app-post-header>
333 </div>
334 <app-post-fragment [fragment]="data.quote" fragmentType="quote"></app-post-fragment>
335 }
336 </div>
337 </section>
338 }
339
340 <div class="px-3 pt-3 bottom-information-bar">
341 <div class="min-w-0">
342 <div class="posting-label">{{ 'editor.usernameLabel' | translate }}</div>
343 @if (accountList().length > 1) {
344 <button mat-button [mat-menu-trigger-for]="accountMenu" class="user-info-button">
345 <div class="user-info">
346 <img [src]="toAvatarUrl(currentUser())" class="user-avatar" />
347 <span [innerHTML]="currentUser()?.url ?? 'No one?!'" class="user-name"></span>
348 <fa-icon [icon]="dropdownIcon" class="icon-dropdown"></fa-icon>
349 </div>
350 </button>
351 } @else {
352 <div class="user-info">
353 <img [src]="toAvatarUrl(currentUser())" class="user-avatar" />
354 <span [innerHTML]="currentUser()?.url ?? 'No one?!'" class="user-name"></span>
355 </div>
356 }
357 </div>
358 @if (showBlueskyOptions) {
359 <div
360 class="ml-auto post-length-note"
361 [class.warning-text]="calculateBskyPostLengthPercent() > 1"
362 [matTooltip]="
363 (calculateBskyPostLength() > 300 ? 'editor.bskyLengthWarning' : 'editor.bskyLengthNotice') | translate
364 "
365 >
366 <p>{{ calculateBskyPostLength() }}/300 {{ 'common.characters' | translate }}</p>
367 <p class="post-length-warning">
368 {{ 'editor.bskyLengthLabel' | translate }} <fa-icon [icon]="infoIcon"></fa-icon>
369 </p>
370 </div>
371 }
372 </div>
373
374 <!-- Popup Menus -->
375 <mat-menu #accountMenu="matMenu">
376 @for (account of accountList(); track $index) {
377 <button mat-menu-item (click)="setPoster($index)">
378 <div class="user-info"
379 ><img [src]="toAvatarUrl(account.blog)" class="user-avatar" />
380 <span [innerHTML]="account.blog.url" class="user-name"></span
381 ></div>
382 </button>
383 }
384 </mat-menu>
385 <mat-menu #menu="matMenu">
386 @for (option of privacyOptions; track $index) {
387 @if (option.level != 20) {
388 <button (click)="this.privacy = option.level" mat-menu-item>
389 <fa-icon [icon]="option.icon"></fa-icon>
390 {{ option.name }}
391 </button>
392 }
393 }
394 </mat-menu>
395 <div
396 #suggestionsMenu
397 class="suggestions-menu"
398 [hidden]="!suggestionLoading() && !suggestionMatches()"
399 [style.left.px]="cursorPosition.x"
400 [style.top.px]="cursorPosition.y"
401 >
402 <div class="flex flex-column">
403 @if (suggestionLoading()) {
404 <div class="flex gap-3 align-items-center suggestion-item">
405 <app-avatar-small></app-avatar-small>
406 <mat-progress-bar class="max-w-16rem suggestion-text-placeholder" mode="indeterminate"></mat-progress-bar>
407 </div>
408 } @else if (suggestions.length === 0 && emojiSuggestions.length === 0) {
409 <div class="suggestion-item">{{ 'editor.noResults' | translate }}</div>
410 }
411 @for (emoji of emojiSuggestions; track $index) {
412 <div (click)="insertEmoji(emoji)" class="flex gap-2 align-items-center suggestion-item">
413 @if (emoji.img) {
414 <img [src]="emoji.img" class="emoji-suggestion-image" />
415 }
416 {{ emoji.id }}
417 @if (emoji.id !== emoji.name) {
418 {{ emoji.name }}
419 }
420 </div>
421 }
422 @for (user of suggestions; track $index) {
423 <div (click)="insertMention(user)" class="flex gap-2 align-items-center suggestion-item">
424 <app-avatar-small
425 [disabled]="true"
426 [user]="{
427 avatar: user.img,
428 url: user.text,
429 name: user.text,
430 id: ''
431 }"
432 ></app-avatar-small>
433 {{ user.text }}
434 </div>
435 }
436 </div>
437 </div>
438 </div>
439 </mat-card>
440
441 <p style="text-align: center">
442 <span [innerHTML]="'editor.editorGuide' | translate" addClass="mb-3"> </span>
443 </p>