unoffical wafrn mirror wafrn.net
atproto social-network activitypub
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: post actions revamp

Adds a copy of the bottom bar icons with its own customizable ordering.

+512 -524
+3 -143
packages/frontend/src/app/components/bottom-reply-bar/bottom-reply-bar.component.html
··· 2 2 <div class="flex align-items-center"> 3 3 @if (notes) { 4 4 <p class="m-0 text-sm"> 5 - <a class="subtle-link notes-link" mat-stroked-button [postLink]="fragment" 5 + <a class="subtle-link notes-link" mat-stroked-button [postLink]="fragment()" 6 6 ><b>{{ notes }}</b> notes</a 7 7 > 8 8 </p> 9 9 } @else { 10 10 <p class="m-0 text-sm"> 11 - <a [postLink]="fragment" class="subtle-link notes-link">View thread</a> 11 + <a [postLink]="fragment()" class="subtle-link notes-link">View thread</a> 12 12 </p> 13 13 } 14 14 </div> 15 - @if (loggedIn()) { 16 - <div class="flex flex-wrap gap-1 action-list" aria-label="Action list"> 17 - @for (button of buttonList; track $index) { 18 - @if (button.enabled) { 19 - @switch (button.value) { 20 - @case ('quote') { 21 - @if (fragment.privacy !== 10 && fragment.privacy !== 1 && fragment.privacy !== 2) { 22 - <button 23 - [disabled]="!fragment.canQuote" 24 - aria-label="Quote woot" 25 - mat-button 26 - color="accent" 27 - (click)="quotePost()" 28 - class="cursor-pointer quote-icon" 29 - > 30 - <fa-icon size="lg" [icon]="quoteIcon" matTooltip="Quote woot"></fa-icon> 31 - </button> 32 - } 33 - } 34 - @case ('rewoot') { 35 - @if (myRewootsIncludePost) { 36 - <button 37 - aria-label="Delete rewoot" 38 - mat-button 39 - color="accent" 40 - (click)="deleteRewoots()" 41 - class="cursor-pointer delete-rewoot-icon" 42 - [disabled]="loadingAction" 43 - > 44 - <fa-icon size="lg" [icon]="closeIcon" matTooltip="Delete rewoot"></fa-icon> 45 - </button> 46 - } @else if (!myRewootsIncludePost && fragment.privacy !== 10 && fragment.privacy !== 1) { 47 - <button 48 - aria-label="Rewoot" 49 - [disabled]="!fragment.canReblog" 50 - mat-button 51 - color="accent" 52 - (click)="quickReblog()" 53 - class="cursor-pointer rewoot-icon" 54 - [disabled]="loadingAction" 55 - > 56 - <fa-icon size="lg" [icon]="quickReblogIcon" matTooltip="Rewoot"></fa-icon> 57 - </button> 58 - } 59 - } 60 - @case ('reply') { 61 - <button 62 - aria-label="Reply woot" 63 - [disabled]="!fragment.canReply" 64 - mat-button 65 - color="accent" 66 - (click)="replyPost()" 67 - class="cursor-pointer reply-icon" 68 - > 69 - <fa-icon size="lg" [icon]="reblogIcon" matTooltip="Reply woot"></fa-icon> 70 - </button> 71 - } 72 - @case ('bookmark') { 73 - @if (bookmarked()) { 74 - <button 75 - aria-label="Unbookmark woot" 76 - mat-button 77 - color="accent" 78 - (click)="unbookmarkPost()" 79 - class="cursor-pointer unlike-icon" 80 - [disabled]="loadingAction" 81 - > 82 - <fa-icon size="lg" [icon]="unbookmarkIcon" matTooltip="Unbookmark woot"></fa-icon> 83 - </button> 84 - } @else { 85 - <button 86 - aria-label="Bookmark woot" 87 - mat-button 88 - color="accent" 89 - (click)="bookmarkPost()" 90 - class="cursor-pointer unlike-icon" 91 - [disabled]="loadingAction" 92 - > 93 - <fa-icon size="lg" [icon]="bookmarkIcon" matTooltip="Bookmark woot"></fa-icon> 94 - </button> 95 - } 96 - } 97 - @case ('like') { 98 - @if (fragment.userId !== myId) { 99 - @if (fragment.userLikesPostRelations.includes(myId)) { 100 - <button 101 - aria-label="Remove like" 102 - mat-button 103 - color="accent" 104 - (click)="unlikePost()" 105 - class="cursor-pointer unlike-icon" 106 - [disabled]="loadingAction" 107 - > 108 - <fa-icon size="lg" [icon]="clearHeartIcon" matTooltip="Remove like"></fa-icon> 109 - </button> 110 - } @else { 111 - <button 112 - aria-label="Like woot" 113 - [disabled]="!fragment.canLike" 114 - mat-button 115 - color="accent" 116 - (click)="likePost()" 117 - class="cursor-pointer like-icon" 118 - [disabled]="loadingAction" 119 - > 120 - <fa-icon size="lg" [icon]="solidHeartIcon" matTooltip="Like woot"></fa-icon> 121 - </button> 122 - } 123 - } 124 - } 125 - @case ('edit') { 126 - @if (fragment.userId === myId) { 127 - <button 128 - aria-label="Edit woot" 129 - mat-button 130 - color="accent" 131 - (click)="editPost(fragment)" 132 - class="cursor-pointer" 133 - > 134 - <fa-icon size="lg" [icon]="editedIcon" matTooltip="Edit woot"></fa-icon> 135 - </button> 136 - } 137 - } 138 - @case ('delete') { 139 - @if (fragment.userId === myId) { 140 - <button 141 - aria-label="Delete woot" 142 - mat-button 143 - color="accent" 144 - (click)="deletePost(fragment.id)" 145 - class="cursor-pointer delete-woot-icon" 146 - > 147 - <fa-icon size="lg" [icon]="deleteIcon" matTooltip="Delete woot"></fa-icon> 148 - </button> 149 - } 150 - } 151 - } 152 - } 153 - } 154 - </div> 155 - } 15 + <app-post-action-buttons [fragment]="fragment()" settingKey="postReplyBarOrder"></app-post-action-buttons> 156 16 </div>
+6 -268
packages/frontend/src/app/components/bottom-reply-bar/bottom-reply-bar.component.ts
··· 1 - import { Component, Input, OnChanges, Signal, signal, SimpleChanges } from '@angular/core' 1 + import { Component, input, Input, viewChild } from '@angular/core' 2 2 import { MatButtonModule } from '@angular/material/button' 3 - import { MatTooltipModule } from '@angular/material/tooltip' 4 3 import { RouterModule } from '@angular/router' 5 - import { FontAwesomeModule } from '@fortawesome/angular-fontawesome' 6 - import { 7 - faArrowUpRightFromSquare, 8 - faBookBookmark, 9 - faBookmark, 10 - faCheck, 11 - faChevronDown, 12 - faClose, 13 - faEnvelope, 14 - faGlobe, 15 - faHeart, 16 - faHeartBroken, 17 - faPen, 18 - faQuoteLeft, 19 - faRepeat, 20 - faReply, 21 - faServer, 22 - faShareNodes, 23 - faTrash, 24 - faUnlock, 25 - faUser 26 - } from '@fortawesome/free-solid-svg-icons' 27 - import { firstValueFrom } from 'rxjs' 28 4 import { PostLinkModule } from 'src/app/directives/post-link/post-link.module' 29 5 import { ProcessedPost } from 'src/app/interfaces/processed-post' 30 - import { DeletePostService } from 'src/app/services/delete-post.service' 31 - import { EditorService } from 'src/app/services/editor.service' 32 - import { LoginService } from 'src/app/services/login.service' 33 - import { MessageService } from 'src/app/services/message.service' 34 - import { PostsService } from 'src/app/services/posts.service' 35 - import { SettingListItem, SettingsService } from 'src/app/services/settings.service' 36 - 37 - export const replyBarItems = ['quote', 'rewoot', 'reply', 'bookmark', 'like', 'edit', 'delete'] as const 38 - type replyBarItemsVariants = typeof replyBarItems 39 - export type ReplyBarItem = replyBarItemsVariants[number] 6 + import { PostActionButtonsComponent } from '../post-action-buttons/post-action-buttons.component' 40 7 41 8 @Component({ 42 9 selector: 'app-bottom-reply-bar', 43 - imports: [RouterModule, FontAwesomeModule, MatButtonModule, MatTooltipModule, PostLinkModule], 10 + imports: [RouterModule, MatButtonModule, PostLinkModule, PostActionButtonsComponent], 44 11 templateUrl: './bottom-reply-bar.component.html', 45 12 styleUrl: './bottom-reply-bar.component.scss' 46 13 }) 47 - export class BottomReplyBarComponent implements OnChanges { 48 - @Input() fragment!: ProcessedPost 49 - @Input() post!: ProcessedPost[] 14 + export class BottomReplyBarComponent { 15 + fragment = input.required<ProcessedPost>() 50 16 @Input() notes: string = '' 51 - loggedIn: Signal<boolean> 52 - isEmptyReblog = false 53 - myId = '' 54 - loadingAction = false 55 - myRewootsIncludePost = false 56 - bookmarked = signal<boolean>(false) 57 - 58 - // icons 59 - shareIcon = faShareNodes 60 - expandDownIcon = faChevronDown 61 - solidHeartIcon = faHeart 62 - clearHeartIcon = faHeartBroken 63 - reblogIcon = faReply 64 - quickReblogIcon = faRepeat 65 - quoteIcon = faQuoteLeft 66 - shareExternalIcon = faArrowUpRightFromSquare 67 - deleteIcon = faTrash 68 - closeIcon = faClose 69 - worldIcon = faGlobe 70 - unlockIcon = faUnlock 71 - envelopeIcon = faEnvelope 72 - serverIcon = faServer 73 - userIcon = faUser 74 - editedIcon = faPen 75 - checkIcon = faCheck 76 - bookmarkIcon = faBookmark 77 - unbookmarkIcon = faBookBookmark 78 - 79 - // Ordering 80 - buttonList: SettingListItem[] 81 - 82 - constructor( 83 - readonly loginService: LoginService, 84 - private readonly postService: PostsService, 85 - private readonly editorService: EditorService, 86 - private readonly deletePostService: DeletePostService, 87 - private readonly messages: MessageService, 88 - private readonly editor: EditorService, 89 - settingsService: SettingsService 90 - ) { 91 - this.loggedIn = loginService.loggedIn 92 - if (this.loggedIn()) { 93 - this.myId = loginService.getLoggedUserUUID() 94 - } 95 - this.buttonList = settingsService.values.postReplyBarOrder as SettingListItem[] 96 - } 97 - 98 - ngOnInit(): void { 99 - this.bookmarked.set(this.fragment.bookmarkers.includes(this.myId)) 100 - } 101 - 102 - ngOnChanges(changes: SimpleChanges): void { 103 - this.myRewootsIncludePost = this.postService.rewootedPosts.includes(this.fragment.id) 104 - 105 - const finalOne = this.fragment 106 - this.isEmptyReblog = 107 - this.fragment && 108 - finalOne.content == '' && 109 - finalOne.tags.length == 0 && 110 - finalOne.quotes.length == 0 && 111 - !finalOne.questionPoll && 112 - finalOne.medias?.length == 0 113 - } 114 - 115 - async replyPost() { 116 - await this.editorService.replyPost(this.fragment) 117 - } 118 - 119 - async quotePost() { 120 - await this.editorService.quotePost(this.fragment) 121 - } 122 - 123 - async editPost(post: ProcessedPost) { 124 - await this.editorService.replyPost(post, true) 125 - } 126 - 127 - async deletePost(id: string) { 128 - this.deletePostService.openDeletePostDialog(id) 129 - } 130 - 131 - async deleteRewoots() { 132 - this.loadingAction = true 133 - const success = await firstValueFrom(this.deletePostService.deleteRewoots(this.fragment.id)) 134 - if (success) { 135 - this.myRewootsIncludePost = false 136 - this.messages.add({ 137 - severity: 'success', 138 - summary: 'You successfully deleted your rewoot' 139 - }) 140 - } else { 141 - this.messages.add({ 142 - severity: 'error', 143 - summary: 'Something went wrong! Check your internet connectivity and try again' 144 - }) 145 - } 146 - this.loadingAction = false 147 - } 148 - 149 - async toggleLike() { 150 - if (this.loadingAction || this.fragment.userId === this.myId) return 151 - 152 - const hasLikedPost = this.fragment.userLikesPostRelations.includes(this.myId) 153 - if (!hasLikedPost) { 154 - this.likePost() 155 - } else { 156 - this.unlikePost() 157 - } 158 - } 159 - 160 - async likePost() { 161 - this.loadingAction = true 162 - if (await this.postService.likePost(this.fragment.id)) { 163 - this.fragment.userLikesPostRelations.push(this.myId) 164 - const disableConfetti = localStorage.getItem('disableConfetti') == 'true' 165 - this.messages.add({ 166 - severity: 'success', 167 - summary: 'You successfully liked this woot', 168 - confettiEmojis: disableConfetti ? [] : ['❤️', '💚', '💙'], 169 - soundName: 'like' 170 - }) 171 - } else { 172 - this.messages.add({ 173 - severity: 'error', 174 - summary: 'Something went wrong. Please try again' 175 - }) 176 - } 177 - this.loadingAction = false 178 - } 179 - 180 - async unlikePost() { 181 - this.loadingAction = true 182 - if (await this.postService.unlikePost(this.fragment.id)) { 183 - this.fragment.userLikesPostRelations = this.fragment.userLikesPostRelations.filter((elem) => elem != this.myId) 184 - this.messages.add({ 185 - severity: 'success', 186 - summary: 'You no longer like this woot' 187 - }) 188 - } else { 189 - this.messages.add({ 190 - severity: 'error', 191 - summary: 'Something went wrong. Please try again' 192 - }) 193 - } 194 - this.loadingAction = false 195 - } 196 - 197 - async toggleBookmark() { 198 - if (!this.bookmarked()) { 199 - this.bookmarkPost() 200 - } else { 201 - this.unbookmarkPost() 202 - } 203 - } 204 - 205 - async bookmarkPost() { 206 - this.loadingAction = true 207 - if (await this.postService.bookmarkPost(this.fragment.id)) { 208 - this.fragment.bookmarkers.push(this.myId) 209 - const disableConfetti = localStorage.getItem('disableConfetti') == 'true' 210 - this.messages.add({ 211 - severity: 'success', 212 - summary: 'You successfully bookmarked this woot', 213 - confettiEmojis: disableConfetti ? [] : ['💾'] 214 - }) 215 - this.bookmarked.set(true) 216 - } else { 217 - this.messages.add({ 218 - severity: 'error', 219 - summary: 'Something went wrong. Please try again' 220 - }) 221 - } 222 - this.loadingAction = false 223 - } 224 - 225 - async unbookmarkPost() { 226 - this.loadingAction = true 227 - if (await this.postService.unbookmarkPost(this.fragment.id)) { 228 - this.fragment.bookmarkers = this.fragment.bookmarkers.filter((elem) => elem != this.myId) 229 - this.messages.add({ 230 - severity: 'success', 231 - summary: 'You successfully unbookmarked this woot' 232 - }) 233 - this.bookmarked.set(false) 234 - } else { 235 - this.messages.add({ 236 - severity: 'error', 237 - summary: 'Something went wrong. Please try again' 238 - }) 239 - } 240 - this.loadingAction = false 241 - } 242 - 243 - async toggleReblog() { 244 - if (!this.myRewootsIncludePost) { 245 - this.quickReblog() 246 - } else { 247 - this.deleteRewoots() 248 - } 249 - } 250 - 251 - async quickReblog() { 252 - this.loadingAction = true 253 - if (this.fragment.privacy !== 10) { 254 - const response = await this.editor.createPost({ 255 - mentionedUsers: [], 256 - content: '', 257 - idPostToReblog: this.fragment.id, 258 - privacy: 0, 259 - media: [] 260 - }) 261 - if (response) { 262 - const disableConfetti = localStorage.getItem('disableConfetti') == 'true' 263 - 264 - this.myRewootsIncludePost = true 265 - this.messages.add({ 266 - severity: 'success', 267 - summary: 'You rewooted the woot!', 268 - confettiEmojis: disableConfetti ? [] : ['🔁'], 269 - soundName: 'sendWoot' 270 - }) 271 - } 272 - } else { 273 - this.messages.add({ 274 - severity: 'warn', 275 - summary: 'Sorry, this woot is not rebloggeable as requested by the user' 276 - }) 277 - } 278 - this.loadingAction = false 279 - } 17 + postActionButtons = viewChild.required(PostActionButtonsComponent) 280 18 }
+132
packages/frontend/src/app/components/post-action-buttons/post-action-buttons.component.html
··· 1 + @if (loggedIn()) { 2 + @let fragment = this.fragment(); 3 + <div class="flex flex-wrap justify-content-evenly gap-1 action-list" aria-label="Action list"> 4 + @for (button of buttonList; track $index) { 5 + @if (button.enabled) { 6 + @switch (button.value) { 7 + @case ('quote') { 8 + @if (fragment.privacy !== 10 && fragment.privacy !== 1 && fragment.privacy !== 2) { 9 + <button 10 + [disabled]="!fragment.canQuote" 11 + aria-label="Quote woot" 12 + mat-button 13 + (click)="quotePost()" 14 + class="cursor-pointer mat-circle-button quote-icon" 15 + > 16 + <fa-icon size="lg" [icon]="quoteIcon" matTooltip="Quote woot"></fa-icon> 17 + </button> 18 + } 19 + } 20 + @case ('rewoot') { 21 + @if (myRewootsIncludePost) { 22 + <button 23 + aria-label="Delete rewoot" 24 + mat-button 25 + (click)="deleteRewoots()" 26 + class="cursor-pointer mat-circle-button delete-rewoot-icon" 27 + [disabled]="loadingAction" 28 + > 29 + <fa-icon size="lg" [icon]="closeIcon" matTooltip="Delete rewoot"></fa-icon> 30 + </button> 31 + } @else if (!myRewootsIncludePost && fragment.privacy !== 10 && fragment.privacy !== 1) { 32 + <button 33 + aria-label="Rewoot" 34 + [disabled]="!fragment.canReblog" 35 + mat-button 36 + (click)="quickReblog()" 37 + class="cursor-pointer mat-circle-button rewoot-icon" 38 + [disabled]="loadingAction" 39 + > 40 + <fa-icon size="lg" [icon]="quickReblogIcon" matTooltip="Rewoot"></fa-icon> 41 + </button> 42 + } 43 + } 44 + @case ('reply') { 45 + <button 46 + aria-label="Reply woot" 47 + [disabled]="!fragment.canReply" 48 + mat-button 49 + (click)="replyPost()" 50 + class="cursor-pointer mat-circle-button reply-icon" 51 + > 52 + <fa-icon size="lg" [icon]="reblogIcon" matTooltip="Reply woot"></fa-icon> 53 + </button> 54 + } 55 + @case ('bookmark') { 56 + @if (bookmarked()) { 57 + <button 58 + aria-label="Unbookmark woot" 59 + mat-button 60 + (click)="unbookmarkPost()" 61 + class="cursor-pointer mat-circle-button unlike-icon" 62 + [disabled]="loadingAction" 63 + > 64 + <fa-icon size="lg" [icon]="unbookmarkIcon" matTooltip="Unbookmark woot"></fa-icon> 65 + </button> 66 + } @else { 67 + <button 68 + aria-label="Bookmark woot" 69 + mat-button 70 + (click)="bookmarkPost()" 71 + class="cursor-pointer mat-circle-button unlike-icon" 72 + [disabled]="loadingAction" 73 + > 74 + <fa-icon size="lg" [icon]="bookmarkIcon" matTooltip="Bookmark woot"></fa-icon> 75 + </button> 76 + } 77 + } 78 + @case ('like') { 79 + @if (fragment.userId !== myId) { 80 + @if (fragment.userLikesPostRelations.includes(myId)) { 81 + <button 82 + aria-label="Remove like" 83 + mat-button 84 + (click)="unlikePost()" 85 + class="cursor-pointer mat-circle-button unlike-icon" 86 + [disabled]="loadingAction" 87 + > 88 + <fa-icon size="lg" [icon]="clearHeartIcon" matTooltip="Remove like"></fa-icon> 89 + </button> 90 + } @else { 91 + <button 92 + aria-label="Like woot" 93 + [disabled]="!fragment.canLike" 94 + mat-button 95 + (click)="likePost()" 96 + class="cursor-pointer mat-circle-button like-icon" 97 + [disabled]="loadingAction" 98 + > 99 + <fa-icon size="lg" [icon]="solidHeartIcon" matTooltip="Like woot"></fa-icon> 100 + </button> 101 + } 102 + } 103 + } 104 + @case ('edit') { 105 + @if (fragment.userId === myId) { 106 + <button 107 + aria-label="Edit woot" 108 + mat-button 109 + (click)="editPost(fragment)" 110 + class="cursor-pointer mat-circle-button edit-woot-icon" 111 + > 112 + <fa-icon size="lg" [icon]="editedIcon" matTooltip="Edit woot"></fa-icon> 113 + </button> 114 + } 115 + } 116 + @case ('delete') { 117 + @if (fragment.userId === myId) { 118 + <button 119 + aria-label="Delete woot" 120 + mat-button 121 + (click)="deletePost(fragment.id)" 122 + class="cursor-pointer mat-circle-button delete-woot-icon" 123 + > 124 + <fa-icon size="lg" [icon]="deleteIcon" matTooltip="Delete woot"></fa-icon> 125 + </button> 126 + } 127 + } 128 + } 129 + } 130 + } 131 + </div> 132 + }
packages/frontend/src/app/components/post-action-buttons/post-action-buttons.component.scss

This is a binary file and will not be displayed.

+282
packages/frontend/src/app/components/post-action-buttons/post-action-buttons.component.ts
··· 1 + import { Component, input, OnChanges, Signal, signal } from '@angular/core' 2 + import { MatButtonModule } from '@angular/material/button' 3 + import { MatTooltipModule } from '@angular/material/tooltip' 4 + import { RouterModule } from '@angular/router' 5 + import { FontAwesomeModule } from '@fortawesome/angular-fontawesome' 6 + import { 7 + faShareNodes, 8 + faChevronDown, 9 + faHeart, 10 + faHeartBroken, 11 + faReply, 12 + faRepeat, 13 + faQuoteLeft, 14 + faArrowUpRightFromSquare, 15 + faTrash, 16 + faClose, 17 + faGlobe, 18 + faUnlock, 19 + faEnvelope, 20 + faServer, 21 + faUser, 22 + faPen, 23 + faCheck, 24 + faBookmark, 25 + faBookBookmark 26 + } from '@fortawesome/free-solid-svg-icons' 27 + import { firstValueFrom } from 'rxjs' 28 + import { PostLinkModule } from 'src/app/directives/post-link/post-link.module' 29 + import { ProcessedPost } from 'src/app/interfaces/processed-post' 30 + import { DeletePostService } from 'src/app/services/delete-post.service' 31 + import { EditorService } from 'src/app/services/editor.service' 32 + import { LoginService } from 'src/app/services/login.service' 33 + import { MessageService } from 'src/app/services/message.service' 34 + import { PostsService } from 'src/app/services/posts.service' 35 + import { SettingKey, SettingListItem, SettingsService } from 'src/app/services/settings.service' 36 + 37 + export const replyBarItems = ['quote', 'rewoot', 'reply', 'bookmark', 'like', 'edit', 'delete'] as const 38 + export type replyBarItemsVariants = typeof replyBarItems 39 + export type ReplyBarItem = replyBarItemsVariants[number] 40 + 41 + @Component({ 42 + selector: 'app-post-action-buttons', 43 + imports: [RouterModule, FontAwesomeModule, MatButtonModule, MatTooltipModule, PostLinkModule], 44 + templateUrl: './post-action-buttons.component.html', 45 + styleUrl: './post-action-buttons.component.scss' 46 + }) 47 + export class PostActionButtonsComponent implements OnChanges { 48 + fragment = input.required<ProcessedPost>() 49 + settingKey = input.required<SettingKey>() 50 + 51 + loggedIn: Signal<boolean> 52 + isEmptyReblog = false 53 + myId = '' 54 + loadingAction = false 55 + myRewootsIncludePost = false 56 + bookmarked = signal<boolean>(false) 57 + 58 + // icons 59 + shareIcon = faShareNodes 60 + expandDownIcon = faChevronDown 61 + solidHeartIcon = faHeart 62 + clearHeartIcon = faHeartBroken 63 + reblogIcon = faReply 64 + quickReblogIcon = faRepeat 65 + quoteIcon = faQuoteLeft 66 + shareExternalIcon = faArrowUpRightFromSquare 67 + deleteIcon = faTrash 68 + closeIcon = faClose 69 + worldIcon = faGlobe 70 + unlockIcon = faUnlock 71 + envelopeIcon = faEnvelope 72 + serverIcon = faServer 73 + userIcon = faUser 74 + editedIcon = faPen 75 + checkIcon = faCheck 76 + bookmarkIcon = faBookmark 77 + unbookmarkIcon = faBookBookmark 78 + 79 + // Ordering 80 + buttonList: SettingListItem[] = [] 81 + 82 + constructor( 83 + readonly loginService: LoginService, 84 + private readonly postService: PostsService, 85 + private readonly editorService: EditorService, 86 + private readonly deletePostService: DeletePostService, 87 + private readonly messages: MessageService, 88 + private readonly editor: EditorService, 89 + private settingsService: SettingsService 90 + ) { 91 + this.loggedIn = loginService.loggedIn 92 + if (this.loggedIn()) { 93 + this.myId = loginService.getLoggedUserUUID() 94 + } 95 + } 96 + 97 + ngOnInit(): void { 98 + this.buttonList = this.settingsService.values[this.settingKey()] as SettingListItem[] 99 + this.bookmarked.set(this.fragment().bookmarkers.includes(this.myId)) 100 + } 101 + 102 + ngOnChanges(): void { 103 + this.myRewootsIncludePost = this.postService.rewootedPosts.includes(this.fragment().id) 104 + 105 + const finalOne = this.fragment() 106 + this.isEmptyReblog = 107 + finalOne && 108 + finalOne.content == '' && 109 + finalOne.tags.length == 0 && 110 + finalOne.quotes.length == 0 && 111 + !finalOne.questionPoll && 112 + finalOne.medias?.length == 0 113 + } 114 + 115 + async replyPost() { 116 + await this.editorService.replyPost(this.fragment()) 117 + } 118 + 119 + async quotePost() { 120 + await this.editorService.quotePost(this.fragment()) 121 + } 122 + 123 + async editPost(post: ProcessedPost) { 124 + await this.editorService.replyPost(post, true) 125 + } 126 + 127 + async deletePost(id: string) { 128 + this.deletePostService.openDeletePostDialog(id) 129 + } 130 + 131 + async deleteRewoots() { 132 + this.loadingAction = true 133 + const success = await firstValueFrom(this.deletePostService.deleteRewoots(this.fragment().id)) 134 + if (success) { 135 + this.myRewootsIncludePost = false 136 + this.messages.add({ 137 + severity: 'success', 138 + summary: 'You successfully deleted your rewoot' 139 + }) 140 + } else { 141 + this.messages.add({ 142 + severity: 'error', 143 + summary: 'Something went wrong! Check your internet connectivity and try again' 144 + }) 145 + } 146 + this.loadingAction = false 147 + } 148 + 149 + async toggleLike() { 150 + if (this.loadingAction || this.fragment().userId === this.myId) return 151 + 152 + const hasLikedPost = this.fragment().userLikesPostRelations.includes(this.myId) 153 + if (!hasLikedPost) { 154 + this.likePost() 155 + } else { 156 + this.unlikePost() 157 + } 158 + } 159 + 160 + async likePost() { 161 + this.loadingAction = true 162 + if (await this.postService.likePost(this.fragment().id)) { 163 + this.fragment().userLikesPostRelations.push(this.myId) 164 + const disableConfetti = localStorage.getItem('disableConfetti') == 'true' 165 + this.messages.add({ 166 + severity: 'success', 167 + summary: 'You successfully liked this woot', 168 + confettiEmojis: disableConfetti ? [] : ['❤️', '💚', '💙'], 169 + soundName: 'like' 170 + }) 171 + } else { 172 + this.messages.add({ 173 + severity: 'error', 174 + summary: 'Something went wrong. Please try again' 175 + }) 176 + } 177 + this.loadingAction = false 178 + } 179 + 180 + async unlikePost() { 181 + this.loadingAction = true 182 + if (await this.postService.unlikePost(this.fragment().id)) { 183 + this.fragment().userLikesPostRelations = this.fragment().userLikesPostRelations.filter( 184 + (elem) => elem != this.myId 185 + ) 186 + this.messages.add({ 187 + severity: 'success', 188 + summary: 'You no longer like this woot' 189 + }) 190 + } else { 191 + this.messages.add({ 192 + severity: 'error', 193 + summary: 'Something went wrong. Please try again' 194 + }) 195 + } 196 + this.loadingAction = false 197 + } 198 + 199 + async toggleBookmark() { 200 + if (!this.bookmarked()) { 201 + this.bookmarkPost() 202 + } else { 203 + this.unbookmarkPost() 204 + } 205 + } 206 + 207 + async bookmarkPost() { 208 + this.loadingAction = true 209 + if (await this.postService.bookmarkPost(this.fragment().id)) { 210 + this.fragment().bookmarkers.push(this.myId) 211 + const disableConfetti = localStorage.getItem('disableConfetti') == 'true' 212 + this.messages.add({ 213 + severity: 'success', 214 + summary: 'You successfully bookmarked this woot', 215 + confettiEmojis: disableConfetti ? [] : ['💾'] 216 + }) 217 + this.bookmarked.set(true) 218 + } else { 219 + this.messages.add({ 220 + severity: 'error', 221 + summary: 'Something went wrong. Please try again' 222 + }) 223 + } 224 + this.loadingAction = false 225 + } 226 + 227 + async unbookmarkPost() { 228 + this.loadingAction = true 229 + if (await this.postService.unbookmarkPost(this.fragment().id)) { 230 + this.fragment().bookmarkers = this.fragment().bookmarkers.filter((elem) => elem != this.myId) 231 + this.messages.add({ 232 + severity: 'success', 233 + summary: 'You successfully unbookmarked this woot' 234 + }) 235 + this.bookmarked.set(false) 236 + } else { 237 + this.messages.add({ 238 + severity: 'error', 239 + summary: 'Something went wrong. Please try again' 240 + }) 241 + } 242 + this.loadingAction = false 243 + } 244 + 245 + async toggleReblog() { 246 + if (!this.myRewootsIncludePost) { 247 + this.quickReblog() 248 + } else { 249 + this.deleteRewoots() 250 + } 251 + } 252 + 253 + async quickReblog() { 254 + this.loadingAction = true 255 + if (this.fragment().privacy !== 10) { 256 + const response = await this.editor.createPost({ 257 + mentionedUsers: [], 258 + content: '', 259 + idPostToReblog: this.fragment().id, 260 + privacy: 0, 261 + media: [] 262 + }) 263 + if (response) { 264 + const disableConfetti = localStorage.getItem('disableConfetti') == 'true' 265 + 266 + this.myRewootsIncludePost = true 267 + this.messages.add({ 268 + severity: 'success', 269 + summary: 'You rewooted the woot!', 270 + confettiEmojis: disableConfetti ? [] : ['🔁'], 271 + soundName: 'sendWoot' 272 + }) 273 + } 274 + } else { 275 + this.messages.add({ 276 + severity: 'warn', 277 + summary: 'Sorry, this woot is not rebloggeable as requested by the user' 278 + }) 279 + } 280 + this.loadingAction = false 281 + } 282 + }
+31 -99
packages/frontend/src/app/components/post-actions/post-actions.component.html
··· 1 1 @let post = this.post(); 2 + @let isBskyPost = !!post.bskyUri; 3 + @let isExternalPost = post.user.url.startsWith('@') && post.privacy !== 1 && post.privacy !== 10; 4 + @let isMyPost = myId == post.userId && post.privacy != 2; 5 + @let hasPostActions = isBskyPost || isExternalPost || isMyPost || loggedIn(); 2 6 <div class="flex gap-0 flex-nowrap" id="post-actions"> 3 7 <button 4 8 aria-label="Share post" 5 9 mat-button 6 10 class="split-button-left" 7 - (click)="sharePost()" 11 + [matMenuTriggerFor]="shareMenu" 8 12 matTooltip="Copy Wafrn link" 9 13 > 10 - <fa-icon [icon]="shareIcon"></fa-icon> 14 + <fa-icon [icon]="shareMenuIcon"></fa-icon> 11 15 </button> 12 - <button 13 - aria-label="Post actions" 14 - mat-button 15 - class="relative split-button-right" 16 - matTooltip="Post actions" 17 - [matMenuTriggerFor]="menu" 18 - > 19 - <fa-icon [icon]="expandDownIcon"></fa-icon> 20 - </button> 16 + @if (hasPostActions) { 17 + <button 18 + aria-label="Post actions" 19 + mat-button 20 + class="relative split-button-right" 21 + matTooltip="Post actions" 22 + [matMenuTriggerFor]="menu" 23 + > 24 + <fa-icon [icon]="expandDownIcon"></fa-icon> 25 + </button> 26 + } 21 27 </div> 22 - <mat-menu #menu="matMenu" xPosition="before" id="post-actions-menu"> 28 + <mat-menu #shareMenu="matMenu" xPosition="before" id="post-actions-menu"> 23 29 <ng-template matMenuContent> 24 30 <button (click)="sharePost()" mat-menu-item> 25 31 <span class="post-actions-menu-span-content"> ··· 27 33 {{ 'post-actions.shareUrl' | translate }} 28 34 </span> 29 35 </button> 30 - @if (post.remotePostId && post.user.url.startsWith('@')) { 36 + @if (true || (post.remotePostId && post.user.url.startsWith('@'))) { 31 37 <a (click)="shareOriginalPost()" mat-menu-item> 32 38 <span class="post-actions-menu-span-content"> 33 39 <fa-icon [fixedWidth]="true" [icon]="shareExternalIcon"></fa-icon> ··· 35 41 </span> 36 42 </a> 37 43 } 38 - @if (post.bskyUri) { 44 + </ng-template> 45 + </mat-menu> 46 + <mat-menu #menu="matMenu" xPosition="before" id="post-actions-menu"> 47 + <ng-template matMenuContent> 48 + <div class="post-action-buttons" mat-menu-item> 49 + <app-post-action-buttons [fragment]="post" settingKey="postActionsButtonBarOrder"></app-post-action-buttons> 50 + </div> 51 + <hr class="my-0" /> 52 + @if (isBskyPost) { 39 53 <a [href]="bskyUrl()" target="_blank" mat-menu-item> 40 54 <span class="post-actions-menu-span-content"> 41 55 <fa-icon [fixedWidth]="true" [icon]="bskyIcon"></fa-icon> ··· 43 57 </span> 44 58 </a> 45 59 } 46 - @if (post.user.url.startsWith('@') && post.privacy !== 1 && post.privacy !== 10) { 60 + @if (isExternalPost) { 47 61 <a [href]="this.externalUrl()" target="_blank" mat-menu-item> 48 62 <span class="post-actions-menu-span-content"> 49 63 <fa-icon [fixedWidth]="true" [icon]="goExternalPost"></fa-icon> ··· 51 65 </span> 52 66 </a> 53 67 } 54 - @if (myId == post.userId && post.privacy != 2) { 68 + @if (isMyPost) { 55 69 <button (click)="forceRefederate()" mat-menu-item> 56 70 <span class="post-actions-menu-span-content"> 57 - <fa-icon [fixedWidth]="true" [icon]="globeIcon"></fa-icon> 71 + <fa-icon [fixedWidth]="true" [icon]="refederateIcon"></fa-icon> 58 72 {{ 'post-actions.forceRefederate' | translate }} 59 73 </span> 60 74 </button> 61 75 } 62 76 @if (loggedIn()) { 63 - <button [disabled]="!post.canReply" (click)="replyPost()" mat-menu-item> 64 - <span class="post-actions-menu-span-content"> 65 - <fa-icon [fixedWidth]="true" [icon]="reblogIcon"></fa-icon> 66 - {{ 'post-actions.replyPost' | translate }} 67 - </span> 68 - </button> 69 - @if (myRewootsIncludePost) { 70 - <button (click)="deleteRewoots()" mat-menu-item> 71 - <span class="post-actions-menu-span-content"> 72 - <fa-icon [fixedWidth]="true" [icon]="closeIcon"></fa-icon> 73 - {{ 'post-actions.deleteRewootPost' | translate }} 74 - </span> 75 - </button> 76 - } @else { 77 - @if (post.privacy !== 10 && post.privacy !== 1) { 78 - <button [disabled]="!post.canReblog" (click)="quickReblog()" mat-menu-item> 79 - <span class="post-actions-menu-span-content"> 80 - <fa-icon [fixedWidth]="true" [icon]="quickReblogIcon"></fa-icon> 81 - {{ 'post-actions.rewootPost' | translate }} 82 - </span> 83 - </button> 84 - } 85 - } 86 - @if (post.privacy !== 10 && post.privacy !== 1 && post.privacy !== 2) { 87 - <button [disabled]="!post.canQuote" (click)="quoteWoot()" mat-menu-item> 88 - <span class="post-actions-menu-span-content"> 89 - <fa-icon [fixedWidth]="true" [icon]="quoteIcon"></fa-icon> 90 - {{ 'post-actions.quotePost' | translate }} 91 - </span> 92 - </button> 93 - } 94 - @if (bookmarked()) { 95 - <button (click)="unbookmarkPost()" mat-menu-item> 96 - <span class="post-actions-menu-span-content"> 97 - <fa-icon [fixedWidth]="true" [icon]="unbookmarkIcon"></fa-icon> 98 - {{ 'post-actions.unbookmarkPost' | translate }} 99 - </span> 100 - </button> 101 - } @else { 102 - <button (click)="bookmarkPost()" mat-menu-item> 103 - <span class="post-actions-menu-span-content"> 104 - <fa-icon [fixedWidth]="true" [icon]="bookmarkIcon"></fa-icon> 105 - {{ 'post-actions.bookmarkPost' | translate }} 106 - </span> 107 - </button> 108 - } 109 - @if (post.userId !== myId) { 110 - @if (post.userLikesPostRelations.includes(myId)) { 111 - <button (click)="unlikePost()" mat-menu-item> 112 - <span class="post-actions-menu-span-content"> 113 - <fa-icon [fixedWidth]="true" [icon]="clearHeartIcon"></fa-icon> 114 - {{ 'post-actions.dislikePost' | translate }} 115 - </span> 116 - </button> 117 - } @else { 118 - <button [disabled]="!post.canLike" (click)="likePost()" mat-menu-item> 119 - <span class="post-actions-menu-span-content"> 120 - <fa-icon [fixedWidth]="true" [icon]="solidHeartIcon"></fa-icon> 121 - {{ 'post-actions.likePost' | translate }} 122 - </span> 123 - </button> 124 - } 125 - <button (click)="reportPost()" mat-menu-item> 126 - <span class="post-actions-menu-span-content"> 127 - <fa-icon [fixedWidth]="true" [icon]="reportIcon"></fa-icon> 128 - {{ 'post-actions.reportPost' | translate }} 129 - </span> 130 - </button> 131 - } @else { 132 - <button (click)="editPost()" mat-menu-item> 133 - <span class="post-actions-menu-span-content"> 134 - <fa-icon [fixedWidth]="true" [icon]="editedIcon"></fa-icon> 135 - {{ 'post-actions.editPost' | translate }} 136 - </span> 137 - </button> 138 - <button (click)="deletePost()" mat-menu-item> 139 - <span class="post-actions-menu-span-content"> 140 - <fa-icon [fixedWidth]="true" [icon]="deleteIcon"></fa-icon> 141 - {{ 'post-actions.deletePost' | translate }} 142 - </span> 143 - </button> 144 - } 145 77 @if (!postSilenced) { 146 78 <button (click)="silencePost()" mat-menu-item> 147 79 <span class="post-actions-menu-span-content">
+8
packages/frontend/src/app/components/post-actions/post-actions.component.scss
··· 1 + .post-action-buttons { 2 + --mat-menu-item-hover-state-layer-color: transparent; 3 + } 4 + 1 5 .post-actions > button { 2 6 aspect-ratio: 1 / 1; 3 7 } ··· 23 27 margin-inline: calc(-1 * var(--mat-button-text-horizontal-padding, 12px)); 24 28 margin-block: calc(-1 / 2 * ((var(--mat-button-text-container-height, 40px) - 24px))); 25 29 } 30 + 31 + fa-icon { 32 + color: var(--mat-sys-primary); 33 + }
+8 -4
packages/frontend/src/app/components/post-actions/post-actions.component.ts
··· 20 20 faClose, 21 21 faBookmark, 22 22 faBookBookmark, 23 - faCommentSlash 23 + faCommentSlash, 24 + faLink, 25 + faPaperPlane 24 26 } from '@fortawesome/free-solid-svg-icons' 25 27 import { MatButtonModule } from '@angular/material/button' 26 28 import { MatMenuModule } from '@angular/material/menu' ··· 37 39 import { faBluesky } from '@fortawesome/free-brands-svg-icons' 38 40 import { TranslateModule } from '@ngx-translate/core' 39 41 import { SettingsService } from 'src/app/services/settings.service' 42 + import { PostActionButtonsComponent } from '../post-action-buttons/post-action-buttons.component' 40 43 41 44 @Component({ 42 45 selector: 'app-post-actions', 43 - imports: [MatButtonModule, MatMenuModule, FontAwesomeModule, TranslateModule], 46 + imports: [PostActionButtonsComponent, MatButtonModule, MatMenuModule, FontAwesomeModule, TranslateModule], 44 47 templateUrl: './post-actions.component.html', 45 48 styleUrl: './post-actions.component.scss' 46 49 }) ··· 63 66 externalUrl = computed<string>(() => (this.post().bskyUri ? this.bskyUrl() : this.post().remotePostId)) 64 67 65 68 // icons 66 - shareIcon = faShareNodes 69 + shareIcon = faLink 70 + shareMenuIcon = faShareNodes 67 71 expandDownIcon = faChevronDown 68 72 solidHeartIcon = faHeart 69 73 clearHeartIcon = faHeartBroken ··· 82 86 quoteIcon = faQuoteLeft 83 87 bookmarkIcon = faBookmark 84 88 unbookmarkIcon = faBookBookmark 85 - globeIcon = faGlobe 89 + refederateIcon = faPaperPlane 86 90 87 91 constructor( 88 92 private messages: MessageService,
+1 -1
packages/frontend/src/app/components/post/post.component.html
··· 76 76 } 77 77 } 78 78 79 - <app-bottom-reply-bar [fragment]="finalPost()" [post]="post" [notes]="notes()"></app-bottom-reply-bar> 79 + <app-bottom-reply-bar [fragment]="finalPost()" [notes]="notes()"></app-bottom-reply-bar> 80 80 </mat-card>
+5 -5
packages/frontend/src/app/components/post/post.component.ts
··· 206 206 207 207 switch (action) { 208 208 case 'likePost': 209 - await this.bottomReplyBar()?.toggleLike() 209 + await this.bottomReplyBar().postActionButtons()?.toggleLike() 210 210 break 211 211 case 'rewootPost': 212 - await this.bottomReplyBar()?.toggleReblog() 212 + await this.bottomReplyBar().postActionButtons()?.toggleReblog() 213 213 break 214 214 case 'replyPost': 215 - await this.bottomReplyBar()?.replyPost() 215 + await this.bottomReplyBar().postActionButtons()?.replyPost() 216 216 break 217 217 case 'quotePost': 218 - await this.bottomReplyBar()?.quotePost() 218 + await this.bottomReplyBar().postActionButtons()?.quotePost() 219 219 break 220 220 case 'bookmarkPost': 221 - await this.bottomReplyBar()?.toggleBookmark() 221 + await this.bottomReplyBar().postActionButtons()?.toggleBookmark() 222 222 break 223 223 default: 224 224 break
+31 -1
packages/frontend/src/app/services/settings.service.ts
··· 34 34 import { SettingChangePasswordComponent } from '../components/setting-change-password/setting-change-password.component' 35 35 import { SettingDropListComponent } from '../components/setting-drop-list/setting-drop-list.component' 36 36 import { SETTINGS_TOKEN } from '../pages/settings/settings.component' 37 - import { replyBarItems } from '../components/bottom-reply-bar/bottom-reply-bar.component' 37 + import { replyBarItems } from '../components/post-action-buttons/post-action-buttons.component' 38 38 39 39 // All setting keys for use throughout the app 40 40 const settingKeyVariants = [ ··· 72 72 'replaceAIWithCocaine', 73 73 'replaceAIWord', 74 74 'postReplyBarOrder', 75 + 'postActionsButtonBarOrder', 75 76 'atprotoLinkDestination' 76 77 ] as const 77 78 type SettingKeyTuple = typeof settingKeyVariants ··· 454 455 convertFromStorage: this.convertListFrom, 455 456 convertToStorage: this.convertListTo 456 457 }, 458 + postActionsButtonBarOrder: { 459 + key: 'postActionsButtonBarOrder', 460 + translationKey: 'settings.postActionsButtonBarOrder', 461 + translationDescriptionKey: 'settings.postActionsButtonBarOrderDescription', 462 + serverKey: 'wafrn.postActionsButtonBarOrder', 463 + localStorageKey: 'postActionsButtonBarOrder', 464 + type: 'list', 465 + default: this.convertToListDefault([...replyBarItems]), 466 + dropListData: { 467 + // Duplicate from above 468 + quote: { icon: faQuoteLeft, translationKey: 'settings.postReplyBarOrderOptions.quote' }, 469 + rewoot: { icon: faRepeat, translationKey: 'settings.postReplyBarOrderOptions.rewoot' }, 470 + reply: { icon: faReply, translationKey: 'settings.postReplyBarOrderOptions.reply' }, 471 + bookmark: { icon: faBookmark, translationKey: 'settings.postReplyBarOrderOptions.bookmark' }, 472 + like: { icon: faHeart, translationKey: 'settings.postReplyBarOrderOptions.like' }, 473 + edit: { icon: faPen, translationKey: 'settings.postReplyBarOrderOptions.edit' }, 474 + delete: { icon: faTrash, translationKey: 'settings.postReplyBarOrderOptions.delete' } 475 + }, 476 + convertFromStorage: this.convertListFrom, 477 + convertToStorage: this.convertListTo 478 + }, 457 479 atprotoLinkDestination: { 458 480 key: 'atprotoLinkDestination', 459 481 translationKey: 'settings.atprotoLinkDestination', ··· 518 540 SettingDropListComponent, 519 541 null, 520 542 this.makeInject({ settingKey: 'postReplyBarOrder' }) 543 + ) 544 + }, 545 + { 546 + type: 'component', 547 + value: new ComponentPortal( 548 + SettingDropListComponent, 549 + null, 550 + this.makeInject({ settingKey: 'postActionsButtonBarOrder' }) 521 551 ) 522 552 }, 523 553 { type: 'separator' },
+5 -3
packages/frontend/src/assets/i18n/en.json
··· 382 382 "like": "Like", 383 383 "edit": "Edit", 384 384 "delete": "Delete" 385 - } 385 + }, 386 + "postActionsButtonBarOrder": "Post actions menu button icon order", 387 + "postActionsButtonBarOrderDescription": "Items show in the post actions menu. Some items are shown depending on you are the poster." 386 388 }, 387 389 "dialog": { 388 390 "confirm": "Confirm", ··· 440 442 "smoothScroll": "Smooth scroll" 441 443 }, 442 444 "post-actions": { 443 - "shareUrl": "Share with Wafrn", 444 - "shareExternalUrl": "Share external URL", 445 + "shareUrl": "Copy Wafrn URL", 446 + "shareExternalUrl": "Copy external URL", 445 447 "viewOnAtproto": "View on Atproto", 446 448 "forceRefederate": "Force refederate post", 447 449 "viewOriginalPost": "View original post",