+6
-6
src/api/richtext/segment.ts
+6
-6
src/api/richtext/segment.ts
···
1
-
import type { AppBskyRichtextFacet } from '@atcute/client/lexicons';
1
+
import type { AppBskyRichtextFacet, BlueMojiRichtextFacet, Brand } from '@atcute/client/lexicons';
2
2
3
3
import type { UnwrapArray } from '../utils/types';
4
4
import { textDecoder, textEncoder } from './intl';
···
21
21
};
22
22
23
23
type Facet = AppBskyRichtextFacet.Main;
24
-
type FacetFeature = UnwrapArray<Facet['features']>;
24
+
type FacetFeature = UnwrapArray<Facet['features']> | Brand.Union<BlueMojiRichtextFacet.Main>;
25
25
26
26
export interface RichtextSegment {
27
27
text: string;
28
-
feature: FacetFeature | undefined;
28
+
features: FacetFeature[] | undefined;
29
29
}
30
30
31
-
const createRichtextSegment = (text: string, feature: FacetFeature | undefined): RichtextSegment => {
32
-
return { text: text, feature: feature };
31
+
const createRichtextSegment = (text: string, features: FacetFeature[] | undefined): RichtextSegment => {
32
+
return { text: text, features: features };
33
33
};
34
34
35
35
export const segmentRichText = (rtText: string, facets: Facet[] | undefined): RichtextSegment[] => {
···
65
65
if (features.length === 0 || subtext.trim().length === 0) {
66
66
segments.push(createRichtextSegment(subtext, undefined));
67
67
} else {
68
-
segments.push(createRichtextSegment(subtext, features[0]));
68
+
segments.push(createRichtextSegment(subtext, features));
69
69
}
70
70
}
71
71
+62
-40
src/components/rich-text.tsx
+62
-40
src/components/rich-text.tsx
···
5
5
import { segmentRichText } from '~/api/richtext/segment';
6
6
import { isLinkValid, safeUrlParse } from '~/api/utils/strings';
7
7
8
+
import { getCdnUrl } from '~/lib/bluemoji/render';
8
9
import {
9
10
BSKY_FEED_LINK_RE,
10
11
BSKY_LIST_LINK_RE,
···
38
39
for (let idx = 0, len = segments.length; idx < len; idx++) {
39
40
const segment = segments[idx];
40
41
const subtext = segment.text;
41
-
const feature = segment.feature;
42
+
const features = segment.features;
43
+
44
+
let node: JSX.Element = subtext;
45
+
46
+
if (features) {
47
+
for (let j = 0, jlen = features.length; j < jlen; j++) {
48
+
const feature = features[j];
49
+
const type = feature.$type;
50
+
51
+
if (type === 'app.bsky.richtext.facet#link') {
52
+
const uri = feature.uri;
53
+
const redirect = findLinkRedirect(uri);
54
+
55
+
if (redirect === null) {
56
+
node = renderExternalLink(uri, subtext);
57
+
} else {
58
+
node = renderInternalLink(redirect, subtext);
59
+
}
42
60
43
-
let to: string | undefined;
44
-
let external = false;
61
+
break;
62
+
} else if (type === 'app.bsky.richtext.facet#mention') {
63
+
node = renderInternalLink(`/${feature.did}`, subtext);
45
64
46
-
if (feature) {
47
-
const type = feature.$type;
65
+
break;
66
+
} else if (type === 'app.bsky.richtext.facet#tag') {
67
+
node = renderInternalLink(`/topics/${feature.tag}`, subtext);
48
68
49
-
if (type === 'app.bsky.richtext.facet#link') {
50
-
const uri = feature.uri;
51
-
const redirect = findLinkRedirect(uri);
69
+
break;
70
+
} else if (type === 'blue.moji.richtext.facet') {
71
+
const formats = feature.formats;
72
+
if (formats.$type !== 'blue.moji.richtext.facet#formats_v0' || !formats.png_128) {
73
+
continue;
74
+
}
52
75
53
-
if (redirect === null) {
54
-
to = uri;
55
-
external = true;
56
-
} else {
57
-
to = redirect;
76
+
node = (
77
+
<img
78
+
src={/* @once */ getCdnUrl(feature.did, formats.png_128)}
79
+
title={/* @once */ feature.name}
80
+
class={`mx-px inline-block align-top text-[0]` + (!large ? ` h-5 w-5` : ` h-6 w-6`)}
81
+
/>
82
+
);
83
+
break;
58
84
}
59
-
} else if (type === 'app.bsky.richtext.facet#mention') {
60
-
to = `/${feature.did}`;
61
-
} else if (type === 'app.bsky.richtext.facet#tag') {
62
-
to = `/topics/${feature.tag}`;
63
85
}
64
86
}
65
87
66
-
if (to !== undefined) {
67
-
if (!external) {
68
-
nodes.push(
69
-
<a href={to} class="text-accent hover:underline">
70
-
{subtext}
71
-
</a>,
72
-
);
73
-
} else {
74
-
nodes.push(
75
-
<a
76
-
target="_blank"
77
-
href={to}
78
-
onClick={handleUnsafeLinkNavigation}
79
-
onAuxClick={handleUnsafeLinkNavigation}
80
-
class="text-accent hover:underline"
81
-
>
82
-
{subtext}
83
-
</a>,
84
-
);
85
-
}
86
-
} else {
87
-
nodes.push(subtext);
88
-
}
88
+
nodes.push(node);
89
89
}
90
90
} else {
91
91
nodes = text;
···
108
108
};
109
109
110
110
export default RichText;
111
+
112
+
const renderInternalLink = (to: string, subtext: string) => {
113
+
return (
114
+
<a href={to} class="text-accent hover:underline">
115
+
{subtext}
116
+
</a>
117
+
);
118
+
};
119
+
120
+
const renderExternalLink = (to: string, subtext: string) => {
121
+
return (
122
+
<a
123
+
target="_blank"
124
+
href={to}
125
+
onClick={handleUnsafeLinkNavigation}
126
+
onAuxClick={handleUnsafeLinkNavigation}
127
+
class="text-accent hover:underline"
128
+
>
129
+
{subtext}
130
+
</a>
131
+
);
132
+
};
111
133
112
134
const handleUnsafeLinkNavigation = (ev: MouseEvent) => {
113
135
if (ev.defaultPrevented || (ev.type === 'auxclick' && (ev as MouseEvent).button !== 1)) {
+1
-3
src/lib/bluemoji/render.ts
+1
-3
src/lib/bluemoji/render.ts
···
1
-
import type { At } from '@atcute/client/lexicons';
2
-
3
-
export const getCdnUrl = (did: At.DID, cid: string, format: 'png' | 'jpeg' | 'webp' = 'webp') => {
1
+
export const getCdnUrl = (did: string, cid: string, format: 'png' | 'jpeg' | 'webp' = 'webp') => {
4
2
return `https://cdn.bsky.app/img/avatar_thumbnail/plain/${did}/${cid}@${format}`;
5
3
};