unoffical wafrn mirror wafrn.net
atproto social-network activitypub
at angular21 245 lines 8.1 kB view raw
1import { Component, computed, ElementRef, EventEmitter, input, OnDestroy, OnInit, Output, signal, viewChild, viewChildren, inject } from '@angular/core' 2import { ProcessedPost } from 'src/app/interfaces/processed-post' 3import { LoginService } from 'src/app/services/login.service' 4import { PostsService } from 'src/app/services/posts.service' 5 6import { 7 faArrowUpRightFromSquare, 8 faCheck, 9 faChevronDown, 10 faClose, 11 faEnvelope, 12 faGlobe, 13 faHeart, 14 faHeartBroken, 15 faPen, 16 faQuoteLeft, 17 faRepeat, 18 faReply, 19 faRobot, 20 faServer, 21 faShareNodes, 22 faTrash, 23 faUnlock, 24 faUser 25} from '@fortawesome/free-solid-svg-icons' 26import { SimplifiedUser } from 'src/app/interfaces/simplified-user' 27import { EnvironmentService } from 'src/app/services/environment.service' 28import { Subject, Subscription } from 'rxjs' 29import { BottomReplyBarComponent } from '../bottom-reply-bar/bottom-reply-bar.component' 30import { HotkeyAction } from 'src/app/services/hotkey.service' 31import { PostFragmentComponent } from '../post-fragment/post-fragment.component' 32 33@Component({ 34 selector: 'app-post', 35 templateUrl: './post.component.html', 36 styleUrls: ['./post.component.scss'], 37 standalone: false 38}) 39export class PostComponent implements OnInit, OnDestroy { 40 postService = inject(PostsService); 41 private readonly loginService = inject(LoginService); 42 43 post = input.required<ProcessedPost[]>() 44 postSliced: ProcessedPost[] = [] 45 46 active = input<boolean>(false) 47 showFull: boolean = false 48 postCanExpand = computed(() => { 49 const textLength = this.post() 50 .map((elem) => elem.content) 51 .join('').length 52 53 const textIsLong = textLength > 2500 54 const threadHasMorePosts = this.postSliced.length !== this.post().length 55 56 return (((textIsLong || !this.showFull) && !this.expanded()) || threadHasMorePosts) && !this.startExpanded() 57 }) 58 startExpanded = input<boolean>(false) 59 scrollToPost = input<boolean>(false) 60 61 postsExpanded = EnvironmentService.environment.shortenPosts 62 expanded = signal(false) 63 finalPosts = computed(() => this.post().slice(-2)) 64 mediaBaseUrl = EnvironmentService.environment.baseMediaUrl 65 followedUsers: string[] = [] 66 notYetAcceptedFollows: string[] = [] 67 notes = computed(() => this.uniquePost().notes.toString()) 68 headerText = computed(() => (this.isEmptyReblog() ? 'rewooted' : 'replied')) 69 quickReblogBeingDone = false 70 quickReblogDoneSuccessfully = false 71 reblogging = false 72 myId: string = '' 73 loadingAction = false 74 // 0 no display at all 1 display like 2 display dislike 75 showLikeFinalPost = computed(() => 76 this.finalPost().userId === this.myId ? (this.finalPost().userLikesPostRelations.includes(this.myId) ? 2 : 1) : 0 77 ) 78 // Last post including empty reblog 79 uniquePost = computed(() => this.post()[this.post().length - 1]) 80 // Last non-empty reblog post 81 finalPost = computed(() => 82 this.isEmptyReblog() && this.post().length > 1 83 ? this.post()[this.post().length - 2] 84 : this.post()[this.post().length - 1] 85 ) 86 87 postElemRefs = viewChildren<PostFragmentComponent, ElementRef<HTMLElement>>(PostFragmentComponent, { 88 read: ElementRef<HTMLElement> 89 }) 90 91 // icons 92 shareIcon = faShareNodes 93 expandDownIcon = faChevronDown 94 solidHeartIcon = faHeart 95 clearHeartIcon = faHeartBroken 96 replyIcon = faReply 97 reblogIcon = faRepeat 98 quoteIcon = faQuoteLeft 99 shareExternalIcon = faArrowUpRightFromSquare 100 deleteIcon = faTrash 101 closeIcon = faClose 102 worldIcon = faGlobe 103 unlockIcon = faUnlock 104 envelopeIcon = faEnvelope 105 serverIcon = faServer 106 userIcon = faUser 107 editedIcon = faPen 108 checkIcon = faCheck 109 botIcon = faRobot 110 111 // bottom bar for controls 112 bottomReplyBar = viewChild.required(BottomReplyBarComponent) 113 114 // subscriptions 115 updateFollowersSubscription 116 updateLikesSubscription: Subscription | undefined 117 actionSubscription = input<Subject<HotkeyAction>>() 118 119 // post seen 120 @Output() seenEmitter: EventEmitter<boolean> = new EventEmitter<boolean>() 121 122 // dismiss cw 123 showCw = true 124 125 // VARIABLES FOR TEMPLATE RENDERING 126 ribbonUser: SimplifiedUser | undefined 127 ribbonIcon = this.replyIcon 128 ribbonTime = new Date(0) 129 130 // detect is safari ios because flicker bug on webkit https://stackoverflow.com/questions/3007480/determine-if-user-navigated-from-mobile-safaris 131 ua = window.navigator.userAgent 132 iOS = !!this.ua.match(/iPad/i) || !!this.ua.match(/iPhone/i) 133 webkit = !!this.ua.match(/WebKit/i) 134 iOSSafari = this.iOS && this.webkit && !this.ua.match(/CriOS/i) 135 136 constructor() { 137 const loginService = this.loginService; 138 139 if (this.loginService.loggedIn.value) { 140 this.myId = loginService.getLoggedUserUUID() 141 } 142 this.updateFollowersSubscription = this.postService.updateFollowers.subscribe(() => { 143 this.followedUsers = this.postService.followedUserIds 144 this.notYetAcceptedFollows = this.postService.notYetAcceptedFollowedUsersIds 145 }) 146 } 147 148 ngOnDestroy(): void { 149 this.updateFollowersSubscription.unsubscribe() 150 this.updateLikesSubscription?.unsubscribe() 151 } 152 153 ngOnInit(): void { 154 this.followedUsers = this.postService.followedUserIds 155 this.notYetAcceptedFollows = this.postService.notYetAcceptedFollowedUsersIds 156 157 // Do not auto-expand ultra-hell threads 158 const threadIsExtremelyLong = this.post().length - this.postsExpanded > 50 159 if (this.startExpanded() && !threadIsExtremelyLong) { 160 this.postSliced = this.post() 161 } else { 162 this.postSliced = this.post().slice(0, EnvironmentService.environment.shortenPosts) 163 } 164 165 if (this.post().length === this.postSliced.length) { 166 this.showFull = true 167 } 168 169 this.ribbonUser = this.uniquePost().user 170 this.ribbonIcon = this.headerText() === 'replied' ? this.replyIcon : this.reblogIcon 171 this.ribbonTime = this.uniquePost().createdAt 172 // If user has marked autoexpand we force 1 expand. Doing full could cause EXPLOSIONS 173 if (localStorage.getItem('automaticalyExpandPosts') === 'true') { 174 this.expandPost() 175 } 176 177 this.updateLikesSubscription = this.postService.postLiked.subscribe((likeEvent) => { 178 if (this.post() && likeEvent.id === this.uniquePost().id) { 179 if (likeEvent.like) { 180 this.uniquePost().userLikesPostRelations = [this.loginService.getLoggedUserUUID()] 181 } else { 182 this.uniquePost().userLikesPostRelations = [] 183 } 184 } 185 }) 186 187 this.actionSubscription()?.subscribe((action) => this.handlePostActions(action)) 188 } 189 190 ngAfterViewInit() { 191 if (this.startExpanded()) { 192 const lastPost = this.postElemRefs().at(-1) 193 194 // Scroll as component is loaded and queue up another scroll once the page is fully loaded (evil) 195 // Causes mild screen flash 196 lastPost?.nativeElement.scrollIntoView({ behavior: 'instant', block: 'center' }) 197 setTimeout(() => { 198 lastPost?.nativeElement.scrollIntoView({ behavior: 'instant', block: 'center' }) 199 }) 200 } 201 } 202 203 isEmptyReblog() { 204 const finalOne = this.uniquePost() 205 return !finalOne 206 ? true 207 : this.post() && 208 finalOne.content == '' && 209 finalOne.tags.length == 0 && 210 finalOne.quotes.length == 0 && 211 !finalOne.questionPoll && 212 finalOne.medias?.length == 0 213 } 214 215 // Adds 50 more posts to the sliced list 216 expandPost() { 217 this.expanded.set(true) 218 this.postsExpanded += 50 219 this.postSliced = this.post().slice(0, this.postsExpanded) 220 } 221 222 async handlePostActions(action: HotkeyAction) { 223 if (!this.active()) return 224 225 switch (action) { 226 case 'likePost': 227 await this.bottomReplyBar().postActionButtons()?.toggleLike() 228 break 229 case 'rewootPost': 230 await this.bottomReplyBar().postActionButtons()?.toggleReblog() 231 break 232 case 'replyPost': 233 await this.bottomReplyBar().postActionButtons()?.replyPost() 234 break 235 case 'quotePost': 236 await this.bottomReplyBar().postActionButtons()?.quotePost() 237 break 238 case 'bookmarkPost': 239 await this.bottomReplyBar().postActionButtons()?.toggleBookmark() 240 break 241 default: 242 break 243 } 244 } 245}