Add useHandleRef as a lighter alternative for useAnimatedRef (#6500)
This commit is contained in:
@@ -0,0 +1,39 @@
|
|||||||
|
import {useState} from 'react'
|
||||||
|
import {AnimatedRef, measure, MeasuredDimensions} from 'react-native-reanimated'
|
||||||
|
|
||||||
|
export type HandleRef = {
|
||||||
|
(node: any): void
|
||||||
|
current: null | number
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is a lighterweight alternative to `useAnimatedRef()` for imperative UI thread actions.
|
||||||
|
// Render it like <View ref={ref} />, then pass `ref.current` to `measureHandle()` and such.
|
||||||
|
export function useHandleRef(): HandleRef {
|
||||||
|
return useState(() => {
|
||||||
|
const ref = (node: any) => {
|
||||||
|
if (node) {
|
||||||
|
ref.current =
|
||||||
|
node._nativeTag ??
|
||||||
|
node.__nativeTag ??
|
||||||
|
node.canonical?.nativeTag ??
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
ref.current = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ref.current = null
|
||||||
|
return ref
|
||||||
|
})[0] as HandleRef
|
||||||
|
}
|
||||||
|
|
||||||
|
// When using this version, you need to read ref.current on the JS thread, and pass it to UI.
|
||||||
|
export function measureHandle(
|
||||||
|
current: number | null,
|
||||||
|
): MeasuredDimensions | null {
|
||||||
|
'worklet'
|
||||||
|
if (current !== null) {
|
||||||
|
return measure((() => current) as AnimatedRef<any>)
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,6 @@
|
|||||||
import React, {memo} from 'react'
|
import React, {memo} from 'react'
|
||||||
import {StyleSheet, TouchableWithoutFeedback, View} from 'react-native'
|
import {StyleSheet, TouchableWithoutFeedback, View} from 'react-native'
|
||||||
import Animated, {
|
import {MeasuredDimensions, runOnJS, runOnUI} from 'react-native-reanimated'
|
||||||
measure,
|
|
||||||
MeasuredDimensions,
|
|
||||||
runOnJS,
|
|
||||||
runOnUI,
|
|
||||||
useAnimatedRef,
|
|
||||||
} from 'react-native-reanimated'
|
|
||||||
import {AppBskyActorDefs, ModerationDecision} from '@atproto/api'
|
import {AppBskyActorDefs, ModerationDecision} from '@atproto/api'
|
||||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||||
import {msg} from '@lingui/macro'
|
import {msg} from '@lingui/macro'
|
||||||
@@ -14,6 +8,7 @@ import {useLingui} from '@lingui/react'
|
|||||||
import {useNavigation} from '@react-navigation/native'
|
import {useNavigation} from '@react-navigation/native'
|
||||||
|
|
||||||
import {BACK_HITSLOP} from '#/lib/constants'
|
import {BACK_HITSLOP} from '#/lib/constants'
|
||||||
|
import {measureHandle, useHandleRef} from '#/lib/hooks/useHandleRef'
|
||||||
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
||||||
import {NavigationProp} from '#/lib/routes/types'
|
import {NavigationProp} from '#/lib/routes/types'
|
||||||
import {isIOS} from '#/platform/detection'
|
import {isIOS} from '#/platform/detection'
|
||||||
@@ -49,7 +44,7 @@ let ProfileHeaderShell = ({
|
|||||||
const {openLightbox} = useLightboxControls()
|
const {openLightbox} = useLightboxControls()
|
||||||
const navigation = useNavigation<NavigationProp>()
|
const navigation = useNavigation<NavigationProp>()
|
||||||
const {isDesktop} = useWebMediaQueries()
|
const {isDesktop} = useWebMediaQueries()
|
||||||
const aviRef = useAnimatedRef()
|
const aviRef = useHandleRef()
|
||||||
|
|
||||||
const onPressBack = React.useCallback(() => {
|
const onPressBack = React.useCallback(() => {
|
||||||
if (navigation.canGoBack()) {
|
if (navigation.canGoBack()) {
|
||||||
@@ -86,9 +81,10 @@ let ProfileHeaderShell = ({
|
|||||||
const modui = moderation.ui('avatar')
|
const modui = moderation.ui('avatar')
|
||||||
const avatar = profile.avatar
|
const avatar = profile.avatar
|
||||||
if (avatar && !(modui.blur && modui.noOverride)) {
|
if (avatar && !(modui.blur && modui.noOverride)) {
|
||||||
|
const aviHandle = aviRef.current
|
||||||
runOnUI(() => {
|
runOnUI(() => {
|
||||||
'worklet'
|
'worklet'
|
||||||
const rect = measure(aviRef)
|
const rect = measureHandle(aviHandle)
|
||||||
runOnJS(_openLightbox)(avatar, rect)
|
runOnJS(_openLightbox)(avatar, rect)
|
||||||
})()
|
})()
|
||||||
}
|
}
|
||||||
@@ -170,14 +166,14 @@ let ProfileHeaderShell = ({
|
|||||||
styles.avi,
|
styles.avi,
|
||||||
profile.associated?.labeler && styles.aviLabeler,
|
profile.associated?.labeler && styles.aviLabeler,
|
||||||
]}>
|
]}>
|
||||||
<Animated.View ref={aviRef} collapsable={false}>
|
<View ref={aviRef} collapsable={false}>
|
||||||
<UserAvatar
|
<UserAvatar
|
||||||
type={profile.associated?.labeler ? 'labeler' : 'user'}
|
type={profile.associated?.labeler ? 'labeler' : 'user'}
|
||||||
size={90}
|
size={90}
|
||||||
avatar={profile.avatar}
|
avatar={profile.avatar}
|
||||||
moderation={moderation.ui('avatar')}
|
moderation={moderation.ui('avatar')}
|
||||||
/>
|
/>
|
||||||
</Animated.View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</TouchableWithoutFeedback>
|
</TouchableWithoutFeedback>
|
||||||
</GrowableAvatar>
|
</GrowableAvatar>
|
||||||
|
|||||||
@@ -1,18 +1,13 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {Pressable, StyleSheet, View} from 'react-native'
|
import {Pressable, StyleSheet, View} from 'react-native'
|
||||||
import Animated, {
|
import {MeasuredDimensions, runOnJS, runOnUI} from 'react-native-reanimated'
|
||||||
measure,
|
|
||||||
MeasuredDimensions,
|
|
||||||
runOnJS,
|
|
||||||
runOnUI,
|
|
||||||
useAnimatedRef,
|
|
||||||
} from 'react-native-reanimated'
|
|
||||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {useNavigation} from '@react-navigation/native'
|
import {useNavigation} from '@react-navigation/native'
|
||||||
|
|
||||||
import {BACK_HITSLOP} from '#/lib/constants'
|
import {BACK_HITSLOP} from '#/lib/constants'
|
||||||
|
import {measureHandle, useHandleRef} from '#/lib/hooks/useHandleRef'
|
||||||
import {usePalette} from '#/lib/hooks/usePalette'
|
import {usePalette} from '#/lib/hooks/usePalette'
|
||||||
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
||||||
import {makeProfileLink} from '#/lib/routes/links'
|
import {makeProfileLink} from '#/lib/routes/links'
|
||||||
@@ -60,7 +55,7 @@ export function ProfileSubpageHeader({
|
|||||||
const {openLightbox} = useLightboxControls()
|
const {openLightbox} = useLightboxControls()
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const canGoBack = navigation.canGoBack()
|
const canGoBack = navigation.canGoBack()
|
||||||
const aviRef = useAnimatedRef()
|
const aviRef = useHandleRef()
|
||||||
|
|
||||||
const onPressBack = React.useCallback(() => {
|
const onPressBack = React.useCallback(() => {
|
||||||
if (navigation.canGoBack()) {
|
if (navigation.canGoBack()) {
|
||||||
@@ -101,9 +96,10 @@ export function ProfileSubpageHeader({
|
|||||||
if (
|
if (
|
||||||
avatar // TODO && !(view.moderation.avatar.blur && view.moderation.avatar.noOverride)
|
avatar // TODO && !(view.moderation.avatar.blur && view.moderation.avatar.noOverride)
|
||||||
) {
|
) {
|
||||||
|
const aviHandle = aviRef.current
|
||||||
runOnUI(() => {
|
runOnUI(() => {
|
||||||
'worklet'
|
'worklet'
|
||||||
const rect = measure(aviRef)
|
const rect = measureHandle(aviHandle)
|
||||||
runOnJS(_openLightbox)(avatar, rect)
|
runOnJS(_openLightbox)(avatar, rect)
|
||||||
})()
|
})()
|
||||||
}
|
}
|
||||||
@@ -155,7 +151,7 @@ export function ProfileSubpageHeader({
|
|||||||
paddingBottom: 6,
|
paddingBottom: 6,
|
||||||
paddingHorizontal: isMobile ? 12 : 14,
|
paddingHorizontal: isMobile ? 12 : 14,
|
||||||
}}>
|
}}>
|
||||||
<Animated.View ref={aviRef} collapsable={false}>
|
<View ref={aviRef} collapsable={false}>
|
||||||
<Pressable
|
<Pressable
|
||||||
testID="headerAviButton"
|
testID="headerAviButton"
|
||||||
onPress={onPressAvi}
|
onPress={onPressAvi}
|
||||||
@@ -169,7 +165,7 @@ export function ProfileSubpageHeader({
|
|||||||
<UserAvatar type={avatarType} size={58} avatar={avatar} />
|
<UserAvatar type={avatarType} size={58} avatar={avatar} />
|
||||||
)}
|
)}
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</Animated.View>
|
</View>
|
||||||
<View style={{flex: 1}}>
|
<View style={{flex: 1}}>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<LoadingPlaceholder
|
<LoadingPlaceholder
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import React, {useRef} from 'react'
|
import React, {useRef} from 'react'
|
||||||
import {DimensionValue, Pressable, View} from 'react-native'
|
import {DimensionValue, Pressable, View} from 'react-native'
|
||||||
import Animated, {AnimatedRef, useAnimatedRef} from 'react-native-reanimated'
|
|
||||||
import {Image} from 'expo-image'
|
import {Image} from 'expo-image'
|
||||||
import {AppBskyEmbedImages} from '@atproto/api'
|
import {AppBskyEmbedImages} from '@atproto/api'
|
||||||
import {msg} from '@lingui/macro'
|
import {msg} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
|
import {HandleRef, useHandleRef} from '#/lib/hooks/useHandleRef'
|
||||||
import type {Dimensions} from '#/lib/media/types'
|
import type {Dimensions} from '#/lib/media/types'
|
||||||
import {isNative} from '#/platform/detection'
|
import {isNative} from '#/platform/detection'
|
||||||
import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge'
|
import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge'
|
||||||
@@ -68,17 +68,14 @@ export function AutoSizedImage({
|
|||||||
image: AppBskyEmbedImages.ViewImage
|
image: AppBskyEmbedImages.ViewImage
|
||||||
crop?: 'none' | 'square' | 'constrained'
|
crop?: 'none' | 'square' | 'constrained'
|
||||||
hideBadge?: boolean
|
hideBadge?: boolean
|
||||||
onPress?: (
|
onPress?: (containerRef: HandleRef, fetchedDims: Dimensions | null) => void
|
||||||
containerRef: AnimatedRef<React.Component<{}, {}, any>>,
|
|
||||||
fetchedDims: Dimensions | null,
|
|
||||||
) => void
|
|
||||||
onLongPress?: () => void
|
onLongPress?: () => void
|
||||||
onPressIn?: () => void
|
onPressIn?: () => void
|
||||||
}) {
|
}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const largeAlt = useLargeAltBadgeEnabled()
|
const largeAlt = useLargeAltBadgeEnabled()
|
||||||
const containerRef = useAnimatedRef()
|
const containerRef = useHandleRef()
|
||||||
const fetchedDimsRef = useRef<{width: number; height: number} | null>(null)
|
const fetchedDimsRef = useRef<{width: number; height: number} | null>(null)
|
||||||
|
|
||||||
let aspectRatio: number | undefined
|
let aspectRatio: number | undefined
|
||||||
@@ -109,7 +106,7 @@ export function AutoSizedImage({
|
|||||||
const hasAlt = !!image.alt
|
const hasAlt = !!image.alt
|
||||||
|
|
||||||
const contents = (
|
const contents = (
|
||||||
<Animated.View ref={containerRef} collapsable={false} style={{flex: 1}}>
|
<View ref={containerRef} collapsable={false} style={{flex: 1}}>
|
||||||
<Image
|
<Image
|
||||||
style={[a.w_full, a.h_full]}
|
style={[a.w_full, a.h_full]}
|
||||||
source={image.thumb}
|
source={image.thumb}
|
||||||
@@ -188,7 +185,7 @@ export function AutoSizedImage({
|
|||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
) : null}
|
) : null}
|
||||||
</Animated.View>
|
</View>
|
||||||
)
|
)
|
||||||
|
|
||||||
if (cropDisabled) {
|
if (cropDisabled) {
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {Pressable, StyleProp, View, ViewStyle} from 'react-native'
|
import {Pressable, StyleProp, View, ViewStyle} from 'react-native'
|
||||||
import Animated, {AnimatedRef} from 'react-native-reanimated'
|
|
||||||
import {Image, ImageStyle} from 'expo-image'
|
import {Image, ImageStyle} from 'expo-image'
|
||||||
import {AppBskyEmbedImages} from '@atproto/api'
|
import {AppBskyEmbedImages} from '@atproto/api'
|
||||||
import {msg} from '@lingui/macro'
|
import {msg} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
|
import {HandleRef} from '#/lib/hooks/useHandleRef'
|
||||||
import {Dimensions} from '#/lib/media/types'
|
import {Dimensions} from '#/lib/media/types'
|
||||||
import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge'
|
import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge'
|
||||||
import {PostEmbedViewContext} from '#/view/com/util/post-embeds/types'
|
import {PostEmbedViewContext} from '#/view/com/util/post-embeds/types'
|
||||||
@@ -20,7 +20,7 @@ interface Props {
|
|||||||
index: number
|
index: number
|
||||||
onPress?: (
|
onPress?: (
|
||||||
index: number,
|
index: number,
|
||||||
containerRefs: AnimatedRef<React.Component<{}, {}, any>>[],
|
containerRefs: HandleRef[],
|
||||||
fetchedDims: (Dimensions | null)[],
|
fetchedDims: (Dimensions | null)[],
|
||||||
) => void
|
) => void
|
||||||
onLongPress?: EventFunction
|
onLongPress?: EventFunction
|
||||||
@@ -28,7 +28,7 @@ interface Props {
|
|||||||
imageStyle?: StyleProp<ImageStyle>
|
imageStyle?: StyleProp<ImageStyle>
|
||||||
viewContext?: PostEmbedViewContext
|
viewContext?: PostEmbedViewContext
|
||||||
insetBorderStyle?: StyleProp<ViewStyle>
|
insetBorderStyle?: StyleProp<ViewStyle>
|
||||||
containerRefs: AnimatedRef<React.Component<{}, {}, any>>[]
|
containerRefs: HandleRef[]
|
||||||
thumbDimsRef: React.MutableRefObject<(Dimensions | null)[]>
|
thumbDimsRef: React.MutableRefObject<(Dimensions | null)[]>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,10 +52,7 @@ export function GalleryItem({
|
|||||||
const hideBadges =
|
const hideBadges =
|
||||||
viewContext === PostEmbedViewContext.FeedEmbedRecordWithMedia
|
viewContext === PostEmbedViewContext.FeedEmbedRecordWithMedia
|
||||||
return (
|
return (
|
||||||
<Animated.View
|
<View style={a.flex_1} ref={containerRefs[index]} collapsable={false}>
|
||||||
style={a.flex_1}
|
|
||||||
ref={containerRefs[index]}
|
|
||||||
collapsable={false}>
|
|
||||||
<Pressable
|
<Pressable
|
||||||
onPress={
|
onPress={
|
||||||
onPress
|
onPress
|
||||||
@@ -118,6 +115,6 @@ export function GalleryItem({
|
|||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
) : null}
|
) : null}
|
||||||
</Animated.View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
|
import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
|
||||||
import {AnimatedRef, useAnimatedRef} from 'react-native-reanimated'
|
|
||||||
import {AppBskyEmbedImages} from '@atproto/api'
|
import {AppBskyEmbedImages} from '@atproto/api'
|
||||||
|
|
||||||
|
import {HandleRef, useHandleRef} from '#/lib/hooks/useHandleRef'
|
||||||
import {PostEmbedViewContext} from '#/view/com/util/post-embeds/types'
|
import {PostEmbedViewContext} from '#/view/com/util/post-embeds/types'
|
||||||
import {atoms as a, useBreakpoints} from '#/alf'
|
import {atoms as a, useBreakpoints} from '#/alf'
|
||||||
import {Dimensions} from '../../lightbox/ImageViewing/@types'
|
import {Dimensions} from '../../lightbox/ImageViewing/@types'
|
||||||
@@ -12,7 +12,7 @@ interface ImageLayoutGridProps {
|
|||||||
images: AppBskyEmbedImages.ViewImage[]
|
images: AppBskyEmbedImages.ViewImage[]
|
||||||
onPress?: (
|
onPress?: (
|
||||||
index: number,
|
index: number,
|
||||||
containerRefs: AnimatedRef<React.Component<{}, {}, any>>[],
|
containerRefs: HandleRef[],
|
||||||
fetchedDims: (Dimensions | null)[],
|
fetchedDims: (Dimensions | null)[],
|
||||||
) => void
|
) => void
|
||||||
onLongPress?: (index: number) => void
|
onLongPress?: (index: number) => void
|
||||||
@@ -43,7 +43,7 @@ interface ImageLayoutGridInnerProps {
|
|||||||
images: AppBskyEmbedImages.ViewImage[]
|
images: AppBskyEmbedImages.ViewImage[]
|
||||||
onPress?: (
|
onPress?: (
|
||||||
index: number,
|
index: number,
|
||||||
containerRefs: AnimatedRef<React.Component<{}, {}, any>>[],
|
containerRefs: HandleRef[],
|
||||||
fetchedDims: (Dimensions | null)[],
|
fetchedDims: (Dimensions | null)[],
|
||||||
) => void
|
) => void
|
||||||
onLongPress?: (index: number) => void
|
onLongPress?: (index: number) => void
|
||||||
@@ -56,10 +56,10 @@ function ImageLayoutGridInner(props: ImageLayoutGridInnerProps) {
|
|||||||
const gap = props.gap
|
const gap = props.gap
|
||||||
const count = props.images.length
|
const count = props.images.length
|
||||||
|
|
||||||
const containerRef1 = useAnimatedRef()
|
const containerRef1 = useHandleRef()
|
||||||
const containerRef2 = useAnimatedRef()
|
const containerRef2 = useHandleRef()
|
||||||
const containerRef3 = useAnimatedRef()
|
const containerRef3 = useHandleRef()
|
||||||
const containerRef4 = useAnimatedRef()
|
const containerRef4 = useHandleRef()
|
||||||
const thumbDimsRef = React.useRef<(Dimensions | null)[]>([])
|
const thumbDimsRef = React.useRef<(Dimensions | null)[]>([])
|
||||||
|
|
||||||
switch (count) {
|
switch (count) {
|
||||||
|
|||||||
@@ -6,13 +6,7 @@ import {
|
|||||||
View,
|
View,
|
||||||
ViewStyle,
|
ViewStyle,
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
import {
|
import {MeasuredDimensions, runOnJS, runOnUI} from 'react-native-reanimated'
|
||||||
AnimatedRef,
|
|
||||||
measure,
|
|
||||||
MeasuredDimensions,
|
|
||||||
runOnJS,
|
|
||||||
runOnUI,
|
|
||||||
} from 'react-native-reanimated'
|
|
||||||
import {Image} from 'expo-image'
|
import {Image} from 'expo-image'
|
||||||
import {
|
import {
|
||||||
AppBskyEmbedExternal,
|
AppBskyEmbedExternal,
|
||||||
@@ -27,6 +21,7 @@ import {
|
|||||||
ModerationDecision,
|
ModerationDecision,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
|
|
||||||
|
import {HandleRef, measureHandle} from '#/lib/hooks/useHandleRef'
|
||||||
import {usePalette} from '#/lib/hooks/usePalette'
|
import {usePalette} from '#/lib/hooks/usePalette'
|
||||||
import {useLightboxControls} from '#/state/lightbox'
|
import {useLightboxControls} from '#/state/lightbox'
|
||||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||||
@@ -163,12 +158,13 @@ export function PostEmbeds({
|
|||||||
}
|
}
|
||||||
const onPress = (
|
const onPress = (
|
||||||
index: number,
|
index: number,
|
||||||
refs: AnimatedRef<React.Component<{}, {}, any>>[],
|
refs: HandleRef[],
|
||||||
fetchedDims: (Dimensions | null)[],
|
fetchedDims: (Dimensions | null)[],
|
||||||
) => {
|
) => {
|
||||||
|
const handles = refs.map(r => r.current)
|
||||||
runOnUI(() => {
|
runOnUI(() => {
|
||||||
'worklet'
|
'worklet'
|
||||||
const rects = refs.map(ref => (ref ? measure(ref) : null))
|
const rects = handles.map(measureHandle)
|
||||||
runOnJS(_openLightbox)(index, rects, fetchedDims)
|
runOnJS(_openLightbox)(index, rects, fetchedDims)
|
||||||
})()
|
})()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user