Couple of starter packs tweaks (#4604)
parent
f769564edf
commit
77a512ae32
|
@ -45,9 +45,7 @@ module.exports = function (config) {
|
||||||
'appclips:bsky.app',
|
'appclips:bsky.app',
|
||||||
'appclips:go.bsky.app', // Allows App Clip to work when scanning QR codes
|
'appclips:go.bsky.app', // Allows App Clip to work when scanning QR codes
|
||||||
// When testing local services, enter an ngrok (et al) domain here. It must use a standard HTTP/HTTPS port.
|
// When testing local services, enter an ngrok (et al) domain here. It must use a standard HTTP/HTTPS port.
|
||||||
...(IS_DEV || IS_TESTFLIGHT
|
...(IS_DEV || IS_TESTFLIGHT ? [] : []),
|
||||||
? ['appclips:sptesting.haileyok.com', 'applinks:sptesting.haileyok.com']
|
|
||||||
: []),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
const UPDATES_CHANNEL = IS_TESTFLIGHT
|
const UPDATES_CHANNEL = IS_TESTFLIGHT
|
||||||
|
|
|
@ -1,16 +1,14 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import ViewShot from 'react-native-view-shot'
|
import ViewShot from 'react-native-view-shot'
|
||||||
import * as FS from 'expo-file-system'
|
|
||||||
import {requestMediaLibraryPermissionsAsync} from 'expo-image-picker'
|
import {requestMediaLibraryPermissionsAsync} from 'expo-image-picker'
|
||||||
|
import {createAssetAsync} from 'expo-media-library'
|
||||||
import * as Sharing from 'expo-sharing'
|
import * as Sharing from 'expo-sharing'
|
||||||
import {AppBskyGraphDefs, AppBskyGraphStarterpack} from '@atproto/api'
|
import {AppBskyGraphDefs, AppBskyGraphStarterpack} from '@atproto/api'
|
||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {nanoid} from 'nanoid/non-secure'
|
|
||||||
|
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {saveImageToMediaLibrary} from 'lib/media/manip'
|
|
||||||
import {logEvent} from 'lib/statsig/statsig'
|
import {logEvent} from 'lib/statsig/statsig'
|
||||||
import {isNative, isWeb} from 'platform/detection'
|
import {isNative, isWeb} from 'platform/detection'
|
||||||
import * as Toast from '#/view/com/util/Toast'
|
import * as Toast from '#/view/com/util/Toast'
|
||||||
|
@ -65,13 +63,9 @@ export function QrCodeDialog({
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const filename = `${FS.documentDirectory}/${nanoid(12)}.png`
|
|
||||||
|
|
||||||
// Incase of a FS failure, don't crash the app
|
// Incase of a FS failure, don't crash the app
|
||||||
try {
|
try {
|
||||||
await FS.copyAsync({from: uri, to: filename})
|
await createAssetAsync(`file://${uri}`)
|
||||||
await saveImageToMediaLibrary({uri: filename})
|
|
||||||
await FS.deleteAsync(filename)
|
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
Toast.show(_(msg`An error occurred while saving the QR code!`))
|
Toast.show(_(msg`An error occurred while saving the QR code!`))
|
||||||
logger.error('Failed to save QR code', {error: e})
|
logger.error('Failed to save QR code', {error: e})
|
||||||
|
|
|
@ -1,12 +1,10 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import * as FS from 'expo-file-system'
|
|
||||||
import {Image} from 'expo-image'
|
import {Image} from 'expo-image'
|
||||||
import {requestMediaLibraryPermissionsAsync} from 'expo-image-picker'
|
import {requestMediaLibraryPermissionsAsync} from 'expo-image-picker'
|
||||||
import {AppBskyGraphDefs} from '@atproto/api'
|
import {AppBskyGraphDefs} from '@atproto/api'
|
||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {nanoid} from 'nanoid/non-secure'
|
|
||||||
|
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||||
|
@ -72,19 +70,8 @@ function ShareDialogInner({
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const cachePath = await Image.getCachePathAsync(imageUrl)
|
|
||||||
const filename = `${FS.documentDirectory}/${nanoid(12)}.png`
|
|
||||||
|
|
||||||
if (!cachePath) {
|
|
||||||
Toast.show(_(msg`An error occurred while saving the image.`))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await FS.copyAsync({from: cachePath, to: filename})
|
await saveImageToMediaLibrary({uri: imageUrl})
|
||||||
await saveImageToMediaLibrary({uri: filename})
|
|
||||||
await FS.deleteAsync(filename)
|
|
||||||
|
|
||||||
Toast.show(_(msg`Image saved to your camera roll!`))
|
Toast.show(_(msg`Image saved to your camera roll!`))
|
||||||
control.close()
|
control.close()
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
|
@ -133,18 +120,18 @@ function ShareDialogInner({
|
||||||
isWeb && [a.gap_sm, a.flex_row_reverse, {marginLeft: 'auto'}],
|
isWeb && [a.gap_sm, a.flex_row_reverse, {marginLeft: 'auto'}],
|
||||||
]}>
|
]}>
|
||||||
<Button
|
<Button
|
||||||
label="Share link"
|
label={isWeb ? _(msg`Copy link`) : _(msg`Share link`)}
|
||||||
variant="solid"
|
variant="solid"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
size="small"
|
size="small"
|
||||||
style={[isWeb && a.self_center]}
|
style={[isWeb && a.self_center]}
|
||||||
onPress={onShareLink}>
|
onPress={onShareLink}>
|
||||||
<ButtonText>
|
<ButtonText>
|
||||||
{isWeb ? <Trans>Copy Link</Trans> : <Trans>Share Link</Trans>}
|
{isWeb ? <Trans>Copy Link</Trans> : <Trans>Share link</Trans>}
|
||||||
</ButtonText>
|
</ButtonText>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
label="Create QR code"
|
label={_(msg`Share QR code`)}
|
||||||
variant="solid"
|
variant="solid"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
size="small"
|
size="small"
|
||||||
|
@ -155,7 +142,7 @@ function ShareDialogInner({
|
||||||
})
|
})
|
||||||
}}>
|
}}>
|
||||||
<ButtonText>
|
<ButtonText>
|
||||||
<Trans>Create QR code</Trans>
|
<Trans>Share QR code</Trans>
|
||||||
</ButtonText>
|
</ButtonText>
|
||||||
</Button>
|
</Button>
|
||||||
{isNative && (
|
{isNative && (
|
||||||
|
|
|
@ -58,6 +58,7 @@ export function WizardEditListDialog({
|
||||||
state.currentStep === 'Profiles' ? (
|
state.currentStep === 'Profiles' ? (
|
||||||
<WizardProfileCard
|
<WizardProfileCard
|
||||||
profile={item}
|
profile={item}
|
||||||
|
btnType="remove"
|
||||||
state={state}
|
state={state}
|
||||||
dispatch={dispatch}
|
dispatch={dispatch}
|
||||||
moderationOpts={moderationOpts}
|
moderationOpts={moderationOpts}
|
||||||
|
@ -65,6 +66,7 @@ export function WizardEditListDialog({
|
||||||
) : (
|
) : (
|
||||||
<WizardFeedCard
|
<WizardFeedCard
|
||||||
generator={item}
|
generator={item}
|
||||||
|
btnType="remove"
|
||||||
state={state}
|
state={state}
|
||||||
dispatch={dispatch}
|
dispatch={dispatch}
|
||||||
moderationOpts={moderationOpts}
|
moderationOpts={moderationOpts}
|
||||||
|
|
|
@ -9,7 +9,7 @@ import {
|
||||||
ModerationUI,
|
ModerationUI,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
import {GeneratorView} from '@atproto/api/dist/client/types/app/bsky/feed/defs'
|
import {GeneratorView} from '@atproto/api/dist/client/types/app/bsky/feed/defs'
|
||||||
import {msg} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
import {DISCOVER_FEED_URI} from 'lib/constants'
|
import {DISCOVER_FEED_URI} from 'lib/constants'
|
||||||
|
@ -19,12 +19,14 @@ import {useSession} from 'state/session'
|
||||||
import {UserAvatar} from 'view/com/util/UserAvatar'
|
import {UserAvatar} from 'view/com/util/UserAvatar'
|
||||||
import {WizardAction, WizardState} from '#/screens/StarterPack/Wizard/State'
|
import {WizardAction, WizardState} from '#/screens/StarterPack/Wizard/State'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
|
import {Button, ButtonText} from '#/components/Button'
|
||||||
import * as Toggle from '#/components/forms/Toggle'
|
import * as Toggle from '#/components/forms/Toggle'
|
||||||
import {Checkbox} from '#/components/forms/Toggle'
|
import {Checkbox} from '#/components/forms/Toggle'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
|
||||||
function WizardListCard({
|
function WizardListCard({
|
||||||
type,
|
type,
|
||||||
|
btnType,
|
||||||
displayName,
|
displayName,
|
||||||
subtitle,
|
subtitle,
|
||||||
onPress,
|
onPress,
|
||||||
|
@ -34,6 +36,7 @@ function WizardListCard({
|
||||||
moderationUi,
|
moderationUi,
|
||||||
}: {
|
}: {
|
||||||
type: 'user' | 'algo'
|
type: 'user' | 'algo'
|
||||||
|
btnType: 'checkbox' | 'remove'
|
||||||
profile?: AppBskyActorDefs.ProfileViewBasic
|
profile?: AppBskyActorDefs.ProfileViewBasic
|
||||||
feed?: AppBskyFeedDefs.GeneratorView
|
feed?: AppBskyFeedDefs.GeneratorView
|
||||||
displayName: string
|
displayName: string
|
||||||
|
@ -56,7 +59,7 @@ function WizardListCard({
|
||||||
: _(msg`Add ${displayName} to starter pack`)
|
: _(msg`Add ${displayName} to starter pack`)
|
||||||
}
|
}
|
||||||
value={included}
|
value={included}
|
||||||
disabled={disabled}
|
disabled={btnType === 'remove' || disabled}
|
||||||
onChange={onPress}
|
onChange={onPress}
|
||||||
style={[
|
style={[
|
||||||
a.flex_row,
|
a.flex_row,
|
||||||
|
@ -85,17 +88,33 @@ function WizardListCard({
|
||||||
{subtitle}
|
{subtitle}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
{btnType === 'checkbox' ? (
|
||||||
<Checkbox />
|
<Checkbox />
|
||||||
|
) : !disabled ? (
|
||||||
|
<Button
|
||||||
|
label={_(msg`Remove`)}
|
||||||
|
variant="solid"
|
||||||
|
color="secondary"
|
||||||
|
size="xsmall"
|
||||||
|
style={[a.self_center, {marginLeft: 'auto'}]}
|
||||||
|
onPress={onPress}>
|
||||||
|
<ButtonText>
|
||||||
|
<Trans>Remove</Trans>
|
||||||
|
</ButtonText>
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
</Toggle.Item>
|
</Toggle.Item>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function WizardProfileCard({
|
export function WizardProfileCard({
|
||||||
|
btnType,
|
||||||
state,
|
state,
|
||||||
dispatch,
|
dispatch,
|
||||||
profile,
|
profile,
|
||||||
moderationOpts,
|
moderationOpts,
|
||||||
}: {
|
}: {
|
||||||
|
btnType: 'checkbox' | 'remove'
|
||||||
state: WizardState
|
state: WizardState
|
||||||
dispatch: (action: WizardAction) => void
|
dispatch: (action: WizardAction) => void
|
||||||
profile: AppBskyActorDefs.ProfileViewBasic
|
profile: AppBskyActorDefs.ProfileViewBasic
|
||||||
|
@ -127,6 +146,7 @@ export function WizardProfileCard({
|
||||||
return (
|
return (
|
||||||
<WizardListCard
|
<WizardListCard
|
||||||
type="user"
|
type="user"
|
||||||
|
btnType={btnType}
|
||||||
displayName={displayName}
|
displayName={displayName}
|
||||||
subtitle={`@${sanitizeHandle(profile.handle)}`}
|
subtitle={`@${sanitizeHandle(profile.handle)}`}
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
|
@ -139,11 +159,13 @@ export function WizardProfileCard({
|
||||||
}
|
}
|
||||||
|
|
||||||
export function WizardFeedCard({
|
export function WizardFeedCard({
|
||||||
|
btnType,
|
||||||
generator,
|
generator,
|
||||||
state,
|
state,
|
||||||
dispatch,
|
dispatch,
|
||||||
moderationOpts,
|
moderationOpts,
|
||||||
}: {
|
}: {
|
||||||
|
btnType: 'checkbox' | 'remove'
|
||||||
generator: GeneratorView
|
generator: GeneratorView
|
||||||
state: WizardState
|
state: WizardState
|
||||||
dispatch: (action: WizardAction) => void
|
dispatch: (action: WizardAction) => void
|
||||||
|
@ -170,6 +192,7 @@ export function WizardFeedCard({
|
||||||
return (
|
return (
|
||||||
<WizardListCard
|
<WizardListCard
|
||||||
type="algo"
|
type="algo"
|
||||||
|
btnType={btnType}
|
||||||
displayName={sanitizeDisplayName(generator.displayName)}
|
displayName={sanitizeDisplayName(generator.displayName)}
|
||||||
subtitle={`Feed by @${sanitizeHandle(generator.creator.handle)}`}
|
subtitle={`Feed by @${sanitizeHandle(generator.creator.handle)}`}
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
|
|
|
@ -40,12 +40,14 @@ export function StepFeeds({moderationOpts}: {moderationOpts: ModerationOpts}) {
|
||||||
const {data: popularFeedsPages, fetchNextPage} = useGetPopularFeedsQuery({
|
const {data: popularFeedsPages, fetchNextPage} = useGetPopularFeedsQuery({
|
||||||
limit: 30,
|
limit: 30,
|
||||||
})
|
})
|
||||||
const popularFeeds =
|
const popularFeeds = popularFeedsPages?.pages.flatMap(p => p.feeds) ?? []
|
||||||
popularFeedsPages?.pages
|
|
||||||
.flatMap(page => page.feeds)
|
|
||||||
.filter(f => !savedFeeds?.some(sf => sf?.uri === f.uri)) ?? []
|
|
||||||
|
|
||||||
const suggestedFeeds = savedFeeds?.concat(popularFeeds)
|
const suggestedFeeds =
|
||||||
|
savedFeeds.length === 0
|
||||||
|
? popularFeeds
|
||||||
|
: savedFeeds.concat(
|
||||||
|
popularFeeds.filter(f => !savedFeeds.some(sf => sf.uri === f.uri)),
|
||||||
|
)
|
||||||
|
|
||||||
const {data: searchedFeeds, isLoading: isLoadingSearch} =
|
const {data: searchedFeeds, isLoading: isLoadingSearch} =
|
||||||
useSearchPopularFeedsQuery({q: throttledQuery})
|
useSearchPopularFeedsQuery({q: throttledQuery})
|
||||||
|
@ -56,6 +58,7 @@ export function StepFeeds({moderationOpts}: {moderationOpts: ModerationOpts}) {
|
||||||
return (
|
return (
|
||||||
<WizardFeedCard
|
<WizardFeedCard
|
||||||
generator={item}
|
generator={item}
|
||||||
|
btnType="checkbox"
|
||||||
state={state}
|
state={state}
|
||||||
dispatch={dispatch}
|
dispatch={dispatch}
|
||||||
moderationOpts={moderationOpts}
|
moderationOpts={moderationOpts}
|
||||||
|
|
|
@ -45,6 +45,7 @@ export function StepProfiles({
|
||||||
return (
|
return (
|
||||||
<WizardProfileCard
|
<WizardProfileCard
|
||||||
profile={item}
|
profile={item}
|
||||||
|
btnType="checkbox"
|
||||||
state={state}
|
state={state}
|
||||||
dispatch={dispatch}
|
dispatch={dispatch}
|
||||||
moderationOpts={moderationOpts}
|
moderationOpts={moderationOpts}
|
||||||
|
|
|
@ -21,6 +21,7 @@ import {NativeStackScreenProps} from '@react-navigation/native-stack'
|
||||||
|
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {HITSLOP_10} from 'lib/constants'
|
import {HITSLOP_10} from 'lib/constants'
|
||||||
|
import {createSanitizedDisplayName} from 'lib/moderation/create-sanitized-display-name'
|
||||||
import {CommonNavigatorParams, NavigationProp} from 'lib/routes/types'
|
import {CommonNavigatorParams, NavigationProp} from 'lib/routes/types'
|
||||||
import {logEvent} from 'lib/statsig/statsig'
|
import {logEvent} from 'lib/statsig/statsig'
|
||||||
import {sanitizeDisplayName} from 'lib/strings/display-names'
|
import {sanitizeDisplayName} from 'lib/strings/display-names'
|
||||||
|
@ -170,15 +171,7 @@ function WizardInner({
|
||||||
)
|
)
|
||||||
|
|
||||||
const getDefaultName = () => {
|
const getDefaultName = () => {
|
||||||
let displayName
|
const displayName = createSanitizedDisplayName(currentProfile!, true)
|
||||||
if (
|
|
||||||
currentProfile?.displayName != null &&
|
|
||||||
currentProfile?.displayName !== ''
|
|
||||||
) {
|
|
||||||
displayName = sanitizeDisplayName(currentProfile.displayName)
|
|
||||||
} else {
|
|
||||||
displayName = sanitizeHandle(currentProfile!.handle)
|
|
||||||
}
|
|
||||||
return _(msg`${displayName}'s Starter Pack`).slice(0, 50)
|
return _(msg`${displayName}'s Starter Pack`).slice(0, 50)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -191,16 +184,12 @@ function WizardInner({
|
||||||
nextBtn: _(msg`Next`),
|
nextBtn: _(msg`Next`),
|
||||||
},
|
},
|
||||||
Profiles: {
|
Profiles: {
|
||||||
header: _(msg`People`),
|
header: _(msg`Choose People`),
|
||||||
nextBtn: _(msg`Next`),
|
nextBtn: _(msg`Next`),
|
||||||
subtitle: _(
|
|
||||||
msg`Add people to your starter pack that you think others will enjoy following`,
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
Feeds: {
|
Feeds: {
|
||||||
header: _(msg`Feeds`),
|
header: _(msg`Choose Feeds`),
|
||||||
nextBtn: state.feeds.length === 0 ? _(msg`Skip`) : _(msg`Finish`),
|
nextBtn: state.feeds.length === 0 ? _(msg`Skip`) : _(msg`Finish`),
|
||||||
subtitle: _(msg`Some subtitle`),
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
const currUiStrings = wizardUiStrings[state.currentStep]
|
const currUiStrings = wizardUiStrings[state.currentStep]
|
||||||
|
@ -254,8 +243,8 @@ function WizardInner({
|
||||||
dispatch({type: 'SetProcessing', processing: true})
|
dispatch({type: 'SetProcessing', processing: true})
|
||||||
if (currentStarterPack && currentListItems) {
|
if (currentStarterPack && currentListItems) {
|
||||||
editStarterPack({
|
editStarterPack({
|
||||||
name: state.name ?? getDefaultName(),
|
name: state.name?.trim() || getDefaultName(),
|
||||||
description: state.description,
|
description: state.description?.trim(),
|
||||||
descriptionFacets: [],
|
descriptionFacets: [],
|
||||||
profiles: state.profiles,
|
profiles: state.profiles,
|
||||||
feeds: state.feeds,
|
feeds: state.feeds,
|
||||||
|
@ -264,8 +253,8 @@ function WizardInner({
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
createStarterPack({
|
createStarterPack({
|
||||||
name: state.name ?? getDefaultName(),
|
name: state.name?.trim() || getDefaultName(),
|
||||||
description: state.description,
|
description: state.description?.trim(),
|
||||||
descriptionFacets: [],
|
descriptionFacets: [],
|
||||||
profiles: state.profiles,
|
profiles: state.profiles,
|
||||||
feeds: state.feeds,
|
feeds: state.feeds,
|
||||||
|
@ -483,13 +472,10 @@ function Footer({
|
||||||
</Trans>
|
</Trans>
|
||||||
) : items.length === 2 ? (
|
) : items.length === 2 ? (
|
||||||
<Trans>
|
<Trans>
|
||||||
<Text style={[a.font_bold, textStyles]}>
|
<Text style={[a.font_bold, textStyles]}>You</Text> and
|
||||||
{getName(items[initialNamesIndex])}{' '}
|
|
||||||
</Text>
|
|
||||||
and
|
|
||||||
<Text> </Text>
|
<Text> </Text>
|
||||||
<Text style={[a.font_bold, textStyles]}>
|
<Text style={[a.font_bold, textStyles]}>
|
||||||
{getName(items[state.currentStep === 'Profiles' ? 0 : 1])}{' '}
|
{getName(items[initialNamesIndex])}{' '}
|
||||||
</Text>
|
</Text>
|
||||||
are included in your starter pack
|
are included in your starter pack
|
||||||
</Trans>
|
</Trans>
|
||||||
|
@ -579,9 +565,9 @@ function Footer({
|
||||||
|
|
||||||
function getName(item: AppBskyActorDefs.ProfileViewBasic | GeneratorView) {
|
function getName(item: AppBskyActorDefs.ProfileViewBasic | GeneratorView) {
|
||||||
if (typeof item.displayName === 'string') {
|
if (typeof item.displayName === 'string') {
|
||||||
return enforceLen(sanitizeDisplayName(item.displayName), 16, true)
|
return enforceLen(sanitizeDisplayName(item.displayName), 28, true)
|
||||||
} else if (typeof item.handle === 'string') {
|
} else if (typeof item.handle === 'string') {
|
||||||
return enforceLen(sanitizeHandle(item.handle), 16, true)
|
return enforceLen(sanitizeHandle(item.handle), 28, true)
|
||||||
}
|
}
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue