mirror of
https://github.com/maelgangloff/domain-watchdog.git
synced 2025-12-18 02:05:36 +00:00
Merge branch 'feature/various-ux' into develop
This commit is contained in:
commit
d229dd5904
@ -1,4 +1,4 @@
|
||||
import {Button, ConfigProvider, Drawer, Flex, FloatButton, Layout, theme, Tooltip, Typography} from 'antd'
|
||||
import {Button, ConfigProvider, Drawer, Flex, Layout, theme, Typography} from 'antd'
|
||||
import {Link, Navigate, Route, Routes, useLocation, useNavigate} from 'react-router-dom'
|
||||
import TextPage from './pages/TextPage'
|
||||
import DomainSearchPage from './pages/search/DomainSearchPage'
|
||||
@ -16,7 +16,7 @@ import NotFoundPage from './pages/NotFoundPage'
|
||||
import useBreakpoint from './hooks/useBreakpoint'
|
||||
import {Sider} from './components/Sider'
|
||||
import {jt, t} from 'ttag'
|
||||
import {BugOutlined, InfoCircleOutlined, MergeOutlined, MenuOutlined} from '@ant-design/icons'
|
||||
import {MenuOutlined} from '@ant-design/icons'
|
||||
import TrackedDomainPage from './pages/tracking/TrackedDomainPage'
|
||||
import IcannRegistrarPage from "./pages/infrastructure/IcannRegistrarPage"
|
||||
|
||||
@ -165,9 +165,24 @@ export default function App(): React.ReactElement {
|
||||
target='_blank'
|
||||
href='https://github.com/maelgangloff/domain-watchdog/wiki'
|
||||
>
|
||||
<Button
|
||||
type='text'
|
||||
>{t`Documentation`}
|
||||
<Button type='text'>
|
||||
{t`Documentation`}
|
||||
</Button>
|
||||
</Typography.Link>
|
||||
<Typography.Link
|
||||
target='_blank'
|
||||
href={PROJECT_LINK}
|
||||
>
|
||||
<Button type='text'>
|
||||
{t`Source code`}
|
||||
</Button>
|
||||
</Typography.Link>
|
||||
<Typography.Link
|
||||
target='_blank'
|
||||
href={PROJECT_LINK + '/issues'}
|
||||
>
|
||||
<Button type='text'>
|
||||
{t`Submit an issue`}
|
||||
</Button>
|
||||
</Typography.Link>
|
||||
</Flex>
|
||||
@ -176,22 +191,6 @@ export default function App(): React.ReactElement {
|
||||
</Typography.Paragraph>
|
||||
</Layout.Footer>
|
||||
</Layout>
|
||||
<FloatButton.Group
|
||||
trigger='hover'
|
||||
style={{
|
||||
position: 'fixed',
|
||||
insetInlineEnd: (100 - 40) / 2,
|
||||
bottom: 100 - 40 / 2
|
||||
}}
|
||||
icon={<InfoCircleOutlined/>}
|
||||
>
|
||||
<Tooltip title={t`Official git repository`} placement='left'>
|
||||
<FloatButton icon={<MergeOutlined/>} target='_blank' href={PROJECT_LINK}/>
|
||||
</Tooltip>
|
||||
<Tooltip title={t`Submit an issue`} placement='left'>
|
||||
<FloatButton icon={<BugOutlined/>} target='_blank' href={PROJECT_LINK + '/issues'}/>
|
||||
</Tooltip>
|
||||
</FloatButton.Group>
|
||||
</Layout>
|
||||
</AuthenticatedContext.Provider>
|
||||
</ConfigProvider>
|
||||
|
||||
101
assets/components/tracking/watchlist/WatchlistSelectionModal.tsx
Normal file
101
assets/components/tracking/watchlist/WatchlistSelectionModal.tsx
Normal file
@ -0,0 +1,101 @@
|
||||
import React, {useEffect, useState} from "react"
|
||||
import type {ModalProps} from "antd"
|
||||
import {Tag, Tooltip} from "antd"
|
||||
import {Flex, Modal, Select, Typography} from "antd"
|
||||
import type {Domain, Watchlist} from "../../../utils/api"
|
||||
import {getWatchlists} from "../../../utils/api"
|
||||
import {t} from 'ttag'
|
||||
import {DomainToTag} from "../../../utils/functions/DomainToTag"
|
||||
import {EllipsisOutlined} from '@ant-design/icons'
|
||||
|
||||
const MAX_DOMAIN_TAGS = 25
|
||||
|
||||
function WatchlistOption({watchlist}: {watchlist: Watchlist}) {
|
||||
let domains = watchlist.domains
|
||||
let rest: Domain[]|undefined = undefined
|
||||
|
||||
if (domains.length > MAX_DOMAIN_TAGS) {
|
||||
rest = domains.slice(MAX_DOMAIN_TAGS)
|
||||
domains = domains.slice(0, MAX_DOMAIN_TAGS)
|
||||
}
|
||||
|
||||
return <Flex vertical>
|
||||
<Typography.Text strong>{watchlist.name}</Typography.Text>
|
||||
<Flex wrap gap='4px'>
|
||||
{domains.map(d => <DomainToTag link={false} domain={d} key={d.ldhName} />)}
|
||||
{rest
|
||||
&& <Tooltip title={rest.map(d => <DomainToTag link={false} domain={d} key={d.ldhName} />)}>
|
||||
<Tag icon={<EllipsisOutlined/>} color='processing'>
|
||||
{t`${rest.length} more`}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
}
|
||||
</Flex>
|
||||
</Flex>
|
||||
}
|
||||
|
||||
interface WatchlistSelectionModalProps {
|
||||
onFinish: (watchlist: Watchlist) => Promise<void>|void
|
||||
description?: string
|
||||
open?: boolean
|
||||
modalProps?: Partial<ModalProps>
|
||||
}
|
||||
|
||||
export default function WatchlistSelectionModal(props: WatchlistSelectionModalProps) {
|
||||
const [watchlists, setWatchlists] = useState<Watchlist[] | undefined>()
|
||||
const [selectedWatchlist, setSelectedWatchlist] = useState<Watchlist | undefined>()
|
||||
const [validationLoading, setValidationLoading] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (props.open && !watchlists) {
|
||||
getWatchlists().then(list => setWatchlists(list["hydra:member"]))
|
||||
}
|
||||
}, [props.open])
|
||||
|
||||
const onFinish = () => {
|
||||
const promise = props.onFinish(selectedWatchlist as Watchlist)
|
||||
|
||||
if (promise) {
|
||||
setValidationLoading(true)
|
||||
promise.finally(() => {
|
||||
setSelectedWatchlist(undefined)
|
||||
setValidationLoading(false)
|
||||
})
|
||||
} else {
|
||||
setSelectedWatchlist(undefined)
|
||||
}
|
||||
}
|
||||
|
||||
return <Modal
|
||||
open={props.open}
|
||||
onOk={onFinish}
|
||||
okButtonProps={{
|
||||
disabled: !selectedWatchlist,
|
||||
loading: validationLoading,
|
||||
}}
|
||||
{...props.modalProps ?? {}}
|
||||
>
|
||||
<Flex vertical>
|
||||
<Typography.Paragraph>
|
||||
{
|
||||
props.description
|
||||
|| t`Select one of your available Watchlists`
|
||||
}
|
||||
</Typography.Paragraph>
|
||||
<Select
|
||||
placeholder={t`Watchlist`}
|
||||
style={{width: '100%'}}
|
||||
onChange={(_, option) => setSelectedWatchlist(option as Watchlist)}
|
||||
options={watchlists}
|
||||
value={selectedWatchlist?.token}
|
||||
fieldNames={{
|
||||
label: 'name',
|
||||
value: 'token',
|
||||
}}
|
||||
loading={!watchlists}
|
||||
status={selectedWatchlist ? '' : 'error'}
|
||||
optionRender={(watchlist) => <WatchlistOption watchlist={watchlist.data}/>}
|
||||
/>
|
||||
</Flex>
|
||||
</Modal>
|
||||
}
|
||||
@ -1,8 +1,10 @@
|
||||
import React, {useEffect, useState} from 'react'
|
||||
import type { FormProps} from 'antd'
|
||||
import {FloatButton} from 'antd'
|
||||
import {Empty, Flex, message, Skeleton} from 'antd'
|
||||
import type {Domain} from '../../utils/api'
|
||||
import { getDomain} from '../../utils/api'
|
||||
import type {Domain, Watchlist} from '../../utils/api'
|
||||
import {addDomainToWatchlist} from '../../utils/api'
|
||||
import {getDomain} from '../../utils/api'
|
||||
import type {AxiosError} from 'axios'
|
||||
import {t} from 'ttag'
|
||||
import type { FieldType} from '../../components/search/DomainSearchBar'
|
||||
@ -10,17 +12,19 @@ import {DomainSearchBar} from '../../components/search/DomainSearchBar'
|
||||
import {DomainResult} from '../../components/search/DomainResult'
|
||||
import {showErrorAPI} from '../../utils/functions/showErrorAPI'
|
||||
import {useNavigate, useParams} from 'react-router-dom'
|
||||
import {PlusOutlined} from '@ant-design/icons'
|
||||
import WatchlistSelectionModal from '../../components/tracking/watchlist/WatchlistSelectionModal'
|
||||
|
||||
export default function DomainSearchPage() {
|
||||
const {query} = useParams()
|
||||
const [domain, setDomain] = useState<Domain | null>()
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const domainLdhName = domain?.ldhName
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [addToWatchlistModal, setAddToWatchlistModal] = useState(false)
|
||||
|
||||
const [messageApi, contextHolder] = message.useMessage()
|
||||
const navigate = useNavigate()
|
||||
|
||||
|
||||
|
||||
const onFinish: FormProps<FieldType>['onFinish'] = (values) => {
|
||||
navigate('/search/domain/' + values.ldhName)
|
||||
|
||||
@ -41,7 +45,18 @@ export default function DomainSearchPage() {
|
||||
onFinish({ldhName: query, isRefreshForced: false})
|
||||
}, [])
|
||||
|
||||
return (
|
||||
const addToWatchlist = async (watchlist: Watchlist) => {
|
||||
await addDomainToWatchlist(watchlist, domain!.ldhName).then(() => {
|
||||
setAddToWatchlistModal(false)
|
||||
|
||||
const ldhName = domain?.ldhName
|
||||
messageApi.success(t`${ldhName} added to ${watchlist.name}`)
|
||||
}).catch((e: AxiosError) => {
|
||||
showErrorAPI(e, messageApi)
|
||||
})
|
||||
}
|
||||
|
||||
return <>
|
||||
<Flex gap='middle' align='center' justify='center' vertical>
|
||||
{contextHolder}
|
||||
<DomainSearchBar initialValue={query} onFinish={onFinish}/>
|
||||
@ -57,5 +72,29 @@ export default function DomainSearchPage() {
|
||||
}
|
||||
</Skeleton>
|
||||
</Flex>
|
||||
)
|
||||
{domain
|
||||
&& <FloatButton
|
||||
style={{
|
||||
position: 'fixed',
|
||||
insetInlineEnd: (100 - 40) / 2,
|
||||
bottom: 100 - 40 / 2
|
||||
}}
|
||||
tooltip={t`Add to Watchlist`}
|
||||
type="primary"
|
||||
icon={<PlusOutlined/>}
|
||||
onClick={() => setAddToWatchlistModal(true)}
|
||||
/>
|
||||
}
|
||||
<WatchlistSelectionModal
|
||||
open={addToWatchlistModal}
|
||||
onFinish={addToWatchlist}
|
||||
modalProps={{
|
||||
title: t`Add ${domainLdhName} to a Watchlist`,
|
||||
onCancel: () => setAddToWatchlistModal(false),
|
||||
onClose: () => setAddToWatchlistModal(false),
|
||||
cancelText: t`Cancel`,
|
||||
okText: t`Add`
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
|
||||
@ -44,6 +44,13 @@ export async function patchWatchlist(token: string, watchlist: Partial<Watchlist
|
||||
return response.data
|
||||
}
|
||||
|
||||
export async function addDomainToWatchlist(watchlist: Watchlist, ldhName: string) {
|
||||
const domains = watchlist.domains.map(d => '/api/domains/' + d.ldhName)
|
||||
domains.push('/api/domains/' + ldhName)
|
||||
|
||||
return patchWatchlist(watchlist.token, {domains})
|
||||
}
|
||||
|
||||
export async function deleteWatchlist(token: string): Promise<void> {
|
||||
await request({
|
||||
method: 'DELETE',
|
||||
|
||||
@ -6,32 +6,38 @@ import React from 'react'
|
||||
import type {Event} from "../api"
|
||||
import {t} from "ttag"
|
||||
|
||||
export function DomainToTag({domain}: { domain: { ldhName: string, deleted: boolean, status: string[], events?: Event[] } }) {
|
||||
return (
|
||||
<Link to={'/search/domain/' + domain.ldhName}>
|
||||
<Badge dot={domain.events?.find(e =>
|
||||
e.action === 'last changed' &&
|
||||
!e.deleted &&
|
||||
((new Date().getTime() - new Date(e.date).getTime()) < 7*24*60*60*1e3)
|
||||
) !== undefined} color='blue' title={t`The domain name was updated less than a week ago.`}>
|
||||
<Tag
|
||||
color={
|
||||
domain.deleted
|
||||
? 'magenta'
|
||||
: domain.status.includes('redemption period')
|
||||
? 'yellow'
|
||||
: domain.status.includes('pending delete') ? 'volcano' : 'default'
|
||||
}
|
||||
icon={
|
||||
domain.deleted
|
||||
? <DeleteOutlined/>
|
||||
: domain.status.includes('redemption period')
|
||||
? <ExclamationCircleOutlined/>
|
||||
: domain.status.includes('pending delete') ? <DeleteOutlined/> : null
|
||||
}
|
||||
>{punycode.toUnicode(domain.ldhName)}
|
||||
</Tag>
|
||||
</Badge>
|
||||
</Link>
|
||||
)
|
||||
export function DomainToTag({domain, link}: { domain: { ldhName: string, deleted: boolean, status: string[], events?: Event[] }, link?: boolean }) {
|
||||
const tag = <Badge dot={domain.events?.find(e =>
|
||||
e.action === 'last changed' &&
|
||||
!e.deleted &&
|
||||
((new Date().getTime() - new Date(e.date).getTime()) < 7*24*60*60*1e3)
|
||||
) !== undefined} color='blue' title={t`The domain name was updated less than a week ago.`}>
|
||||
<Tag
|
||||
color={
|
||||
domain.deleted
|
||||
? 'magenta'
|
||||
: domain.status.includes('redemption period')
|
||||
? 'yellow'
|
||||
: domain.status.includes('pending delete') ? 'volcano' : 'default'
|
||||
}
|
||||
icon={
|
||||
domain.deleted
|
||||
? <DeleteOutlined/>
|
||||
: domain.status.includes('redemption period')
|
||||
? <ExclamationCircleOutlined/>
|
||||
: domain.status.includes('pending delete') ? <DeleteOutlined/> : null
|
||||
}
|
||||
>{punycode.toUnicode(domain.ldhName)}
|
||||
</Tag>
|
||||
</Badge>
|
||||
|
||||
if (link ?? true) {
|
||||
return (
|
||||
<Link to={'/search/domain/' + domain.ldhName}>
|
||||
{tag}
|
||||
</Link>
|
||||
)
|
||||
} else {
|
||||
return tag
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,7 +11,6 @@ use App\Repository\DomainRepository;
|
||||
use App\Repository\WatchlistRepository;
|
||||
use App\Service\CalendarService;
|
||||
use App\Service\RDAPService;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Eluceo\iCal\Domain\Entity\Calendar;
|
||||
use Eluceo\iCal\Presentation\Component\Property;
|
||||
use Eluceo\iCal\Presentation\Component\Property\Value\TextValue;
|
||||
@ -36,23 +35,6 @@ class WatchlistController extends AbstractController
|
||||
) {
|
||||
}
|
||||
|
||||
#[Route(
|
||||
path: '/api/watchlists',
|
||||
name: 'watchlist_get_all_mine',
|
||||
defaults: [
|
||||
'_api_resource_class' => Watchlist::class,
|
||||
'_api_operation_name' => 'get_all_mine',
|
||||
],
|
||||
methods: ['GET']
|
||||
)]
|
||||
public function getWatchlists(): Collection
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = $this->getUser();
|
||||
|
||||
return $user->getWatchlists();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ParseException
|
||||
* @throws EofException
|
||||
|
||||
@ -10,6 +10,7 @@ use ApiPlatform\Metadata\Patch;
|
||||
use ApiPlatform\Metadata\Post;
|
||||
use ApiPlatform\Metadata\Put;
|
||||
use App\Repository\WatchlistRepository;
|
||||
use App\State\MyWatchlistsProvider;
|
||||
use App\State\WatchlistUpdateProcessor;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
@ -25,7 +26,6 @@ use Symfony\Component\Validator\Constraints as Assert;
|
||||
shortName: 'Watchlist',
|
||||
operations: [
|
||||
new GetCollection(
|
||||
routeName: 'watchlist_get_all_mine',
|
||||
normalizationContext: [
|
||||
'groups' => [
|
||||
'watchlist:list',
|
||||
@ -34,6 +34,7 @@ use Symfony\Component\Validator\Constraints as Assert;
|
||||
],
|
||||
],
|
||||
name: 'get_all_mine',
|
||||
provider: MyWatchlistsProvider::class,
|
||||
),
|
||||
new GetCollection(
|
||||
uriTemplate: '/tracked',
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Entity\Watchlist;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
@ -33,6 +34,24 @@ class WatchlistRepository extends ServiceEntityRepository
|
||||
->getQuery()->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Watchlist[]
|
||||
*/
|
||||
public function fetchWatchlistsForUser(User $user): array
|
||||
{
|
||||
return $this->createQueryBuilder('w')
|
||||
->addSelect('d')
|
||||
->addSelect('e')
|
||||
->addSelect('p')
|
||||
->leftJoin('w.domains', 'd')
|
||||
->leftJoin('d.events', 'e')
|
||||
->leftJoin('d.domainPurchases', 'p')
|
||||
->where('w.user = :user')
|
||||
->setParameter('user', $user)
|
||||
->getQuery()
|
||||
->getResult();
|
||||
}
|
||||
|
||||
// /**
|
||||
// * @return Watchlist[] Returns an array of Watchlist objects
|
||||
// */
|
||||
|
||||
24
src/State/MyWatchlistsProvider.php
Normal file
24
src/State/MyWatchlistsProvider.php
Normal file
@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\State;
|
||||
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProviderInterface;
|
||||
use App\Entity\User;
|
||||
use App\Repository\WatchlistRepository;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
|
||||
readonly class MyWatchlistsProvider implements ProviderInterface
|
||||
{
|
||||
public function __construct(private Security $security, private WatchlistRepository $watchlistRepository)
|
||||
{
|
||||
}
|
||||
|
||||
public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = $this->security->getUser();
|
||||
|
||||
return $this->watchlistRepository->fetchWatchlistsForUser($user);
|
||||
}
|
||||
}
|
||||
@ -15,25 +15,25 @@ msgstr ""
|
||||
msgid "FAQ"
|
||||
msgstr ""
|
||||
|
||||
#: assets/App.tsx:170
|
||||
#: assets/App.tsx:169
|
||||
msgid "Documentation"
|
||||
msgstr ""
|
||||
|
||||
#: assets/App.tsx:175
|
||||
#: assets/App.tsx:177
|
||||
msgid "Source code"
|
||||
msgstr ""
|
||||
|
||||
#: assets/App.tsx:185
|
||||
msgid "Submit an issue"
|
||||
msgstr ""
|
||||
|
||||
#: assets/App.tsx:190
|
||||
#, javascript-format
|
||||
msgid ""
|
||||
"${ ProjectLink } is an open source project distributed under the ${ "
|
||||
"LicenseLink } license."
|
||||
msgstr ""
|
||||
|
||||
#: assets/App.tsx:188
|
||||
msgid "Official git repository"
|
||||
msgstr ""
|
||||
|
||||
#: assets/App.tsx:191
|
||||
msgid "Submit an issue"
|
||||
msgstr ""
|
||||
|
||||
#: assets/components/LoginForm.tsx:53
|
||||
#: assets/components/RegisterForm.tsx:37
|
||||
msgid "Email address"
|
||||
@ -480,6 +480,7 @@ msgid "This Watchlist is not linked to a Connector."
|
||||
msgstr ""
|
||||
|
||||
#: assets/components/tracking/watchlist/WatchlistCard.tsx:59
|
||||
#: assets/components/tracking/watchlist/WatchlistSelectionModal.tsx:86
|
||||
msgid "Watchlist"
|
||||
msgstr ""
|
||||
|
||||
@ -564,6 +565,15 @@ msgstr ""
|
||||
msgid "Reset"
|
||||
msgstr ""
|
||||
|
||||
#: assets/components/tracking/watchlist/WatchlistSelectionModal.tsx:29
|
||||
#, javascript-format
|
||||
msgid "${ rest.length } more"
|
||||
msgstr ""
|
||||
|
||||
#: assets/components/tracking/watchlist/WatchlistSelectionModal.tsx:82
|
||||
msgid "Select one of your available Watchlists"
|
||||
msgstr ""
|
||||
|
||||
#: assets/pages/infrastructure/IcannRegistrarPage.tsx:45
|
||||
msgid "ID"
|
||||
msgstr ""
|
||||
@ -688,16 +698,38 @@ msgstr ""
|
||||
msgid "Sorry, the page you visited does not exist."
|
||||
msgstr ""
|
||||
|
||||
#: assets/pages/search/DomainSearchPage.tsx:32
|
||||
#: assets/pages/search/DomainSearchPage.tsx:36
|
||||
msgid "Found !"
|
||||
msgstr ""
|
||||
|
||||
#: assets/pages/search/DomainSearchPage.tsx:55
|
||||
#: assets/pages/search/DomainSearchPage.tsx:53
|
||||
#, javascript-format
|
||||
msgid "${ ldhName } added to ${ watchlist.name }"
|
||||
msgstr ""
|
||||
|
||||
#: assets/pages/search/DomainSearchPage.tsx:70
|
||||
msgid ""
|
||||
"Although the domain exists in my database, it has been deleted from the "
|
||||
"WHOIS by its registrar."
|
||||
msgstr ""
|
||||
|
||||
#: assets/pages/search/DomainSearchPage.tsx:82
|
||||
msgid "Add to Watchlist"
|
||||
msgstr ""
|
||||
|
||||
#: assets/pages/search/DomainSearchPage.tsx:92
|
||||
#, javascript-format
|
||||
msgid "Add ${ domainLdhName } to a Watchlist"
|
||||
msgstr ""
|
||||
|
||||
#: assets/pages/search/DomainSearchPage.tsx:95
|
||||
msgid "Cancel"
|
||||
msgstr ""
|
||||
|
||||
#: assets/pages/search/DomainSearchPage.tsx:96
|
||||
msgid "Add"
|
||||
msgstr ""
|
||||
|
||||
#: assets/pages/StatisticsPage.tsx:33
|
||||
msgid "RDAP queries"
|
||||
msgstr ""
|
||||
@ -760,7 +792,7 @@ msgstr ""
|
||||
msgid "Roles"
|
||||
msgstr ""
|
||||
|
||||
#: assets/utils/functions/DomainToTag.tsx:16
|
||||
#: assets/utils/functions/DomainToTag.tsx:14
|
||||
msgid "The domain name was updated less than a week ago."
|
||||
msgstr ""
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user