forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import {useCallback} from 'react'
2import {View} from 'react-native'
3import {Image} from 'expo-image'
4import {type AppBskyActorDefs, type AppBskyEmbedExternal} from '@atproto/api'
5import {msg} from '@lingui/core/macro'
6import {useLingui} from '@lingui/react'
7import {Trans} from '@lingui/react/macro'
8import {useNavigation} from '@react-navigation/native'
9import {useQueryClient} from '@tanstack/react-query'
10
11import {useOpenLink} from '#/lib/hooks/useOpenLink'
12import {type NavigationProp} from '#/lib/routes/types'
13import {sanitizeHandle} from '#/lib/strings/handles'
14import {toNiceDomain} from '#/lib/strings/url-helpers'
15import {useModerationOpts} from '#/state/preferences/moderation-opts'
16import {unstableCacheProfileView} from '#/state/queries/profile'
17import {android, atoms as a, platform, tokens, useTheme, web} from '#/alf'
18import {Button, ButtonIcon, ButtonText} from '#/components/Button'
19import * as Dialog from '#/components/Dialog'
20import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfoIcon} from '#/components/icons/CircleInfo'
21import {Globe_Stroke2_Corner0_Rounded} from '#/components/icons/Globe'
22import {SquareArrowTopRight_Stroke2_Corner0_Rounded as SquareArrowTopRightIcon} from '#/components/icons/SquareArrowTopRight'
23import {createStaticClick, SimpleInlineLinkText} from '#/components/Link'
24import {useGlobalReportDialogControl} from '#/components/moderation/ReportDialog'
25import * as ProfileCard from '#/components/ProfileCard'
26import {Text} from '#/components/Typography'
27import {useAnalytics} from '#/analytics'
28import {LiveIndicator} from '#/features/liveNow/components/LiveIndicator'
29import type * as bsky from '#/types/bsky'
30
31export function LiveStatusDialog({
32 control,
33 profile,
34 embed,
35 status,
36}: {
37 control: Dialog.DialogControlProps
38 profile: bsky.profile.AnyProfileView
39 status: AppBskyActorDefs.StatusView
40 embed: AppBskyEmbedExternal.View
41}) {
42 const navigation = useNavigation<NavigationProp>()
43 return (
44 <Dialog.Outer control={control} nativeOptions={{preventExpansion: true}}>
45 <Dialog.Handle difference={!!embed.external.thumb} />
46 <DialogInner
47 status={status}
48 profile={profile}
49 embed={embed}
50 navigation={navigation}
51 />
52 </Dialog.Outer>
53 )
54}
55
56function DialogInner({
57 profile,
58 embed,
59 navigation,
60 status,
61}: {
62 profile: bsky.profile.AnyProfileView
63 embed: AppBskyEmbedExternal.View
64 navigation: NavigationProp
65 status: AppBskyActorDefs.StatusView
66}) {
67 const {_} = useLingui()
68 const control = Dialog.useDialogContext()
69
70 const onPressOpenProfile = useCallback(() => {
71 control.close(() => {
72 navigation.push('Profile', {
73 name: profile.handle,
74 })
75 })
76 }, [navigation, profile.handle, control])
77
78 return (
79 <Dialog.ScrollableInner
80 label={_(msg`${sanitizeHandle(profile.handle)} is live`)}
81 contentContainerStyle={[a.pt_0, a.px_0]}
82 style={[web({maxWidth: 420}), a.overflow_hidden]}>
83 <LiveStatus
84 status={status}
85 profile={profile}
86 embed={embed}
87 onPressOpenProfile={onPressOpenProfile}
88 />
89 <Dialog.Close />
90 </Dialog.ScrollableInner>
91 )
92}
93
94export function LiveStatus({
95 status,
96 profile,
97 embed,
98 padding = 'xl',
99 onPressOpenProfile,
100}: {
101 status: AppBskyActorDefs.StatusView
102 profile: bsky.profile.AnyProfileView
103 embed: AppBskyEmbedExternal.View
104 padding?: 'lg' | 'xl'
105 onPressOpenProfile: () => void
106}) {
107 const ax = useAnalytics()
108 const {_} = useLingui()
109 const t = useTheme()
110 const queryClient = useQueryClient()
111 const openLink = useOpenLink()
112 const moderationOpts = useModerationOpts()
113 const reportDialogControl = useGlobalReportDialogControl()
114 const dialogContext = Dialog.useDialogContext()
115
116 return (
117 <>
118 {embed.external.thumb && (
119 <View
120 style={[
121 t.atoms.bg_contrast_25,
122 a.w_full,
123 a.aspect_card,
124 android([
125 a.overflow_hidden,
126 {
127 borderTopLeftRadius: a.rounded_md.borderRadius,
128 borderTopRightRadius: a.rounded_md.borderRadius,
129 },
130 ]),
131 ]}>
132 <Image
133 source={embed.external.thumb}
134 contentFit="cover"
135 style={[a.absolute, a.inset_0]}
136 accessibilityIgnoresInvertColors
137 />
138 <LiveIndicator
139 size="large"
140 style={[
141 a.absolute,
142 {top: tokens.space.lg, left: tokens.space.lg},
143 a.align_start,
144 ]}
145 />
146 </View>
147 )}
148 <View
149 style={[
150 a.gap_lg,
151 padding === 'xl'
152 ? [a.px_xl, !embed.external.thumb ? a.pt_2xl : a.pt_lg]
153 : a.p_lg,
154 ]}>
155 <View style={[a.w_full, a.justify_center, a.gap_2xs]}>
156 <Text
157 numberOfLines={3}
158 style={[a.leading_snug, a.font_semi_bold, a.text_xl]}>
159 {embed.external.title || embed.external.uri}
160 </Text>
161 <View style={[a.flex_row, a.align_center, a.gap_2xs]}>
162 <Globe_Stroke2_Corner0_Rounded
163 size="xs"
164 style={[t.atoms.text_contrast_medium]}
165 />
166 <Text
167 numberOfLines={1}
168 style={[a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]}>
169 {toNiceDomain(embed.external.uri)}
170 </Text>
171 </View>
172 </View>
173 <Button
174 label={_(msg`Watch now`)}
175 size={platform({native: 'large', web: 'small'})}
176 color="primary"
177 variant="solid"
178 onPress={() => {
179 ax.metric('live:card:watch', {subject: profile.did})
180 openLink(embed.external.uri, false)
181 }}>
182 <ButtonText>
183 <Trans>Watch now</Trans>
184 </ButtonText>
185 <ButtonIcon icon={SquareArrowTopRightIcon} />
186 </Button>
187 <View style={[t.atoms.border_contrast_low, a.border_t, a.w_full]} />
188 {moderationOpts && (
189 <ProfileCard.Header>
190 <ProfileCard.Avatar
191 profile={profile}
192 moderationOpts={moderationOpts}
193 disabledPreview
194 />
195 {/* Ensure wide enough on web hover */}
196 <View style={[a.flex_1, web({minWidth: 100})]}>
197 <ProfileCard.NameAndHandle
198 profile={profile}
199 moderationOpts={moderationOpts}
200 />
201 </View>
202 <Button
203 label={_(msg`Open profile`)}
204 size="small"
205 color="secondary"
206 variant="solid"
207 onPress={() => {
208 ax.metric('live:card:openProfile', {subject: profile.did})
209 unstableCacheProfileView(queryClient, profile)
210 onPressOpenProfile()
211 }}>
212 <ButtonText>
213 <Trans>Open profile</Trans>
214 </ButtonText>
215 </Button>
216 </ProfileCard.Header>
217 )}
218 <View
219 style={[
220 a.flex_row,
221 a.align_center,
222 a.justify_between,
223 a.w_full,
224 a.pt_sm,
225 ]}>
226 <View style={[a.flex_row, a.align_center, a.gap_xs, a.flex_1]}>
227 <CircleInfoIcon size="sm" fill={t.atoms.text_contrast_low.color} />
228 <Text style={[t.atoms.text_contrast_low, a.text_sm]}>
229 <Trans>Live feature is in beta</Trans>
230 </Text>
231 </View>
232 {status && (
233 <SimpleInlineLinkText
234 label={_(msg`Report this livestream`)}
235 {...createStaticClick(() => {
236 function open() {
237 reportDialogControl.open({
238 subject: {
239 ...status,
240 $type: 'app.bsky.actor.defs#statusView',
241 },
242 })
243 }
244 if (dialogContext.isWithinDialog) {
245 dialogContext.close(open)
246 } else {
247 open()
248 }
249 })}
250 style={[a.text_sm, a.underline, t.atoms.text_contrast_medium]}>
251 <Trans>Report</Trans>
252 </SimpleInlineLinkText>
253 )}
254 </View>
255 </View>
256 </>
257 )
258}