+63
src/components/feeds/feed-info-prompt.tsx
+63
src/components/feeds/feed-info-prompt.tsx
···
1
+
import type { AppBskyFeedDefs } from '@atcute/client/lexicons';
2
+
3
+
import { parseAtUri } from '~/api/types/at-uri';
4
+
5
+
import { useModalContext } from '~/globals/modals';
6
+
7
+
import { formatLong } from '~/lib/intl/number';
8
+
9
+
import Avatar from '~/components/avatar';
10
+
import Button from '~/components/button';
11
+
import Divider from '~/components/divider';
12
+
import * as Prompt from '~/components/prompt';
13
+
14
+
import HeartOutlinedIcon from '../icons-central/heart-outline';
15
+
16
+
export interface FeedInfoPromptProps {
17
+
/** Expected to be static */
18
+
feed: AppBskyFeedDefs.GeneratorView;
19
+
}
20
+
21
+
const FeedInfoPrompt = (props: FeedInfoPromptProps) => {
22
+
const { close } = useModalContext();
23
+
24
+
const feed = props.feed;
25
+
26
+
const authorUrl = `/${feed.creator.did}`;
27
+
const feedUrl = `${authorUrl}/feeds/${parseAtUri(feed.uri).rkey}`;
28
+
29
+
return (
30
+
<Prompt.Container maxWidth="md">
31
+
<div>
32
+
<Avatar type="generator" src={/* @once */ feed.avatar} size={null} class="h-12 w-12" />
33
+
34
+
<p class="mt-4 text-xl font-bold">{/* @once */ feed.displayName.trim()}</p>
35
+
<p class="mt-2 text-sm empty:hidden">{feed.description}</p>
36
+
37
+
<div class="mt-2">
38
+
<a href={`${feedUrl}/likes`} onClick={close} class="text-de text-contrast-muted hover:underline">
39
+
{feed.likeCount === 1
40
+
? `Liked by ${formatLong(feed.likeCount)} user`
41
+
: `Liked by ${formatLong(feed.likeCount ?? 0)} users`}
42
+
</a>
43
+
</div>
44
+
45
+
<div class="mt-4 flex gap-2">
46
+
<Avatar
47
+
type="user"
48
+
src={/* @once */ feed.creator.avatar}
49
+
href={authorUrl}
50
+
onClick={close}
51
+
size="xs"
52
+
/>
53
+
54
+
<a href={authorUrl} onClick={close} class="text-sm font-medium text-contrast-muted">
55
+
{/* @once */ feed.creator.handle}
56
+
</a>
57
+
</div>
58
+
</div>
59
+
</Prompt.Container>
60
+
);
61
+
};
62
+
63
+
export default FeedInfoPrompt;
+7
-3
src/components/prompt.tsx
+7
-3
src/components/prompt.tsx
···
18
18
19
19
const PromptContainer = (props: PromptContainerProps) => {
20
20
const { close, isActive } = useModalContext();
21
-
const isDesktop = useMediaQuery('(width >= 688px) and (height >= 500px)');
21
+
const isDesktop = useMediaQuery('((width >= 480px) and (height >= 500px))');
22
22
23
23
const isDisabled = () => !!props.disabled;
24
24
···
43
43
} else {
44
44
return (
45
45
<Fieldset standalone disabled={isDisabled()}>
46
-
<div class="flex grow flex-col self-stretch overflow-y-auto bg-contrast-overlay/40">
46
+
<div class="flex grow flex-col items-center self-stretch overflow-y-auto bg-contrast-overlay/40">
47
47
<div class="h-[40dvh] shrink-0"></div>
48
-
<div ref={containerRef} role="menu" class="mt-auto flex flex-col rounded-t-xl bg-background p-4">
48
+
<div
49
+
ref={containerRef}
50
+
role="menu"
51
+
class="mt-auto flex w-full max-w-120 flex-col rounded-t-xl bg-background p-4"
52
+
>
49
53
{props.children}
50
54
</div>
51
55
</div>
-7
src/routes.ts
-7
src/routes.ts
···
206
206
return isValidDidOrHandle(params.didOrHandle);
207
207
},
208
208
},
209
-
{
210
-
path: '/:did/feeds/:rkey/info',
211
-
component: lazy(() => import('./views/profile-feed-info')),
212
-
validate(params) {
213
-
return isValidDidOrHandle(params.did);
214
-
},
215
-
},
216
209
217
210
{
218
211
path: '/:did/lists',
-119
src/views/profile-feed-info.tsx
-119
src/views/profile-feed-info.tsx
···
1
-
import { Match, Show, Switch, createMemo } from 'solid-js';
2
-
3
-
import type { AppBskyFeedDefs } from '@atcute/client/lexicons';
4
-
import { useQueryClient } from '@mary/solid-query';
5
-
6
-
import { ContextContentMedia } from '~/api/moderation/constants';
7
-
import { moderateGeneric } from '~/api/moderation/entities/generic';
8
-
import { moderateProfile } from '~/api/moderation/entities/profile';
9
-
import { precacheProfile } from '~/api/queries-cache/profile-precache';
10
-
import { createFeedMetaQuery } from '~/api/queries/feed';
11
-
import { makeAtUri } from '~/api/types/at-uri';
12
-
13
-
import { useParams, useTitle } from '~/lib/navigation/router';
14
-
import { inject } from '~/lib/states/singleton';
15
-
import ModerationService from '~/lib/states/singletons/moderation';
16
-
17
-
import Avatar, { getUserAvatarType } from '~/components/avatar';
18
-
import * as Boxed from '~/components/boxed';
19
-
import CircularProgressView from '~/components/circular-progress-view';
20
-
import * as Page from '~/components/page';
21
-
22
-
const FeedInfoPage = () => {
23
-
const { did, rkey } = useParams();
24
-
25
-
const uri = makeAtUri(did, 'app.bsky.feed.generator', rkey);
26
-
const feed = createFeedMetaQuery(() => uri);
27
-
28
-
useTitle(() => {
29
-
const data = feed.data;
30
-
if (data) {
31
-
return `Feed info (${data.displayName}) — ${import.meta.env.VITE_APP_NAME}`;
32
-
}
33
-
34
-
return `Feed info — ${import.meta.env.VITE_APP_NAME}`;
35
-
});
36
-
37
-
return (
38
-
<>
39
-
<Page.Header>
40
-
<Page.HeaderAccessory>
41
-
<Page.Back to={`/${did}/feeds/${rkey}`} />
42
-
</Page.HeaderAccessory>
43
-
</Page.Header>
44
-
45
-
<Switch>
46
-
<Match when={feed.data}>{(info) => <InfoView feed={info()} />}</Match>
47
-
48
-
<Match when>
49
-
<CircularProgressView />
50
-
</Match>
51
-
</Switch>
52
-
</>
53
-
);
54
-
};
55
-
56
-
export default FeedInfoPage;
57
-
58
-
const InfoView = (props: { feed: AppBskyFeedDefs.GeneratorView }) => {
59
-
const queryClient = useQueryClient();
60
-
61
-
const moderationOptions = inject(ModerationService);
62
-
63
-
const feed = () => props.feed;
64
-
const creator = () => feed().creator;
65
-
66
-
const moderation = createMemo(() => {
67
-
return [
68
-
...moderateGeneric(feed(), creator().did, moderationOptions()),
69
-
...moderateProfile(creator(), moderationOptions()),
70
-
];
71
-
});
72
-
73
-
return (
74
-
<Boxed.Container>
75
-
<div class="px-4">
76
-
<div class="flex gap-4">
77
-
<Avatar
78
-
type="generator"
79
-
src={feed().avatar}
80
-
size={null}
81
-
moderation={moderation()}
82
-
modContext={ContextContentMedia}
83
-
class="h-13 w-13"
84
-
/>
85
-
86
-
<div class="flex min-w-0 grow flex-col">
87
-
<p class="mb-1 mt-0.75 overflow-hidden text-ellipsis break-words text-lg font-bold leading-none">
88
-
{feed().displayName}
89
-
</p>
90
-
91
-
<a
92
-
href={`/${creator().did}`}
93
-
onClick={() => precacheProfile(queryClient, creator())}
94
-
class="group mt-1 flex items-center"
95
-
>
96
-
<Avatar type={getUserAvatarType(creator())} src={creator().avatar} size="xs" class="mr-2" />
97
-
98
-
<span class="mr-1 overflow-hidden text-ellipsis whitespace-nowrap text-sm font-medium text-contrast-muted">
99
-
{creator().handle.toLowerCase()}
100
-
</span>
101
-
</a>
102
-
</div>
103
-
</div>
104
-
</div>
105
-
106
-
<Boxed.Group>
107
-
<Show when={feed().description?.trim()}>
108
-
{(description) => (
109
-
<Boxed.List>
110
-
<div class="flex flex-col whitespace-pre-wrap px-4 py-3 text-left text-sm empty:hidden">
111
-
{description()}
112
-
</div>
113
-
</Boxed.List>
114
-
)}
115
-
</Show>
116
-
</Boxed.Group>
117
-
</Boxed.Container>
118
-
);
119
-
};
+26
-11
src/views/profile-feed.tsx
+26
-11
src/views/profile-feed.tsx
···
11
11
12
12
import { useParams, useTitle } from '~/lib/navigation/router';
13
13
14
+
import Avatar from '~/components/avatar';
14
15
import CircularProgressView from '~/components/circular-progress-view';
15
16
import ErrorView from '~/components/error-view';
17
+
import FeedInfoPrompt from '~/components/feeds/feed-info-prompt';
16
18
import FeedOverflowMenu from '~/components/feeds/feed-overflow-menu';
17
19
import IconButton from '~/components/icon-button';
18
-
import CircleInfoOutlinedIcon from '~/components/icons-central/circle-info-outline';
20
+
import ChevronRightOutlinedIcon from '~/components/icons-central/chevron-right-outline';
19
21
import MoreHorizOutlinedIcon from '~/components/icons-central/more-horiz-outline';
20
22
import * as Page from '~/components/page';
21
23
import TimelineList from '~/components/timeline/timeline-list';
···
44
46
<Page.Back to={`/${didOrHandle}`} />
45
47
</Page.HeaderAccessory>
46
48
47
-
<Page.Heading
49
+
<Avatar type="generator" src={meta.data?.avatar} size={null} class="-ml-4 h-7 w-7" />
50
+
{/* <Page.Heading
48
51
title={(() => {
49
52
const feed = meta.data;
50
53
if (feed) {
···
53
56
54
57
return `Feed`;
55
58
})()}
56
-
/>
59
+
/> */}
60
+
61
+
<div class="flex min-w-0 grow">
62
+
<button
63
+
disabled={!meta.data}
64
+
onClick={() => {
65
+
const feed = meta.data;
66
+
if (!feed) {
67
+
return;
68
+
}
69
+
70
+
openModal(() => <FeedInfoPrompt feed={feed} />);
71
+
}}
72
+
class="-mx-2 flex items-center gap-1 overflow-hidden rounded px-2 py-1 hover:bg-contrast-hinted/md active:bg-contrast-hinted/md-pressed"
73
+
>
74
+
<span class="overflow-hidden text-ellipsis whitespace-nowrap text-base font-bold">
75
+
{meta.data?.displayName.trim() || 'Feed'}
76
+
</span>
77
+
<ChevronRightOutlinedIcon class="-mr-1 shrink-0 rotate-90 text-lg text-contrast-muted" />
78
+
</button>
79
+
</div>
57
80
58
81
<Show when={meta.data}>
59
82
{(feed) => (
60
83
<Page.HeaderAccessory>
61
-
<IconButton
62
-
title="Feed information"
63
-
icon={CircleInfoOutlinedIcon}
64
-
onClick={() => {
65
-
history.navigate(`/${didOrHandle}/feeds/${rkey}/info`);
66
-
}}
67
-
/>
68
-
69
84
<IconButton
70
85
title="More actions"
71
86
icon={MoreHorizOutlinedIcon}