+3
-3
src/components/BskyPost.svelte
+3
-3
src/components/BskyPost.svelte
···
81
81
const p = await client.getProfile(did);
82
82
if (!p.ok) return;
83
83
profile = p.value;
84
-
console.log(profile.description);
84
+
// console.log(profile.description);
85
85
});
86
86
// const replies = replyBacklinks
87
87
// ? Promise.resolve(ok(replyBacklinks))
···
97
97
98
98
const scrollToAndPulse = (targetUri: ResourceUri) => {
99
99
const targetId = `timeline-post-${targetUri}-0`;
100
-
console.log(`Scrolling to ${targetId}`);
100
+
// console.log(`Scrolling to ${targetId}`);
101
101
const element = document.getElementById(targetId);
102
102
if (!element) return;
103
103
···
171
171
// reply: backlinks[2],
172
172
// quote: backlinks[3]
173
173
};
174
-
console.log('findAllBacklinks', did, aturi, actions);
174
+
// console.log('findAllBacklinks', did, aturi, actions);
175
175
postActions.set(`${did}:${aturi}`, actions);
176
176
};
177
177
onMount(() => {
+30
-34
src/components/PostComposer.svelte
+30
-34
src/components/PostComposer.svelte
···
8
8
import { parseCanonicalResourceUri } from '@atcute/lexicons';
9
9
import type { ComAtprotoRepoStrongRef } from '@atcute/atproto';
10
10
11
+
export type State =
12
+
| { type: 'null' }
13
+
| { type: 'focused'; quoting?: PostWithUri; replying?: PostWithUri };
14
+
11
15
interface Props {
12
16
client: AtpClient;
13
17
onPostSent: (post: PostWithUri) => void;
14
-
quoting?: PostWithUri;
15
-
replying?: PostWithUri;
18
+
_state: State;
16
19
}
17
20
18
-
let {
19
-
client,
20
-
onPostSent,
21
-
quoting = $bindable(undefined),
22
-
replying = $bindable(undefined)
23
-
}: Props = $props();
21
+
let { client, onPostSent, _state = $bindable({ type: 'null' }) }: Props = $props();
24
22
25
-
let color = $derived(
23
+
const isFocused = $derived(_state.type === 'focused');
24
+
25
+
const color = $derived(
26
26
client.user?.did ? generateColorForDid(client.user?.did) : 'var(--nucleus-accent2)'
27
27
);
28
28
···
35
35
const record: AppBskyFeedPost.Main = {
36
36
$type: 'app.bsky.feed.post',
37
37
text,
38
-
reply: replying
39
-
? {
40
-
root: replying.record.reply?.root ?? strongRef(replying),
41
-
parent: strongRef(replying)
42
-
}
43
-
: undefined,
44
-
embed: quoting
45
-
? {
46
-
$type: 'app.bsky.embed.record',
47
-
record: strongRef(quoting)
48
-
}
49
-
: undefined,
38
+
reply:
39
+
_state.type === 'focused' && _state.replying
40
+
? {
41
+
root: _state.replying.record.reply?.root ?? strongRef(_state.replying),
42
+
parent: strongRef(_state.replying)
43
+
}
44
+
: undefined,
45
+
embed:
46
+
_state.type === 'focused' && _state.quoting
47
+
? {
48
+
$type: 'app.bsky.embed.record',
49
+
record: strongRef(_state.quoting)
50
+
}
51
+
: undefined,
50
52
createdAt: new Date().toISOString()
51
53
};
52
54
···
75
77
76
78
let postText = $state('');
77
79
let info = $state('');
78
-
let isFocused = $state(false);
79
80
let textareaEl: HTMLTextAreaElement | undefined = $state();
80
81
81
82
const unfocus = () => {
82
-
isFocused = false;
83
-
quoting = undefined;
84
-
replying = undefined;
83
+
_state.type = 'null';
85
84
};
86
85
87
86
const doPost = () => {
···
104
103
$effect(() => {
105
104
document.documentElement.style.setProperty('--acc-color', color);
106
105
if (isFocused && textareaEl) textareaEl.focus();
107
-
if (quoting || replying) isFocused = true;
108
106
});
109
107
</script>
110
108
···
119
117
/>
120
118
{/snippet}
121
119
122
-
{#snippet composer()}
120
+
{#snippet composer(replying?: PostWithUri, quoting?: PostWithUri)}
123
121
<div class="flex items-center gap-2">
124
122
<div class="grow"></div>
125
123
<span
···
149
147
<textarea
150
148
bind:this={textareaEl}
151
149
bind:value={postText}
152
-
onfocus={() => (isFocused = true)}
150
+
onfocus={() => (_state.type = 'focused')}
153
151
onblur={unfocus}
154
152
onkeydown={(event) => {
155
153
if (event.key === 'Escape') unfocus();
···
174
172
<!-- svelte-ignore a11y_no_static_element_interactions -->
175
173
<div
176
174
onmousedown={(e) => {
177
-
if (isFocused) {
178
-
e.preventDefault();
179
-
}
175
+
if (isFocused) e.preventDefault();
180
176
}}
181
177
class="flex max-w-full rounded-sm border-2 shadow-lg transition-all duration-300
182
178
{!isFocused ? 'min-h-13 items-center' : ''}
···
196
192
</div>
197
193
{:else}
198
194
<div class="flex flex-col gap-2">
199
-
{#if isFocused}
200
-
{@render composer()}
195
+
{#if _state.type === 'focused'}
196
+
{@render composer(_state.replying, _state.quoting)}
201
197
{:else}
202
198
<input
203
199
bind:value={postText}
204
-
onfocus={() => (isFocused = true)}
200
+
onfocus={() => (_state = { type: 'focused' })}
205
201
type="text"
206
202
placeholder="what's on your mind?"
207
203
class="flex-1"
+7
-8
src/routes/+page.svelte
+7
-8
src/routes/+page.svelte
···
1
1
<script lang="ts">
2
2
import BskyPost from '$components/BskyPost.svelte';
3
-
import PostComposer from '$components/PostComposer.svelte';
3
+
import PostComposer, { type State as PostComposerState } from '$components/PostComposer.svelte';
4
4
import AccountSelector from '$components/AccountSelector.svelte';
5
5
import SettingsPopup from '$components/SettingsPopup.svelte';
6
6
import { AtpClient, type NotificationsStreamEvent } from '$lib/at/client';
···
23
23
24
24
const { data: loadData }: PageProps = $props();
25
25
26
+
// svelte-ignore state_referenced_locally
26
27
let errors = $state(loadData.client.ok ? [] : [loadData.client.error]);
27
28
let errorsOpen = $state(false);
28
29
···
72
73
73
74
const threads = $derived(filterThreads(buildThreads(posts), $accounts, { viewOwnPosts }));
74
75
75
-
let quoting = $state<PostWithUri | undefined>(undefined);
76
-
let replying = $state<PostWithUri | undefined>(undefined);
76
+
let postComposerState = $state<PostComposerState>({ type: 'null' });
77
77
78
78
const expandedThreads = new SvelteSet<ResourceUri>();
79
79
···
330
330
<PostComposer
331
331
client={selectedClient}
332
332
onPostSent={(post) => posts.get(selectedDid!)?.set(post.uri, post)}
333
-
bind:quoting
334
-
bind:replying
333
+
bind:_state={postComposerState}
335
334
/>
336
335
</div>
337
336
{:else}
···
342
341
</div>
343
342
{/if}
344
343
345
-
{#if showScrollToTop}
344
+
{#if postComposerState.type === 'null' && showScrollToTop}
346
345
{@render appButton(scrollToTop, 'heroicons:arrow-up-16-solid', 'scroll to top')}
347
346
{/if}
348
347
</div>
···
415
414
<div class="mb-1.5">
416
415
<BskyPost
417
416
client={selectedClient ?? viewClient}
418
-
onQuote={(post) => (quoting = post)}
419
-
onReply={(post) => (replying = post)}
417
+
onQuote={(post) => (postComposerState = { type: 'focused', quoting: post })}
418
+
onReply={(post) => (postComposerState = { type: 'focused', replying: post })}
420
419
{...post}
421
420
/>
422
421
</div>