Multicolumn Bluesky client powered by Angular
4
fork

Configure Feed

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

refactor: reverted auxpanes to not have children

+63 -101
+2 -2
src/app/components/aux-panes/author-view/author-view.component.html
··· 11 11 > 12 12 <avatar 13 13 [src]="author().banner" 14 - class="w-full" 14 + class="w-full aspect-[3_/_1]" 15 15 /> 16 16 17 17 <div ··· 19 19 > 20 20 <avatar 21 21 [src]="author().avatar" 22 - class="h-18 w-18 absolute -top-8 left-2 border-[0.25rem] border-bg !box-content" 22 + class="h-18 w-18 absolute -top-9 left-2 border-[0.25rem] border-bg !box-content" 23 23 /> 24 24 </div> 25 25
+4 -4
src/app/components/aux-panes/thread-view/thread-view.component.html
··· 13 13 <post-card 14 14 [post]="parent()" 15 15 (postChange)="parent.set($event)" 16 - (click)="dialogService.openThread(parent().uri, true)" 17 - (onEmbedRecord)="dialogService.openRecord($event, true)" 16 + (click)="dialogService.openThread(parent().uri)" 17 + (onEmbedRecord)="dialogService.openRecord($event)" 18 18 class="cursor-pointer hover:bg-primary/2 w-full px-3 pt-3 pb-1" 19 19 parent 20 20 /> ··· 34 34 <post-card 35 35 [post]="child.post()" 36 36 (postChange)="child.post.set($event)" 37 - (click)="dialogService.openThread(child.post().uri, true)" 38 - (onEmbedRecord)="dialogService.openRecord($event, true)" 37 + (click)="dialogService.openThread(child.post().uri)" 38 + (onEmbedRecord)="dialogService.openRecord($event)" 39 39 class="cursor-pointer hover:bg-primary/2 w-full px-3 pt-3 pb-1" 40 40 [parent]="i !== reply.thread.length - 1" 41 41 />
+6 -6
src/app/components/cards/notification-card/notification-card.component.html
··· 70 70 > 71 71 @for (author of notification().authors | slice : 0: 5; track author.did) { 72 72 <a 73 - (click)="openAuthor($event, author.did)" 73 + (click)="openAuthor($event, author)" 74 74 class="h-8 w-8 relative cursor-pointer" 75 75 > 76 76 <avatar 77 77 [src]="author.avatar" 78 - (click)="openAuthor($event, author.did)" 78 + (click)="openAuthor($event, author)" 79 79 /> 80 80 </a> 81 81 } ··· 97 97 @switch (notification().authors.length) { 98 98 @case (1) { 99 99 <a 100 - (click)="openAuthor($event, notification().authors[0].did)" 100 + (click)="openAuthor($event, notification().authors[0])" 101 101 class="font-bold hover:underline cursor-pointer" 102 102 >{{notification().authors[0] | displayName}}</a> 103 103 } 104 104 @case (2) { 105 105 <a 106 - (click)="openAuthor($event, notification().authors[0].did)" 106 + (click)="openAuthor($event, notification().authors[0])" 107 107 class="font-bold hover:underline cursor-pointer" 108 108 >{{notification().authors[0] | displayName}}</a> 109 109 110 110 and 111 111 112 112 <a 113 - (click)="openAuthor($event, notification().authors[1].did)" 113 + (click)="openAuthor($event, notification().authors[1])" 114 114 class="font-bold hover:underline cursor-pointer" 115 115 >{{notification().authors[1] | displayName}}</a> 116 116 } 117 117 @default { 118 118 <a 119 - (click)="openAuthor($event, notification().authors[0].did)" 119 + (click)="openAuthor($event, notification().authors[0])" 120 120 class="font-bold hover:underline cursor-pointer" 121 121 >{{notification().authors[0] | displayName}}</a> 122 122
+2 -3
src/app/components/cards/notification-card/notification-card.component.ts
··· 45 45 this.cdRef.markForCheck(); 46 46 } 47 47 48 - openAuthor(event: Event, did: string) { 49 - //TODO: OpenAuthor 48 + openAuthor(event: Event, author: Partial<{did: string, handle: string}>) { 50 49 event.stopPropagation(); 51 - this.dialogService.openAuthor(did); 50 + this.dialogService.openAuthor(author); 52 51 } 53 52 54 53 openImage(event: Event, images: AppBskyEmbedImages.ViewImage[], index: number) {
+1 -1
src/app/components/embeds/external-embed/external-embed.component.html
··· 43 43 @if (snippet.type == BlueskyGifSnippetType) { 44 44 <video 45 45 #target 46 - class="video-js vjs-show-big-play-button-on-pause rounded-md overflow-hidden" 46 + class="video-js vjs-show-big-play-button-on-pause" 47 47 (click)="$event.stopPropagation()" 48 48 ></video> 49 49 }
+2 -14
src/app/components/embeds/external-embed/external-embed.component.ts
··· 27 27 ], 28 28 templateUrl: './external-embed.component.html', 29 29 styles: ` 30 - :host(::ng-deep youtube-player) { 31 - display: flex; 32 - } 33 - :host(::ng-deep youtube-player) youtube-player-placeholder { 34 - width: 100% !important; 35 - height: unset !important; 36 - } 37 - :host(::ng-deep youtube-player) > div { 38 - width: 100%; 39 - } 40 - :host(::ng-deep youtube-player) > div iframe { 41 - height: unset; 30 + .video-js { 31 + height: auto; 42 32 width: 100%; 43 - aspect-ratio: 16 / 9; 44 33 } 45 - 46 34 `, 47 35 changeDetection: ChangeDetectionStrategy.OnPush 48 36 })
+2 -1
src/app/components/feeds/timeline-feed/timeline-feed.component.ts
··· 1 1 import { 2 2 ChangeDetectionStrategy, 3 3 ChangeDetectorRef, 4 - Component, effect, 4 + Component, 5 + effect, 5 6 ElementRef, 6 7 input, 7 8 OnDestroy,
+9 -22
src/app/components/navigation/auxbar/auxbar.component.html
··· 8 8 [ngClass]="{'border-b border-primary': dialogService.auxPanes().length}" 9 9 > 10 10 @if (dialogService.auxPanes().length) { 11 - @if (dialogService.auxPanes()[0].children.length) { 11 + @if (dialogService.auxPanes().length > 1) { 12 12 <button 13 - (click)="dialogService.closeAuxPaneChildren()" 13 + (click)="dialogService.closeAuxPane()" 14 14 class="btn-primary border-b-0 font-black text-lg" 15 15 ><</button> 16 16 } 17 17 18 18 <span 19 - class="bg-primary text-bg text-xl font-medium flex items-center pr-3" 20 - [ngClass]="{ 21 - 'pl-2': dialogService.auxPanes()[0].children.length, 22 - 'pl-3': !dialogService.auxPanes()[0].children.length, 23 - }" 19 + class="bg-primary text-bg text-xl font-medium flex items-center p-3" 24 20 >thread view</span> 25 21 26 22 @if (dialogService.auxPanes().length > 1) { ··· 36 32 } 37 33 38 34 <button 39 - (click)="dialogService.closeAuxPane(); cdRef.markForCheck()" 35 + (click)="dialogService.auxPanes.set([])" 40 36 class="h-9 w-9 text-2xl font-semibold border border-primary hover:bg-primary/10 cursor-pointer ml-auto" 41 37 >x</button> 42 38 } ··· 59 55 @if (pane | isAuxPaneThread) { 60 56 thread 61 57 } 58 + @if (pane | isAuxPaneAuthor) { 59 + {{'@' + pane.handle}} 60 + } 62 61 </button> 63 62 </li> 64 63 } ··· 99 98 <ng-template 100 99 #auxPane 101 100 > 102 - @for (pane of dialogService.auxPanes(); track pane.uuid; let iPane = $index) { 101 + @for (pane of dialogService.auxPanes(); track pane.uuid; let i = $index) { 103 102 <div 104 103 class="absolute h-full w-full bg-bg" 105 - [class.hidden]="iPane" 104 + [class.hidden]="i" 106 105 > 107 - @if (pane.children.length) { 108 - @for (child of pane.children; track child.uuid; let iChild = $index) { 109 - @if (child | isAuxPaneThread) { 110 - <thread-view 111 - [uri]="child.uri" 112 - [class.hidden]="iChild" 113 - /> 114 - } 115 - } 116 - } 117 106 @if (pane | isAuxPaneThread) { 118 107 <thread-view 119 108 [uri]="pane.uri" 120 - [class.hidden]="pane.children.length" 121 109 /> 122 110 } @else if (pane | isAuxPaneAuthor) { 123 111 <author-view 124 112 [did]="pane.did" 125 - [class.hidden]="pane.children.length" 126 113 /> 127 114 } 128 115 </div>
+2 -3
src/app/components/navigation/auxbar/auxbar.component.ts
··· 1 - import {ChangeDetectionStrategy, ChangeDetectorRef, Component, signal} from '@angular/core'; 1 + import {ChangeDetectorRef, Component, signal} from '@angular/core'; 2 2 import {from} from 'rxjs'; 3 3 import {agent} from '@core/bsky.api'; 4 4 import {AuthService} from '@core/auth/auth.service'; ··· 26 26 IsAuxPaneAuthorPipe, 27 27 AuthorViewComponent, 28 28 ], 29 - templateUrl: './auxbar.component.html', 30 - changeDetection: ChangeDetectionStrategy.OnPush 29 + templateUrl: './auxbar.component.html' 31 30 }) 32 31 export class AuxbarComponent { 33 32 topics = signal<AppBskyUnspeccedDefs.TrendingTopic[]>([]);
+11 -2
src/app/models/aux-pane.ts
··· 2 2 3 3 export class AuxPane { 4 4 uuid: string = uuid.v4(); 5 - children: Partial<AuxPane>[] = []; 6 5 } 7 6 8 7 export class ThreadAuxPane extends AuxPane { 9 8 type: AuxPaneType.THREAD = AuxPaneType.THREAD; 10 9 uri: string; 10 + 11 + constructor(uri: string) { 12 + super(); 13 + this.uri = uri; 14 + } 11 15 } 12 16 13 17 export class AuthorAuxPane extends AuxPane { 14 18 type: AuxPaneType.AUTHOR = AuxPaneType.AUTHOR; 15 19 did: string; 16 20 handle: string; 17 - displayName: string; 21 + 22 + constructor(author: Partial<{did: string, handle: string}>) { 23 + super(); 24 + this.did = author.did; 25 + this.handle = author.handle; 26 + } 18 27 } 19 28 20 29 export class ListAuxPane extends AuxPane {
+22 -43
src/app/services/dialog.service.ts
··· 36 36 }) 37 37 } 38 38 39 - closeAuxPaneChildren() { 40 - this.auxPanes.update(panes => { 41 - panes[0].children.shift(); 42 - return panes; 43 - }) 44 - } 45 - 46 - openThread(uri: string, children?: boolean) { 39 + openThread(uri: string) { 47 40 // Cancel action if user is selecting text 48 41 if (window.getSelection().toString().length) return; 49 - // Cancel action if post is the same than the last opened thread 50 - if ( 51 - this.auxPanes().length && 52 - (this.auxPanes()[this.auxPanes().length-1] as ThreadAuxPane).uri && 53 - (this.auxPanes()[this.auxPanes().length-1] as ThreadAuxPane).uri == uri 54 - ) return; 42 + // Bring pane to front if thread is already open 43 + const pane = this.auxPanes().find(p => (p as ThreadAuxPane).uri && (p as ThreadAuxPane).uri == uri); 44 + if (pane) { 45 + this.reorderAuxPane(pane.uuid); 46 + return; 47 + } 55 48 // Mute all video players on auxbar 56 49 document.querySelector('auxbar').querySelectorAll('video').forEach((video: HTMLVideoElement) => { 57 50 video.muted = true; 58 51 }); 59 52 60 - const pane = new ThreadAuxPane(); 61 - pane.uri = uri; 62 - 63 - if (children) { 64 - this.auxPanes.update(panes => { 65 - panes[0].children.unshift(pane); 66 - return panes; 67 - }); 68 - } else { 69 - this.auxPanes.update(panes => { 70 - return [pane, ...panes]; 71 - }); 72 - } 53 + this.auxPanes.update(panes => { 54 + return [new ThreadAuxPane(uri), ...panes]; 55 + }); 73 56 } 74 57 75 - openAuthor(did: string, children?: boolean) { 58 + openAuthor(author: Partial<{did: string, handle: string}>) { 76 59 // Cancel action if user is selecting text 77 60 if (window.getSelection().toString().length) return; 61 + // Bring pane to front if author is already open 62 + const pane = this.auxPanes().find(p => (p as AuthorAuxPane).did && (p as AuthorAuxPane).did == author.did); 63 + if (pane) { 64 + this.reorderAuxPane(pane.uuid); 65 + return; 66 + } 78 67 // Mute all video players on auxbar 79 68 document.querySelector('auxbar').querySelectorAll('video').forEach((video: HTMLVideoElement) => { 80 69 video.muted = true; 81 70 }); 82 71 83 - const pane = new AuthorAuxPane(); 84 - pane.did = did; 85 - 86 - if (children) { 87 - this.auxPanes.update(panes => { 88 - panes[0].children.unshift(pane); 89 - return panes; 90 - }); 91 - } else { 92 - this.auxPanes.update(panes => { 93 - return [pane, ...panes]; 94 - }); 95 - } 72 + this.auxPanes.update(panes => { 73 + return [new AuthorAuxPane(author), ...panes]; 74 + }); 96 75 } 97 76 98 - openRecord(record: AppBskyEmbedRecord.View, children?: boolean) { 77 + openRecord(record: AppBskyEmbedRecord.View) { 99 78 switch (record.record.$type) { 100 79 case 'app.bsky.embed.record#viewRecord': 101 - this.openThread((record.record as AppBskyEmbedRecord.ViewRecord).uri, children); 80 + this.openThread((record.record as AppBskyEmbedRecord.ViewRecord).uri); 102 81 break; 103 82 case 'app.bsky.graph.defs#listView': 104 83 break;