unoffical wafrn mirror wafrn.net
atproto social-network activitypub
at angular21 443 lines 18 kB view raw
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>