[SDK] Remove @atproto/api (#11386)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
import {useCallback, useState} from 'react'
|
||||
import {type StyleProp, View, type ViewStyle} from 'react-native'
|
||||
import {ToolsOzoneReportDefs} from '@atproto/api'
|
||||
import {type DidString} from '@atproto/syntax'
|
||||
import {Trans, useLingui} from '@lingui/react/macro'
|
||||
import {useMutation} from '@tanstack/react-query'
|
||||
@@ -15,7 +14,7 @@ import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons
|
||||
import {Loader} from '#/components/Loader'
|
||||
import * as Toast from '#/components/Toast'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {com} from '#/lexicons'
|
||||
import {com, tools} from '#/lexicons'
|
||||
|
||||
export function ChatDisabled({
|
||||
shape = 'pill',
|
||||
@@ -104,7 +103,7 @@ function DialogInner() {
|
||||
await client.call(
|
||||
com.atproto.moderation.createReport,
|
||||
{
|
||||
reasonType: ToolsOzoneReportDefs.REASONAPPEAL,
|
||||
reasonType: tools.ozone.report.defs.reasonAppeal.value,
|
||||
subject: {
|
||||
$type: 'com.atproto.admin.defs#repoRef',
|
||||
// the persisted account did is already resolved
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {useCallback, useMemo, useState} from 'react'
|
||||
import {type GestureResponderEvent, View} from 'react-native'
|
||||
import {ChatBskyConvoDefs} from '@atproto/api'
|
||||
import {
|
||||
moderateProfile,
|
||||
type ModerationDecision,
|
||||
type ModerationOpts,
|
||||
} from '@bsky.app/sdk/moderation'
|
||||
@@ -12,7 +12,6 @@ import {useQueryClient} from '@tanstack/react-query'
|
||||
import {GestureActionView} from '#/lib/custom-animations/GestureActionView'
|
||||
import {useHaptics} from '#/lib/haptics'
|
||||
import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
|
||||
import {moderateProfile} from '#/lib/moderation/subjects'
|
||||
import {decrementBadgeCount} from '#/lib/notifications/notifications'
|
||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||
import {
|
||||
@@ -53,7 +52,8 @@ import {ProfileBadges} from '#/components/ProfileBadges'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {useAnalytics} from '#/analytics'
|
||||
import {IS_NATIVE} from '#/env'
|
||||
import type * as bsky from '#/types/bsky'
|
||||
import {chat} from '#/lexicons'
|
||||
import * as bsky from '#/types/bsky'
|
||||
import {useIsWithinSplitView} from './splitView/context'
|
||||
|
||||
export const ChatListItemPortal = createPortalGroup()
|
||||
@@ -64,7 +64,7 @@ export function ChatListItem({
|
||||
selected = false,
|
||||
children,
|
||||
}: {
|
||||
convo: ChatBskyConvoDefs.ConvoView
|
||||
convo: chat.bsky.convo.defs.ConvoView
|
||||
showMenu?: boolean
|
||||
selected?: boolean
|
||||
children?: React.ReactNode
|
||||
@@ -314,7 +314,12 @@ function BaseChatItem({
|
||||
let lastMessageSentAt: string | null = null
|
||||
|
||||
// Deleted message
|
||||
if (ChatBskyConvoDefs.isDeletedMessageView(convo.view.lastMessage)) {
|
||||
if (
|
||||
bsky.isType(
|
||||
chat.bsky.convo.defs.deletedMessageView,
|
||||
convo.view.lastMessage,
|
||||
)
|
||||
) {
|
||||
lastMessageSentAt = convo.view.lastMessage.sentAt
|
||||
|
||||
lastMessage = isDeletedAccount
|
||||
@@ -323,7 +328,7 @@ function BaseChatItem({
|
||||
}
|
||||
|
||||
// Message
|
||||
if (ChatBskyConvoDefs.isMessageView(convo.view.lastMessage)) {
|
||||
if (bsky.isType(chat.bsky.convo.defs.messageView, convo.view.lastMessage)) {
|
||||
const info = getMessageInfo({
|
||||
convo: convo.view,
|
||||
currentAccountDid: currentAccount?.did,
|
||||
@@ -339,7 +344,12 @@ function BaseChatItem({
|
||||
}
|
||||
|
||||
// Reaction
|
||||
if (ChatBskyConvoDefs.isMessageAndReactionView(convo.view.lastReaction)) {
|
||||
if (
|
||||
bsky.isType(
|
||||
chat.bsky.convo.defs.messageAndReactionView,
|
||||
convo.view.lastReaction,
|
||||
)
|
||||
) {
|
||||
const info = getReactionInfo({
|
||||
convo: convo.view,
|
||||
currentAccountDid: currentAccount?.did,
|
||||
@@ -358,7 +368,12 @@ function BaseChatItem({
|
||||
}
|
||||
|
||||
// System message
|
||||
if (ChatBskyConvoDefs.isSystemMessageView(convo.view.lastMessage)) {
|
||||
if (
|
||||
bsky.isType(
|
||||
chat.bsky.convo.defs.systemMessageView,
|
||||
convo.view.lastMessage,
|
||||
)
|
||||
) {
|
||||
const info = getSystemMessageInfo(
|
||||
convo.view.lastMessage.data,
|
||||
new Map(convo.view.members.map(m => [m.did, m])),
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import {useCallback, useMemo} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {LinearGradient} from 'expo-linear-gradient'
|
||||
import {moderateProfile} from '@bsky.app/sdk/moderation'
|
||||
import {Trans, useLingui} from '@lingui/react/macro'
|
||||
|
||||
import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
|
||||
import {moderateProfile} from '#/lib/moderation/subjects'
|
||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||
import {useProfileShadow} from '#/state/cache/profile-shadow'
|
||||
import {type ActiveConvoStates} from '#/state/messages/convo'
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {View} from 'react-native'
|
||||
import {type ChatBskyConvoDefs} from '@atproto/api'
|
||||
import {Trans} from '@lingui/react/macro'
|
||||
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
@@ -8,13 +7,14 @@ import {atoms as a, tokens} from '#/alf'
|
||||
import {parseConvoView} from '#/components/dms/util'
|
||||
import {KnownFollowers} from '#/components/KnownFollowers'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {type chat} from '#/lexicons'
|
||||
import {ChatListItem, ChatListItemPortal} from './ChatListItem'
|
||||
import {AcceptChatButton, DeleteChatButton, RejectMenu} from './RequestButtons'
|
||||
|
||||
export function IncomingRequestListItem({
|
||||
convo: convoView,
|
||||
}: {
|
||||
convo: ChatBskyConvoDefs.ConvoView
|
||||
convo: chat.bsky.convo.defs.ConvoView
|
||||
}) {
|
||||
const {currentAccount} = useSession()
|
||||
const moderationOpts = useModerationOpts()
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import {useState} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {Image} from 'expo-image'
|
||||
import {type ChatBskyGroupDefs} from '@atproto/api'
|
||||
import {type ModerationOpts} from '@bsky.app/sdk/moderation'
|
||||
import {moderateProfile, type ModerationOpts} from '@bsky.app/sdk/moderation'
|
||||
import {Plural, Trans, useLingui} from '@lingui/react/macro'
|
||||
|
||||
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
|
||||
import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
|
||||
import {moderateProfile} from '#/lib/moderation/subjects'
|
||||
import {shareUrl} from '#/lib/sharing'
|
||||
import {useCreateJoinLink} from '#/state/queries/messages/create-join-link'
|
||||
import {useDisableJoinLink} from '#/state/queries/messages/disable-join-link'
|
||||
@@ -35,6 +33,7 @@ import * as Toast from '#/components/Toast'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {useAnalytics} from '#/analytics'
|
||||
import {IS_WEB} from '#/env'
|
||||
import {type chat} from '#/lexicons'
|
||||
import {CopyTextButton} from './CopyTextButton'
|
||||
import {EditTextButton} from './EditTextButton'
|
||||
|
||||
@@ -560,13 +559,13 @@ export function InviteLinkDialog({
|
||||
)
|
||||
}
|
||||
|
||||
function joinLinkToKey(joinLink: ChatBskyGroupDefs.JoinLinkView): string {
|
||||
function joinLinkToKey(joinLink: chat.bsky.group.defs.JoinLinkView): string {
|
||||
return `${joinLink.joinRule}${joinLink.requireApproval ? ':requireApproval' : ''}`
|
||||
}
|
||||
|
||||
function keyToJoinLink(
|
||||
key: string,
|
||||
): Pick<ChatBskyGroupDefs.JoinLinkView, 'joinRule' | 'requireApproval'> {
|
||||
): Pick<chat.bsky.group.defs.JoinLinkView, 'joinRule' | 'requireApproval'> {
|
||||
const [joinRule, requireApproval] = key.split(':')
|
||||
return {
|
||||
joinRule,
|
||||
|
||||
@@ -13,7 +13,7 @@ import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||
import {scheduleOnRN} from 'react-native-worklets'
|
||||
import {GlassContainer} from 'expo-glass-effect'
|
||||
import {LinearGradient} from 'expo-linear-gradient'
|
||||
import {type $Typed, type ChatBskyConvoDefs} from '@atproto/api'
|
||||
import {type $Typed} from '@atproto/lex'
|
||||
import {ScrollEdgeEffect} from '@bsky.app/expo-scroll-edge-effect'
|
||||
import {useLingui} from '@lingui/react/macro'
|
||||
import {countGraphemes} from 'unicode-segmenter/grapheme'
|
||||
@@ -37,6 +37,7 @@ import {PaperPlaneVertical_Filled_Stroke2_Corner1_Rounded as PaperPlaneIcon} fro
|
||||
import {Loader} from '#/components/Loader'
|
||||
import * as Toast from '#/components/Toast'
|
||||
import {IS_ANDROID, IS_IOS, IS_LIQUID_GLASS, IS_NATIVE, IS_WEB} from '#/env'
|
||||
import {type chat} from '#/lexicons'
|
||||
import {type MessageEmbedState} from './MessageInputEmbed'
|
||||
|
||||
const MIN_HEIGHT = 40
|
||||
@@ -53,7 +54,7 @@ export function MessageComposer({
|
||||
onSendMessage: (
|
||||
message: string,
|
||||
embed?: MessageEmbedState,
|
||||
replyTo?: $Typed<ChatBskyConvoDefs.MessageView>,
|
||||
replyTo?: $Typed<chat.bsky.convo.defs.MessageView>,
|
||||
) => void
|
||||
messageEmbed: MessageEmbedState | undefined
|
||||
setEmbed: (embedUrl: string | undefined) => void
|
||||
@@ -106,7 +107,7 @@ export function MessageComposer({
|
||||
const onSubmit = (
|
||||
message: string,
|
||||
embed: MessageEmbedState | undefined,
|
||||
replyTo: ChatBskyConvoDefs.MessageView | null,
|
||||
replyTo: chat.bsky.convo.defs.MessageView | null,
|
||||
) => {
|
||||
if (!editable) return
|
||||
if (!embed && message.trim() === '') return
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
import {useCallback, useEffect, useMemo, useState} from 'react'
|
||||
import {LayoutAnimation, View} from 'react-native'
|
||||
import {AppBskyFeedPost, AtUri} from '@atproto/api'
|
||||
import {AtUri} from '@atproto/syntax'
|
||||
import {moderatePost} from '@bsky.app/sdk/moderation'
|
||||
import {RichText as RichTextAPI} from '@bsky.app/sdk/richtext'
|
||||
import {Trans, useLingui} from '@lingui/react/macro'
|
||||
import {type RouteProp, useNavigation, useRoute} from '@react-navigation/native'
|
||||
|
||||
import {HITSLOP_20} from '#/lib/constants'
|
||||
import {moderatePost} from '#/lib/moderation/subjects'
|
||||
import {makeProfileLink} from '#/lib/routes/links'
|
||||
import {
|
||||
type CommonNavigatorParams,
|
||||
type NavigationProp,
|
||||
} from '#/lib/routes/types'
|
||||
import {asSdkFacets} from '#/lib/strings/rich-text-helpers'
|
||||
import {
|
||||
convertBskyAppUrlIfNeeded,
|
||||
getChatInviteCodeFromUrl,
|
||||
@@ -164,17 +163,11 @@ function MessageInputPostEmbed({
|
||||
)
|
||||
|
||||
const {rt, record} = useMemo(() => {
|
||||
if (
|
||||
post &&
|
||||
bsky.dangerousIsType<AppBskyFeedPost.Record>(
|
||||
post.record,
|
||||
AppBskyFeedPost.isRecord,
|
||||
)
|
||||
) {
|
||||
if (post && bsky.isType(app.bsky.feed.post, post.record)) {
|
||||
return {
|
||||
rt: new RichTextAPI({
|
||||
text: post.record.text,
|
||||
facets: asSdkFacets(post.record.facets),
|
||||
facets: post.record.facets,
|
||||
}),
|
||||
record: post.record,
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {LayoutAnimation, View} from 'react-native'
|
||||
import {type ChatBskyConvoDefs} from '@atproto/api'
|
||||
import {useLingui} from '@lingui/react/macro'
|
||||
|
||||
import {HITSLOP_20} from '#/lib/constants'
|
||||
@@ -11,6 +10,7 @@ import {useMessageReplies} from '#/components/dms/MessageReplies'
|
||||
import {useReplyPreviewText} from '#/components/dms/replyPreview'
|
||||
import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {type chat} from '#/lexicons'
|
||||
|
||||
/**
|
||||
* The reply staged in the message composer. Renders a preview of the message
|
||||
@@ -29,7 +29,7 @@ export function MessageInputReply() {
|
||||
function MessageInputReplyInner({
|
||||
replyTo,
|
||||
}: {
|
||||
replyTo: ChatBskyConvoDefs.MessageView
|
||||
replyTo: chat.bsky.convo.defs.MessageView
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {t: l} = useLingui()
|
||||
|
||||
@@ -22,13 +22,7 @@ import Animated, {
|
||||
} from 'react-native-reanimated'
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||
import {scheduleOnRN} from 'react-native-worklets'
|
||||
import {
|
||||
type $Typed,
|
||||
type AppBskyEmbedRecord,
|
||||
ChatBskyConvoDefs,
|
||||
type ChatBskyEmbedJoinLink,
|
||||
ChatBskyGroupDefs,
|
||||
} from '@atproto/api'
|
||||
import {type $Typed} from '@atproto/lex'
|
||||
import {useScrollEdgeEffectRef} from '@bsky.app/expo-scroll-edge-effect'
|
||||
import {RichText} from '@bsky.app/sdk/richtext'
|
||||
|
||||
@@ -70,7 +64,7 @@ import {Loader} from '#/components/Loader'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {useAnalytics} from '#/analytics'
|
||||
import {IS_ANDROID, IS_NATIVE, IS_WEB} from '#/env'
|
||||
import {app, type chat, type com} from '#/lexicons'
|
||||
import {app, chat} from '#/lexicons'
|
||||
import * as bsky from '#/types/bsky'
|
||||
import {ChatStatusInfo} from './ChatStatusInfo'
|
||||
import {groupSystemMessages, type RenderItem} from './groupSystemMessages'
|
||||
@@ -117,8 +111,8 @@ function getNeighborMessage(
|
||||
neighbor.type === 'deleted-message'
|
||||
) {
|
||||
if (
|
||||
ChatBskyConvoDefs.isMessageView(neighbor.message) ||
|
||||
ChatBskyConvoDefs.isDeletedMessageView(neighbor.message)
|
||||
bsky.isType(chat.bsky.convo.defs.messageView, neighbor.message) ||
|
||||
bsky.isType(chat.bsky.convo.defs.deletedMessageView, neighbor.message)
|
||||
) {
|
||||
return neighbor.message
|
||||
}
|
||||
@@ -525,7 +519,7 @@ export function MessagesList({
|
||||
async (
|
||||
text: string,
|
||||
embedState?: MessageEmbedState,
|
||||
reply?: $Typed<ChatBskyConvoDefs.MessageView>,
|
||||
reply?: $Typed<chat.bsky.convo.defs.MessageView>,
|
||||
) => {
|
||||
let rt = new RichText({text: text.trimEnd()}, {cleanNewlines: true})
|
||||
|
||||
@@ -539,10 +533,10 @@ export function MessagesList({
|
||||
|
||||
let embed: chat.bsky.convo.defs.MessageInput['embed']
|
||||
let embedView:
|
||||
| $Typed<AppBskyEmbedRecord.View>
|
||||
| $Typed<ChatBskyEmbedJoinLink.View>
|
||||
| $Typed<app.bsky.embed.record.View>
|
||||
| $Typed<chat.bsky.embed.joinLink.View>
|
||||
| undefined
|
||||
let replyTo: ChatBskyConvoDefs.ReplyRef | undefined
|
||||
let replyTo: chat.bsky.convo.defs.ReplyRef | undefined
|
||||
|
||||
/**
|
||||
* Find the embedded link facet and, if it's at the start or end of the
|
||||
@@ -574,14 +568,13 @@ export function MessagesList({
|
||||
embed = {
|
||||
$type: 'app.bsky.embed.record',
|
||||
/*
|
||||
* `getPost` still returns an `@atproto/api` view, whose `uri` and
|
||||
* `cid` are plain strings rather than the branded syntax types
|
||||
* the lexicon input declares.
|
||||
* `getPost` hands back `uri` and `cid` as plain strings rather
|
||||
* than the branded syntax types the lexicon input declares.
|
||||
*/
|
||||
record: {
|
||||
uri: post.uri,
|
||||
cid: post.cid,
|
||||
} as com.atproto.repo.strongRef.Main,
|
||||
},
|
||||
}
|
||||
|
||||
embedView = {
|
||||
@@ -666,7 +659,10 @@ export function MessagesList({
|
||||
}
|
||||
if (
|
||||
embedView?.$type === 'chat.bsky.embed.joinLink#view' &&
|
||||
ChatBskyGroupDefs.isJoinLinkPreviewView(embedView.joinLinkPreview)
|
||||
bsky.isType(
|
||||
chat.bsky.group.defs.joinLinkPreviewView,
|
||||
embedView.joinLinkPreview,
|
||||
)
|
||||
) {
|
||||
ax.metric('groupchat:inviteLink:shared', {
|
||||
convoId: embedView.joinLinkPreview.convoId,
|
||||
@@ -904,7 +900,7 @@ function Composer({
|
||||
onSendMessage: (
|
||||
message: string,
|
||||
embed?: MessageEmbedState,
|
||||
replyTo?: $Typed<ChatBskyConvoDefs.MessageView>,
|
||||
replyTo?: $Typed<chat.bsky.convo.defs.MessageView>,
|
||||
) => Promise<void>
|
||||
messageEmbed: MessageEmbedState | undefined
|
||||
setEmbed: (embedUrl: string | undefined) => void
|
||||
@@ -914,7 +910,7 @@ function Composer({
|
||||
(
|
||||
message: string,
|
||||
embed?: MessageEmbedState,
|
||||
replyTo?: $Typed<ChatBskyConvoDefs.MessageView>,
|
||||
replyTo?: $Typed<chat.bsky.convo.defs.MessageView>,
|
||||
) => {
|
||||
void onSendMessage(message, embed, replyTo)
|
||||
},
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import {View} from 'react-native'
|
||||
import {moderateProfile} from '@bsky.app/sdk/moderation'
|
||||
import {Trans, useLingui} from '@lingui/react/macro'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
|
||||
import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
|
||||
import {moderateProfile} from '#/lib/moderation/subjects'
|
||||
import {type NavigationProp} from '#/lib/routes/types'
|
||||
import {isInvalidHandle, sanitizeHandle} from '#/lib/strings/handles'
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {View} from 'react-native'
|
||||
import {type ChatBskyGroupDefs} from '@atproto/api'
|
||||
import {Trans, useLingui} from '@lingui/react/macro'
|
||||
|
||||
import {isNetworkError} from '#/lib/strings/errors'
|
||||
@@ -17,7 +16,7 @@ import {chat} from '#/lexicons'
|
||||
export function OutgoingRequestListItem({
|
||||
convo: convoView,
|
||||
}: {
|
||||
convo: ChatBskyGroupDefs.JoinRequestConvoView
|
||||
convo: chat.bsky.group.defs.JoinRequestConvoView
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {t: l} = useLingui()
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {useCallback} from 'react'
|
||||
import {type ChatBskyActorDefs, type ChatBskyConvoDefs} from '@atproto/api'
|
||||
import {Trans, useLingui} from '@lingui/react/macro'
|
||||
import {StackActions, useNavigation} from '@react-navigation/native'
|
||||
import {useQueryClient} from '@tanstack/react-query'
|
||||
@@ -41,6 +40,7 @@ import {Loader} from '#/components/Loader'
|
||||
import * as Menu from '#/components/Menu'
|
||||
import {ReportDialog} from '#/components/moderation/ReportDialog'
|
||||
import * as Toast from '#/components/Toast'
|
||||
import {type chat} from '#/lexicons'
|
||||
|
||||
export function RejectMenu({
|
||||
convo,
|
||||
@@ -56,7 +56,7 @@ export function RejectMenu({
|
||||
label?: string
|
||||
icon?: boolean
|
||||
convo: ConvoWithDetails
|
||||
profile: ChatBskyActorDefs.ProfileViewBasic
|
||||
profile: chat.bsky.actor.defs.ProfileViewBasic
|
||||
showDeleteConvo?: boolean
|
||||
currentScreen: 'list' | 'conversation'
|
||||
}) {
|
||||
@@ -214,7 +214,7 @@ export function AcceptChatButton({
|
||||
}: Omit<ButtonProps, 'onPress' | 'children' | 'label'> & {
|
||||
label?: string
|
||||
icon?: boolean
|
||||
convo: ChatBskyConvoDefs.ConvoView
|
||||
convo: chat.bsky.convo.defs.ConvoView
|
||||
onAcceptConvo?: () => void
|
||||
currentScreen: 'list' | 'conversation'
|
||||
}) {
|
||||
@@ -301,7 +301,7 @@ export function DeleteChatButton({
|
||||
}: Omit<ButtonProps, 'children' | 'label'> & {
|
||||
label?: string
|
||||
icon?: boolean
|
||||
convo: ChatBskyConvoDefs.ConvoView
|
||||
convo: chat.bsky.convo.defs.ConvoView
|
||||
currentScreen: 'list' | 'conversation'
|
||||
}) {
|
||||
const {t: l} = useLingui()
|
||||
|
||||
Reference in New Issue
Block a user