+4
-1
src/lib/moderation/create-sanitized-display-name.ts
+4
-1
src/lib/moderation/create-sanitized-display-name.ts
···
1
+
import {type ModerationUI} from '@atproto/api'
2
+
1
3
import {sanitizeDisplayName} from '#/lib/strings/display-names'
2
4
import {sanitizeHandle} from '#/lib/strings/handles'
3
5
import type * as bsky from '#/types/bsky'
···
5
7
export function createSanitizedDisplayName(
6
8
profile: bsky.profile.AnyProfileView,
7
9
noAt = false,
10
+
moderation?: ModerationUI,
8
11
) {
9
12
if (profile.displayName != null && profile.displayName !== '') {
10
-
return sanitizeDisplayName(profile.displayName)
13
+
return sanitizeDisplayName(profile.displayName, moderation)
11
14
} else {
12
15
return sanitizeHandle(profile.handle, noAt ? '' : '@')
13
16
}
+11
-116
src/view/com/posts/PostFeedItem.tsx
+11
-116
src/view/com/posts/PostFeedItem.tsx
···
9
9
type ModerationDecision,
10
10
RichText as RichTextAPI,
11
11
} from '@atproto/api'
12
-
import {msg, Trans} from '@lingui/macro'
13
-
import {useLingui} from '@lingui/react'
14
12
import {useNavigation} from '@react-navigation/native'
15
13
import {useQueryClient} from '@tanstack/react-query'
16
14
17
15
import {useActorStatus} from '#/lib/actor-status'
18
-
import {isReasonFeedSource, type ReasonFeedSource} from '#/lib/api/feed/types'
16
+
import {type ReasonFeedSource} from '#/lib/api/feed/types'
19
17
import {MAX_POST_LINES} from '#/lib/constants'
20
18
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
21
19
import {usePalette} from '#/lib/hooks/usePalette'
22
20
import {makeProfileLink} from '#/lib/routes/links'
23
21
import {type NavigationProp} from '#/lib/routes/types'
24
22
import {useGate} from '#/lib/statsig/statsig'
25
-
import {sanitizeDisplayName} from '#/lib/strings/display-names'
26
-
import {sanitizeHandle} from '#/lib/strings/handles'
27
23
import {countLines} from '#/lib/strings/helpers'
28
24
import {
29
25
POST_TOMBSTONE,
···
38
34
buildPostSourceKey,
39
35
setUnstablePostSource,
40
36
} from '#/state/unstable-post-source'
41
-
import {FeedNameText} from '#/view/com/util/FeedInfoText'
42
-
import {Link, TextLinkOnWebOnly} from '#/view/com/util/Link'
37
+
import {Link} from '#/view/com/util/Link'
43
38
import {PostMeta} from '#/view/com/util/PostMeta'
44
-
import {Text} from '#/view/com/util/text/Text'
45
39
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
46
40
import {atoms as a} from '#/alf'
47
-
import {Pin_Stroke2_Corner0_Rounded as PinIcon} from '#/components/icons/Pin'
48
-
import {Repost_Stroke2_Corner2_Rounded as RepostIcon} from '#/components/icons/Repost'
49
41
import {ContentHider} from '#/components/moderation/ContentHider'
50
42
import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe'
51
43
import {PostAlerts} from '#/components/moderation/PostAlerts'
···
56
48
import {ShowMoreTextButton} from '#/components/Post/ShowMoreTextButton'
57
49
import {PostControls} from '#/components/PostControls'
58
50
import {DiscoverDebug} from '#/components/PostControls/DiscoverDebug'
59
-
import {ProfileHoverCard} from '#/components/ProfileHoverCard'
60
51
import {RichText} from '#/components/RichText'
61
52
import {SubtleHover} from '#/components/SubtleHover'
62
53
import * as bsky from '#/types/bsky'
54
+
import {PostFeedReason} from './PostFeedReason'
63
55
64
56
interface FeedItemProps {
65
57
record: AppBskyFeedPost.Record
···
173
165
const navigation = useNavigation<NavigationProp>()
174
166
const pal = usePalette('default')
175
167
const gate = useGate()
176
-
const {_} = useLingui()
177
168
178
169
const [hover, setHover] = useState(false)
179
170
···
275
266
},
276
267
]
277
268
278
-
const {currentAccount} = useSession()
279
-
const isOwner =
280
-
AppBskyFeedDefs.isReasonRepost(reason) &&
281
-
reason.by.did === currentAccount?.did
282
-
283
269
/**
284
270
* If `post[0]` in this slice is the actual root post (not an orphan thread),
285
271
* then we may have a threadgate record to reference
···
334
320
)}
335
321
</View>
336
322
337
-
<View style={{paddingTop: 10, flexShrink: 1}}>
338
-
{isReasonFeedSource(reason) ? (
339
-
<Link href={reason.href}>
340
-
<Text
341
-
type="sm-bold"
342
-
style={pal.textLight}
343
-
lineHeight={1.2}
344
-
numberOfLines={1}>
345
-
<Trans context="from-feed">
346
-
From{' '}
347
-
<FeedNameText
348
-
type="sm-bold"
349
-
uri={reason.uri}
350
-
href={reason.href}
351
-
lineHeight={1.2}
352
-
numberOfLines={1}
353
-
style={pal.textLight}
354
-
/>
355
-
</Trans>
356
-
</Text>
357
-
</Link>
358
-
) : AppBskyFeedDefs.isReasonRepost(reason) ? (
359
-
<Link
360
-
style={styles.includeReason}
361
-
href={makeProfileLink(reason.by)}
362
-
title={
363
-
isOwner
364
-
? _(msg`Reposted by you`)
365
-
: _(
366
-
msg`Reposted by ${sanitizeDisplayName(
367
-
reason.by.displayName || reason.by.handle,
368
-
)}`,
369
-
)
370
-
}
371
-
onBeforePress={onOpenReposter}>
372
-
<RepostIcon
373
-
style={{color: pal.colors.textLight, marginRight: 3}}
374
-
width={13}
375
-
height={13}
376
-
/>
377
-
<Text
378
-
type="sm-bold"
379
-
style={pal.textLight}
380
-
lineHeight={1.2}
381
-
numberOfLines={1}>
382
-
{isOwner ? (
383
-
<Trans>Reposted by you</Trans>
384
-
) : (
385
-
<Trans>
386
-
Reposted by{' '}
387
-
<ProfileHoverCard did={reason.by.did}>
388
-
<TextLinkOnWebOnly
389
-
type="sm-bold"
390
-
style={pal.textLight}
391
-
lineHeight={1.2}
392
-
numberOfLines={1}
393
-
text={
394
-
<Text
395
-
emoji
396
-
type="sm-bold"
397
-
style={pal.textLight}
398
-
lineHeight={1.2}>
399
-
{sanitizeDisplayName(
400
-
reason.by.displayName ||
401
-
sanitizeHandle(reason.by.handle),
402
-
moderation.ui('displayName'),
403
-
)}
404
-
</Text>
405
-
}
406
-
href={makeProfileLink(reason.by)}
407
-
onBeforePress={onOpenReposter}
408
-
/>
409
-
</ProfileHoverCard>
410
-
</Trans>
411
-
)}
412
-
</Text>
413
-
</Link>
414
-
) : AppBskyFeedDefs.isReasonPin(reason) ? (
415
-
<View style={styles.includeReason}>
416
-
<PinIcon
417
-
style={{color: pal.colors.textLight, marginRight: 3}}
418
-
width={13}
419
-
height={13}
420
-
/>
421
-
<Text
422
-
type="sm-bold"
423
-
style={pal.textLight}
424
-
lineHeight={1.2}
425
-
numberOfLines={1}>
426
-
<Trans>Pinned</Trans>
427
-
</Text>
428
-
</View>
429
-
) : null}
323
+
<View style={[a.pt_sm, a.flex_shrink]}>
324
+
{reason && (
325
+
<PostFeedReason
326
+
reason={reason}
327
+
moderation={moderation}
328
+
onOpenReposter={onOpenReposter}
329
+
/>
330
+
)}
430
331
</View>
431
332
</View>
432
333
···
601
502
width: 2,
602
503
marginLeft: 'auto',
603
504
marginRight: 'auto',
604
-
},
605
-
includeReason: {
606
-
flexDirection: 'row',
607
-
alignItems: 'center',
608
-
marginBottom: 2,
609
-
marginLeft: -16,
610
505
},
611
506
layout: {
612
507
flexDirection: 'row',
+139
src/view/com/posts/PostFeedReason.tsx
+139
src/view/com/posts/PostFeedReason.tsx
···
1
+
import {StyleSheet, View} from 'react-native'
2
+
import {AppBskyFeedDefs, type ModerationDecision} from '@atproto/api'
3
+
import {msg, Trans} from '@lingui/macro'
4
+
import {useLingui} from '@lingui/react'
5
+
6
+
import {isReasonFeedSource, type ReasonFeedSource} from '#/lib/api/feed/types'
7
+
import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
8
+
import {makeProfileLink} from '#/lib/routes/links'
9
+
import {useSession} from '#/state/session'
10
+
import {atoms as a, useTheme} from '#/alf'
11
+
import {Pin_Stroke2_Corner0_Rounded as PinIcon} from '#/components/icons/Pin'
12
+
import {Repost_Stroke2_Corner2_Rounded as RepostIcon} from '#/components/icons/Repost'
13
+
import {Link, WebOnlyInlineLinkText} from '#/components/Link'
14
+
import {ProfileHoverCard} from '#/components/ProfileHoverCard'
15
+
import {Text} from '#/components/Typography'
16
+
import {FeedNameText} from '../util/FeedInfoText'
17
+
18
+
export function PostFeedReason({
19
+
reason,
20
+
moderation,
21
+
onOpenReposter,
22
+
}: {
23
+
reason:
24
+
| ReasonFeedSource
25
+
| AppBskyFeedDefs.ReasonRepost
26
+
| AppBskyFeedDefs.ReasonPin
27
+
| {[k: string]: unknown; $type: string}
28
+
moderation?: ModerationDecision
29
+
onOpenReposter?: () => void
30
+
}) {
31
+
const t = useTheme()
32
+
const {_} = useLingui()
33
+
34
+
const {currentAccount} = useSession()
35
+
36
+
if (isReasonFeedSource(reason)) {
37
+
return (
38
+
<Link label={_(msg`Go to feed`)} to={reason.href}>
39
+
<Text
40
+
style={[
41
+
t.atoms.text_contrast_medium,
42
+
a.font_medium,
43
+
a.leading_snug,
44
+
a.leading_snug,
45
+
]}
46
+
numberOfLines={1}>
47
+
<Trans context="from-feed">
48
+
From{' '}
49
+
<FeedNameText
50
+
uri={reason.uri}
51
+
href={reason.href}
52
+
style={[
53
+
t.atoms.text_contrast_medium,
54
+
a.font_medium,
55
+
a.leading_snug,
56
+
]}
57
+
numberOfLines={1}
58
+
/>
59
+
</Trans>
60
+
</Text>
61
+
</Link>
62
+
)
63
+
}
64
+
65
+
if (AppBskyFeedDefs.isReasonRepost(reason)) {
66
+
const isOwner = reason.by.did === currentAccount?.did
67
+
const reposter = createSanitizedDisplayName(
68
+
reason.by,
69
+
false,
70
+
moderation?.ui('displayName'),
71
+
)
72
+
return (
73
+
<Link
74
+
style={styles.includeReason}
75
+
to={makeProfileLink(reason.by)}
76
+
label={
77
+
isOwner ? _(msg`Reposted by you`) : _(msg`Reposted by ${reposter}`)
78
+
}
79
+
onPress={onOpenReposter}>
80
+
<RepostIcon
81
+
style={[t.atoms.text_contrast_medium, {marginRight: 3}]}
82
+
width={13}
83
+
height={13}
84
+
/>
85
+
<Text
86
+
style={[t.atoms.text_contrast_medium, a.font_medium, a.leading_snug]}
87
+
numberOfLines={1}>
88
+
{isOwner ? (
89
+
<Trans>Reposted by you</Trans>
90
+
) : (
91
+
<Trans>
92
+
Reposted by{' '}
93
+
<ProfileHoverCard did={reason.by.did}>
94
+
<WebOnlyInlineLinkText
95
+
label={reposter}
96
+
numberOfLines={1}
97
+
to={makeProfileLink(reason.by)}
98
+
onPress={onOpenReposter}
99
+
style={[
100
+
t.atoms.text_contrast_medium,
101
+
a.font_medium,
102
+
a.leading_snug,
103
+
]}>
104
+
{reposter}
105
+
</WebOnlyInlineLinkText>
106
+
</ProfileHoverCard>
107
+
</Trans>
108
+
)}
109
+
</Text>
110
+
</Link>
111
+
)
112
+
}
113
+
114
+
if (AppBskyFeedDefs.isReasonPin(reason)) {
115
+
return (
116
+
<View style={styles.includeReason}>
117
+
<PinIcon
118
+
style={[t.atoms.text_contrast_medium, {marginRight: 3}]}
119
+
width={13}
120
+
height={13}
121
+
/>
122
+
<Text
123
+
style={[t.atoms.text_contrast_medium, a.font_medium, a.leading_snug]}
124
+
numberOfLines={1}>
125
+
<Trans>Pinned</Trans>
126
+
</Text>
127
+
</View>
128
+
)
129
+
}
130
+
}
131
+
132
+
const styles = StyleSheet.create({
133
+
includeReason: {
134
+
flexDirection: 'row',
135
+
alignItems: 'center',
136
+
marginBottom: 2,
137
+
marginLeft: -16,
138
+
},
139
+
})
+17
-20
src/view/com/util/FeedInfoText.tsx
+17
-20
src/view/com/util/FeedInfoText.tsx
···
1
-
import {type StyleProp, StyleSheet, type TextStyle} from 'react-native'
1
+
import {type StyleProp, type TextStyle} from 'react-native'
2
2
3
3
import {sanitizeDisplayName} from '#/lib/strings/display-names'
4
-
import {type TypographyVariant} from '#/lib/ThemeContext'
5
4
import {useFeedSourceInfoQuery} from '#/state/queries/feed'
6
-
import {TextLinkOnWebOnly} from './Link'
5
+
import {atoms as a, platform} from '#/alf'
6
+
import {WebOnlyInlineLinkText} from '#/components/Link'
7
7
import {LoadingPlaceholder} from './LoadingPlaceholder'
8
8
9
9
export function FeedNameText({
10
-
type = 'md',
11
10
uri,
12
11
href,
13
-
lineHeight,
14
12
numberOfLines,
15
13
style,
16
14
}: {
17
-
type?: TypographyVariant
18
15
uri: string
19
16
href: string
20
-
lineHeight?: number
21
17
numberOfLines?: number
22
18
style?: StyleProp<TextStyle>
23
19
}) {
24
20
const {data, isError} = useFeedSourceInfoQuery({uri})
25
21
26
22
let inner
27
-
if (data?.displayName || isError) {
23
+
if (data || isError) {
28
24
const displayName = data?.displayName || uri.split('/').pop() || ''
29
25
inner = (
30
-
<TextLinkOnWebOnly
31
-
type={type}
26
+
<WebOnlyInlineLinkText
27
+
to={href}
28
+
label={displayName}
32
29
style={style}
33
-
lineHeight={lineHeight}
34
-
numberOfLines={numberOfLines}
35
-
href={href}
36
-
text={sanitizeDisplayName(displayName)}
37
-
/>
30
+
numberOfLines={numberOfLines}>
31
+
{sanitizeDisplayName(displayName)}
32
+
</WebOnlyInlineLinkText>
38
33
)
39
34
} else {
40
35
inner = (
41
36
<LoadingPlaceholder
42
37
width={80}
43
38
height={8}
44
-
style={styles.loadingPlaceholder}
39
+
style={[
40
+
a.ml_2xs,
41
+
platform({
42
+
native: [a.mt_2xs],
43
+
web: [{top: -1}],
44
+
}),
45
+
]}
45
46
/>
46
47
)
47
48
}
48
49
49
50
return inner
50
51
}
51
-
52
-
const styles = StyleSheet.create({
53
-
loadingPlaceholder: {position: 'relative', top: 1, left: 2},
54
-
})