add the clients
This commit is contained in:
@@ -70,6 +70,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@atproto/api": "^0.15.26",
|
"@atproto/api": "^0.15.26",
|
||||||
|
"@atproto/oauth-client-browser": "^0.3.27",
|
||||||
"@bitdrift/react-native": "^0.6.8",
|
"@bitdrift/react-native": "^0.6.8",
|
||||||
"@braintree/sanitize-url": "^6.0.2",
|
"@braintree/sanitize-url": "^6.0.2",
|
||||||
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
|
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
|
||||||
@@ -132,6 +133,7 @@
|
|||||||
"eventemitter3": "^5.0.1",
|
"eventemitter3": "^5.0.1",
|
||||||
"expo": "53.0.11",
|
"expo": "53.0.11",
|
||||||
"expo-application": "~6.1.4",
|
"expo-application": "~6.1.4",
|
||||||
|
"expo-atproto-auth": "^0.1.1",
|
||||||
"expo-blur": "~14.1.5",
|
"expo-blur": "~14.1.5",
|
||||||
"expo-build-properties": "~0.14.6",
|
"expo-build-properties": "~0.14.6",
|
||||||
"expo-camera": "~16.1.8",
|
"expo-camera": "~16.1.8",
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export type Gate =
|
|||||||
| 'debug_show_feedcontext'
|
| 'debug_show_feedcontext'
|
||||||
| 'debug_subscriptions'
|
| 'debug_subscriptions'
|
||||||
| 'explore_show_suggested_feeds'
|
| 'explore_show_suggested_feeds'
|
||||||
|
| 'oauth'
|
||||||
| 'old_postonboarding'
|
| 'old_postonboarding'
|
||||||
| 'onboarding_add_video_feed'
|
| 'onboarding_add_video_feed'
|
||||||
| 'post_threads_v2_unspecced'
|
| 'post_threads_v2_unspecced'
|
||||||
|
|||||||
+108
-14
@@ -32,22 +32,13 @@ import {Ticket_Stroke2_Corner0_Rounded as Ticket} from '#/components/icons/Ticke
|
|||||||
import {Loader} from '#/components/Loader'
|
import {Loader} from '#/components/Loader'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import {FormContainer} from './FormContainer'
|
import {FormContainer} from './FormContainer'
|
||||||
|
import {useGate} from '#/lib/statsig/statsig'
|
||||||
|
import {isWeb} from '#/platform/detection'
|
||||||
|
import {getNativeOAuthClient, getWebOAuthClient} from '#/state/session/oauth'
|
||||||
|
|
||||||
type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema
|
type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema
|
||||||
|
|
||||||
export const LoginForm = ({
|
interface LoginFormProps {
|
||||||
error,
|
|
||||||
serviceUrl,
|
|
||||||
serviceDescription,
|
|
||||||
initialHandle,
|
|
||||||
setError,
|
|
||||||
setServiceUrl,
|
|
||||||
onPressRetryConnect,
|
|
||||||
onPressBack,
|
|
||||||
onPressForgotPassword,
|
|
||||||
onAttemptSuccess,
|
|
||||||
onAttemptFailed,
|
|
||||||
}: {
|
|
||||||
error: string
|
error: string
|
||||||
serviceUrl: string
|
serviceUrl: string
|
||||||
serviceDescription: ServiceDescription | undefined
|
serviceDescription: ServiceDescription | undefined
|
||||||
@@ -59,7 +50,110 @@ export const LoginForm = ({
|
|||||||
onPressForgotPassword: () => void
|
onPressForgotPassword: () => void
|
||||||
onAttemptSuccess: () => void
|
onAttemptSuccess: () => void
|
||||||
onAttemptFailed: () => void
|
onAttemptFailed: () => void
|
||||||
}) => {
|
}
|
||||||
|
|
||||||
|
export function LoginForm(props: LoginFormProps) {
|
||||||
|
const gate = useGate()
|
||||||
|
if (gate('oauth')) {
|
||||||
|
return <OAuthLoginForm {...props} />
|
||||||
|
} else {
|
||||||
|
return <LoginFormInner {...props} />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function OAuthLoginForm({error, initialHandle, onPressBack}: LoginFormProps) {
|
||||||
|
const {_} = useLingui()
|
||||||
|
const [isProcessing, setIsProcessing] = React.useState(false)
|
||||||
|
const identifierValueRef = useRef<string>(initialHandle || '')
|
||||||
|
|
||||||
|
const onPressNext = async () => {
|
||||||
|
setIsProcessing(true)
|
||||||
|
if (isWeb) {
|
||||||
|
const client = getWebOAuthClient()
|
||||||
|
await client.signIn(identifierValueRef.current)
|
||||||
|
} else {
|
||||||
|
const client = getNativeOAuthClient()
|
||||||
|
const res = await client.signIn(identifierValueRef.current)
|
||||||
|
// redirect after result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormContainer testID="loginForm" titleText={<Trans>Sign in</Trans>}>
|
||||||
|
<View>
|
||||||
|
<TextField.LabelText>
|
||||||
|
<Trans>Account</Trans>
|
||||||
|
</TextField.LabelText>
|
||||||
|
<View style={[a.gap_sm]}>
|
||||||
|
<TextField.Root>
|
||||||
|
<TextField.Icon icon={At} />
|
||||||
|
<TextField.Input
|
||||||
|
testID="loginUsernameInput"
|
||||||
|
label={_(msg`Username or email address`)}
|
||||||
|
autoCapitalize="none"
|
||||||
|
autoFocus
|
||||||
|
autoCorrect={false}
|
||||||
|
autoComplete="username"
|
||||||
|
returnKeyType="next"
|
||||||
|
textContentType="username"
|
||||||
|
defaultValue={initialHandle || ''}
|
||||||
|
onChangeText={v => {
|
||||||
|
identifierValueRef.current = v
|
||||||
|
}}
|
||||||
|
onSubmitEditing={() => {}}
|
||||||
|
blurOnSubmit={false} // prevents flickering due to onSubmitEditing going to next field
|
||||||
|
editable={!isProcessing}
|
||||||
|
accessibilityHint={_(
|
||||||
|
msg`Enter the username or email address you used when you created your account`,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</TextField.Root>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<FormError error={error} />
|
||||||
|
<View style={[a.flex_row, a.align_center, a.pt_md]}>
|
||||||
|
<Button
|
||||||
|
label={_(msg`Back`)}
|
||||||
|
variant="solid"
|
||||||
|
color="secondary"
|
||||||
|
size="large"
|
||||||
|
onPress={onPressBack}>
|
||||||
|
<ButtonText>
|
||||||
|
<Trans>Back</Trans>
|
||||||
|
</ButtonText>
|
||||||
|
</Button>
|
||||||
|
<View style={a.flex_1} />
|
||||||
|
<Button
|
||||||
|
testID="loginNextButton"
|
||||||
|
label={_(msg`Login`)}
|
||||||
|
accessibilityHint={_(msg`Navigates to the login screen`)}
|
||||||
|
variant="solid"
|
||||||
|
color="primary"
|
||||||
|
size="large"
|
||||||
|
onPress={onPressNext}>
|
||||||
|
<ButtonText>
|
||||||
|
<Trans>Login</Trans>
|
||||||
|
</ButtonText>
|
||||||
|
{isProcessing && <ButtonIcon icon={Loader} />}
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
</FormContainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const LoginFormInner = ({
|
||||||
|
error,
|
||||||
|
serviceUrl,
|
||||||
|
serviceDescription,
|
||||||
|
initialHandle,
|
||||||
|
setError,
|
||||||
|
setServiceUrl,
|
||||||
|
onPressRetryConnect,
|
||||||
|
onPressBack,
|
||||||
|
onPressForgotPassword,
|
||||||
|
onAttemptSuccess,
|
||||||
|
onAttemptFailed,
|
||||||
|
}: LoginFormProps) => {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const [isProcessing, setIsProcessing] = useState<boolean>(false)
|
const [isProcessing, setIsProcessing] = useState<boolean>(false)
|
||||||
const [isAuthFactorTokenNeeded, setIsAuthFactorTokenNeeded] =
|
const [isAuthFactorTokenNeeded, setIsAuthFactorTokenNeeded] =
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ import {
|
|||||||
} from './moderation'
|
} from './moderation'
|
||||||
import {SessionAccount} from './types'
|
import {SessionAccount} from './types'
|
||||||
import {isSessionExpired, isSignupQueued} from './util'
|
import {isSessionExpired, isSignupQueued} from './util'
|
||||||
|
import {BSKY_OAUTH_CLIENT} from './oauth'
|
||||||
|
import {ExpoOAuthClient} from 'expo-atproto-auth'
|
||||||
|
|
||||||
export function createPublicAgent() {
|
export function createPublicAgent() {
|
||||||
configureModerationForGuest() // Side effect but only relevant for tests
|
configureModerationForGuest() // Side effect but only relevant for tests
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import {ExpoOAuthClient} from 'expo-atproto-auth'
|
||||||
|
import {BrowserOAuthClient} from '@atproto/oauth-client-browser'
|
||||||
|
import {OAuthClient} from '@atproto/oauth-client'
|
||||||
|
import {Platform} from 'react-native'
|
||||||
|
|
||||||
|
export const BSKY_OAUTH_CLIENT: OAuthClient =
|
||||||
|
Platform.OS === 'web' ? createWebOAuthClient() : createNativeOAuthClient()
|
||||||
|
|
||||||
|
export function createWebOAuthClient() {
|
||||||
|
return new BrowserOAuthClient({
|
||||||
|
clientMetadata: {
|
||||||
|
client_id: 'https://bsky.hailey.at/oauth-client-metadata.json',
|
||||||
|
client_name: 'Bluesky (Hailey Demo)',
|
||||||
|
client_uri: 'https://bsky.hailey.at',
|
||||||
|
redirect_uris: ['https://bsky.hailey.at/auth/callback'],
|
||||||
|
scope: 'atproto transition:generic',
|
||||||
|
token_endpoint_auth_method: 'none',
|
||||||
|
response_types: ['code'],
|
||||||
|
grant_types: ['authorization_code', 'refresh_token'],
|
||||||
|
application_type: 'web',
|
||||||
|
dpop_bound_access_tokens: true,
|
||||||
|
},
|
||||||
|
handleResolver: 'https://bsky.social',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createNativeOAuthClient() {
|
||||||
|
return new ExpoOAuthClient({
|
||||||
|
clientMetadata: {
|
||||||
|
client_id: 'https://bsky.hailey.at/oauth-client-metadata.native.json',
|
||||||
|
client_name: 'Bluesky Native App (Hailey Demo)',
|
||||||
|
client_uri: 'https://hailey.at',
|
||||||
|
redirect_uris: ['at.hailey:/auth/callback'],
|
||||||
|
scope: 'atproto transition:generic',
|
||||||
|
token_endpoint_auth_method: 'none',
|
||||||
|
response_types: ['code'],
|
||||||
|
grant_types: ['authorization_code', 'refresh_token'],
|
||||||
|
application_type: 'native',
|
||||||
|
dpop_bound_access_tokens: true,
|
||||||
|
},
|
||||||
|
handleResolver: 'https://bsky.social',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getNativeOAuthClient() {
|
||||||
|
return BSKY_OAUTH_CLIENT as ExpoOAuthClient
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getWebOAuthClient() {
|
||||||
|
return BSKY_OAUTH_CLIENT as BrowserOAuthClient
|
||||||
|
}
|
||||||
@@ -20,6 +20,18 @@
|
|||||||
"@jridgewell/gen-mapping" "^0.3.0"
|
"@jridgewell/gen-mapping" "^0.3.0"
|
||||||
"@jridgewell/trace-mapping" "^0.3.9"
|
"@jridgewell/trace-mapping" "^0.3.9"
|
||||||
|
|
||||||
|
"@atproto-labs/did-resolver@0.2.0":
|
||||||
|
version "0.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@atproto-labs/did-resolver/-/did-resolver-0.2.0.tgz#85762cd6992555e78bde9e202dea803a23e7d896"
|
||||||
|
integrity sha512-y9GOx2gUETynDKmANnBrU5DTf+u0AwKBJpGns1vDDOYMdLdRCFIeYy3UH+TI8YOkcEazjgF5Q3m+LjwriE1KqQ==
|
||||||
|
dependencies:
|
||||||
|
"@atproto-labs/fetch" "0.2.3"
|
||||||
|
"@atproto-labs/pipe" "0.1.1"
|
||||||
|
"@atproto-labs/simple-store" "0.2.0"
|
||||||
|
"@atproto-labs/simple-store-memory" "0.1.3"
|
||||||
|
"@atproto/did" "0.1.5"
|
||||||
|
zod "^3.23.8"
|
||||||
|
|
||||||
"@atproto-labs/fetch-node@0.1.9":
|
"@atproto-labs/fetch-node@0.1.9":
|
||||||
version "0.1.9"
|
version "0.1.9"
|
||||||
resolved "https://registry.yarnpkg.com/@atproto-labs/fetch-node/-/fetch-node-0.1.9.tgz#5df902413cc2ebfff914999ad3fbbc13b20e1dd0"
|
resolved "https://registry.yarnpkg.com/@atproto-labs/fetch-node/-/fetch-node-0.1.9.tgz#5df902413cc2ebfff914999ad3fbbc13b20e1dd0"
|
||||||
@@ -37,6 +49,24 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@atproto-labs/pipe" "0.1.1"
|
"@atproto-labs/pipe" "0.1.1"
|
||||||
|
|
||||||
|
"@atproto-labs/handle-resolver@0.3.0":
|
||||||
|
version "0.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@atproto-labs/handle-resolver/-/handle-resolver-0.3.0.tgz#b7928358e59cbacd4f0337a807222798fe133f60"
|
||||||
|
integrity sha512-TREelvXB6P2eHxx6QjINRkBzUZu/aXWrdY9iN57shQe3C8rzsHNEHHuTVvRa33Hc7vFdQbZN0TnCgKveoyiL/A==
|
||||||
|
dependencies:
|
||||||
|
"@atproto-labs/simple-store" "0.2.0"
|
||||||
|
"@atproto-labs/simple-store-memory" "0.1.3"
|
||||||
|
"@atproto/did" "0.1.5"
|
||||||
|
zod "^3.23.8"
|
||||||
|
|
||||||
|
"@atproto-labs/identity-resolver@0.3.0":
|
||||||
|
version "0.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@atproto-labs/identity-resolver/-/identity-resolver-0.3.0.tgz#8988fc2e91c2060614e0f7698f5561e5c84904fc"
|
||||||
|
integrity sha512-ZmmRV6m17kIaX4WllYrFIa7d23lNng0fIk6pLyepRGZobQhM5d4wDezICTESAG+RoD0e5fisWs+Tamdvx3mx/Q==
|
||||||
|
dependencies:
|
||||||
|
"@atproto-labs/did-resolver" "0.2.0"
|
||||||
|
"@atproto-labs/handle-resolver" "0.3.0"
|
||||||
|
|
||||||
"@atproto-labs/pipe@0.1.1":
|
"@atproto-labs/pipe@0.1.1":
|
||||||
version "0.1.1"
|
version "0.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/@atproto-labs/pipe/-/pipe-0.1.1.tgz#1c4232d16bf95f251e993cb6ee440f9aa4e87ce6"
|
resolved "https://registry.yarnpkg.com/@atproto-labs/pipe/-/pipe-0.1.1.tgz#1c4232d16bf95f251e993cb6ee440f9aa4e87ce6"
|
||||||
@@ -270,6 +300,15 @@
|
|||||||
"@atproto/jwk" "0.4.0"
|
"@atproto/jwk" "0.4.0"
|
||||||
jose "^5.2.0"
|
jose "^5.2.0"
|
||||||
|
|
||||||
|
"@atproto/jwk-webcrypto@0.1.9":
|
||||||
|
version "0.1.9"
|
||||||
|
resolved "https://registry.yarnpkg.com/@atproto/jwk-webcrypto/-/jwk-webcrypto-0.1.9.tgz#b58abc4a77a3352abd6f6d33825916325d82496a"
|
||||||
|
integrity sha512-ecciePHT0JEDZNAbMKSkdqoBYsjvhwuVno0jsS600SZmuvi2fAMhGraDZ5ZOO5M0hHHBiDbN7Ar/qcnIwyoxsA==
|
||||||
|
dependencies:
|
||||||
|
"@atproto/jwk" "0.4.0"
|
||||||
|
"@atproto/jwk-jose" "0.1.9"
|
||||||
|
zod "^3.23.8"
|
||||||
|
|
||||||
"@atproto/jwk@0.4.0":
|
"@atproto/jwk@0.4.0":
|
||||||
version "0.4.0"
|
version "0.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/@atproto/jwk/-/jwk-0.4.0.tgz#f32265be172492c38434c556a124b954f249cee8"
|
resolved "https://registry.yarnpkg.com/@atproto/jwk/-/jwk-0.4.0.tgz#f32265be172492c38434c556a124b954f249cee8"
|
||||||
@@ -289,6 +328,38 @@
|
|||||||
multiformats "^9.9.0"
|
multiformats "^9.9.0"
|
||||||
zod "^3.23.8"
|
zod "^3.23.8"
|
||||||
|
|
||||||
|
"@atproto/oauth-client-browser@^0.3.27":
|
||||||
|
version "0.3.27"
|
||||||
|
resolved "https://registry.yarnpkg.com/@atproto/oauth-client-browser/-/oauth-client-browser-0.3.27.tgz#929045d9dc834339b8c76c372d44e6444f34cfb2"
|
||||||
|
integrity sha512-sUZP27KjlS3qJVPMC+RgWNARQZo7n6CWCXN55+QqLnHTfh+dLCXDS9jMUreXUGMQkVETEogDZ/v0Pb0xHQwBsg==
|
||||||
|
dependencies:
|
||||||
|
"@atproto-labs/did-resolver" "0.2.0"
|
||||||
|
"@atproto-labs/handle-resolver" "0.3.0"
|
||||||
|
"@atproto-labs/simple-store" "0.2.0"
|
||||||
|
"@atproto/did" "0.1.5"
|
||||||
|
"@atproto/jwk" "0.4.0"
|
||||||
|
"@atproto/jwk-webcrypto" "0.1.9"
|
||||||
|
"@atproto/oauth-client" "0.5.1"
|
||||||
|
"@atproto/oauth-types" "0.4.0"
|
||||||
|
|
||||||
|
"@atproto/oauth-client@0.5.1":
|
||||||
|
version "0.5.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@atproto/oauth-client/-/oauth-client-0.5.1.tgz#e0b7995f395a898e1faa736f177f242e5b92f0fa"
|
||||||
|
integrity sha512-wNC9RdfH1LGyZKF+UOmY+z4TFNx1gBur3fx91MCCrNaU0aTHBzgEH9UquL2031J7VNXhBsKJnHfEB5ZYy0AEHQ==
|
||||||
|
dependencies:
|
||||||
|
"@atproto-labs/did-resolver" "0.2.0"
|
||||||
|
"@atproto-labs/fetch" "0.2.3"
|
||||||
|
"@atproto-labs/handle-resolver" "0.3.0"
|
||||||
|
"@atproto-labs/identity-resolver" "0.3.0"
|
||||||
|
"@atproto-labs/simple-store" "0.2.0"
|
||||||
|
"@atproto-labs/simple-store-memory" "0.1.3"
|
||||||
|
"@atproto/did" "0.1.5"
|
||||||
|
"@atproto/jwk" "0.4.0"
|
||||||
|
"@atproto/oauth-types" "0.4.0"
|
||||||
|
"@atproto/xrpc" "0.7.1"
|
||||||
|
multiformats "^9.9.0"
|
||||||
|
zod "^3.23.8"
|
||||||
|
|
||||||
"@atproto/oauth-provider-api@0.1.6":
|
"@atproto/oauth-provider-api@0.1.6":
|
||||||
version "0.1.6"
|
version "0.1.6"
|
||||||
resolved "https://registry.yarnpkg.com/@atproto/oauth-provider-api/-/oauth-provider-api-0.1.6.tgz#769a70caaac9b5144f9f867518523d1568a6b47c"
|
resolved "https://registry.yarnpkg.com/@atproto/oauth-provider-api/-/oauth-provider-api-0.1.6.tgz#769a70caaac9b5144f9f867518523d1568a6b47c"
|
||||||
@@ -480,7 +551,7 @@
|
|||||||
ws "^8.12.0"
|
ws "^8.12.0"
|
||||||
zod "^3.23.8"
|
zod "^3.23.8"
|
||||||
|
|
||||||
"@atproto/xrpc@^0.7.1":
|
"@atproto/xrpc@0.7.1", "@atproto/xrpc@^0.7.1":
|
||||||
version "0.7.1"
|
version "0.7.1"
|
||||||
resolved "https://registry.yarnpkg.com/@atproto/xrpc/-/xrpc-0.7.1.tgz#51a8fc131eb21bd1229129d0a46384accc50ad65"
|
resolved "https://registry.yarnpkg.com/@atproto/xrpc/-/xrpc-0.7.1.tgz#51a8fc131eb21bd1229129d0a46384accc50ad65"
|
||||||
integrity sha512-ANHEzlskYlMEdH18m+Itp3a8d0pEJao2qoDybDoMupTnoeNkya4VKIaOgAi6ERQnqatBBZyn9asW+7rJmSt/8g==
|
integrity sha512-ANHEzlskYlMEdH18m+Itp3a8d0pEJao2qoDybDoMupTnoeNkya4VKIaOgAi6ERQnqatBBZyn9asW+7rJmSt/8g==
|
||||||
@@ -11182,6 +11253,11 @@ expo-asset@~11.1.5:
|
|||||||
"@expo/image-utils" "^0.7.4"
|
"@expo/image-utils" "^0.7.4"
|
||||||
expo-constants "~17.1.5"
|
expo-constants "~17.1.5"
|
||||||
|
|
||||||
|
expo-atproto-auth@^0.1.1:
|
||||||
|
version "0.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/expo-atproto-auth/-/expo-atproto-auth-0.1.1.tgz#250798e87c22e963a3c3edb9914c13f46ca88de4"
|
||||||
|
integrity sha512-tJ9y3uJiTFXMaviKOWfbSKYcWaWdvEByMJGXB4h8RxEhmnD7C7CTdVRR4ZLfcUucaDQLRczMFWUwBdZKbS1slA==
|
||||||
|
|
||||||
expo-blur@~14.1.5:
|
expo-blur@~14.1.5:
|
||||||
version "14.1.5"
|
version "14.1.5"
|
||||||
resolved "https://registry.yarnpkg.com/expo-blur/-/expo-blur-14.1.5.tgz#910712389e19286ccdc136275bf569f427aa05ef"
|
resolved "https://registry.yarnpkg.com/expo-blur/-/expo-blur-14.1.5.tgz#910712389e19286ccdc136275bf569f427aa05ef"
|
||||||
|
|||||||
Reference in New Issue
Block a user