A modified version of Wafrn used on https://wf.jbc.lol (mirror of https://git.jbc.lol/jbcrn/wf.jbc.lol which is a mirror of https://codeberg.org/jbcarreon123/wf.jbc.lol)
1
fork

Configure Feed

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

Merge pull request 'Add raw JSON button, if enabled' (#320) from jbcarreon123/wafrn:feature/raw-jsonld into development

Reviewed-on: https://codeberg.org/wafrn/wafrn/pulls/320

gabboman e29e7034 0654586b

+413 -323
+3
.env.example
··· 53 53 POSTGRES_METRICS_PASSWORD= 54 54 POSTGRES_METRICS_DBNAME=pgwatch_metrics 55 55 GF_SECURITY_ADMIN_PASSWORD= 56 + 57 + # Debugging 58 + ENABLE_RAW_OUTPUT=false
+19 -48
package-lock.json
··· 14 14 ], 15 15 "dependencies": { 16 16 "cheerio": "^1.1.0", 17 + "nxt-json-view": "^20.0.0", 17 18 "tsx": "^4.19.1" 18 19 }, 19 20 "devDependencies": { ··· 11959 11960 "node": ">=0.2.0" 11960 11961 } 11961 11962 }, 11962 - "node_modules/bufferutil": { 11963 - "version": "4.0.9", 11964 - "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.9.tgz", 11965 - "integrity": "sha512-WDtdLmJvAuNNPzByAYpRo2rF1Mmradw6gvWsQKf63476DDXmomT9zUiGypLcG4ibIM67vhAj8jJRdbmEws2Aqw==", 11966 - "hasInstallScript": true, 11967 - "license": "MIT", 11968 - "optional": true, 11969 - "peer": true, 11970 - "dependencies": { 11971 - "node-gyp-build": "^4.3.0" 11972 - }, 11973 - "engines": { 11974 - "node": ">=6.14.2" 11975 - } 11976 - }, 11977 11963 "node_modules/bullmq": { 11978 11964 "version": "5.61.2", 11979 11965 "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.61.2.tgz", ··· 14544 14530 "express": "^4.0.0 || ^5.0.0-alpha.1" 14545 14531 } 14546 14532 }, 14547 - "node_modules/express-ws/node_modules/utf-8-validate": { 14548 - "version": "5.0.10", 14549 - "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", 14550 - "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", 14551 - "hasInstallScript": true, 14552 - "license": "MIT", 14553 - "optional": true, 14554 - "peer": true, 14555 - "dependencies": { 14556 - "node-gyp-build": "^4.3.0" 14557 - }, 14558 - "engines": { 14559 - "node": ">=6.14.2" 14560 - } 14561 - }, 14562 14533 "node_modules/express-ws/node_modules/ws": { 14563 14534 "version": "7.5.10", 14564 14535 "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", ··· 17767 17738 "integrity": "sha512-SL0JY3DaxylDuo/MecFeiC+7pedM0zia33zl0vcjgwcq1q1FWWF1To9EIauPbl8GbMCU0R2e0uJ8bZunhYKD2g==", 17768 17739 "dev": true, 17769 17740 "license": "MIT", 17741 + "peer": true, 17770 17742 "dependencies": { 17771 17743 "cli-truncate": "^4.0.0", 17772 17744 "colorette": "^2.0.20", ··· 19459 19431 "integrity": "sha512-ujSMe1OWVn55euT1ihwCI1ZcAaAU3nxUiDwfDQldc51ZXaB9m2AyOn6/jh1BLe2t/G8xd6uKG1UBF2aZJeg2SQ==", 19460 19432 "license": "MIT" 19461 19433 }, 19434 + "node_modules/nxt-json-view": { 19435 + "version": "20.0.0", 19436 + "resolved": "https://registry.npmjs.org/nxt-json-view/-/nxt-json-view-20.0.0.tgz", 19437 + "integrity": "sha512-//xUAboII2oPbixtW4mSzZ6J3PVlX4dQ/JxtiLqwv/5lfgu7Cpbo5hhsud+H1YOkybyq5smjt6FLWlytqB2JQA==", 19438 + "license": "MIT", 19439 + "dependencies": { 19440 + "tslib": "^2.8.1" 19441 + }, 19442 + "peerDependencies": { 19443 + "@angular/common": "^20.0.0", 19444 + "@angular/core": "^20.0.0" 19445 + } 19446 + }, 19462 19447 "node_modules/object-assign": { 19463 19448 "version": "4.1.1", 19464 19449 "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", ··· 24600 24585 "integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==", 24601 24586 "license": "MIT" 24602 24587 }, 24603 - "node_modules/utf-8-validate": { 24604 - "version": "6.0.5", 24605 - "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-6.0.5.tgz", 24606 - "integrity": "sha512-EYZR+OpIXp9Y1eG1iueg8KRsY8TuT8VNgnanZ0uA3STqhHQTLwbl+WX76/9X5OY12yQubymBpaBSmMPkSTQcKA==", 24607 - "hasInstallScript": true, 24608 - "license": "MIT", 24609 - "optional": true, 24610 - "peer": true, 24611 - "dependencies": { 24612 - "node-gyp-build": "^4.3.0" 24613 - }, 24614 - "engines": { 24615 - "node": ">=6.14.2" 24616 - } 24617 - }, 24618 24588 "node_modules/util-deprecate": { 24619 24589 "version": "1.0.2", 24620 24590 "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", ··· 24724 24694 "integrity": "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==", 24725 24695 "dev": true, 24726 24696 "license": "MIT", 24697 + "peer": true, 24727 24698 "dependencies": { 24728 24699 "esbuild": "^0.25.0", 24729 24700 "fdir": "^6.5.0", ··· 25093 25064 "integrity": "sha512-QcQ72gh8a+7JO63TAx/6XZf/CWhgMzu5m0QirvPfGvptOusAxG12w2+aua1Jkjr7hzaWDnJ2n6JFeexMHI+Zjg==", 25094 25065 "dev": true, 25095 25066 "license": "MIT", 25067 + "peer": true, 25096 25068 "dependencies": { 25097 25069 "@types/bonjour": "^3.5.13", 25098 25070 "@types/connect-history-api-fallback": "^1.5.4", ··· 26021 25993 "version": "0.15.1", 26022 25994 "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.15.1.tgz", 26023 25995 "integrity": "sha512-XE96n56IQpJM7NAoXswY3XRLcWFW83xe0BiAOeMD7K5k5xecOeul3Qcpx6GqEeeHNkW5DWL5zOyTbEfB4eti8w==", 26024 - "license": "MIT", 26025 - "peer": true 25996 + "license": "MIT" 26026 25997 }, 26027 25998 "packages/backend": { 26028 25999 "name": "wafrn-backend", ··· 26703 26674 } 26704 26675 } 26705 26676 } 26706 - } 26677 + }
+1
package.json
··· 50 50 }, 51 51 "dependencies": { 52 52 "cheerio": "^1.1.0", 53 + "nxt-json-view": "^20.0.0", 53 54 "tsx": "^4.19.1" 54 55 } 55 56 }
+2 -1
packages/backend/environment.example.ts
··· 120 120 externalCacheurl: '${{FRONTEND_CACHE_URL}}', 121 121 shortenPosts: ${{FRONTEND_SHORTEN_POSTS:-3}}, 122 122 disablePWA: ${{FRONTEND_DISABLE_PWA:-false}}, 123 - maintenance: ${{FRONTEND_MAINTENANCE:-false}} 123 + maintenance: ${{FRONTEND_MAINTENANCE:-false}}, 124 + enableRawOutput: ${{ENABLE_RAW_OUTPUT:-false}} 124 125 } 125 126 }
+2 -1
packages/backend/environment.local.example.ts
··· 117 117 externalCacheurl: '/api/cache?media=', 118 118 shortenPosts: 3, 119 119 disablePWA: false, 120 - maintenance: false 120 + maintenance: false, 121 + enableRawOutput: true 121 122 } 122 123 }
+28 -16
packages/backend/routes/activitypub/activitypub.ts
··· 12 12 import { userToJSONLD } from '../../utils/activitypub/userToJSONLD.js' 13 13 import { completeEnvironment } from '../../utils/backendOptions.js' 14 14 import { activityPubObject } from '../../interfaces/fediverse/activityPubObject.js' 15 - import { getPostUrlForQuote } from '../../utils/activitypub/postToJSONLD.js' 15 + import { getPostUrlForQuote, postToJSONLD } from '../../utils/activitypub/postToJSONLD.js' 16 + import { getPostAndUserFromPostId } from '../../utils/cacheGetters/getPostAndUserFromPostId.js' 16 17 17 18 // we get the user from the memory cache. if does not exist we try to find it 18 19 async function getLocalUserByUrl(url: string): Promise<any> { ··· 116 117 orderedItems: itemsToSend 117 118 } 118 119 if (page > 1) { 119 - response['prev'] = `${ 120 - completeEnvironment.frontendUrl 121 - }/fediverse/blog/${user.url.toLowerCase()}/following?page=${page - 1}` 120 + response['prev'] = `${completeEnvironment.frontendUrl 121 + }/fediverse/blog/${user.url.toLowerCase()}/following?page=${page - 1}` 122 122 } 123 123 if (followedUsers.length > pageSize * page) { 124 - response['next'] = `${ 125 - completeEnvironment.frontendUrl 126 - }/fediverse/blog/${user.url.toLowerCase()}/following?page=${page + 1}` 124 + response['next'] = `${completeEnvironment.frontendUrl 125 + }/fediverse/blog/${user.url.toLowerCase()}/following?page=${page + 1}` 127 126 } 128 127 } else { 129 128 response = { ··· 174 173 const itemsToSend = followers.slice((page - 1) * pageSize, page * pageSize) 175 174 response = { 176 175 '@context': 'https://www.w3.org/ns/activitystreams', 177 - id: `${completeEnvironment.frontendUrl}/fediverse/blog/${user.url.toLowerCase()}/followers?page=${ 178 - req.query.page 179 - }`, 176 + id: `${completeEnvironment.frontendUrl}/fediverse/blog/${user.url.toLowerCase()}/followers?page=${req.query.page 177 + }`, 180 178 type: 'OrderedCollectionPage', 181 179 orderedItems: itemsToSend, 182 180 totalItems: followersNumber 183 181 } 184 182 if (page > 1) { 185 - response['prev'] = `${ 186 - completeEnvironment.frontendUrl 187 - }/fediverse/blog/${user.url.toLowerCase()}/followers?page=${page - 1}` 183 + response['prev'] = `${completeEnvironment.frontendUrl 184 + }/fediverse/blog/${user.url.toLowerCase()}/followers?page=${page - 1}` 188 185 } 189 186 if (followers.length > pageSize * page) { 190 - response['next'] = `${ 191 - completeEnvironment.frontendUrl 192 - }/fediverse/blog/${user.url.toLowerCase()}/followers?page=${page + 1}` 187 + response['next'] = `${completeEnvironment.frontendUrl 188 + }/fediverse/blog/${user.url.toLowerCase()}/followers?page=${page + 1}` 193 189 } 194 190 } else { 195 191 response = { ··· 329 325 res.sendStatus(404) 330 326 } 331 327 }) 328 + 329 + if (completeEnvironment.frontendEnvironment.enableRawOutput) { 330 + app.get('/fediverse/post/:id/raw', async (req: Request, res: Response) => { 331 + const id = req.params.id; 332 + const jsonLd = await postToJSONLD(id) 333 + if (jsonLd) { 334 + res 335 + .set({ 336 + 'content-type': 'application/activity+json' 337 + }) 338 + .send(jsonLd) 339 + } else { 340 + return404(res) 341 + } 342 + }) 343 + } 332 344 333 345 app.get('/fediverse/post/:id/replies', async (req: Request, res: Response) => { 334 346 res.send({
+150 -163
packages/frontend/src/app/components/blog-header/blog-header.component.html
··· 7 7 <div> 8 8 <img [src]="avatarUrl()" alt="user avatar" class="blog-avatar-image" /> 9 9 </div> 10 + @if (rawOutputEnabled && blog) { 11 + <button matButton="outlined" style="width: 40px; padding: 0; margin-right: 4px" (click)="getRawJson(blog.url)" 12 + matTooltip="{{ 'dialog.common.rawData' | translate }}"> 13 + <fa-icon [fixedWidth]="true" [icon]="rawJsonIcon"></fa-icon> 14 + </button> 15 + } 10 16 @if (loggedIn && !isMe && blog) { 11 - @if (loggedIn) { 12 - <div class="flex"> 13 - @if (!blog.isBlueskyUser) { 14 - <button 15 - matButton="outlined" 16 - style="width: 40px; padding: 0; margin-right: 4px" 17 - (click)="biteAccount(blog.id)" 18 - matTooltip="{{ 'post-actions.biteUser' | translate }}" 19 - > 20 - <fa-icon [fixedWidth]="true" [icon]="biteUserIcon"></fa-icon> 21 - </button> 22 - } 23 - @if (!postService.notYetAcceptedFollowedUsersIds.includes(blog.id)) { 24 - <button 25 - mat-stroked-button 26 - [color]="postService.followedUserIds.indexOf(blog.id) === -1 ? 'primary' : 'warn'" 27 - class="split-button-left" 28 - (click)=" 17 + @if (loggedIn) { 18 + <div class="flex"> 19 + 20 + @if (!blog.isBlueskyUser) { 21 + <button matButton="outlined" style="width: 40px; padding: 0; margin-right: 4px" (click)="biteAccount(blog.id)" 22 + matTooltip="{{ 'post-actions.biteUser' | translate }}"> 23 + <fa-icon [fixedWidth]="true" [icon]="biteUserIcon"></fa-icon> 24 + </button> 25 + } 26 + @if (!postService.notYetAcceptedFollowedUsersIds.includes(blog.id)) { 27 + <button mat-stroked-button [color]="postService.followedUserIds.indexOf(blog.id) === -1 ? 'primary' : 'warn'" 28 + class="split-button-left" (click)=" 29 29 postService.followedUserIds.indexOf(blog.id) === -1 ? followUser(blog.id) : unfollowUser(blog.id) 30 - " 31 - > 32 - {{ postService.followedUserIds.indexOf(blog.id) === -1 ? 'Follow' : 'Unfollow' }} 33 - </button> 34 - } 35 - @if (postService.notYetAcceptedFollowedUsersIds.includes(blog.id) && loggedIn) { 36 - <button mat-stroked-button color="accent" class="split-button-left" (click)="unfollowUser(blog.id)"> 37 - Awaiting approval 38 - </button> 39 - } 40 - <button 41 - aria-label="More options" 42 - [matMenuTriggerFor]="menu" 43 - mat-stroked-button 44 - [color]="postService.followedUserIds.indexOf(blog.id) === -1 ? 'primary' : 'warn'" 45 - class="split-button-right" 46 - > 47 - <fa-icon [icon]="expandDownIcon"></fa-icon> 48 - </button> 49 - </div> 30 + "> 31 + {{ postService.followedUserIds.indexOf(blog.id) === -1 ? 'Follow' : 'Unfollow' }} 32 + </button> 33 + } 34 + @if (postService.notYetAcceptedFollowedUsersIds.includes(blog.id) && loggedIn) { 35 + <button mat-stroked-button color="accent" class="split-button-left" (click)="unfollowUser(blog.id)"> 36 + Awaiting approval 37 + </button> 38 + } 39 + <button aria-label="More options" [matMenuTriggerFor]="menu" mat-stroked-button 40 + [color]="postService.followedUserIds.indexOf(blog.id) === -1 ? 'primary' : 'warn'" class="split-button-right"> 41 + <fa-icon [icon]="expandDownIcon"></fa-icon> 42 + </button> 43 + </div> 44 + } 45 + <mat-menu #menu="matMenu" xPosition="before"> 46 + @if (!blog.muted) { 47 + <button (click)="muteAccount()" mat-menu-item class="dangerous-option"> 48 + <span class="flex gap-2"> 49 + <fa-icon [fixedWidth]="true" [icon]="muteUserIcon"></fa-icon> 50 + {{ 'post-actions.muteUser' | translate }} 51 + </span> 52 + </button> 53 + } @else { 54 + <button (click)="unmuteAccount()" mat-menu-item class="dangerous-option"> 55 + <span class="flex gap-2"> 56 + <fa-icon [fixedWidth]="true" [icon]="unmuteUserIcon"></fa-icon> 57 + {{ 'post-actions.unmuteUser' | translate }} 58 + </span> 59 + </button> 60 + } 61 + @if (!blog.blocked) { 62 + <button (click)="blockAccount()" mat-menu-item class="dangerous-option"> 63 + <span class="flex gap-2"> 64 + <fa-icon [fixedWidth]="true" [icon]="blockUserIcon"></fa-icon> 65 + {{ 'post-actions.blockUser' | translate }} 66 + </span> 67 + </button> 68 + } @else { 69 + <button (click)="unblockAccount()" mat-menu-item class="dangerous-option"> 70 + <span class="flex gap-2"> 71 + <fa-icon [fixedWidth]="true" [icon]="userIcon"></fa-icon> 72 + {{ 'post-actions.unblockUser' | translate }} 73 + </span> 74 + </button> 50 75 } 51 - <mat-menu #menu="matMenu" xPosition="before"> 52 - @if (!blog.muted) { 53 - <button (click)="muteAccount()" mat-menu-item class="dangerous-option"> 54 - <span class="flex gap-2"> 55 - <fa-icon [fixedWidth]="true" [icon]="muteUserIcon"></fa-icon> 56 - {{ 'post-actions.muteUser' | translate }} 57 - </span> 58 - </button> 59 - } @else { 60 - <button (click)="unmuteAccount()" mat-menu-item class="dangerous-option"> 61 - <span class="flex gap-2"> 62 - <fa-icon [fixedWidth]="true" [icon]="unmuteUserIcon"></fa-icon> 63 - {{ 'post-actions.unmuteUser' | translate }} 64 - </span> 65 - </button> 66 - } 67 - @if (!blog.blocked) { 68 - <button (click)="blockAccount()" mat-menu-item class="dangerous-option"> 69 - <span class="flex gap-2"> 70 - <fa-icon [fixedWidth]="true" [icon]="blockUserIcon"></fa-icon> 71 - {{ 'post-actions.blockUser' | translate }} 72 - </span> 73 - </button> 74 - } @else { 75 - <button (click)="unblockAccount()" mat-menu-item class="dangerous-option"> 76 - <span class="flex gap-2"> 77 - <fa-icon [fixedWidth]="true" [icon]="userIcon"></fa-icon> 78 - {{ 'post-actions.unblockUser' | translate }} 79 - </span> 80 - </button> 81 - } 82 - @if (blog.url.startsWith('@')) { 83 - @if (!blog.isBlueskyUser) { 84 - @if (!blog.serverBlocked) { 85 - <button mat-menu-item (click)="blockService.blockServer(blog.id)"> 86 - <fa-icon class="mr-2" [icon]="unblockServerIcon"></fa-icon>Block server 87 - </button> 88 - } @else { 89 - @if (blog.federatedHost) { 90 - <button mat-menu-item (click)="blockService.unblockServer(blog.federatedHost!.id)"> 91 - <fa-icon class="mr-2" [icon]="unblockServerIcon"></fa-icon>Unblock server 92 - </button> 93 - } 94 - } 95 - } 96 - } 97 - @if ( 98 - !isMe && 99 - (postService.notYetAcceptedFollowedUsersIds.includes(blog.id) || 100 - postService.followedUserIds.includes(blog.id)) 101 - ) { 102 - @if (postService.usersRewootsDisabled.includes(blog.id)) { 103 - <button mat-menu-item (click)="updateDisableRewoots()"> 104 - <fa-icon class="mr-2" [icon]="disableRewootIcon"></fa-icon>Re-show rewoots made by this user 105 - </button> 106 - } @else { 107 - <button mat-menu-item (click)="updateDisableRewoots()"> 108 - <fa-icon class="mr-2" [icon]="disableRewootIcon"></fa-icon>Hide rewoots made by this user 109 - </button> 110 - } 111 - @if (postService.usersQuotesDisabled.includes(blog.id)) { 112 - <button mat-menu-item (click)="updateDisableQuotes()"> 113 - <fa-icon class="mr-2" [icon]="disableQuotesIcon"></fa-icon>Re-hide quotes from this user 114 - </button> 115 - } @else { 116 - <button mat-menu-item (click)="updateDisableQuotes()"> 117 - <fa-icon class="mr-2" [icon]="disableQuotesIcon"></fa-icon>Hide quotes from this user 118 - </button> 119 - } 120 - } 121 - <button (click)="reportService.report(blog.id)" mat-menu-item class="dangerous-option"> 122 - <span class="flex gap-2"> 123 - <fa-icon [fixedWidth]="true" [icon]="reportUserIcon"></fa-icon> 124 - {{ 'post-actions.reportUser' | translate }} 125 - </span> 126 - </button> 127 - </mat-menu> 76 + @if (blog.url.startsWith('@')) { 77 + @if (!blog.isBlueskyUser) { 78 + @if (!blog.serverBlocked) { 79 + <button mat-menu-item (click)="blockService.blockServer(blog.id)"> 80 + <fa-icon class="mr-2" [icon]="unblockServerIcon"></fa-icon>Block server 81 + </button> 82 + } @else { 83 + @if (blog.federatedHost) { 84 + <button mat-menu-item (click)="blockService.unblockServer(blog.federatedHost!.id)"> 85 + <fa-icon class="mr-2" [icon]="unblockServerIcon"></fa-icon>Unblock server 86 + </button> 87 + } 88 + } 89 + } 90 + } 91 + @if ( 92 + !isMe && 93 + (postService.notYetAcceptedFollowedUsersIds.includes(blog.id) || 94 + postService.followedUserIds.includes(blog.id)) 95 + ) { 96 + @if (postService.usersRewootsDisabled.includes(blog.id)) { 97 + <button mat-menu-item (click)="updateDisableRewoots()"> 98 + <fa-icon class="mr-2" [icon]="disableRewootIcon"></fa-icon>Re-show rewoots made by this user 99 + </button> 100 + } @else { 101 + <button mat-menu-item (click)="updateDisableRewoots()"> 102 + <fa-icon class="mr-2" [icon]="disableRewootIcon"></fa-icon>Hide rewoots made by this user 103 + </button> 104 + } 105 + @if (postService.usersQuotesDisabled.includes(blog.id)) { 106 + <button mat-menu-item (click)="updateDisableQuotes()"> 107 + <fa-icon class="mr-2" [icon]="disableQuotesIcon"></fa-icon>Re-hide quotes from this user 108 + </button> 109 + } @else { 110 + <button mat-menu-item (click)="updateDisableQuotes()"> 111 + <fa-icon class="mr-2" [icon]="disableQuotesIcon"></fa-icon>Hide quotes from this user 112 + </button> 113 + } 114 + } 115 + <button (click)="reportService.report(blog.id)" mat-menu-item class="dangerous-option"> 116 + <span class="flex gap-2"> 117 + <fa-icon [fixedWidth]="true" [icon]="reportUserIcon"></fa-icon> 118 + {{ 'post-actions.reportUser' | translate }} 119 + </span> 120 + </button> 121 + </mat-menu> 128 122 } 129 123 </div> 130 124 <div class="min-w-0"> ··· 132 126 <p class="m-0 text-sm line-height-3 blog-url">{{ blog?.url ?? '-' }}</p> 133 127 <div class="flex gap-2 mt-2 mb-0 text-sm line-height-3 profile-badges"> 134 128 @if (blog?.isBlueskyUser) { 135 - <span class="badge bsky-badge"> <fa-icon class="mr-2" [icon]="bskyIcon"></fa-icon>Bluesky </span> 129 + <span class="badge bsky-badge"> <fa-icon class="mr-2" [icon]="bskyIcon"></fa-icon>Bluesky </span> 136 130 } 137 131 @if (blog && blog.followers / blog.followed > 5) { 138 - <span 139 - class="badge ratio-badge" 140 - matTooltip="{{ 132 + <span class="badge ratio-badge" matTooltip="{{ 141 133 'For every ' + 142 134 (blog.followers / blog.followed | number: '1.0-0') + 143 135 ' followers ' + 144 136 blog.url + 145 137 ' follows 1 user.' 146 - }}" 147 - > 148 - <fa-icon class="mr-2" [icon]="usersIcon"></fa-icon>Follower Ratio: 149 - {{ blog.followers / blog.followed | number: '1.0-0' }}:1 150 - </span> 138 + }}"> 139 + <fa-icon class="mr-2" [icon]="usersIcon"></fa-icon>Follower Ratio: 140 + {{ blog.followers / blog.followed | number: '1.0-0' }}:1 141 + </span> 151 142 } 152 143 </div> 153 144 </div> 154 145 </div> 155 146 <div [innerHtml]="headerHTML ?? '-'" class="mt-2 post-text"></div> 156 147 @if (fediComp().length !== 0) { 157 - <hr class="my-3" /> 158 - <div class="w-full overflow-hidden flex flex-column gap-2"> 159 - <p class="m-0 text-sm subtle-title">Fediverse Attachments</p> 160 - @for (fediAt of fediComp(); track fediAt) { 161 - <dl class="flex m-0"> 162 - <dt [innerHTML]="fediAt.name" class="w-3 font-bold"></dt> 163 - <dd [innerHTML]="fediAt.value" class="w-8 ml-2"></dd> 164 - </dl> 165 - } 166 - </div> 148 + <hr class="my-3" /> 149 + <div class="w-full overflow-hidden flex flex-column gap-2"> 150 + <p class="m-0 text-sm subtle-title">Fediverse Attachments</p> 151 + @for (fediAt of fediComp(); track fediAt) { 152 + <dl class="flex m-0"> 153 + <dt [innerHTML]="fediAt.name" class="w-3 font-bold"></dt> 154 + <dd [innerHTML]="fediAt.value" class="w-8 ml-2"></dd> 155 + </dl> 156 + } 157 + </div> 167 158 } 168 159 @if (allowAsk) { 169 - <hr class="my-4" /> 170 - <div class="flex flex-wrap justify-content-center gap-3"> 171 - <button mat-stroked-button color="accent" (click)="openAskDialog()">Ask a question</button> 172 - </div> 160 + <hr class="my-4" /> 161 + <div class="flex flex-wrap justify-content-center gap-3"> 162 + <button mat-stroked-button color="accent" (click)="openAskDialog()">Ask a question</button> 163 + </div> 173 164 } 174 165 @if (allowRemoteAsk) { 175 - <details class="mt-2"> 176 - <summary class="text-center">Send ask from outside Wafrn</summary> 177 - <p class="m-0"> 178 - You can send this user a non-anonymous ask from your fedi instance with the following structure: " !ask &#64;{{ 179 - blog?.url 180 - }} 181 - YOUR QUESTION HERE". The medias and emojis you attach will be ignored, and you can only send one mention on the 182 - post. If it goes wrong, the user will receive it as a regular DM. Also, the content of the DM will be published 183 - as the AP object to verify the ask as genuine 184 - </p> 185 - </details> 166 + <details class="mt-2"> 167 + <summary class="text-center">Send ask from outside Wafrn</summary> 168 + <p class="m-0"> 169 + You can send this user a non-anonymous ask from your fedi instance with the following structure: " !ask &#64;{{ 170 + blog?.url 171 + }} 172 + YOUR QUESTION HERE". The medias and emojis you attach will be ignored, and you can only send one mention on the 173 + post. If it goes wrong, the user will receive it as a regular DM. Also, the content of the DM will be published 174 + as the AP object to verify the ask as genuine 175 + </p> 176 + </details> 186 177 } 187 178 @if (blog?.url?.startsWith('@')) { 188 - <app-info-card [type]="loggedIn ? 'info' : 'caution'"> 189 - {{ 190 - loggedIn 191 - ? 'As this user is from a remote instance, the shown information may be incomplete.' 192 - : 'To see external users you need to be logged in. You can view their profile on their actual instance.' 193 - }} 194 - <a 195 - [href]=" 179 + <app-info-card [type]="loggedIn ? 'info' : 'caution'"> 180 + {{ 181 + loggedIn 182 + ? 'As this user is from a remote instance, the shown information may be incomplete.' 183 + : 'To see external users you need to be logged in. You can view their profile on their actual instance.' 184 + }} 185 + <a [href]=" 196 186 blog?.url?.split('@')?.length === 3 197 187 ? blog?.remoteId 198 188 : 'https://bsky.app/profile/' + blog?.url?.split('@')?.at(1) 199 - " 200 - target="_blank" 201 - >View on remote instance</a 202 - > 203 - </app-info-card> 189 + " target="_blank">View on remote instance</a> 190 + </app-info-card> 204 191 } 205 192 <footer class="mt-4 py-3 flex justify-content-evenly follow-counts"> 206 193 <a [routerLink]="['/blog', blog?.url]" class="flex flex-column justify-content-center subtle-link"> ··· 216 203 <p class="m-0 text-xs">Followers</p> 217 204 </a> 218 205 </footer> 219 - </mat-card> 206 + </mat-card>
+22 -2
packages/frontend/src/app/components/blog-header/blog-header.component.ts
··· 17 17 faTriangleExclamation, 18 18 faRepeat, 19 19 faQuoteRight, 20 - faCookieBite 20 + faCookieBite, 21 + faCode 21 22 } from '@fortawesome/free-solid-svg-icons' 22 23 import { BlogDetails } from 'src/app/interfaces/blogDetails' 23 24 import { BlocksService } from 'src/app/services/blocks.service' 24 25 import { LoginService } from 'src/app/services/login.service' 25 26 import { MessageService } from 'src/app/services/message.service' 26 27 import { PostsService } from 'src/app/services/posts.service' 28 + import { UtilsService } from 'src/app/services/utils.service' 27 29 import { MatTooltipModule } from '@angular/material/tooltip' 28 30 import { EnvironmentService } from 'src/app/services/environment.service' 29 31 import { InfoCardComponent } from '../info-card/info-card.component' ··· 32 34 import { TranslatePipe } from '@ngx-translate/core' 33 35 import { SimpleDialogService } from 'src/app/services/simple-dialog.service' 34 36 import { BlogService } from 'src/app/services/blog.service' 37 + import { RawJsonDialogComponent } from '../raw-json-dialog/raw-json-dialog.component' 35 38 36 39 @Component({ 37 40 selector: 'app-blog-header', ··· 68 71 reportUserIcon = faTriangleExclamation 69 72 disableRewootIcon = faRepeat 70 73 disableQuotesIcon = faQuoteRight 74 + rawJsonIcon = faCode 71 75 72 76 userIcon = faUser 73 77 bskyIcon = faBluesky ··· 80 84 isBlueskyUser = false 81 85 headerHTML: string | undefined 82 86 87 + rawOutputEnabled = EnvironmentService.environment.enableRawOutput 88 + 83 89 fediComp = computed<{ name: string; value: string }[]>(() => { 84 90 const fediAttachment = this.blogDetails()?.publicOptions.find( 85 91 (elem) => elem.optionName == 'fediverse.public.attachment' ··· 100 106 public environmentService: EnvironmentService, 101 107 public reportService: ReportService, 102 108 public simpleDialog: SimpleDialogService, 103 - public blogService: BlogService 109 + public blogService: BlogService, 110 + public utilsService: UtilsService 104 111 ) { } 105 112 ngOnChanges(changes: SimpleChanges): void { 106 113 const blog = this.blogDetails() ··· 220 227 translate: true 221 228 }) 222 229 } 230 + } 231 + 232 + async getRawJsonComponent(): Promise<typeof RawJsonDialogComponent> { 233 + const { RawJsonDialogComponent } = await import('../raw-json-dialog/raw-json-dialog.component') 234 + return RawJsonDialogComponent 235 + } 236 + 237 + async getRawJson(id: string) { 238 + const raw = await this.utilsService.getRawJsonUser(id); 239 + this.dialogService.open(await this.getRawJsonComponent(), { 240 + data: raw, 241 + width: '800px' 242 + }) 223 243 } 224 244 225 245 async getAskDialogComponent(): Promise<typeof AskDialogContentComponent> {
+1
packages/frontend/src/app/components/post-action-buttons/post-action-buttons.component.ts
··· 87 87 bookmarkIcon = faBookmark 88 88 unbookmarkIcon = faBookBookmark 89 89 90 + 90 91 // Ordering 91 92 buttonList: SettingListItem[] = [] 92 93
+93 -89
packages/frontend/src/app/components/post-actions/post-actions.component.html
··· 3 3 @let isBskyPost = !!post.bskyUri; 4 4 @let isExternalPost = post.user.url.startsWith('@') && post.privacy !== 1 && post.privacy !== 10; 5 5 @let isMyPost = myId == post.userId && post.privacy != 2; 6 - <button 7 - aria-label="Post actions" 8 - mat-button 9 - class="mat-circle-button split-button-right" 10 - matTooltip="Post actions" 11 - [matMenuTriggerFor]="menu" 12 - > 6 + <button aria-label="Post actions" mat-button class="mat-circle-button split-button-right" matTooltip="Post actions" 7 + [matMenuTriggerFor]="menu"> 13 8 <fa-icon [icon]="expandDownIcon"></fa-icon> 14 9 </button> 15 10 <mat-menu #menu="matMenu" xPosition="before" id="post-actions-menu"> 16 11 <ng-template matMenuContent> 17 12 @if (loggedIn) { 18 - <div class="post-action-buttons" mat-menu-item> 19 - <app-post-action-buttons [fragment]="post" settingKey="postActionsButtonBarOrder"></app-post-action-buttons> 20 - </div> 21 - <hr class="my-0" /> 13 + <div class="post-action-buttons" mat-menu-item> 14 + <app-post-action-buttons [fragment]="post" settingKey="postActionsButtonBarOrder"></app-post-action-buttons> 15 + </div> 16 + <hr class="my-0" /> 22 17 } 23 18 <button (click)="sharePost()" mat-menu-item> 24 19 <span class="post-actions-menu-span-content"> ··· 27 22 </span> 28 23 </button> 29 24 @if (post.remotePostId && post.user.url.startsWith('@')) { 30 - <a (click)="shareOriginalPost()" mat-menu-item> 31 - <span class="post-actions-menu-span-content"> 32 - {{ 'post-actions.shareExternalUrl' | translate }} 33 - <fa-icon [fixedWidth]="true" [icon]="shareExternalIcon"></fa-icon> 34 - </span> 35 - </a> 25 + <a (click)="shareOriginalPost()" mat-menu-item> 26 + <span class="post-actions-menu-span-content"> 27 + {{ 'post-actions.shareExternalUrl' | translate }} 28 + <fa-icon [fixedWidth]="true" [icon]="shareExternalIcon"></fa-icon> 29 + </span> 30 + </a> 36 31 } 37 32 @if (isBskyPost) { 38 - <a [href]="bskyUrl()" target="_blank" mat-menu-item> 39 - <span class="post-actions-menu-span-content"> 40 - {{ 'post-actions.viewOnAtproto' | translate }} 41 - <fa-icon size="lg" [fixedWidth]="true" [icon]="bskyIcon"></fa-icon> 42 - </span> 43 - </a> 33 + <a [href]="bskyUrl()" target="_blank" mat-menu-item> 34 + <span class="post-actions-menu-span-content"> 35 + {{ 'post-actions.viewOnAtproto' | translate }} 36 + <fa-icon size="lg" [fixedWidth]="true" [icon]="bskyIcon"></fa-icon> 37 + </span> 38 + </a> 44 39 } 45 40 @if (isExternalPost && externalUrl() !== bskyUrl()) { 46 - <a [href]="externalUrl()" target="_blank" mat-menu-item> 47 - <span class="post-actions-menu-span-content"> 48 - {{ 'post-actions.viewOriginalPost' | translate }} 49 - <fa-icon size="lg" [fixedWidth]="true" [icon]="goExternalPost"></fa-icon> 50 - </span> 51 - </a> 41 + <a [href]="externalUrl()" target="_blank" mat-menu-item> 42 + <span class="post-actions-menu-span-content"> 43 + {{ 'post-actions.viewOriginalPost' | translate }} 44 + <fa-icon size="lg" [fixedWidth]="true" [icon]="goExternalPost"></fa-icon> 45 + </span> 46 + </a> 52 47 } 53 48 @if (isMyPost) { 54 - <button (click)="forceRefederate()" mat-menu-item> 55 - <span class="post-actions-menu-span-content"> 56 - {{ 'post-actions.forceRefederate' | translate }} 57 - <fa-icon size="lg" [fixedWidth]="true" [icon]="refederateIcon"></fa-icon> 58 - </span> 59 - </button> 49 + <button (click)="forceRefederate()" mat-menu-item> 50 + <span class="post-actions-menu-span-content"> 51 + {{ 'post-actions.forceRefederate' | translate }} 52 + <fa-icon size="lg" [fixedWidth]="true" [icon]="refederateIcon"></fa-icon> 53 + </span> 54 + </button> 60 55 } 61 56 @if (loggedIn) { 62 - @if (!postSilenced) { 63 - <button (click)="silencePost()" mat-menu-item> 64 - <span class="post-actions-menu-span-content"> 65 - {{ 'post-actions.silenceInteractions' | translate }} 66 - <fa-icon size="lg" [fixedWidth]="true" [icon]="silenceIcon"></fa-icon> 67 - </span> 68 - </button> 69 - } 70 - @if (!postSilenced) { 71 - <button (click)="silencePost(true)" mat-menu-item> 72 - <span class="post-actions-menu-span-content"> 73 - {{ 'post-actions.silenceReplyNotifications' | translate }} 74 - <fa-icon size="lg" [fixedWidth]="true" [icon]="silenceReplyIcon"></fa-icon> 75 - </span> 76 - </button> 77 - } 78 - @if (postSilenced) { 79 - <button (click)="unsilencePost()" mat-menu-item> 80 - <span class="post-actions-menu-span-content"> 81 - {{ 'post-actions.unsilenceReplyNotifications' | translate }} 82 - <fa-icon size="lg" [fixedWidth]="true" [icon]="unsilenceIcon"></fa-icon> 83 - </span> 84 - </button> 85 - } 86 - @if (!isMyPost) { 87 - <a (click)="bitePost()" mat-menu-item> 88 - <span class="post-actions-menu-span-content"> 89 - {{ 'post-actions.bitePost' | translate }} 90 - <fa-icon [fixedWidth]="true" [icon]="biteIcon"></fa-icon> 91 - </span> 92 - </a> 93 - } 57 + @if (!postSilenced) { 58 + <button (click)="silencePost()" mat-menu-item> 59 + <span class="post-actions-menu-span-content"> 60 + {{ 'post-actions.silenceInteractions' | translate }} 61 + <fa-icon size="lg" [fixedWidth]="true" [icon]="silenceIcon"></fa-icon> 62 + </span> 63 + </button> 64 + } 65 + @if (!postSilenced) { 66 + <button (click)="silencePost(true)" mat-menu-item> 67 + <span class="post-actions-menu-span-content"> 68 + {{ 'post-actions.silenceReplyNotifications' | translate }} 69 + <fa-icon size="lg" [fixedWidth]="true" [icon]="silenceReplyIcon"></fa-icon> 70 + </span> 71 + </button> 72 + } 73 + @if (postSilenced) { 74 + <button (click)="unsilencePost()" mat-menu-item> 75 + <span class="post-actions-menu-span-content"> 76 + {{ 'post-actions.unsilenceReplyNotifications' | translate }} 77 + <fa-icon size="lg" [fixedWidth]="true" [icon]="unsilenceIcon"></fa-icon> 78 + </span> 79 + </button> 94 80 } 95 81 @if (!isMyPost) { 96 - @if (loggedIn) { 97 - <hr class="my-0" /> 98 - <button (click)="muteAccount()" mat-menu-item class="dangerous-option"> 99 - <span class="post-actions-menu-span-content"> 100 - {{ 'post-actions.muteUser' | translate }} 101 - <fa-icon size="lg" [fixedWidth]="true" [icon]="muteIcon"></fa-icon> 102 - </span> 103 - </button> 104 - <button (click)="blockAccount()" mat-menu-item class="dangerous-option"> 105 - <span class="post-actions-menu-span-content"> 106 - {{ 'post-actions.blockUser' | translate }} 107 - <fa-icon size="lg" [fixedWidth]="true" [icon]="blockIcon"></fa-icon> 108 - </span> 109 - </button> 110 - } 111 - <button (click)="reportPost()" mat-menu-item class="dangerous-option"> 112 - <span class="post-actions-menu-span-content"> 113 - {{ 'post-actions.reportPost' | translate }} 114 - <fa-icon size="lg" [fixedWidth]="true" [icon]="reportIcon"></fa-icon> 115 - </span> 116 - </button> 82 + <a (click)="bitePost()" mat-menu-item> 83 + <span class="post-actions-menu-span-content"> 84 + {{ 'post-actions.bitePost' | translate }} 85 + <fa-icon [fixedWidth]="true" [icon]="biteIcon"></fa-icon> 86 + </span> 87 + </a> 88 + } 89 + } 90 + @if (!isMyPost) { 91 + @if (loggedIn) { 92 + <hr class="my-0" /> 93 + <button (click)="muteAccount()" mat-menu-item class="dangerous-option"> 94 + <span class="post-actions-menu-span-content"> 95 + {{ 'post-actions.muteUser' | translate }} 96 + <fa-icon size="lg" [fixedWidth]="true" [icon]="muteIcon"></fa-icon> 97 + </span> 98 + </button> 99 + <button (click)="blockAccount()" mat-menu-item class="dangerous-option"> 100 + <span class="post-actions-menu-span-content"> 101 + {{ 'post-actions.blockUser' | translate }} 102 + <fa-icon size="lg" [fixedWidth]="true" [icon]="blockIcon"></fa-icon> 103 + </span> 104 + </button> 105 + } 106 + <button (click)="reportPost()" mat-menu-item class="dangerous-option"> 107 + <span class="post-actions-menu-span-content"> 108 + {{ 'post-actions.reportPost' | translate }} 109 + <fa-icon size="lg" [fixedWidth]="true" [icon]="reportIcon"></fa-icon> 110 + </span> 111 + </button> 112 + } 113 + @if (rawOutputEnabled && post) { 114 + <hr class="my-0" /> 115 + <button (click)="getRawJson(post.id)" mat-menu-item> 116 + <span class="post-actions-menu-span-content"> 117 + {{ 'dialog.common.rawData' | translate }} 118 + <fa-icon [fixedWidth]="true" [icon]="rawJsonIcon"></fa-icon> 119 + </span> 120 + </button> 117 121 } 118 122 </ng-template> 119 - </mat-menu> 123 + </mat-menu>
+20 -1
packages/frontend/src/app/components/post-actions/post-actions.component.ts
··· 25 25 faPaperPlane, 26 26 faUserSlash, 27 27 faVolumeMute, 28 - faCookieBite 28 + faCookieBite, 29 + faCode 29 30 } from '@fortawesome/free-solid-svg-icons' 30 31 import { MatButtonModule } from '@angular/material/button' 31 32 import { MatMenuModule } from '@angular/material/menu' ··· 44 45 import { PostActionButtonsComponent } from '../post-action-buttons/post-action-buttons.component' 45 46 import { SimpleDialogService } from 'src/app/services/simple-dialog.service' 46 47 import { BlocksService } from 'src/app/services/blocks.service' 48 + import { MatDialog } from '@angular/material/dialog' 47 49 48 50 @Component({ 49 51 selector: 'app-post-actions', ··· 93 95 muteIcon = faVolumeMute 94 96 blockIcon = faUserSlash 95 97 biteIcon = faCookieBite 98 + rawJsonIcon = faCode 99 + 100 + rawOutputEnabled = EnvironmentService.environment.enableRawOutput 96 101 97 102 constructor( 98 103 private messages: MessageService, ··· 102 107 private utilsService: UtilsService, 103 108 private settingsService: SettingsService, 104 109 private simpleDialog: SimpleDialogService, 110 + public dialogService: MatDialog, 105 111 private blockService: BlocksService 106 112 ) { 107 113 if (loginService.loggedIn.value) { ··· 202 208 translate: true 203 209 }) 204 210 } 211 + } 212 + 213 + async getRawJsonComponent(): Promise<typeof RawJsonDialogComponent> { 214 + const { RawJsonDialogComponent } = await import('../raw-json-dialog/raw-json-dialog.component') 215 + return RawJsonDialogComponent 216 + } 217 + 218 + async getRawJson(id: string) { 219 + const raw = await this.utilsService.getRawJsonPost(id); 220 + this.dialogService.open(await this.getRawJsonComponent(), { 221 + data: raw, 222 + width: '800px' 223 + }) 205 224 } 206 225 207 226 // Dangerous options
+5
packages/frontend/src/app/components/raw-json-dialog/raw-json-dialog.component.html
··· 1 + <h1 mat-dialog-title>{{ 'dialog.common.rawData' | translate }}</h1> 2 + <div class="raw-dialog-data" mat-dialog-content> 3 + <nxt-json-view [data]="data" [levelOpen]="0"></nxt-json-view> 4 + </div> 5 + <button mat-flat-button (click)="closeDialog()" class="w-full">{{ 'dialog.close' | translate }}</button>
+5
packages/frontend/src/app/components/raw-json-dialog/raw-json-dialog.component.scss
··· 1 + nxt-json-view { 2 + --nxt-json-view-color-value: var(--mat-sys-on-primary-container); 3 + --nxt-json-view-color-key: var(--mat-sys-on-tertiary-container); 4 + --nxt-json-view-color-boolean: var(--mat-sys-error); 5 + }
+34
packages/frontend/src/app/components/raw-json-dialog/raw-json-dialog.component.ts
··· 1 + import { Component, Inject } from '@angular/core'; 2 + import { MatButtonModule } from '@angular/material/button'; 3 + import { MAT_DIALOG_DATA, MatDialogContent, MatDialogRef, MatDialogTitle } from '@angular/material/dialog'; 4 + import { LoaderComponent } from '../loader/loader.component'; 5 + import { JsonViewModule } from 'nxt-json-view'; 6 + import { TranslateModule } from '@ngx-translate/core'; 7 + 8 + @Component({ 9 + selector: 'app-raw-json-dialog', 10 + imports: [ 11 + MatButtonModule, 12 + MatDialogTitle, 13 + MatDialogContent, 14 + JsonViewModule, 15 + TranslateModule 16 + ], 17 + templateUrl: './raw-json-dialog.component.html', 18 + styleUrl: './raw-json-dialog.component.scss', 19 + }) 20 + export class RawJsonDialogComponent { 21 + data = {} 22 + 23 + constructor( 24 + private dialogRef: MatDialogRef<RawJsonDialogComponent>, 25 + @Inject(MAT_DIALOG_DATA) 26 + public jsonData: object 27 + ) { 28 + this.data = jsonData 29 + } 30 + 31 + closeDialog() { 32 + this.dialogRef.close() 33 + } 34 + }
+23 -1
packages/frontend/src/app/services/utils.service.ts
··· 11 11 constructor( 12 12 private postsService: PostsService, 13 13 private http: HttpClient 14 - ) {} 14 + ) { } 15 15 16 16 objectToFormData(obj: any): FormData { 17 17 const res = new FormData() ··· 31 31 ) 32 32 let result = servers.map((elem) => elem.displayName.toLowerCase().trim()).filter((elem) => elem != '') 33 33 return result.sort() 34 + } 35 + 36 + async getRawJsonUser(id: string): Promise<object> { 37 + const raw = await firstValueFrom( 38 + this.http.get(`${EnvironmentService.environment.frontUrl}/fediverse/blog/${id}`, { 39 + headers: { 40 + Accept: 'application/json' 41 + } 42 + }) 43 + ) 44 + return raw 45 + } 46 + 47 + async getRawJsonPost(id: string): Promise<object> { 48 + const raw = await firstValueFrom( 49 + this.http.get(`${EnvironmentService.environment.frontUrl}/fediverse/post/${id}/raw`, { 50 + headers: { 51 + Accept: 'application/json' 52 + } 53 + }) 54 + ) 55 + return raw 34 56 } 35 57 }
+5 -1
packages/frontend/src/assets/i18n/en.json
··· 400 400 "dialog": { 401 401 "confirm": "Confirm", 402 402 "cancel": "Cancel", 403 + "close": "Close", 403 404 "bluesky": { 404 405 "generateInviteCodeWarningTitle": "Attention", 405 406 "generateInviteCodeWarning": "Generating an invite code will delete IRREVERSIBLY any associated bsky account" ··· 459 460 "reportDescription": "Description", 460 461 "reportBlockUser": "Block this user", 461 462 "reportConfirm": "Report" 463 + }, 464 + "common": { 465 + "rawData": "Raw JSON data" 462 466 } 463 467 }, 464 468 "ask-dialog-content": { ··· 581 585 "couldNotFind": "does not lead to a page", 582 586 "returnHome": "Go Home" 583 587 } 584 - } 588 + }