unoffical wafrn mirror
wafrn.net
atproto
social-network
activitypub
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}