+203
-62
src/app/components/navigation/post-composer/post-composer.component.html
+203
-62
src/app/components/navigation/post-composer/post-composer.component.html
···
1
1
+
@let mediaEmbed = postService.postCompose().mediaEmbed();
2
2
+
@let recordEmbed = postService.postCompose().recordEmbed();
3
3
+
@let reply = postService.postCompose().reply();
4
4
+
@let suggestion = embedSuggestions().length ? (embedSuggestions() | slice : 0 : 1)[0] : undefined;
5
5
+
1
6
<div
2
2
-
class="flex w-full border-b border-primary box-border"
7
7
+
class="flex relative flex-col w-full border-b border-primary"
3
8
>
4
4
-
<div
5
5
-
class="relative flex-1"
6
6
-
>
9
9
+
@if (showDragOver()) {
7
10
<div
8
8
-
#text autofocus
9
9
-
contenteditable="plaintext-only"
10
10
-
spellcheck="false"
11
11
-
class="absolute top-0 left-0 z-1 w-full h-full p-2 bg-transparent text-transparent outline-0 caret-black"
12
12
-
(input)="formatText($event)"
13
13
-
(paste)="postService.attachMedia($any($event.clipboardData.files))"
14
14
-
(keydown.control.enter)="postBtn.click()"
15
15
-
[mention]="mentionItems"
16
16
-
[mentionConfig]="{
17
17
-
triggerChar: '@',
18
18
-
labelKey: 'value',
19
19
-
disableSearch: true,
20
20
-
dropUp: true
21
21
-
}"
22
22
-
(searchTerm)="searchMentions($event)"
23
23
-
></div>
11
11
+
class="absolute top-0 left-0 flex items-center justify-center w-full h-full bg-primary/5 z-1 backdrop-blur-[1px]"
12
12
+
>
13
13
+
<span
14
14
+
class="text-xl underline text-primary/70"
15
15
+
>Drop here</span>
16
16
+
</div>
17
17
+
}
18
18
+
19
19
+
@if (reply) {
24
20
<div
25
25
-
[innerHTML]="text.textContent.length ? formattedText : undefined"
26
26
-
class="w-full h-full p-2 bg-white text-black empty:text-primary/50 outline-0 break-words whitespace-pre-wrap empty:before:content-['user@consolesky:/$_\_']"
27
27
-
></div>
28
28
-
</div>
21
21
+
class="relative w-full h-7 flex items-center px-2 bg-primary text-bg font-semibold cursor-default"
22
22
+
>
23
23
+
<span
24
24
+
>Replying to</span>
25
25
+
26
26
+
<span
27
27
+
class="ml-2"
28
28
+
>{{reply.author | displayName}}</span>
29
29
+
30
30
+
<span
31
31
+
class="ml-auto font-semibold cursor-pointer hover:underline"
32
32
+
(click)="showReply.set(!showReply())"
33
33
+
>{{ showReply() ? 'hide post' : 'view post' }}</span>
34
34
+
35
35
+
<span
36
36
+
class="ml-6 font-semibold cursor-pointer hover:underline"
37
37
+
(click)="showReply.set(false); postService.postCompose().reply.set(undefined)"
38
38
+
>cancel</span>
39
39
+
40
40
+
@if (showReply()) {
41
41
+
<div
42
42
+
class="absolute bg-bg border border-primary bottom-7 right-[-1px]"
43
43
+
>
44
44
+
<post-card
45
45
+
[post]="reply"
46
46
+
class="block w-[30rem] text-primary font-normal"
47
47
+
/>
48
48
+
</div>
49
49
+
}
50
50
+
</div>
51
51
+
}
29
52
30
53
<div
31
31
-
class="flex items-end shrink-0 p-[0.35rem]"
54
54
+
class="flex w-full"
32
55
>
33
33
-
<button
34
34
-
class="btn-secondary h-22 w-22 flex flex-col justify-center items-center"
56
56
+
<div
57
57
+
class="flex-1 min-w-0"
58
58
+
[class.relative]="!showDragOver()"
35
59
>
36
60
<div
37
37
-
class="flex items-center justify-center h-10"
61
61
+
#text autofocus
62
62
+
contenteditable="plaintext-only"
63
63
+
spellcheck="false"
64
64
+
class="absolute top-0 left-0 z-1 w-full h-full p-2 bg-transparent text-transparent outline-0 caret-black"
65
65
+
(input)="formatText($event)"
66
66
+
(paste)="postService.attachMedia($any($event.clipboardData.files))"
67
67
+
(keydown.control.enter)="postBtn.click()"
68
68
+
[mention]="mentionItems"
69
69
+
[mentionConfig]="{
70
70
+
triggerChar: '@',
71
71
+
labelKey: 'value',
72
72
+
disableSearch: true,
73
73
+
dropUp: true
74
74
+
}"
75
75
+
(searchTerm)="searchMentions($event)"
76
76
+
></div>
77
77
+
<div
78
78
+
[innerHTML]="text.textContent.length ? formattedText : undefined"
79
79
+
class="w-full h-full p-2 bg-white text-black empty:text-primary/50 outline-0 break-words whitespace-pre-wrap empty:before:content-['user@consolesky:/$_\_']"
80
80
+
></div>
81
81
+
82
82
+
83
83
+
<div
84
84
+
class="relative w-full"
38
85
>
39
86
<span
40
40
-
class="material-icons !text-6xl"
41
41
-
>format_quote</span>
87
87
+
class="absolute bottom-1 right-2 text-primary/50"
88
88
+
[class.text-repost]="(300 - text.textContent.length) > 50"
89
89
+
>{{300 - text.textContent.length}}</span>
42
90
</div>
91
91
+
</div>
43
92
44
44
-
Quote
45
45
-
</button>
46
46
-
</div>
93
93
+
<div
94
94
+
class="flex items-end shrink-0 p-[0.35rem] empty:hidden"
95
95
+
>
96
96
+
97
97
+
@if (mediaEmbed) {
98
98
+
<button
99
99
+
class="btn-secondary h-22 w-22 flex flex-col justify-center items-center"
100
100
+
>
101
101
+
<div
102
102
+
class="flex items-center justify-center h-10"
103
103
+
>
104
104
+
<span
105
105
+
class="material-icons !text-5xl -translate-y-0.5"
106
106
+
>attachment</span>
107
107
+
</div>
108
108
+
109
109
+
@if (mediaEmbed | isMediaEmbedImage) {
110
110
+
images
111
111
+
}
112
112
+
@else if (mediaEmbed | isMediaEmbedVideo) {
113
113
+
video
114
114
+
}
115
115
+
@else if (mediaEmbed | isMediaEmbedExternal) {
116
116
+
link
117
117
+
}
118
118
+
</button>
119
119
+
} @else if (suggestion | isMediaEmbedExternal) {
120
120
+
<button
121
121
+
class="btn-secondary h-22 w-22 flex flex-col justify-center items-center border-dashed"
122
122
+
(click)="embedLink()"
123
123
+
>
124
124
+
<div
125
125
+
class="flex items-center justify-center h-8"
126
126
+
>
127
127
+
<span
128
128
+
class="material-icons !text-5xl"
129
129
+
>attachment</span>
130
130
+
</div>
131
131
+
132
132
+
add link?
133
133
+
</button>
134
134
+
}
47
135
48
48
-
<div
49
49
-
class="w-28 flex flex-col shrink-0 justify-end"
50
50
-
>
136
136
+
@if (recordEmbed) {
137
137
+
<button
138
138
+
class="btn-secondary h-22 w-22 ml-2 flex flex-col justify-center items-center"
139
139
+
>
140
140
+
<div
141
141
+
class="flex items-center justify-center h-10"
142
142
+
>
143
143
+
<span
144
144
+
class="material-icons !text-6xl"
145
145
+
>format_quote</span>
146
146
+
</div>
147
147
+
148
148
+
quote
149
149
+
</button>
150
150
+
} @else if (suggestion | isRecordEmbed) {
151
151
+
<button
152
152
+
class="btn-secondary h-22 w-22 flex flex-col justify-center items-center border-dashed"
153
153
+
(click)="embedRecord()"
154
154
+
>
155
155
+
<div
156
156
+
class="flex items-center justify-center h-10"
157
157
+
>
158
158
+
<span
159
159
+
class="material-icons !text-6xl"
160
160
+
>format_quote</span>
161
161
+
</div>
162
162
+
163
163
+
add quote?
164
164
+
</button>
165
165
+
}
166
166
+
</div>
167
167
+
51
168
<div
52
52
-
class="flex h-fit w-full border-primary"
53
53
-
[class.border-t]="text | postComposerHeight"
169
169
+
class="w-28 flex flex-col shrink-0 justify-end"
54
170
>
55
55
-
<button
56
56
-
class="btn-secondary h-9 flex-1 border-r-0 border-t-0 p-0 flex items-center justify-center"
171
171
+
<div
172
172
+
class="flex h-fit w-full border-primary border-l border-b"
173
173
+
[class.border-t]="text | postComposerHeight"
57
174
>
58
58
-
<span
59
59
-
class="material-icons-outlined"
60
60
-
>mode_comment</span>
61
61
-
</button>
175
175
+
<button
176
176
+
class="btn-secondary h-9 flex-1 border-0 p-0 flex items-center justify-center"
177
177
+
(click)="uploader.click()"
178
178
+
>
179
179
+
<span
180
180
+
class="material-icons-outlined !text-xl"
181
181
+
>image</span>
182
182
+
183
183
+
<input
184
184
+
#uploader
185
185
+
type="file"
186
186
+
class="hidden"
187
187
+
(change)="postService.attachMedia($any(uploader.files))"
188
188
+
/>
189
189
+
</button>
62
190
63
63
-
<button
64
64
-
class="btn-secondary h-9 flex-1 border-r-0 border-t-0 p-0 flex items-center justify-center"
65
65
-
>
66
66
-
<span
67
67
-
class="material-icons-outlined"
68
68
-
>mode_comment</span>
69
69
-
</button>
191
191
+
<div
192
192
+
class="h-full border-l border-primary"
193
193
+
></div>
194
194
+
195
195
+
<button
196
196
+
disabled
197
197
+
class="btn-secondary h-9 flex-1 border-0 p-0 flex items-center justify-center"
198
198
+
>
199
199
+
<span
200
200
+
class="material-icons-outlined !text-xl"
201
201
+
>sentiment_satisfied_alt</span>
202
202
+
</button>
203
203
+
204
204
+
<div
205
205
+
class="h-full border-l border-primary"
206
206
+
></div>
207
207
+
208
208
+
<button
209
209
+
disabled
210
210
+
class="btn-secondary h-9 flex-1 border-0 p-0 flex items-center justify-center"
211
211
+
>
212
212
+
<span
213
213
+
class="material-icons-outlined !text-[2em]"
214
214
+
>gif</span>
215
215
+
</button>
216
216
+
</div>
70
217
71
218
<button
72
72
-
class="btn-secondary h-9 flex-1 border-r-0 border-t-0 p-0 flex items-center justify-center"
73
73
-
>
74
74
-
<span
75
75
-
class="material-icons-outlined"
76
76
-
>mode_comment</span>
77
77
-
</button>
219
219
+
#postBtn
220
220
+
class="btn-primary font-semibold h-16 w-full border-0 border-l"
221
221
+
[disabled]="loading || text.innerText.length > 300 || (!text.innerText.length && !mediaEmbed && !recordEmbed)"
222
222
+
(click)="publishPost()"
223
223
+
>post</button>
78
224
</div>
79
79
-
<button
80
80
-
#postBtn
81
81
-
class="btn-primary h-16 w-full border-r-0 border-b-0"
82
82
-
(click)="publishPost()"
83
83
-
>Post</button>
84
225
</div>
85
226
</div>
+73
-3
src/app/components/navigation/post-composer/post-composer.component.ts
+73
-3
src/app/components/navigation/post-composer/post-composer.component.ts
···
1
1
-
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, signal, WritableSignal} from '@angular/core';
1
1
+
import {
2
2
+
ChangeDetectionStrategy,
3
3
+
ChangeDetectorRef,
4
4
+
Component, ElementRef,
5
5
+
HostListener,
6
6
+
signal,
7
7
+
WritableSignal
8
8
+
} from '@angular/core';
2
9
import {$Typed, AppBskyFeedDefs, AppBskyGraphDefs, RichText} from '@atproto/api';
3
3
-
import {ExternalEmbed, ImageEmbed, RecordEmbed} from '@models/embed';
10
10
+
import {ExternalEmbed, ImageEmbed, RecordEmbed, RecordEmbedType} from '@models/embed';
4
11
import {EmbedUtils} from '@shared/utils/embed-utils';
5
12
import {PostService} from '@services/post.service';
6
13
import {EmbedService} from '@services/embed.service';
···
10
17
import {SnippetUtils} from '@shared/utils/snippet-utils';
11
18
import {MentionModule} from 'angular-mentions';
12
19
import {PostComposerHeightPipe} from '@shared/pipes/post-composer-height.pipe';
20
20
+
import {DisplayNamePipe} from '@shared/pipes/display-name.pipe';
21
21
+
import {PostCardComponent} from '@components/cards/post-card/post-card.component';
22
22
+
import {IsMediaEmbedImagePipe} from '@shared/pipes/type-guards/is-media-embed-image';
23
23
+
import {IsMediaEmbedVideoPipe} from '@shared/pipes/type-guards/is-media-embed-video';
24
24
+
import {IsMediaEmbedExternalPipe} from '@shared/pipes/type-guards/is-media-embed-external';
25
25
+
import {SlicePipe} from '@angular/common';
26
26
+
import {IsRecordEmbedPipe} from '@shared/pipes/type-guards/is-record-embed';
13
27
14
28
@Component({
15
29
selector: 'post-composer',
16
30
imports: [
17
31
MentionModule,
18
18
-
PostComposerHeightPipe
32
32
+
PostComposerHeightPipe,
33
33
+
DisplayNamePipe,
34
34
+
PostCardComponent,
35
35
+
IsMediaEmbedImagePipe,
36
36
+
IsMediaEmbedVideoPipe,
37
37
+
IsMediaEmbedExternalPipe,
38
38
+
SlicePipe,
39
39
+
IsRecordEmbedPipe
19
40
],
20
41
templateUrl: './post-composer.component.html',
21
42
styles: `
···
46
67
mentionItems = [];
47
68
loading = false;
48
69
embedSuggestions = signal<Array<RecordEmbed | ExternalEmbed>>([]);
70
70
+
showReply = signal(false);
71
71
+
showMedia = signal(false);
72
72
+
showRecord = signal(false);
73
73
+
showDragOver = signal(false);
49
74
50
75
constructor(
51
76
protected postService: PostService,
52
77
private embedService: EmbedService,
78
78
+
private elementRef: ElementRef,
53
79
private cdRef: ChangeDetectorRef
54
80
) {}
55
81
···
124
150
}
125
151
}
126
152
153
153
+
embedRecord() {
154
154
+
const embed = this.embedSuggestions()[0] as RecordEmbed;
155
155
+
switch (embed.recordType) {
156
156
+
case RecordEmbedType.POST:
157
157
+
this.embedQuote();
158
158
+
break;
159
159
+
case RecordEmbedType.FEED:
160
160
+
this.embedFeed();
161
161
+
break;
162
162
+
case RecordEmbedType.LIST:
163
163
+
this.embedList();
164
164
+
break;
165
165
+
case RecordEmbedType.STARTER_PACK:
166
166
+
this.embedStarterPack();
167
167
+
break;
168
168
+
}
169
169
+
}
170
170
+
127
171
embedQuote() {
128
172
const embed = this.embedSuggestions()[0] as RecordEmbed;
129
173
agent.resolveHandle({
···
186
230
//TODO: MessageService
187
231
err => console.log(err.message)
188
232
).finally(() => this.loading = false);
233
233
+
}
234
234
+
235
235
+
@HostListener('dragenter', ['$event'])
236
236
+
onDragEnter(event: Event) {
237
237
+
event.preventDefault();
238
238
+
239
239
+
if (this.elementRef.nativeElement.contains((event as any).currentTarget)) {
240
240
+
this.showDragOver.set(true);
241
241
+
}
242
242
+
}
243
243
+
244
244
+
@HostListener('dragleave', ['$event'])
245
245
+
onDragLeave(event: Event) {
246
246
+
event.preventDefault();
247
247
+
248
248
+
if (!this.elementRef.nativeElement.contains((event as any).relatedTarget)) {
249
249
+
this.showDragOver.set(false);
250
250
+
}
251
251
+
}
252
252
+
253
253
+
@HostListener('drop', ['$event'])
254
254
+
onDrop(event: Event) {
255
255
+
event.preventDefault();
256
256
+
257
257
+
this.showDragOver.set(false);
258
258
+
this.postService.attachMedia((event as any).dataTransfer.files);
189
259
}
190
260
}
+1
-1
src/app/components/navigation/sidebar/sidebar.component.html
+1
-1
src/app/components/navigation/sidebar/sidebar.component.html
+11
src/app/shared/pipes/type-guards/is-record-embed.ts
+11
src/app/shared/pipes/type-guards/is-record-embed.ts
···
1
1
+
import {Pipe, PipeTransform} from '@angular/core';
2
2
+
import {EmbedType, RecordEmbed} from "@models/embed";
3
3
+
4
4
+
@Pipe({
5
5
+
name: 'isRecordEmbed'
6
6
+
})
7
7
+
export class IsRecordEmbedPipe implements PipeTransform {
8
8
+
transform(value: unknown): value is RecordEmbed {
9
9
+
return (value as RecordEmbed)?.type == EmbedType.RECORD;
10
10
+
}
11
11
+
}
+19
-3
src/styles.css
+19
-3
src/styles.css
···
192
192
@layer components {
193
193
.btn-primary {
194
194
box-sizing: border-box;
195
195
-
border: 1px solid var(--color-primary);
195
195
+
border: 1px solid;
196
196
+
border-color: var(--color-primary);
196
197
background-color: var(--color-primary);
197
198
color: var(--color-bg);
198
199
width: fit-content;
···
205
206
background-color: var(--color-bg);
206
207
color: var(--color-primary);
207
208
}
209
209
+
210
210
+
&:disabled {
211
211
+
pointer-events: none;
212
212
+
opacity: 0.3;
213
213
+
}
208
214
}
209
215
210
216
.btn-secondary {
211
217
box-sizing: border-box;
212
212
-
border: 1px solid var(--color-primary);
218
218
+
border: 1px solid;
219
219
+
border-color: var(--color-primary);
213
220
background-color: var(--color-bg);
214
221
color: var(--color-primary);
215
222
width: fit-content;
···
219
226
min-height: 2em;
220
227
221
228
&:hover {
222
222
-
background-color: color-mix(in oklab, var(--color-primary) /* #000 = #000000 */ 15%, transparent);
229
229
+
background-color: color-mix(in oklab, var(--color-primary) /* #000 = #000000 */ 10%, transparent);
223
230
color: var(--color-primary);
231
231
+
}
232
232
+
233
233
+
&:disabled {
234
234
+
pointer-events: none;
235
235
+
opacity: 0.3;
224
236
}
225
237
}
226
238
···
238
250
&:hover {
239
251
background-color: var(--color-primary);
240
252
color: var(--color-bg);
253
253
+
}
254
254
+
255
255
+
&:disabled {
256
256
+
pointer-events: none;
241
257
}
242
258
}
243
259
}