diff --git a/assets/icons/circle_stroke2_corner0_rounded.svg b/assets/icons/circle_stroke2_corner0_rounded.svg
new file mode 100644
index 0000000000..98fe755b09
--- /dev/null
+++ b/assets/icons/circle_stroke2_corner0_rounded.svg
@@ -0,0 +1 @@
+
diff --git a/src/components/icons/Circle.tsx b/src/components/icons/Circle.tsx
new file mode 100644
index 0000000000..93d837119e
--- /dev/null
+++ b/src/components/icons/Circle.tsx
@@ -0,0 +1,5 @@
+import {createSinglePathSVG} from './TEMPLATE'
+
+export const Circle_Stroke2_Corner0_Rounded = createSinglePathSVG({
+ path: 'M12 4a8 8 0 1 0 0 16 8 8 0 0 0 0-16ZM2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12Z',
+})
diff --git a/src/lib/strings/handles.ts b/src/lib/strings/handles.ts
index 78a2e1a09a..7666bc7084 100644
--- a/src/lib/strings/handles.ts
+++ b/src/lib/strings/handles.ts
@@ -2,6 +2,8 @@
// https://github.com/bluesky-social/indigo/blob/main/atproto/syntax/handle.go#L10
import {forceLTR} from '#/lib/strings/bidi'
+export * from './reserved'
+
const VALIDATE_REGEX =
/^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/
@@ -34,7 +36,8 @@ export function sanitizeHandle(handle: string, prefix = ''): string {
export interface IsValidHandle {
handleChars: boolean
hyphenStartOrEnd: boolean
- frontLength: boolean
+ frontLengthLongEnough: boolean
+ frontLengthNotTooLong: boolean
totalLength: boolean
overall: boolean
}
@@ -50,7 +53,8 @@ export function validateServiceHandle(
handleChars:
!str || (VALIDATE_REGEX.test(fullHandle) && !str.includes('.')),
hyphenStartOrEnd: !str.startsWith('-') && !str.endsWith('-'),
- frontLength: str.length >= 3 && str.length <= MAX_SERVICE_HANDLE_LENGTH,
+ frontLengthLongEnough: str.length >= 3,
+ frontLengthNotTooLong: str.length <= MAX_SERVICE_HANDLE_LENGTH,
totalLength: fullHandle.length <= 253,
}
diff --git a/src/lib/strings/reserved.ts b/src/lib/strings/reserved.ts
new file mode 100644
index 0000000000..a0954ed35f
--- /dev/null
+++ b/src/lib/strings/reserved.ts
@@ -0,0 +1,1066 @@
+// reserved handles for Bluesky PBC's PDS implementation
+// copied from: https://github.com/bluesky-social/atproto/blob/main/packages/pds/src/handle/reserved.ts
+
+const atpSpecific = [
+ 'at',
+ 'atp',
+ 'plc',
+ 'pds',
+ 'did',
+ 'repo',
+ 'tid',
+ 'nsid',
+ 'xrpc',
+ 'lex',
+ 'lexicon',
+ 'bsky',
+ 'bluesky',
+ 'handle',
+]
+
+// naively pulled from: https://radwebhosting.com/client_area/knowledgebase/196/List-of-Banned-Subdomain-Prefixes.html
+const commonlyReserved = [
+ 'about',
+ 'abuse',
+ 'access',
+ 'account',
+ 'accounts',
+ 'acme',
+ 'activate',
+ 'activities',
+ 'activity',
+ 'ad',
+ 'add',
+ 'address',
+ 'adm',
+ 'admanager',
+ 'admin',
+ 'administration',
+ 'administrator',
+ 'administrators',
+ 'admins',
+ 'ads',
+ 'adsense',
+ 'adult',
+ 'advertising',
+ 'adwords',
+ 'affiliate',
+ 'affiliatepage',
+ 'affiliates',
+ 'afp',
+ 'ajax',
+ 'all',
+ 'alpha',
+ 'analysis',
+ 'analytics',
+ 'android',
+ 'anon',
+ 'anonymous',
+ 'answer',
+ 'answers',
+ 'ap',
+ 'api',
+ 'apis',
+ 'app',
+ 'appengine',
+ 'appnews',
+ 'apps',
+ 'archive',
+ 'archives',
+ 'article',
+ 'asdf',
+ 'asset',
+ 'assets',
+ 'auth',
+ 'authentication',
+ 'avatar',
+ 'backup',
+ 'bank',
+ 'banner',
+ 'banners',
+ 'base',
+ 'beginners',
+ 'beta',
+ 'billing',
+ 'bin',
+ 'binaries',
+ 'binary',
+ 'blackberry',
+ 'blog',
+ 'blogs',
+ 'blogsearch',
+ 'board',
+ 'book',
+ 'bookmark',
+ 'bookmarks',
+ 'books',
+ 'bot',
+ 'bots',
+ 'bug',
+ 'bugs',
+ 'business',
+ 'buy',
+ 'buzz',
+ 'cache',
+ 'calendar',
+ 'call',
+ 'campaign',
+ 'cancel',
+ 'captcha',
+ 'career',
+ 'careers',
+ 'cart',
+ 'catalog',
+ 'catalogs',
+ 'categories',
+ 'category',
+ 'cdn',
+ 'cgi',
+ 'cgi-bin',
+ 'changelog',
+ 'chart',
+ 'charts',
+ 'chat',
+ 'check',
+ 'checked',
+ 'checking',
+ 'checkout',
+ 'client',
+ 'cliente',
+ 'clients',
+ 'clients1',
+ 'cnarne',
+ 'code',
+ 'comercial',
+ 'comment',
+ 'comments',
+ 'communities',
+ 'community',
+ 'company',
+ 'compare',
+ 'compras',
+ 'config',
+ 'configuration',
+ 'confirm',
+ 'confirmation',
+ 'connect',
+ 'contact',
+ 'contacts',
+ 'contactus',
+ 'contact-us',
+ 'contact_us',
+ 'content',
+ 'contest',
+ 'contribute',
+ 'contributor',
+ 'contributors',
+ 'coppa',
+ 'copyright',
+ 'copyrights',
+ 'core',
+ 'corp',
+ 'countries',
+ 'country',
+ 'cpanel',
+ 'create',
+ 'css',
+ 'cssproxy',
+ 'customise',
+ 'customize',
+ 'dashboard',
+ 'data',
+ 'db',
+ 'default',
+ 'delete',
+ 'demo',
+ 'design',
+ 'designer',
+ 'desktop',
+ 'destroy',
+ 'dev',
+ 'devel',
+ 'developer',
+ 'developers',
+ 'devs',
+ 'diagram',
+ 'diary',
+ 'dict',
+ 'dictionary',
+ 'die',
+ 'dir',
+ 'directory',
+ 'direct_messages',
+ 'direct-messages',
+ 'dist',
+ 'diversity',
+ 'dl',
+ 'dmca',
+ 'doc',
+ 'docs',
+ 'documentation',
+ 'documentations',
+ 'documents',
+ 'domain',
+ 'domains',
+ 'donate',
+ 'download',
+ 'downloads',
+ 'e',
+ 'e-mail',
+ 'earth',
+ 'ecommerce',
+ 'edit',
+ 'edits',
+ 'editor',
+ 'edu',
+ 'education',
+ 'email',
+ 'embed',
+ 'embedded',
+ 'employment',
+ 'employments',
+ 'empty',
+ 'enable',
+ 'encrypted',
+ 'end',
+ 'engine',
+ 'enterprise',
+ 'enterprises',
+ 'entries',
+ 'entry',
+ 'error',
+ 'errorlog',
+ 'errors',
+ 'eval',
+ 'event',
+ 'example',
+ 'examplecommunity',
+ 'exampleopenid',
+ 'examplesyn',
+ 'examplesyndicated',
+ 'exampleusername',
+ 'exchange',
+ 'exit',
+ 'explore',
+ 'faq',
+ 'faqs',
+ 'favorite',
+ 'favorites',
+ 'favourite',
+ 'favourites',
+ 'feature',
+ 'features',
+ 'feed',
+ 'feedback',
+ 'feedburner',
+ 'feedproxy',
+ 'feeds',
+ 'file',
+ 'files',
+ 'finance',
+ 'folder',
+ 'folders',
+ 'first',
+ 'following',
+ 'forgot',
+ 'form',
+ 'forms',
+ 'forum',
+ 'forums',
+ 'founder',
+ 'free',
+ 'friend',
+ 'friends',
+ 'ftp',
+ 'fuck',
+ 'fun',
+ 'fusion',
+ 'gadget',
+ 'gadgets',
+ 'game',
+ 'games',
+ 'gears',
+ 'general',
+ 'geographic',
+ 'get',
+ 'gettingstarted',
+ 'gift',
+ 'gifts',
+ 'gist',
+ 'git',
+ 'github',
+ 'gmail',
+ 'go',
+ 'golang',
+ 'goto',
+ 'gov',
+ 'graph',
+ 'graphs',
+ 'group',
+ 'groups',
+ 'guest',
+ 'guests',
+ 'guide',
+ 'guides',
+ 'hack',
+ 'hacks',
+ 'head',
+ 'help',
+ 'home',
+ 'homepage',
+ 'host',
+ 'hosting',
+ 'hostmaster',
+ 'hostname',
+ 'howto',
+ 'how-to',
+ 'how_to',
+ 'html',
+ 'htrnl',
+ 'http',
+ 'httpd',
+ 'https',
+ 'i',
+ 'iamges',
+ 'icon',
+ 'icons',
+ 'id',
+ 'idea',
+ 'ideas',
+ 'im',
+ 'image',
+ 'images',
+ 'img',
+ 'imap',
+ 'inbox',
+ 'inboxes',
+ 'index',
+ 'indexes',
+ 'info',
+ 'information',
+ 'inquiry',
+ 'intranet',
+ 'investor',
+ 'investors',
+ 'invitation',
+ 'invitations',
+ 'invite',
+ 'invoice',
+ 'invoices',
+ 'imac',
+ 'ios',
+ 'ipad',
+ 'iphone',
+ 'irc',
+ 'irnages',
+ 'irng',
+ 'is',
+ 'issue',
+ 'issues',
+ 'it',
+ 'item',
+ 'items',
+ 'java',
+ 'javascript',
+ 'job',
+ 'jobs',
+ 'join',
+ 'js',
+ 'json',
+ 'jump',
+ 'kb',
+ 'knowledge-base',
+ 'knowledgebase',
+ 'lab',
+ 'labs',
+ 'language',
+ 'languages',
+ 'last',
+ 'ldap_status',
+ 'ldap-status',
+ 'ldapstatus',
+ 'legal',
+ 'license',
+ 'licenses',
+ 'link',
+ 'links',
+ 'linux',
+ 'list',
+ 'lists',
+ 'livejournal',
+ 'lj',
+ 'local',
+ 'locale',
+ 'location',
+ 'log',
+ 'log-in',
+ 'log-out',
+ 'login',
+ 'logout',
+ 'logs',
+ 'log_in',
+ 'log_out',
+ 'm',
+ 'mac',
+ 'macos',
+ 'macosx',
+ 'mac-os',
+ 'mac-os-x',
+ 'mac_os_x',
+ 'mail',
+ 'mailer',
+ 'mailing',
+ 'main',
+ 'maintenance',
+ 'manage',
+ 'manager',
+ 'manual',
+ 'map',
+ 'maps',
+ 'marketing',
+ 'master',
+ 'me',
+ 'media',
+ 'member',
+ 'members',
+ 'memories',
+ 'memory',
+ 'merchandise',
+ 'message',
+ 'messages',
+ 'messenger',
+ 'mg',
+ 'microblog',
+ 'microblogs',
+ 'mine',
+ 'mis',
+ 'misc',
+ 'mms',
+ 'mob',
+ 'mobile',
+ 'model',
+ 'models',
+ 'money',
+ 'movie',
+ 'movies',
+ 'mp3',
+ 'mp4',
+ 'msg',
+ 'msn',
+ 'music',
+ 'mx',
+ 'my',
+ 'mymme',
+ 'mysql',
+ 'name',
+ 'named',
+ 'nan',
+ 'navi',
+ 'navigation',
+ 'net',
+ 'network',
+ 'networks',
+ 'new',
+ 'news',
+ 'newsletter',
+ 'nick',
+ 'nickname',
+ 'nil',
+ 'none',
+ 'notes',
+ 'noticias',
+ 'notification',
+ 'notifications',
+ 'notify',
+ 'ns',
+ 'ns1',
+ 'ns2',
+ 'ns3',
+ 'ns4',
+ 'ns5',
+ 'null',
+ 'oauth',
+ 'oauth-clients',
+ 'oauth_clients',
+ 'ocsp',
+ 'offer',
+ 'offers',
+ 'official',
+ 'old',
+ 'online',
+ 'openid',
+ 'operator',
+ 'option',
+ 'options',
+ 'order',
+ 'orders',
+ 'org',
+ 'organization',
+ 'organizations',
+ 'other',
+ 'overview',
+ 'owner',
+ 'owners',
+ 'p0rn',
+ 'pack',
+ 'page',
+ 'pager',
+ 'pages',
+ 'paid',
+ 'panel',
+ 'partner',
+ 'partnerpage',
+ 'partners',
+ 'password',
+ 'patch',
+ 'pay',
+ 'payment',
+ 'people',
+ 'perl',
+ 'person',
+ 'phone',
+ 'photo',
+ 'photoalbum',
+ 'photos',
+ 'php',
+ 'phpmyadmin',
+ 'phppgadmin',
+ 'phpredisadmin',
+ 'pic',
+ 'pics',
+ 'picture',
+ 'pictures',
+ 'ping',
+ 'pixel',
+ 'places',
+ 'plan',
+ 'plans',
+ 'plugin',
+ 'plugins',
+ 'podcasts',
+ 'policies',
+ 'policy',
+ 'pop',
+ 'pop3',
+ 'popular',
+ 'porn',
+ 'portal',
+ 'portals',
+ 'post',
+ 'postfix',
+ 'postmaster',
+ 'posts',
+ 'pr',
+ 'pr0n',
+ 'premium',
+ 'press',
+ 'price',
+ 'pricing',
+ 'principles',
+ 'print',
+ 'privacy',
+ 'privacy-policy',
+ 'privacypolicy',
+ 'privacy_policy',
+ 'private',
+ 'prod',
+ 'product',
+ 'production',
+ 'products',
+ 'profile',
+ 'profiles',
+ 'project',
+ 'projects',
+ 'promo',
+ 'promotions',
+ 'proxies',
+ 'proxy',
+ 'pub',
+ 'public',
+ 'purchase',
+ 'purpose',
+ 'put',
+ 'python',
+ 'queries',
+ 'query',
+ 'radio',
+ 'random',
+ 'ranking',
+ 'read',
+ 'reader',
+ 'readme',
+ 'recent',
+ 'recruit',
+ 'recruitment',
+ 'redirect',
+ 'register',
+ 'registration',
+ 'release',
+ 'remove',
+ 'replies',
+ 'report',
+ 'reports',
+ 'repositories',
+ 'repository',
+ 'req',
+ 'request',
+ 'requests',
+ 'research',
+ 'reset',
+ 'resolve',
+ 'resolver',
+ 'review',
+ 'rnail',
+ 'rnicrosoft',
+ 'roc',
+ 'root',
+ 'rss',
+ 'ruby',
+ 'rule',
+ 'sag',
+ 'sale',
+ 'sales',
+ 'sample',
+ 'samples',
+ 'sandbox',
+ 'save',
+ 'scholar',
+ 'school',
+ 'schools',
+ 'script',
+ 'scripts',
+ 'search',
+ 'secure',
+ 'security',
+ 'self',
+ 'seminars',
+ 'send',
+ 'server',
+ 'server-info',
+ 'server_info',
+ 'server-status',
+ 'server_status',
+ 'servers',
+ 'service',
+ 'services',
+ 'session',
+ 'sessions',
+ 'setting',
+ 'settings',
+ 'setup',
+ 'share',
+ 'shop',
+ 'shopping',
+ 'shortcut',
+ 'shortcuts',
+ 'show',
+ 'sign-in',
+ 'sign-up',
+ 'signin',
+ 'signout',
+ 'signup',
+ 'sign_in',
+ 'sign_up',
+ 'site',
+ 'sitemap',
+ 'sitemaps',
+ 'sitenews',
+ 'sites',
+ 'sketchup',
+ 'sky',
+ 'slash',
+ 'slashinvoice',
+ 'slut',
+ 'smartphone',
+ 'sms',
+ 'smtp',
+ 'soap',
+ 'software',
+ 'sorry',
+ 'source',
+ 'spec',
+ 'special',
+ 'spreadsheet',
+ 'spreadsheets',
+ 'sql',
+ 'src',
+ 'srntp',
+ 'ssh',
+ 'ssl',
+ 'ssladmin',
+ 'ssladministrator',
+ 'sslwebmaster',
+ 'ssytem',
+ 'staff',
+ 'stage',
+ 'staging',
+ 'start',
+ 'stat',
+ 'state',
+ 'static',
+ 'statistics',
+ 'stats',
+ 'status',
+ 'store',
+ 'stores',
+ 'stories',
+ 'style',
+ 'styleguide',
+ 'styles',
+ 'stylesheet',
+ 'stylesheets',
+ 'subdomain',
+ 'subscribe',
+ 'subscription',
+ 'subscriptions',
+ 'suggest',
+ 'suggestqueries',
+ 'support',
+ 'survey',
+ 'surveys',
+ 'surveytool',
+ 'svn',
+ 'swf',
+ 'syn',
+ 'sync',
+ 'syndicated',
+ 'sys',
+ 'sysadmin',
+ 'sysadministrator',
+ 'sysadmins',
+ 'system',
+ 'tablet',
+ 'tablets',
+ 'tag',
+ 'tags',
+ 'talk',
+ 'talkgadget',
+ 'task',
+ 'tasks',
+ 'team',
+ 'teams',
+ 'tech',
+ 'telnet',
+ 'term',
+ 'terms',
+ 'terms-of-service',
+ 'termsofservice',
+ 'terms_of_service',
+ 'test',
+ 'testing',
+ 'tests',
+ 'text',
+ 'theme',
+ 'themes',
+ 'thread',
+ 'threads',
+ 'ticket',
+ 'tickets',
+ 'tmp',
+ 'todo',
+ 'to-do',
+ 'to_do',
+ 'toml',
+ 'tool',
+ 'toolbar',
+ 'toolbars',
+ 'tools',
+ 'top',
+ 'topic',
+ 'topics',
+ 'tos',
+ 'tour',
+ 'trac',
+ 'translate',
+ 'trace',
+ 'translation',
+ 'translations',
+ 'translator',
+ 'trends',
+ 'tutorial',
+ 'tux',
+ 'tv',
+ 'twitter',
+ 'txt',
+ 'ul',
+ 'undef',
+ 'unfollow',
+ 'unsubscribe',
+ 'update',
+ 'updates',
+ 'upgrade',
+ 'upgrades',
+ 'upi',
+ 'upload',
+ 'uploads',
+ 'url',
+ 'usage',
+ 'user',
+ 'username',
+ 'usernames',
+ 'users',
+ 'uuid',
+ 'validation',
+ 'validations',
+ 'ver',
+ 'version',
+ 'video',
+ 'videos',
+ 'video-stats',
+ 'visitor',
+ 'visitors',
+ 'voice',
+ 'volunteer',
+ 'volunteers',
+ 'w',
+ 'watch',
+ 'wave',
+ 'weather',
+ 'web',
+ 'webdisk',
+ 'webhook',
+ 'webhooks',
+ 'webmail',
+ 'webmaster',
+ 'webmasters',
+ 'webrnail',
+ 'website',
+ 'websites',
+ 'welcome',
+ 'whm',
+ 'whois',
+ 'widget',
+ 'widgets',
+ 'wifi',
+ 'wiki',
+ 'wikis',
+ 'win',
+ 'windows',
+ 'word',
+ 'work',
+ 'works',
+ 'workshop',
+ 'wpad',
+ 'ww',
+ 'wws',
+ 'www',
+ 'wwws',
+ 'wwww',
+ 'xfn',
+ 'xhtml',
+ 'xhtrnl',
+ 'xml',
+ 'xmpp',
+ 'xpg',
+ 'xxx',
+ 'yaml',
+ 'year',
+ 'yml',
+ 'you',
+ 'yourdomain',
+ 'yourname',
+ 'yoursite',
+ 'yourusername',
+]
+
+const famousAccounts = [
+ // reserving some large twitter accounts (top 100 by followers according to wikidata dump)
+ '10ronaldinho',
+ '3gerardpique',
+ 'aclu',
+ 'adele',
+ 'akshaykumar',
+ 'aliaa08',
+ 'aliciakeys',
+ 'amitshah',
+ 'andresiniesta8',
+ 'anushkasharma',
+ 'arianagrande',
+ 'arrahman',
+ 'arvindkejriwal',
+ 'avrillavigne',
+ 'barackobama',
+ 'bbcbreaking',
+ 'bbcworld',
+ 'beingsalmankhan',
+ 'billgates',
+ 'britneyspears',
+ 'brunomars',
+ 'bts_bighit',
+ 'bts_twt',
+ 'championsleague',
+ 'chrisbrown',
+ 'cnnbrk',
+ 'coldplay',
+ 'conanobrien',
+ 'cristiano',
+ 'danieltosh',
+ 'davidguetta',
+ 'ddlovato',
+ 'deepikapadukone',
+ 'drake',
+ 'elisapie',
+ 'ellendegeneres',
+ 'elonmusk',
+ 'eminem',
+ 'emmawatson',
+ 'fcbarcelona',
+ 'foxnews',
+ 'harry_styles',
+ 'hillaryclinton',
+ 'iamsrk',
+ 'ihrithik',
+ 'imvkohli',
+ 'instagram',
+ 'jimmyfallon',
+ 'jlo',
+ 'joebiden',
+ 'jtimberlake',
+ 'justinbieber',
+ 'kaka',
+ 'kanyewest',
+ 'katyperry',
+ 'kendalljenner',
+ 'kevinhart4real',
+ 'khloekardashian',
+ 'kimkardashian',
+ 'kingjames',
+ 'kourtneykardash',
+ 'kyliejenner',
+ 'ladygaga',
+ 'liampayne',
+ 'liltunechi',
+ 'manutd',
+ 'mariahcarey',
+ 'mileycyrus',
+ 'mohamadalarefe',
+ 'narendramodi',
+ 'nasa',
+ 'nba',
+ 'neymarjr',
+ 'nfl',
+ 'niallofficial',
+ 'nickiminaj',
+ 'npr',
+ 'nytimes',
+ 'onedirection',
+ 'oprah',
+ 'pink',
+ 'pitbull',
+ 'playstation',
+ 'pmoindia',
+ 'premierleague',
+ 'priyankachopra',
+ 'realdonaldtrump',
+ 'ricky_martin',
+ 'rihanna',
+ 'sachin_rt',
+ 'selenagomez',
+ 'shakira',
+ 'shawnmendes',
+ 'sportscenter',
+ 'srbachchan',
+ 'subhisharma100',
+ 'taylorswift13',
+ 'theeconomist',
+ 'twitter',
+ 'virendersehwag',
+ 'whitehouse45',
+ 'wizkhalifa',
+ 'youtube',
+ 'zaynmalik',
+
+ // some top instagram (https://en.wikipedia.org/wiki/List_of_most-followed_Instagram_accounts)
+ 'beyonce',
+ 'billieeilish',
+ 'leomessi',
+ 'natgeo',
+ 'nike',
+ 'snoopdogg',
+ 'taylorswift',
+ 'therock',
+
+ // ... and a couple more prominent accounts, subjectively
+ '10downingstreet',
+ 'aoc',
+ 'carterjwm',
+ 'dril',
+ 'gretathunberg',
+ 'kamalaharris',
+ 'kremlinrussia_e',
+ 'potus',
+ 'rondesantisfl',
+ 'ukraine',
+ 'washingtonpost',
+ 'yousuck2020',
+ 'zelenskyyua',
+
+ // top japan-specific accounts
+ 'akiko_lawson',
+ 'ariyoshihiroiki',
+ 'asahi',
+ 'dozle_official',
+ 'famima_now',
+ 'ff_xiv_jp',
+ 'fujitv',
+ 'gigazine',
+ 'hajimesyacho',
+ 'hikakin',
+ 'jocx',
+ 'jotx',
+ 'kiyo_saiore',
+ 'mainichi',
+ 'matsu_bouzu',
+ 'naomiosaka',
+ 'nhk',
+ 'nikkei',
+ 'nintendo',
+ 'ntv',
+ 'oowareware1945',
+ 'pamyurin',
+ 'poke_times',
+ 'rolaworld',
+ 'seikintv',
+ 'starbucksjapan',
+ 'tbs',
+ 'tbs_pr',
+ 'tvasahi',
+ 'tvtokyo',
+ 'yokoono',
+ 'yomiuri_online',
+
+ // top brazil-specific accounts
+ 'brasildefato',
+ 'claudialeitte',
+ 'correio',
+ 'em_com',
+ 'estadao',
+ 'folha',
+ 'gazetadopovo',
+ 'ivetesangalo',
+ 'jairbolsonaro',
+ 'jornaldobrasil',
+ 'jornaloglobo',
+ 'lucianohuck',
+ 'lulaoficial',
+ 'marcosmion',
+ 'paulocoelho',
+ 'portalr7',
+ 'rede_globo',
+ 'zerohora',
+]
+
+/**
+ * Check if a handle is will be rejected by Bluesky PBC's PDS implementation
+ * Note: other PDSes may have different restrictions, so we should only apply this check
+ * if signing up to `bsky.social`
+ *
+ * See: https://github.com/bluesky-social/atproto/blob/main/packages/pds/src/handle/reserved.ts
+ */
+export function isHandleReserved(prefix: string): boolean {
+ return (
+ atpSpecific.includes(prefix) ||
+ commonlyReserved.includes(prefix) ||
+ famousAccounts.includes(prefix)
+ )
+}
diff --git a/src/logger/metrics.ts b/src/logger/metrics.ts
index dfca1f7d8e..3c892bdb57 100644
--- a/src/logger/metrics.ts
+++ b/src/logger/metrics.ts
@@ -68,6 +68,7 @@ export type MetricEvents = {
backgroundCount: number
}
'signup:handleTaken': {}
+ 'signup:handleReserved': {}
'signin:hostingProviderPressed': {
hostingProviderDidChange: boolean
}
diff --git a/src/screens/Signup/BackNextButtons.tsx b/src/screens/Signup/BackNextButtons.tsx
index 888b9071e1..5a85a85d1f 100644
--- a/src/screens/Signup/BackNextButtons.tsx
+++ b/src/screens/Signup/BackNextButtons.tsx
@@ -9,7 +9,7 @@ import {Loader} from '#/components/Loader'
export interface BackNextButtonsProps {
hideNext?: boolean
showRetry?: boolean
- isLoading: boolean
+ isLoading?: boolean
isNextDisabled?: boolean
onBackPress: () => void
onNextPress?: () => void
diff --git a/src/screens/Signup/StepCaptcha/index.tsx b/src/screens/Signup/StepCaptcha/index.tsx
index 388deecaff..e900b96c09 100644
--- a/src/screens/Signup/StepCaptcha/index.tsx
+++ b/src/screens/Signup/StepCaptcha/index.tsx
@@ -75,7 +75,7 @@ export function StepCaptcha() {
return (
-
+
(state.handle)
- const [draftValue, setDraftValue] = React.useState(state.handle)
- const isLoading = useThrottledValue(state.isLoading, 500)
+ const [draftValue, setDraftValue] = useState(state.handle)
+ const isNextLoading = useThrottledValue(state.isLoading, 500)
- const onNextPress = React.useCallback(async () => {
- const handle = handleValueRef.current.trim()
+ const validCheck = validateServiceHandle(draftValue, state.userDomain)
+
+ const {data: isHandleAvailable, isLoading} = useHandleAvailabilityQuery(
+ draftValue,
+ state.userDomain,
+ validCheck.overall,
+ )
+
+ const onNextPress = async () => {
+ if (!isHandleAvailable?.available) return
+
+ const handle = draftValue.trim()
dispatch({
type: 'setHandle',
value: handle,
})
- const newValidCheck = validateServiceHandle(handle, state.userDomain)
- if (!newValidCheck.overall) {
+ if (!validCheck.overall) {
+ return
+ }
+
+ if (isHandleReserved(handle)) {
+ dispatch({
+ type: 'setError',
+ value: _(msg`That username is not available`),
+ field: 'handle',
+ })
+ logger.metric('signup:handleReserved', {}, {statsig: true})
return
}
@@ -52,7 +80,7 @@ export function StepHandle() {
if (res.data.did) {
dispatch({
type: 'setError',
- value: _(msg`That handle is already taken.`),
+ value: _(msg`That username is already taken`),
field: 'handle',
})
logger.metric('signup:handleTaken', {}, {statsig: true})
@@ -82,17 +110,10 @@ export function StepHandle() {
return
}
dispatch({type: 'next'})
- }, [
- _,
- dispatch,
- state.activeStep,
- state.serviceDescription?.phoneVerificationRequired,
- state.userDomain,
- agent,
- ])
+ }
- const onBackPress = React.useCallback(() => {
- const handle = handleValueRef.current.trim()
+ const onBackPress = () => {
+ const handle = draftValue.trim()
dispatch({
type: 'setHandle',
value: handle,
@@ -103,13 +124,18 @@ export function StepHandle() {
{activeStep: state.activeStep},
{statsig: true},
)
- }, [dispatch, state.activeStep])
+ }
- const validCheck = validateServiceHandle(draftValue, state.userDomain)
return (
+
+ This can be changed at any time.
+
+
+ Username
+
+
+ {draftValue.length > 0 && (
+ {draftValue}
+ )}
- {draftValue !== '' && (
-
-
- Your full username will be{' '}
-
- @{createFullHandle(draftValue, state.userDomain)}
-
-
-
- )}
-
- {draftValue !== '' && (
-
- {state.error ? (
-
-
- {state.error}
-
- ) : undefined}
- {validCheck.hyphenStartOrEnd ? (
-
-
-
- Only contains letters, numbers, and hyphens
-
-
- ) : (
-
-
-
- Doesn't begin or end with a hyphen
-
-
+
+
+ {state.error && (
+
+
+ {state.error}
+
)}
-
-
+
+
+ {isLoading ? (
+ Checking...
+ ) : isHandleAvailable?.available ? (
+ Available!
+ ) : isHandleAvailable?.reason === 'taken' ? (
+ Taken
+ ) : (
+ Invalid
+ )}
+
+
+ )}
+
+
- {!validCheck.totalLength ||
- draftValue.length > MAX_SERVICE_HANDLE_LENGTH ? (
-
+
+ {!validCheck.hyphenStartOrEnd ? (
+
+ Doesn't begin or end with a hyphen
+
+ ) : (
+
+ Only contains letters, numbers, and hyphens
+
+ )}
+
+
+
+ {!validCheck.frontLengthNotTooLong || !validCheck.totalLength ? (
+
No longer than{' '}
-
+
) : (
-
+
At least 3 characters
-
+
)}
-
+
- )}
+
@@ -208,10 +252,105 @@ export function StepHandle() {
)
}
-function IsValidIcon({valid}: {valid: boolean}) {
- const t = useTheme()
- if (!valid) {
- return
- }
- return
+function Requirement({
+ children,
+ fade,
+}: {
+ children: React.ReactNode
+ fade?: boolean
+}) {
+ return (
+
+ {children}
+
+ )
+}
+
+function RequirementText({children}: {children: React.ReactNode}) {
+ const t = useTheme()
+ return (
+
+ {children}
+
+ )
+}
+
+function RequirementIcon({
+ state,
+}: {
+ state: 'initial' | 'valid' | 'invalid' | 'loading'
+}) {
+ const t = useTheme()
+
+ switch (state) {
+ case 'initial':
+ return
+ case 'valid':
+ return (
+
+ )
+ case 'invalid':
+ return
+ case 'loading':
+ return
+ }
+}
+
+function GhostText({children, value}: {children: string; value: string}) {
+ const t = useTheme()
+ // eslint-disable-next-line bsky-internal/avoid-unwrapped-text
+ return (
+
+
+ {children}
+
+ {value}
+
+
+
+ )
}
diff --git a/src/screens/Signup/StepInfo/index.tsx b/src/screens/Signup/StepInfo/index.tsx
index f24cd0e45d..cf4a9297e2 100644
--- a/src/screens/Signup/StepInfo/index.tsx
+++ b/src/screens/Signup/StepInfo/index.tsx
@@ -144,7 +144,7 @@ export function StepInfo({
return (
-
+
void}) {
a.pt_2xl,
!gtMobile && {paddingBottom: 100},
]}>
-
-
+
+
Step {state.activeStep + 1} of{' '}
{state.serviceDescription &&
@@ -153,7 +154,7 @@ export function Signup({onPressBack}: {onPressBack: () => void}) {
: '3'}
-
+
{state.activeStep === SignupStep.INFO ? (
Your account
) : state.activeStep === SignupStep.HANDLE ? (
diff --git a/src/state/queries/handle-availablility.ts b/src/state/queries/handle-availablility.ts
new file mode 100644
index 0000000000..6ec2c043ea
--- /dev/null
+++ b/src/state/queries/handle-availablility.ts
@@ -0,0 +1,44 @@
+import {useQuery} from '@tanstack/react-query'
+
+import {createFullHandle, isHandleReserved} from '#/lib/strings/handles'
+import {useAgent} from '#/state/session'
+import {useDebouncedValue} from '#/components/live/utils'
+
+export const RQKEY_handleAvailability = (handle: string) => [
+ 'handle-availability',
+ handle,
+]
+
+export function useHandleAvailabilityQuery(
+ name: string,
+ domain: string,
+ enabled: boolean,
+ debounceDelayMs = 500,
+) {
+ name = name.trim()
+ const handle = createFullHandle(name, domain)
+ const debouncedHandle = useDebouncedValue(handle, debounceDelayMs)
+ const agent = useAgent()
+
+ return useQuery({
+ enabled: enabled && handle === debouncedHandle,
+ queryKey: RQKEY_handleAvailability(debouncedHandle),
+ queryFn: async () => {
+ const frontSegment = debouncedHandle.split('.')[0]
+ if (isHandleReserved(frontSegment)) {
+ return {available: false, reason: 'reserved'} as const
+ }
+ try {
+ const res = await agent.resolveHandle({
+ handle: debouncedHandle,
+ })
+
+ if (res.data.did) {
+ return {available: false, reason: 'taken'} as const
+ }
+ } catch {}
+
+ return {available: true} as const
+ },
+ })
+}