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