Merge branch 'main' into feat/selectable-text

This commit is contained in:
Hailey
2024-01-18 23:48:10 -08:00
committed by GitHub
119 changed files with 9401 additions and 2436 deletions
+2 -1
View File
@@ -14,7 +14,8 @@ async function main() {
await server?.close() await server?.close()
console.log('Starting new server') console.log('Starting new server')
const inviteRequired = url?.query && 'invite' in url.query const inviteRequired = url?.query && 'invite' in url.query
server = await createServer({inviteRequired}) const phoneRequired = url?.query && 'phone' in url.query
server = await createServer({inviteRequired, phoneRequired})
console.log('Listening at', server.pdsUrl) console.log('Listening at', server.pdsUrl)
if (url?.query) { if (url?.query) {
if ('users' in url.query) { if ('users' in url.query) {
+5 -7
View File
@@ -16,14 +16,12 @@ describe('Create account', () => {
await element(by.id('createAccountButton')).tap() await element(by.id('createAccountButton')).tap()
await device.takeScreenshot('1- opened create account screen') await device.takeScreenshot('1- opened create account screen')
await element(by.id('otherServerBtn')).tap() await element(by.id('selectServiceButton')).tap()
await device.takeScreenshot('2- selected other server') await device.takeScreenshot('2- selected other server')
await element(by.id('customServerInput')).clearText() await element(by.id('customServerTextInput')).typeText(service)
await element(by.id('customServerInput')).typeText(service) await element(by.id('customServerTextInput')).tapReturnKey()
await element(by.id('customServerSelectBtn')).tap()
await device.takeScreenshot('3- input test server URL') await device.takeScreenshot('3- input test server URL')
await element(by.id('nextBtn')).tap()
await element(by.id('emailInput')).typeText('example@test.com') await element(by.id('emailInput')).typeText('example@test.com')
await element(by.id('passwordInput')).typeText('hunter2') await element(by.id('passwordInput')).typeText('hunter2')
await device.takeScreenshot('4- entered account details') await device.takeScreenshot('4- entered account details')
@@ -31,7 +29,7 @@ describe('Create account', () => {
await element(by.id('nextBtn')).tap() await element(by.id('nextBtn')).tap()
await element(by.id('handleInput')).typeText('e2e-test') await element(by.id('handleInput')).typeText('e2e-test')
await device.takeScreenshot('4- entered handle') await device.takeScreenshot('5- entered handle')
await element(by.id('nextBtn')).tap() await element(by.id('nextBtn')).tap()
+4 -9
View File
@@ -1,10 +1,5 @@
/* eslint-env detox/detox */ /* eslint-env detox/detox */
/**
* This test is being skipped until we can resolve the detox crash issue
* with the side drawer.
*/
import {describe, beforeAll, it} from '@jest/globals' import {describe, beforeAll, it} from '@jest/globals'
import {expect} from 'detox' import {expect} from 'detox'
import {openApp, loginAsAlice, createServer} from '../util' import {openApp, loginAsAlice, createServer} from '../util'
@@ -31,12 +26,12 @@ describe('invite-codes', () => {
await element(by.id('e2eOpenLoggedOutView')).tap() await element(by.id('e2eOpenLoggedOutView')).tap()
await element(by.id('createAccountButton')).tap() await element(by.id('createAccountButton')).tap()
await device.takeScreenshot('1- opened create account screen') await device.takeScreenshot('1- opened create account screen')
await element(by.id('otherServerBtn')).tap() await element(by.id('selectServiceButton')).tap()
await device.takeScreenshot('2- selected other server') await device.takeScreenshot('2- selected other server')
await element(by.id('customServerInput')).clearText() await element(by.id('customServerTextInput')).typeText(service)
await element(by.id('customServerInput')).typeText(service) await element(by.id('customServerTextInput')).tapReturnKey()
await element(by.id('customServerSelectBtn')).tap()
await device.takeScreenshot('3- input test server URL') await device.takeScreenshot('3- input test server URL')
await element(by.id('nextBtn')).tap()
await element(by.id('inviteCodeInput')).typeText(inviteCode) await element(by.id('inviteCodeInput')).typeText(inviteCode)
await element(by.id('emailInput')).typeText('example@test.com') await element(by.id('emailInput')).typeText('example@test.com')
await element(by.id('passwordInput')).typeText('hunter2') await element(by.id('passwordInput')).typeText('hunter2')
@@ -0,0 +1,57 @@
/* eslint-env detox/detox */
import {describe, beforeAll, it} from '@jest/globals'
import {expect} from 'detox'
import {openApp, loginAsAlice, createServer} from '../util'
describe('invite-codes', () => {
let service: string
let inviteCode = ''
beforeAll(async () => {
service = await createServer('?users&invite&phone')
await openApp({permissions: {notifications: 'YES'}})
})
it('I can fetch invite codes', async () => {
await loginAsAlice()
await element(by.id('e2eOpenInviteCodesModal')).tap()
await expect(element(by.id('inviteCodesModal'))).toBeVisible()
const attrs = await element(by.id('inviteCode-0-code')).getAttributes()
inviteCode = attrs.text
await element(by.id('closeBtn')).tap()
await element(by.id('e2eSignOut')).tap()
})
it('I can create a new account with the invite code', async () => {
await element(by.id('e2eOpenLoggedOutView')).tap()
await element(by.id('createAccountButton')).tap()
await device.takeScreenshot('1- opened create account screen')
await element(by.id('selectServiceButton')).tap()
await device.takeScreenshot('2- selected other server')
await element(by.id('customServerTextInput')).typeText(service)
await element(by.id('customServerTextInput')).tapReturnKey()
await element(by.id('customServerSelectBtn')).tap()
await device.takeScreenshot('3- input test server URL')
await element(by.id('inviteCodeInput')).typeText(inviteCode)
await element(by.id('emailInput')).typeText('example@test.com')
await element(by.id('passwordInput')).typeText('hunter2')
await device.takeScreenshot('4- entered account details')
await element(by.id('nextBtn')).tap()
await element(by.id('phoneInput')).typeText('5558675309')
await element(by.id('requestCodeBtn')).tap()
await device.takeScreenshot('5- requested code')
await element(by.id('codeInput')).typeText('000000')
await device.takeScreenshot('6- entered code')
await element(by.id('nextBtn')).tap()
await element(by.id('handleInput')).typeText('e2e-test')
await device.takeScreenshot('7- entered handle')
await element(by.id('nextBtn')).tap()
await expect(element(by.id('welcomeOnboarding'))).toBeVisible()
await element(by.id('continueBtn')).tap()
await expect(element(by.id('recommendedFeedsOnboarding'))).toBeVisible()
await element(by.id('continueBtn')).tap()
await expect(element(by.id('recommendedFollowsOnboarding'))).toBeVisible()
await element(by.id('continueBtn')).tap()
await expect(element(by.id('homeScreen'))).toBeVisible()
})
})
+85
View File
@@ -0,0 +1,85 @@
/* eslint-env detox/detox */
import {describe, beforeAll, it} from '@jest/globals'
import {expect} from 'detox'
import {openApp, createServer} from '../util'
describe('Create account', () => {
let service: string
beforeAll(async () => {
service = await createServer('?phone')
await openApp({permissions: {notifications: 'YES'}})
})
it('I can create a new account with text verification', async () => {
await element(by.id('e2eOpenLoggedOutView')).tap()
await element(by.id('createAccountButton')).tap()
await device.takeScreenshot('1- opened create account screen')
await element(by.id('selectServiceButton')).tap()
await device.takeScreenshot('2- selected other server')
await element(by.id('customServerTextInput')).typeText(service)
await element(by.id('customServerTextInput')).tapReturnKey()
await element(by.id('customServerSelectBtn')).tap()
await device.takeScreenshot('3- input test server URL')
await element(by.id('emailInput')).typeText('text-verification@test.com')
await element(by.id('passwordInput')).typeText('hunter2')
await device.takeScreenshot('4- entered account details')
await element(by.id('nextBtn')).tap()
await element(by.id('phoneInput')).typeText('1234567890')
await element(by.id('requestCodeBtn')).tap()
await device.takeScreenshot('5- requested code')
await element(by.id('codeInput')).typeText('000000')
await device.takeScreenshot('6- entered code')
await element(by.id('nextBtn')).tap()
await element(by.id('handleInput')).typeText('text-verification-test')
await device.takeScreenshot('7- entered handle')
await element(by.id('nextBtn')).tap()
await expect(element(by.id('welcomeOnboarding'))).toBeVisible()
await element(by.id('continueBtn')).tap()
await expect(element(by.id('recommendedFeedsOnboarding'))).toBeVisible()
await element(by.id('continueBtn')).tap()
await expect(element(by.id('recommendedFollowsOnboarding'))).toBeVisible()
await element(by.id('continueBtn')).tap()
await expect(element(by.id('homeScreen'))).toBeVisible()
})
it('failed text verification correctly goes back to the code input screen', async () => {
await element(by.id('e2eSignOut')).tap()
await element(by.id('e2eOpenLoggedOutView')).tap()
await element(by.id('createAccountButton')).tap()
await device.takeScreenshot('1- opened create account screen')
await element(by.id('selectServiceButton')).tap()
await device.takeScreenshot('2- selected other server')
await element(by.id('customServerTextInput')).typeText(service)
await element(by.id('customServerTextInput')).tapReturnKey()
await element(by.id('customServerSelectBtn')).tap()
await device.takeScreenshot('3- input test server URL')
await element(by.id('emailInput')).typeText('text-verification2@test.com')
await element(by.id('passwordInput')).typeText('hunter2')
await device.takeScreenshot('4- entered account details')
await element(by.id('nextBtn')).tap()
await element(by.id('phoneInput')).typeText('1234567890')
await element(by.id('requestCodeBtn')).tap()
await device.takeScreenshot('5- requested code')
await element(by.id('codeInput')).typeText('111111')
await device.takeScreenshot('6- entered code')
await element(by.id('nextBtn')).tap()
await element(by.id('handleInput')).typeText('text-verification-test2')
await device.takeScreenshot('7- entered handle')
await element(by.id('nextBtn')).tap()
await expect(element(by.id('codeInput'))).toBeVisible()
await device.takeScreenshot('8- got error')
})
})
+1 -1
View File
@@ -105,7 +105,7 @@ async function openAppForDebugBuild(platform: string, opts: any) {
await sleep(3000) await sleep(3000)
} }
export async function createServer(path = '') { export async function createServer(path = ''): Promise<string> {
return new Promise(function (resolve, reject) { return new Promise(function (resolve, reject) {
var req = http.request( var req = http.request(
{ {
+7 -7
View File
@@ -463,44 +463,44 @@ describe('parseEmbedPlayerFromUrl', () => {
type: 'youtube_video', type: 'youtube_video',
source: 'youtube', source: 'youtube',
playerUri: playerUri:
'https://www.youtube.com/embed/videoId?autoplay=1&playsinline=1', 'https://www.youtube.com/embed/videoId?autoplay=1&playsinline=1&start=0',
}, },
{ {
type: 'youtube_video', type: 'youtube_video',
source: 'youtube', source: 'youtube',
playerUri: playerUri:
'https://www.youtube.com/embed/videoId?autoplay=1&playsinline=1', 'https://www.youtube.com/embed/videoId?autoplay=1&playsinline=1&start=0',
}, },
{ {
type: 'youtube_video', type: 'youtube_video',
source: 'youtube', source: 'youtube',
playerUri: playerUri:
'https://www.youtube.com/embed/videoId?autoplay=1&playsinline=1', 'https://www.youtube.com/embed/videoId?autoplay=1&playsinline=1&start=0',
}, },
{ {
type: 'youtube_video', type: 'youtube_video',
source: 'youtube', source: 'youtube',
playerUri: playerUri:
'https://www.youtube.com/embed/videoId?autoplay=1&playsinline=1', 'https://www.youtube.com/embed/videoId?autoplay=1&playsinline=1&start=0',
}, },
{ {
type: 'youtube_video', type: 'youtube_video',
source: 'youtube', source: 'youtube',
playerUri: playerUri:
'https://www.youtube.com/embed/videoId?autoplay=1&playsinline=1', 'https://www.youtube.com/embed/videoId?autoplay=1&playsinline=1&start=0',
}, },
{ {
type: 'youtube_short', type: 'youtube_short',
source: 'youtubeShorts', source: 'youtubeShorts',
hideDetails: true, hideDetails: true,
playerUri: playerUri:
'https://www.youtube.com/embed/videoId?autoplay=1&playsinline=1', 'https://www.youtube.com/embed/videoId?autoplay=1&playsinline=1&start=0',
}, },
{ {
type: 'youtube_video', type: 'youtube_video',
source: 'youtube', source: 'youtube',
playerUri: playerUri:
'https://www.youtube.com/embed/videoId?autoplay=1&playsinline=1', 'https://www.youtube.com/embed/videoId?autoplay=1&playsinline=1&start=0',
}, },
undefined, undefined,
+1 -1
View File
@@ -25,7 +25,7 @@ module.exports = function () {
/** /**
* Android build number. Must be incremented for each release. * Android build number. Must be incremented for each release.
*/ */
const ANDROID_VERSION_CODE = 55 const ANDROID_VERSION_CODE = 57
/** /**
* Uses built-in Expo env vars * Uses built-in Expo env vars
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M8 6a1 1 0 0 1 1-1h9a1 1 0 0 1 1 1v9a1 1 0 1 1-2 0V8.414l-9.793 9.793a1 1 0 0 1-1.414-1.414L15.586 7H9a1 1 0 0 1-1-1Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 260 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M4 3a1 1 0 0 0-1 1v16a1 1 0 0 0 1 1h16a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1H4Zm1 16V9h14v10H5ZM5 7h14V5H5v2Zm3 10.25a1.25 1.25 0 1 0 0-2.5 1.25 1.25 0 0 0 0 2.5ZM17.25 12a1.25 1.25 0 1 1-2.5 0 1.25 1.25 0 0 1 2.5 0ZM12 13.25a1.25 1.25 0 1 0 0-2.5 1.25 1.25 0 0 0 0 2.5ZM9.25 12a1.25 1.25 0 1 1-2.5 0 1.25 1.25 0 0 1 2.5 0ZM12 17.25a1.25 1.25 0 1 0 0-2.5 1.25 1.25 0 0 0 0 2.5Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 512 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M4 12c0-4.09 3.527-7.5 8-7.5s8 3.41 8 7.5c0 1.579-.419 2.056-.708 2.236-.388.241-1.031.286-2.058.153-.33-.043-.652-.096-.991-.152a65.905 65.905 0 0 0-.531-.087c-.52-.081-1.077-.156-1.61-.164-1.065-.016-2.336.245-2.996 1.567-.418.834-.295 1.67-.078 2.314.18.534.47 1.055.683 1.437v.001l.097.175.01.018C7.432 19.407 4 16.033 4 12Zm8-9.5C6.532 2.5 2 6.7 2 12s4.532 9.5 10 9.5c.401 0 .812-.04 1.166-.193.41-.176.761-.517.866-1.028.085-.416-.03-.796-.118-1.029a5.981 5.981 0 0 0-.351-.73l-.12-.215c-.215-.392-.403-.73-.52-1.078-.13-.387-.111-.614-.029-.78.146-.291.404-.473 1.178-.461.385.005.825.06 1.329.14.15.023.308.05.47.077.36.059.742.122 1.105.17 1.021.132 2.325.213 3.373-.439C21.496 15.22 22 13.874 22 12c0-5.3-4.532-9.5-10-9.5Zm3.5 8.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3ZM9 12.25a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0Zm1.5-2.75a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 1011 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M4.062 11h2.961c.103-2.204.545-4.218 1.235-5.77.06-.136.123-.269.188-.399A8.007 8.007 0 0 0 4.062 11ZM12 2C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2Zm0 2c-.227 0-.518.1-.868.432-.354.337-.719.872-1.047 1.61-.561 1.263-.958 2.991-1.06 4.958h5.95c-.102-1.967-.499-3.695-1.06-4.958-.328-.738-.693-1.273-1.047-1.61C12.518 4.099 12.227 4 12 4Zm4.977 7c-.103-2.204-.545-4.218-1.235-5.77a9.78 9.78 0 0 0-.188-.399A8.006 8.006 0 0 1 19.938 11h-2.961Zm-2.003 2H9.026c.101 1.966.498 3.695 1.06 4.958.327.738.692 1.273 1.046 1.61.35.333.641.432.868.432.227 0 .518-.1.868-.432.354-.337.719-.872 1.047-1.61.561-1.263.958-2.991 1.06-4.958Zm.58 6.169c.065-.13.128-.263.188-.399.69-1.552 1.132-3.566 1.235-5.77h2.961a8.006 8.006 0 0 1-4.384 6.169Zm-7.108 0a9.877 9.877 0 0 1-.188-.399c-.69-1.552-1.132-3.566-1.235-5.77H4.062a8.006 8.006 0 0 0 4.384 6.169Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 1005 B

+1 -1
View File
@@ -80,7 +80,7 @@ func (srv *Server) WebProfileRSS(c echo.Context) error {
} }
} }
af, err := appbsky.FeedGetAuthorFeed(ctx, srv.xrpcc, did.String(), "", "", 30) af, err := appbsky.FeedGetAuthorFeed(ctx, srv.xrpcc, did.String(), "", "posts_no_replies", 30)
if err != nil { if err != nil {
log.Warn("failed to fetch author feed", "did", did, "err", err) log.Warn("failed to fetch author feed", "did", did, "err", err)
return err return err
+25 -6
View File
@@ -39,30 +39,49 @@
height: calc(100% + env(safe-area-inset-top)); height: calc(100% + env(safe-area-inset-top));
} }
/* Remove autofill styles on Webkit */
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
textarea:-webkit-autofill,
textarea:-webkit-autofill:hover,
textarea:-webkit-autofill:focus,
select:-webkit-autofill,
select:-webkit-autofill:hover,
select:-webkit-autofill:focus {
border: 0;
-webkit-text-fill-color: transparent;
-webkit-box-shadow: none;
}
/* Force left-align date/time inputs on iOS mobile */
input::-webkit-date-and-time-value {
text-align: left;
}
/* Color theming */ /* Color theming */
:root { :root {
--text: black; --text: black;
--background: white; --background: white;
--backgroundLight: #F3F3F8; --backgroundLight: hsl(211, 20%, 95%);
} }
html.colorMode--dark { html.colorMode--dark {
--text: white; --text: white;
--background: black; --background: hsl(211, 20%, 4%);
--backgroundLight: #26272D; --backgroundLight: hsl(211, 20%, 20%);
color-scheme: dark; color-scheme: dark;
} }
@media (prefers-color-scheme: light) { @media (prefers-color-scheme: light) {
html.colorMode--system { html.colorMode--system {
--text: black; --text: black;
--background: white; --background: white;
--backgroundLight: #F3F3F8; --backgroundLight: hsl(211, 20%, 95%);
} }
} }
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
html.colorMode--system { html.colorMode--system {
--text: white; --text: white;
--background: black; --background: hsl(211, 20%, 4%);
--backgroundLight: #26272D; --backgroundLight: hsl(211, 20%, 20%);
color-scheme: dark; color-scheme: dark;
} }
} }
+35 -3
View File
@@ -1,7 +1,7 @@
import net from 'net' import net from 'net'
import path from 'path' import path from 'path'
import fs from 'fs' import fs from 'fs'
import {TestNetwork} from '@atproto/dev-env' import {TestNetwork, TestPds} from '@atproto/dev-env'
import {AtUri, BskyAgent} from '@atproto/api' import {AtUri, BskyAgent} from '@atproto/api'
export interface TestUser { export interface TestUser {
@@ -55,19 +55,36 @@ class StringIdGenerator {
const ids = new StringIdGenerator() const ids = new StringIdGenerator()
export async function createServer( export async function createServer(
{inviteRequired}: {inviteRequired: boolean} = {inviteRequired: false}, {
inviteRequired,
phoneRequired,
}: {inviteRequired: boolean; phoneRequired: boolean} = {
inviteRequired: false,
phoneRequired: false,
},
): Promise<TestPDS> { ): Promise<TestPDS> {
const port = await getPort() const port = 3000
const port2 = await getPort(port + 1) const port2 = await getPort(port + 1)
const port3 = await getPort(port2 + 1) const port3 = await getPort(port2 + 1)
const pdsUrl = `http://localhost:${port}` const pdsUrl = `http://localhost:${port}`
const id = ids.next() const id = ids.next()
const phoneParams = phoneRequired
? {
phoneVerificationRequired: true,
twilioAccountSid: 'ACXXXXXXX',
twilioAuthToken: 'AUTH',
twilioServiceSid: 'VAXXXXXXXX',
}
: {}
const testNet = await TestNetwork.create({ const testNet = await TestNetwork.create({
pds: { pds: {
port, port,
hostname: 'localhost', hostname: 'localhost',
dbPostgresSchema: `pds_${id}`,
inviteRequired, inviteRequired,
...phoneParams,
}, },
bsky: { bsky: {
dbPostgresSchema: `bsky_${id}`, dbPostgresSchema: `bsky_${id}`,
@@ -76,6 +93,7 @@ export async function createServer(
}, },
plc: {port: port2}, plc: {port: port2},
}) })
mockTwilio(testNet.pds)
const pic = fs.readFileSync( const pic = fs.readFileSync(
path.join(__dirname, '..', 'assets', 'default-avatar.png'), path.join(__dirname, '..', 'assets', 'default-avatar.png'),
@@ -144,6 +162,8 @@ class Mocker {
email, email,
handle: name + '.test', handle: name + '.test',
password: 'hunter2', password: 'hunter2',
verificationPhone: '1234567890',
verificationCode: '000000',
}) })
await agent.upsertProfile(async () => { await agent.upsertProfile(async () => {
const blob = await agent.uploadBlob(this.pic, { const blob = await agent.uploadBlob(this.pic, {
@@ -430,3 +450,15 @@ async function getPort(start = 3000) {
} }
throw new Error('Unable to find an available port') throw new Error('Unable to find an available port')
} }
export const mockTwilio = (pds: TestPds) => {
if (!pds.ctx.twilio) return
pds.ctx.twilio.sendCode = async (_number: string) => {
// do nothing
}
pds.ctx.twilio.verifyCode = async (_number: string, code: string) => {
return code === '000000'
}
}
+13 -1
View File
@@ -1,6 +1,18 @@
/** @type {import('@lingui/conf').LinguiConfig} */ /** @type {import('@lingui/conf').LinguiConfig} */
module.exports = { module.exports = {
locales: ['en', 'de', 'es', 'fr', 'hi', 'id', 'ja', 'ko', 'pt-BR', 'uk'], locales: [
'en',
'de',
'es',
'fr',
'hi',
'id',
'ja',
'ko',
'pt-BR',
'uk',
'ca',
],
catalogs: [ catalogs: [
{ {
path: '<rootDir>/src/locale/locales/{locale}/messages', path: '<rootDir>/src/locale/locales/{locale}/messages',
+4 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "bsky.app", "name": "bsky.app",
"version": "1.64.0", "version": "1.65.0",
"private": true, "private": true,
"engines": { "engines": {
"node": ">=18" "node": ">=18"
@@ -39,7 +39,7 @@
"nuke": "rm -rf ./node_modules && rm -rf ./ios && rm -rf ./android" "nuke": "rm -rf ./node_modules && rm -rf ./ios && rm -rf ./android"
}, },
"dependencies": { "dependencies": {
"@atproto/api": "^0.8.0", "@atproto/api": "^0.9.1",
"@bam.tech/react-native-image-resizer": "^3.0.4", "@bam.tech/react-native-image-resizer": "^3.0.4",
"@braintree/sanitize-url": "^6.0.2", "@braintree/sanitize-url": "^6.0.2",
"@emoji-mart/react": "^1.1.1", "@emoji-mart/react": "^1.1.1",
@@ -70,6 +70,7 @@
"@segment/analytics-react-native": "^2.10.1", "@segment/analytics-react-native": "^2.10.1",
"@segment/sovran-react-native": "^0.4.5", "@segment/sovran-react-native": "^0.4.5",
"@sentry/react-native": "5.5.0", "@sentry/react-native": "5.5.0",
"@tamagui/focus-scope": "^1.84.1",
"@tanstack/react-query": "^5.8.1", "@tanstack/react-query": "^5.8.1",
"@tiptap/core": "^2.0.0-beta.220", "@tiptap/core": "^2.0.0-beta.220",
"@tiptap/extension-document": "^2.0.0-beta.220", "@tiptap/extension-document": "^2.0.0-beta.220",
@@ -119,6 +120,7 @@
"js-sha256": "^0.9.0", "js-sha256": "^0.9.0",
"jwt-decode": "^4.0.0", "jwt-decode": "^4.0.0",
"lande": "^1.0.10", "lande": "^1.0.10",
"libphonenumber-js": "^1.10.53",
"lodash.chunk": "^4.2.0", "lodash.chunk": "^4.2.0",
"lodash.debounce": "^4.0.8", "lodash.debounce": "^4.0.8",
"lodash.isequal": "^4.5.0", "lodash.isequal": "^4.5.0",
+10
View File
@@ -0,0 +1,10 @@
diff --git a/node_modules/@lingui/core/dist/index.mjs b/node_modules/@lingui/core/dist/index.mjs
index 9759736..881f67b 100644
--- a/node_modules/@lingui/core/dist/index.mjs
+++ b/node_modules/@lingui/core/dist/index.mjs
@@ -1,4 +1,4 @@
-import unraw from 'unraw';
+import { unraw } from 'unraw';
import { compileMessage } from '@lingui/message-utils/compileMessage';
const isString = (s) => typeof s === "string";
+35 -24
View File
@@ -13,6 +13,8 @@ import {
import 'view/icons' import 'view/icons'
import {ThemeProvider as Alf} from '#/alf'
import {useColorModeTheme} from '#/alf/util/useColorModeTheme'
import {init as initPersistedState} from '#/state/persisted' import {init as initPersistedState} from '#/state/persisted'
import {listenSessionDropped} from './state/events' import {listenSessionDropped} from './state/events'
import {useColorMode} from 'state/shell' import {useColorMode} from 'state/shell'
@@ -25,6 +27,7 @@ import {queryClient} from 'lib/react-query'
import {TestCtrls} from 'view/com/testing/TestCtrls' import {TestCtrls} from 'view/com/testing/TestCtrls'
import {Provider as ShellStateProvider} from 'state/shell' import {Provider as ShellStateProvider} from 'state/shell'
import {Provider as ModalStateProvider} from 'state/modals' import {Provider as ModalStateProvider} from 'state/modals'
import {Provider as DialogStateProvider} from 'state/dialogs'
import {Provider as LightboxStateProvider} from 'state/lightbox' import {Provider as LightboxStateProvider} from 'state/lightbox'
import {Provider as MutedThreadsProvider} from 'state/muted-threads' import {Provider as MutedThreadsProvider} from 'state/muted-threads'
import {Provider as InvitesStateProvider} from 'state/invites' import {Provider as InvitesStateProvider} from 'state/invites'
@@ -39,6 +42,7 @@ import {
import {Provider as UnreadNotifsProvider} from 'state/queries/notifications/unread' import {Provider as UnreadNotifsProvider} from 'state/queries/notifications/unread'
import * as persisted from '#/state/persisted' import * as persisted from '#/state/persisted'
import {Splash} from '#/Splash' import {Splash} from '#/Splash'
import {Provider as PortalProvider} from '#/components/Portal'
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
@@ -48,6 +52,7 @@ function InnerApp() {
const colorMode = useColorMode() const colorMode = useColorMode()
const {isInitialLoad, currentAccount} = useSession() const {isInitialLoad, currentAccount} = useSession()
const {resumeSession} = useSessionApi() const {resumeSession} = useSessionApi()
const theme = useColorModeTheme(colorMode)
const {_} = useLingui() const {_} = useLingui()
// init // init
@@ -63,25 +68,27 @@ function InnerApp() {
return ( return (
<SafeAreaProvider initialMetrics={initialWindowMetrics}> <SafeAreaProvider initialMetrics={initialWindowMetrics}>
<Splash isReady={!isInitialLoad}> <Alf theme={theme}>
<React.Fragment <Splash isReady={!isInitialLoad}>
// Resets the entire tree below when it changes: <React.Fragment
key={currentAccount?.did}> // Resets the entire tree below when it changes:
<LoggedOutViewProvider> key={currentAccount?.did}>
<UnreadNotifsProvider> <LoggedOutViewProvider>
<ThemeProvider theme={colorMode}> <UnreadNotifsProvider>
{/* All components should be within this provider */} <ThemeProvider theme={colorMode}>
<RootSiblingParent> {/* All components should be within this provider */}
<GestureHandlerRootView style={s.h100pct}> <RootSiblingParent>
<TestCtrls /> <GestureHandlerRootView style={s.h100pct}>
<Shell /> <TestCtrls />
</GestureHandlerRootView> <Shell />
</RootSiblingParent> </GestureHandlerRootView>
</ThemeProvider> </RootSiblingParent>
</UnreadNotifsProvider> </ThemeProvider>
</LoggedOutViewProvider> </UnreadNotifsProvider>
</React.Fragment> </LoggedOutViewProvider>
</Splash> </React.Fragment>
</Splash>
</Alf>
</SafeAreaProvider> </SafeAreaProvider>
) )
} }
@@ -109,11 +116,15 @@ function App() {
<MutedThreadsProvider> <MutedThreadsProvider>
<InvitesStateProvider> <InvitesStateProvider>
<ModalStateProvider> <ModalStateProvider>
<LightboxStateProvider> <DialogStateProvider>
<I18nProvider> <LightboxStateProvider>
<InnerApp /> <I18nProvider>
</I18nProvider> <PortalProvider>
</LightboxStateProvider> <InnerApp />
</PortalProvider>
</I18nProvider>
</LightboxStateProvider>
</DialogStateProvider>
</ModalStateProvider> </ModalStateProvider>
</InvitesStateProvider> </InvitesStateProvider>
</MutedThreadsProvider> </MutedThreadsProvider>
+12 -6
View File
@@ -8,6 +8,7 @@ import {RootSiblingParent} from 'react-native-root-siblings'
import 'view/icons' import 'view/icons'
import {ThemeProvider as Alf} from '#/alf' import {ThemeProvider as Alf} from '#/alf'
import {useColorModeTheme} from '#/alf/util/useColorModeTheme'
import {init as initPersistedState} from '#/state/persisted' import {init as initPersistedState} from '#/state/persisted'
import {useColorMode} from 'state/shell' import {useColorMode} from 'state/shell'
import {Shell} from 'view/shell/index' import {Shell} from 'view/shell/index'
@@ -16,6 +17,7 @@ import {ThemeProvider} from 'lib/ThemeContext'
import {queryClient} from 'lib/react-query' import {queryClient} from 'lib/react-query'
import {Provider as ShellStateProvider} from 'state/shell' import {Provider as ShellStateProvider} from 'state/shell'
import {Provider as ModalStateProvider} from 'state/modals' import {Provider as ModalStateProvider} from 'state/modals'
import {Provider as DialogStateProvider} from 'state/dialogs'
import {Provider as LightboxStateProvider} from 'state/lightbox' import {Provider as LightboxStateProvider} from 'state/lightbox'
import {Provider as MutedThreadsProvider} from 'state/muted-threads' import {Provider as MutedThreadsProvider} from 'state/muted-threads'
import {Provider as InvitesStateProvider} from 'state/invites' import {Provider as InvitesStateProvider} from 'state/invites'
@@ -29,7 +31,7 @@ import {
} from 'state/session' } from 'state/session'
import {Provider as UnreadNotifsProvider} from 'state/queries/notifications/unread' import {Provider as UnreadNotifsProvider} from 'state/queries/notifications/unread'
import * as persisted from '#/state/persisted' import * as persisted from '#/state/persisted'
import {useColorModeTheme} from '#/alf/util/useColorModeTheme' import {Provider as PortalProvider} from '#/components/Portal'
function InnerApp() { function InnerApp() {
const {isInitialLoad, currentAccount} = useSession() const {isInitialLoad, currentAccount} = useSession()
@@ -92,11 +94,15 @@ function App() {
<MutedThreadsProvider> <MutedThreadsProvider>
<InvitesStateProvider> <InvitesStateProvider>
<ModalStateProvider> <ModalStateProvider>
<LightboxStateProvider> <DialogStateProvider>
<I18nProvider> <LightboxStateProvider>
<InnerApp /> <I18nProvider>
</I18nProvider> <PortalProvider>
</LightboxStateProvider> <InnerApp />
</PortalProvider>
</I18nProvider>
</LightboxStateProvider>
</DialogStateProvider>
</ModalStateProvider> </ModalStateProvider>
</InvitesStateProvider> </InvitesStateProvider>
</MutedThreadsProvider> </MutedThreadsProvider>
+4 -4
View File
@@ -61,7 +61,7 @@ import {ProfileListScreen} from './view/screens/ProfileList'
import {PostThreadScreen} from './view/screens/PostThread' import {PostThreadScreen} from './view/screens/PostThread'
import {PostLikedByScreen} from './view/screens/PostLikedBy' import {PostLikedByScreen} from './view/screens/PostLikedBy'
import {PostRepostedByScreen} from './view/screens/PostRepostedBy' import {PostRepostedByScreen} from './view/screens/PostRepostedBy'
import {DebugScreen} from './view/screens/DebugNew' import {Storybook} from './view/screens/Storybook'
import {LogScreen} from './view/screens/Log' import {LogScreen} from './view/screens/Log'
import {SupportScreen} from './view/screens/Support' import {SupportScreen} from './view/screens/Support'
import {PrivacyPolicyScreen} from './view/screens/PrivacyPolicy' import {PrivacyPolicyScreen} from './view/screens/PrivacyPolicy'
@@ -144,7 +144,7 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
name="Profile" name="Profile"
getComponent={() => ProfileScreen} getComponent={() => ProfileScreen}
options={({route}) => ({ options={({route}) => ({
title: title(msg`@${route.params.name}`), title: bskyTitle(`@${route.params.name}`, unreadCountLabel),
animation: 'none', animation: 'none',
})} })}
/> />
@@ -200,8 +200,8 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
/> />
<Stack.Screen <Stack.Screen
name="Debug" name="Debug"
getComponent={() => DebugScreen} getComponent={() => Storybook}
options={{title: title(msg`Debug`), requireAuth: true}} options={{title: title(msg`Storybook`), requireAuth: true}}
/> />
<Stack.Screen <Stack.Screen
name="Log" name="Log"
+266 -78
View File
@@ -4,6 +4,9 @@ export const atoms = {
/* /*
* Positioning * Positioning
*/ */
fixed: {
position: 'fixed',
},
absolute: { absolute: {
position: 'absolute', position: 'absolute',
}, },
@@ -32,6 +35,10 @@ export const atoms = {
zIndex: 50, zIndex: 50,
}, },
overflow_hidden: {
overflow: 'hidden',
},
/* /*
* Width * Width
*/ */
@@ -45,6 +52,12 @@ export const atoms = {
/* /*
* Border radius * Border radius
*/ */
rounded_2xs: {
borderRadius: tokens.borderRadius._2xs,
},
rounded_xs: {
borderRadius: tokens.borderRadius.xs,
},
rounded_sm: { rounded_sm: {
borderRadius: tokens.borderRadius.sm, borderRadius: tokens.borderRadius.sm,
}, },
@@ -58,8 +71,8 @@ export const atoms = {
/* /*
* Flex * Flex
*/ */
gap_xxs: { gap_2xs: {
gap: tokens.space.xxs, gap: tokens.space._2xs,
}, },
gap_xs: { gap_xs: {
gap: tokens.space.xs, gap: tokens.space.xs,
@@ -76,8 +89,17 @@ export const atoms = {
gap_xl: { gap_xl: {
gap: tokens.space.xl, gap: tokens.space.xl,
}, },
gap_xxl: { gap_2xl: {
gap: tokens.space.xxl, gap: tokens.space._2xl,
},
gap_3xl: {
gap: tokens.space._3xl,
},
gap_4xl: {
gap: tokens.space._4xl,
},
gap_5xl: {
gap: tokens.space._5xl,
}, },
flex: { flex: {
display: 'flex', display: 'flex',
@@ -125,9 +147,9 @@ export const atoms = {
text_right: { text_right: {
textAlign: 'right', textAlign: 'right',
}, },
text_xxs: { text_2xs: {
fontSize: tokens.fontSize.xxs, fontSize: tokens.fontSize._2xs,
lineHeight: tokens.fontSize.xxs, lineHeight: tokens.fontSize._2xs,
}, },
text_xs: { text_xs: {
fontSize: tokens.fontSize.xs, fontSize: tokens.fontSize.xs,
@@ -149,9 +171,21 @@ export const atoms = {
fontSize: tokens.fontSize.xl, fontSize: tokens.fontSize.xl,
lineHeight: tokens.fontSize.xl, lineHeight: tokens.fontSize.xl,
}, },
text_xxl: { text_2xl: {
fontSize: tokens.fontSize.xxl, fontSize: tokens.fontSize._2xl,
lineHeight: tokens.fontSize.xxl, lineHeight: tokens.fontSize._2xl,
},
text_3xl: {
fontSize: tokens.fontSize._3xl,
lineHeight: tokens.fontSize._3xl,
},
text_4xl: {
fontSize: tokens.fontSize._4xl,
lineHeight: tokens.fontSize._4xl,
},
text_5xl: {
fontSize: tokens.fontSize._5xl,
lineHeight: tokens.fontSize._5xl,
}, },
leading_tight: { leading_tight: {
lineHeight: 1.25, lineHeight: 1.25,
@@ -162,11 +196,8 @@ export const atoms = {
font_normal: { font_normal: {
fontWeight: tokens.fontWeight.normal, fontWeight: tokens.fontWeight.normal,
}, },
font_semibold: {
fontWeight: tokens.fontWeight.semibold,
},
font_bold: { font_bold: {
fontWeight: tokens.fontWeight.bold, fontWeight: tokens.fontWeight.semibold,
}, },
/* /*
@@ -182,11 +213,30 @@ export const atoms = {
borderBottomWidth: 1, borderBottomWidth: 1,
}, },
/*
* Shadow
*/
shadow_sm: {
shadowRadius: 8,
shadowOpacity: 0.1,
elevation: 8,
},
shadow_md: {
shadowRadius: 16,
shadowOpacity: 0.1,
elevation: 16,
},
shadow_lg: {
shadowRadius: 32,
shadowOpacity: 0.1,
elevation: 24,
},
/* /*
* Padding * Padding
*/ */
p_xxs: { p_2xs: {
padding: tokens.space.xxs, padding: tokens.space._2xs,
}, },
p_xs: { p_xs: {
padding: tokens.space.xs, padding: tokens.space.xs,
@@ -203,12 +253,21 @@ export const atoms = {
p_xl: { p_xl: {
padding: tokens.space.xl, padding: tokens.space.xl,
}, },
p_xxl: { p_2xl: {
padding: tokens.space.xxl, padding: tokens.space._2xl,
}, },
px_xxs: { p_3xl: {
paddingLeft: tokens.space.xxs, padding: tokens.space._3xl,
paddingRight: tokens.space.xxs, },
p_4xl: {
padding: tokens.space._4xl,
},
p_5xl: {
padding: tokens.space._5xl,
},
px_2xs: {
paddingLeft: tokens.space._2xs,
paddingRight: tokens.space._2xs,
}, },
px_xs: { px_xs: {
paddingLeft: tokens.space.xs, paddingLeft: tokens.space.xs,
@@ -230,13 +289,25 @@ export const atoms = {
paddingLeft: tokens.space.xl, paddingLeft: tokens.space.xl,
paddingRight: tokens.space.xl, paddingRight: tokens.space.xl,
}, },
px_xxl: { px_2xl: {
paddingLeft: tokens.space.xxl, paddingLeft: tokens.space._2xl,
paddingRight: tokens.space.xxl, paddingRight: tokens.space._2xl,
}, },
py_xxs: { px_3xl: {
paddingTop: tokens.space.xxs, paddingLeft: tokens.space._3xl,
paddingBottom: tokens.space.xxs, paddingRight: tokens.space._3xl,
},
px_4xl: {
paddingLeft: tokens.space._4xl,
paddingRight: tokens.space._4xl,
},
px_5xl: {
paddingLeft: tokens.space._5xl,
paddingRight: tokens.space._5xl,
},
py_2xs: {
paddingTop: tokens.space._2xs,
paddingBottom: tokens.space._2xs,
}, },
py_xs: { py_xs: {
paddingTop: tokens.space.xs, paddingTop: tokens.space.xs,
@@ -258,12 +329,24 @@ export const atoms = {
paddingTop: tokens.space.xl, paddingTop: tokens.space.xl,
paddingBottom: tokens.space.xl, paddingBottom: tokens.space.xl,
}, },
py_xxl: { py_2xl: {
paddingTop: tokens.space.xxl, paddingTop: tokens.space._2xl,
paddingBottom: tokens.space.xxl, paddingBottom: tokens.space._2xl,
}, },
pt_xxs: { py_3xl: {
paddingTop: tokens.space.xxs, paddingTop: tokens.space._3xl,
paddingBottom: tokens.space._3xl,
},
py_4xl: {
paddingTop: tokens.space._4xl,
paddingBottom: tokens.space._4xl,
},
py_5xl: {
paddingTop: tokens.space._5xl,
paddingBottom: tokens.space._5xl,
},
pt_2xs: {
paddingTop: tokens.space._2xs,
}, },
pt_xs: { pt_xs: {
paddingTop: tokens.space.xs, paddingTop: tokens.space.xs,
@@ -280,11 +363,20 @@ export const atoms = {
pt_xl: { pt_xl: {
paddingTop: tokens.space.xl, paddingTop: tokens.space.xl,
}, },
pt_xxl: { pt_2xl: {
paddingTop: tokens.space.xxl, paddingTop: tokens.space._2xl,
}, },
pb_xxs: { pt_3xl: {
paddingBottom: tokens.space.xxs, paddingTop: tokens.space._3xl,
},
pt_4xl: {
paddingTop: tokens.space._4xl,
},
pt_5xl: {
paddingTop: tokens.space._5xl,
},
pb_2xs: {
paddingBottom: tokens.space._2xs,
}, },
pb_xs: { pb_xs: {
paddingBottom: tokens.space.xs, paddingBottom: tokens.space.xs,
@@ -301,11 +393,20 @@ export const atoms = {
pb_xl: { pb_xl: {
paddingBottom: tokens.space.xl, paddingBottom: tokens.space.xl,
}, },
pb_xxl: { pb_2xl: {
paddingBottom: tokens.space.xxl, paddingBottom: tokens.space._2xl,
}, },
pl_xxs: { pb_3xl: {
paddingLeft: tokens.space.xxs, paddingBottom: tokens.space._3xl,
},
pb_4xl: {
paddingBottom: tokens.space._4xl,
},
pb_5xl: {
paddingBottom: tokens.space._5xl,
},
pl_2xs: {
paddingLeft: tokens.space._2xs,
}, },
pl_xs: { pl_xs: {
paddingLeft: tokens.space.xs, paddingLeft: tokens.space.xs,
@@ -322,11 +423,20 @@ export const atoms = {
pl_xl: { pl_xl: {
paddingLeft: tokens.space.xl, paddingLeft: tokens.space.xl,
}, },
pl_xxl: { pl_2xl: {
paddingLeft: tokens.space.xxl, paddingLeft: tokens.space._2xl,
}, },
pr_xxs: { pl_3xl: {
paddingRight: tokens.space.xxs, paddingLeft: tokens.space._3xl,
},
pl_4xl: {
paddingLeft: tokens.space._4xl,
},
pl_5xl: {
paddingLeft: tokens.space._5xl,
},
pr_2xs: {
paddingRight: tokens.space._2xs,
}, },
pr_xs: { pr_xs: {
paddingRight: tokens.space.xs, paddingRight: tokens.space.xs,
@@ -343,15 +453,24 @@ export const atoms = {
pr_xl: { pr_xl: {
paddingRight: tokens.space.xl, paddingRight: tokens.space.xl,
}, },
pr_xxl: { pr_2xl: {
paddingRight: tokens.space.xxl, paddingRight: tokens.space._2xl,
},
pr_3xl: {
paddingRight: tokens.space._3xl,
},
pr_4xl: {
paddingRight: tokens.space._4xl,
},
pr_5xl: {
paddingRight: tokens.space._5xl,
}, },
/* /*
* Margin * Margin
*/ */
m_xxs: { m_2xs: {
margin: tokens.space.xxs, margin: tokens.space._2xs,
}, },
m_xs: { m_xs: {
margin: tokens.space.xs, margin: tokens.space.xs,
@@ -368,12 +487,21 @@ export const atoms = {
m_xl: { m_xl: {
margin: tokens.space.xl, margin: tokens.space.xl,
}, },
m_xxl: { m_2xl: {
margin: tokens.space.xxl, margin: tokens.space._2xl,
}, },
mx_xxs: { m_3xl: {
marginLeft: tokens.space.xxs, margin: tokens.space._3xl,
marginRight: tokens.space.xxs, },
m_4xl: {
margin: tokens.space._4xl,
},
m_5xl: {
margin: tokens.space._5xl,
},
mx_2xs: {
marginLeft: tokens.space._2xs,
marginRight: tokens.space._2xs,
}, },
mx_xs: { mx_xs: {
marginLeft: tokens.space.xs, marginLeft: tokens.space.xs,
@@ -395,13 +523,25 @@ export const atoms = {
marginLeft: tokens.space.xl, marginLeft: tokens.space.xl,
marginRight: tokens.space.xl, marginRight: tokens.space.xl,
}, },
mx_xxl: { mx_2xl: {
marginLeft: tokens.space.xxl, marginLeft: tokens.space._2xl,
marginRight: tokens.space.xxl, marginRight: tokens.space._2xl,
}, },
my_xxs: { mx_3xl: {
marginTop: tokens.space.xxs, marginLeft: tokens.space._3xl,
marginBottom: tokens.space.xxs, marginRight: tokens.space._3xl,
},
mx_4xl: {
marginLeft: tokens.space._4xl,
marginRight: tokens.space._4xl,
},
mx_5xl: {
marginLeft: tokens.space._5xl,
marginRight: tokens.space._5xl,
},
my_2xs: {
marginTop: tokens.space._2xs,
marginBottom: tokens.space._2xs,
}, },
my_xs: { my_xs: {
marginTop: tokens.space.xs, marginTop: tokens.space.xs,
@@ -423,12 +563,24 @@ export const atoms = {
marginTop: tokens.space.xl, marginTop: tokens.space.xl,
marginBottom: tokens.space.xl, marginBottom: tokens.space.xl,
}, },
my_xxl: { my_2xl: {
marginTop: tokens.space.xxl, marginTop: tokens.space._2xl,
marginBottom: tokens.space.xxl, marginBottom: tokens.space._2xl,
}, },
mt_xxs: { my_3xl: {
marginTop: tokens.space.xxs, marginTop: tokens.space._3xl,
marginBottom: tokens.space._3xl,
},
my_4xl: {
marginTop: tokens.space._4xl,
marginBottom: tokens.space._4xl,
},
my_5xl: {
marginTop: tokens.space._5xl,
marginBottom: tokens.space._5xl,
},
mt_2xs: {
marginTop: tokens.space._2xs,
}, },
mt_xs: { mt_xs: {
marginTop: tokens.space.xs, marginTop: tokens.space.xs,
@@ -445,11 +597,20 @@ export const atoms = {
mt_xl: { mt_xl: {
marginTop: tokens.space.xl, marginTop: tokens.space.xl,
}, },
mt_xxl: { mt_2xl: {
marginTop: tokens.space.xxl, marginTop: tokens.space._2xl,
}, },
mb_xxs: { mt_3xl: {
marginBottom: tokens.space.xxs, marginTop: tokens.space._3xl,
},
mt_4xl: {
marginTop: tokens.space._4xl,
},
mt_5xl: {
marginTop: tokens.space._5xl,
},
mb_2xs: {
marginBottom: tokens.space._2xs,
}, },
mb_xs: { mb_xs: {
marginBottom: tokens.space.xs, marginBottom: tokens.space.xs,
@@ -466,11 +627,20 @@ export const atoms = {
mb_xl: { mb_xl: {
marginBottom: tokens.space.xl, marginBottom: tokens.space.xl,
}, },
mb_xxl: { mb_2xl: {
marginBottom: tokens.space.xxl, marginBottom: tokens.space._2xl,
}, },
ml_xxs: { mb_3xl: {
marginLeft: tokens.space.xxs, marginBottom: tokens.space._3xl,
},
mb_4xl: {
marginBottom: tokens.space._4xl,
},
mb_5xl: {
marginBottom: tokens.space._5xl,
},
ml_2xs: {
marginLeft: tokens.space._2xs,
}, },
ml_xs: { ml_xs: {
marginLeft: tokens.space.xs, marginLeft: tokens.space.xs,
@@ -487,11 +657,20 @@ export const atoms = {
ml_xl: { ml_xl: {
marginLeft: tokens.space.xl, marginLeft: tokens.space.xl,
}, },
ml_xxl: { ml_2xl: {
marginLeft: tokens.space.xxl, marginLeft: tokens.space._2xl,
}, },
mr_xxs: { ml_3xl: {
marginRight: tokens.space.xxs, marginLeft: tokens.space._3xl,
},
ml_4xl: {
marginLeft: tokens.space._4xl,
},
ml_5xl: {
marginLeft: tokens.space._5xl,
},
mr_2xs: {
marginRight: tokens.space._2xs,
}, },
mr_xs: { mr_xs: {
marginRight: tokens.space.xs, marginRight: tokens.space.xs,
@@ -508,7 +687,16 @@ export const atoms = {
mr_xl: { mr_xl: {
marginRight: tokens.space.xl, marginRight: tokens.space.xl,
}, },
mr_xxl: { mr_2xl: {
marginRight: tokens.space.xxl, marginRight: tokens.space._2xl,
},
mr_3xl: {
marginRight: tokens.space._3xl,
},
mr_4xl: {
marginRight: tokens.space._4xl,
},
mr_5xl: {
marginRight: tokens.space._5xl,
}, },
} as const } as const
+1
View File
@@ -5,6 +5,7 @@ import * as themes from '#/alf/themes'
export * as tokens from '#/alf/tokens' export * as tokens from '#/alf/tokens'
export {atoms} from '#/alf/atoms' export {atoms} from '#/alf/atoms'
export * from '#/alf/util/platform' export * from '#/alf/util/platform'
export * from '#/alf/util/flatten'
type BreakpointName = keyof typeof breakpoints type BreakpointName = keyof typeof breakpoints
+259 -47
View File
@@ -1,108 +1,320 @@
import * as tokens from '#/alf/tokens' import * as tokens from '#/alf/tokens'
import type {Mutable} from '#/alf/types' import type {Mutable} from '#/alf/types'
import {atoms} from '#/alf/atoms'
export type ThemeName = 'light' | 'dark' export type ThemeName = 'light' | 'dim' | 'dark'
export type ReadonlyTheme = typeof light export type ReadonlyTheme = typeof light
export type Theme = Mutable<ReadonlyTheme> export type Theme = Mutable<ReadonlyTheme>
export type ReadonlyPalette = typeof lightPalette
export type Palette = Mutable<ReadonlyPalette>
export type Palette = { export const lightPalette = {
primary: string white: tokens.color.gray_0,
positive: string black: tokens.color.gray_1000,
negative: string
}
export const lightPalette: Palette = { contrast_25: tokens.color.gray_25,
primary: tokens.color.blue_500, contrast_50: tokens.color.gray_50,
positive: tokens.color.green_500, contrast_100: tokens.color.gray_100,
negative: tokens.color.red_500, contrast_200: tokens.color.gray_200,
contrast_300: tokens.color.gray_300,
contrast_400: tokens.color.gray_400,
contrast_500: tokens.color.gray_500,
contrast_600: tokens.color.gray_600,
contrast_700: tokens.color.gray_700,
contrast_800: tokens.color.gray_800,
contrast_900: tokens.color.gray_900,
contrast_950: tokens.color.gray_950,
contrast_975: tokens.color.gray_975,
primary_25: tokens.color.blue_25,
primary_50: tokens.color.blue_50,
primary_100: tokens.color.blue_100,
primary_200: tokens.color.blue_200,
primary_300: tokens.color.blue_300,
primary_400: tokens.color.blue_400,
primary_500: tokens.color.blue_500,
primary_600: tokens.color.blue_600,
primary_700: tokens.color.blue_700,
primary_800: tokens.color.blue_800,
primary_900: tokens.color.blue_900,
primary_950: tokens.color.blue_950,
primary_975: tokens.color.blue_975,
positive_25: tokens.color.green_25,
positive_50: tokens.color.green_50,
positive_100: tokens.color.green_100,
positive_200: tokens.color.green_200,
positive_300: tokens.color.green_300,
positive_400: tokens.color.green_400,
positive_500: tokens.color.green_500,
positive_600: tokens.color.green_600,
positive_700: tokens.color.green_700,
positive_800: tokens.color.green_800,
positive_900: tokens.color.green_900,
positive_950: tokens.color.green_950,
positive_975: tokens.color.green_975,
negative_25: tokens.color.red_25,
negative_50: tokens.color.red_50,
negative_100: tokens.color.red_100,
negative_200: tokens.color.red_200,
negative_300: tokens.color.red_300,
negative_400: tokens.color.red_400,
negative_500: tokens.color.red_500,
negative_600: tokens.color.red_600,
negative_700: tokens.color.red_700,
negative_800: tokens.color.red_800,
negative_900: tokens.color.red_900,
negative_950: tokens.color.red_950,
negative_975: tokens.color.red_975,
} as const } as const
export const darkPalette: Palette = { export const darkPalette: Palette = {
primary: tokens.color.blue_500, white: tokens.color.gray_0,
positive: tokens.color.green_400, black: tokens.color.gray_1000,
negative: tokens.color.red_400,
contrast_25: tokens.color.gray_975,
contrast_50: tokens.color.gray_950,
contrast_100: tokens.color.gray_900,
contrast_200: tokens.color.gray_800,
contrast_300: tokens.color.gray_700,
contrast_400: tokens.color.gray_600,
contrast_500: tokens.color.gray_500,
contrast_600: tokens.color.gray_400,
contrast_700: tokens.color.gray_300,
contrast_800: tokens.color.gray_200,
contrast_900: tokens.color.gray_100,
contrast_950: tokens.color.gray_50,
contrast_975: tokens.color.gray_25,
primary_25: tokens.color.blue_25,
primary_50: tokens.color.blue_50,
primary_100: tokens.color.blue_100,
primary_200: tokens.color.blue_200,
primary_300: tokens.color.blue_300,
primary_400: tokens.color.blue_400,
primary_500: tokens.color.blue_500,
primary_600: tokens.color.blue_600,
primary_700: tokens.color.blue_700,
primary_800: tokens.color.blue_800,
primary_900: tokens.color.blue_900,
primary_950: tokens.color.blue_950,
primary_975: tokens.color.blue_975,
positive_25: tokens.color.green_25,
positive_50: tokens.color.green_50,
positive_100: tokens.color.green_100,
positive_200: tokens.color.green_200,
positive_300: tokens.color.green_300,
positive_400: tokens.color.green_400,
positive_500: tokens.color.green_500,
positive_600: tokens.color.green_600,
positive_700: tokens.color.green_700,
positive_800: tokens.color.green_800,
positive_900: tokens.color.green_900,
positive_950: tokens.color.green_950,
positive_975: tokens.color.green_975,
negative_25: tokens.color.red_25,
negative_50: tokens.color.red_50,
negative_100: tokens.color.red_100,
negative_200: tokens.color.red_200,
negative_300: tokens.color.red_300,
negative_400: tokens.color.red_400,
negative_500: tokens.color.red_500,
negative_600: tokens.color.red_600,
negative_700: tokens.color.red_700,
negative_800: tokens.color.red_800,
negative_900: tokens.color.red_900,
negative_950: tokens.color.red_950,
negative_975: tokens.color.red_975,
} as const } as const
export const light = { export const light = {
name: 'light',
palette: lightPalette, palette: lightPalette,
atoms: { atoms: {
text: { text: {
color: tokens.color.gray_1000, color: lightPalette.black,
}, },
text_contrast_700: { text_contrast_700: {
color: tokens.color.gray_700, color: lightPalette.contrast_700,
},
text_contrast_600: {
color: lightPalette.contrast_600,
}, },
text_contrast_500: { text_contrast_500: {
color: tokens.color.gray_500, color: lightPalette.contrast_500,
},
text_contrast_400: {
color: lightPalette.contrast_400,
}, },
text_inverted: { text_inverted: {
color: tokens.color.white, color: lightPalette.white,
}, },
bg: { bg: {
backgroundColor: tokens.color.white, backgroundColor: lightPalette.white,
},
bg_contrast_25: {
backgroundColor: lightPalette.contrast_25,
},
bg_contrast_50: {
backgroundColor: lightPalette.contrast_50,
}, },
bg_contrast_100: { bg_contrast_100: {
backgroundColor: tokens.color.gray_100, backgroundColor: lightPalette.contrast_100,
}, },
bg_contrast_200: { bg_contrast_200: {
backgroundColor: tokens.color.gray_200, backgroundColor: lightPalette.contrast_200,
}, },
bg_contrast_300: { bg_contrast_300: {
backgroundColor: tokens.color.gray_300, backgroundColor: lightPalette.contrast_300,
},
bg_positive: {
backgroundColor: tokens.color.green_500,
},
bg_negative: {
backgroundColor: tokens.color.red_400,
}, },
border: { border: {
borderColor: tokens.color.gray_200, borderColor: lightPalette.contrast_100,
}, },
border_contrast_500: { border_contrast: {
borderColor: tokens.color.gray_500, borderColor: lightPalette.contrast_400,
},
shadow_sm: {
...atoms.shadow_sm,
shadowColor: lightPalette.black,
},
shadow_md: {
...atoms.shadow_md,
shadowColor: lightPalette.black,
},
shadow_lg: {
...atoms.shadow_lg,
shadowColor: lightPalette.black,
},
},
}
export const dim: Theme = {
name: 'dim',
palette: darkPalette,
atoms: {
text: {
color: darkPalette.white,
},
text_contrast_700: {
color: darkPalette.contrast_800,
},
text_contrast_600: {
color: darkPalette.contrast_700,
},
text_contrast_500: {
color: darkPalette.contrast_600,
},
text_contrast_400: {
color: darkPalette.contrast_500,
},
text_inverted: {
color: darkPalette.black,
},
bg: {
backgroundColor: darkPalette.contrast_50,
},
bg_contrast_25: {
backgroundColor: darkPalette.contrast_100,
},
bg_contrast_50: {
backgroundColor: darkPalette.contrast_200,
},
bg_contrast_100: {
backgroundColor: darkPalette.contrast_300,
},
bg_contrast_200: {
backgroundColor: darkPalette.contrast_400,
},
bg_contrast_300: {
backgroundColor: darkPalette.contrast_500,
},
border: {
borderColor: darkPalette.contrast_200,
},
border_contrast: {
borderColor: darkPalette.contrast_400,
},
shadow_sm: {
...atoms.shadow_sm,
shadowOpacity: 0.7,
shadowColor: tokens.color.trueBlack,
},
shadow_md: {
...atoms.shadow_md,
shadowOpacity: 0.7,
shadowColor: tokens.color.trueBlack,
},
shadow_lg: {
...atoms.shadow_lg,
shadowOpacity: 0.7,
shadowColor: tokens.color.trueBlack,
}, },
}, },
} }
export const dark: Theme = { export const dark: Theme = {
name: 'dark',
palette: darkPalette, palette: darkPalette,
atoms: { atoms: {
text: { text: {
color: tokens.color.white, color: darkPalette.white,
}, },
text_contrast_700: { text_contrast_700: {
color: tokens.color.gray_300, color: darkPalette.contrast_700,
},
text_contrast_600: {
color: darkPalette.contrast_600,
}, },
text_contrast_500: { text_contrast_500: {
color: tokens.color.gray_500, color: darkPalette.contrast_500,
},
text_contrast_400: {
color: darkPalette.contrast_400,
}, },
text_inverted: { text_inverted: {
color: tokens.color.gray_1000, color: darkPalette.black,
}, },
bg: { bg: {
backgroundColor: tokens.color.gray_1000, backgroundColor: darkPalette.black,
},
bg_contrast_25: {
backgroundColor: darkPalette.contrast_50,
},
bg_contrast_50: {
backgroundColor: darkPalette.contrast_100,
}, },
bg_contrast_100: { bg_contrast_100: {
backgroundColor: tokens.color.gray_900, backgroundColor: darkPalette.contrast_200,
}, },
bg_contrast_200: { bg_contrast_200: {
backgroundColor: tokens.color.gray_800, backgroundColor: darkPalette.contrast_300,
}, },
bg_contrast_300: { bg_contrast_300: {
backgroundColor: tokens.color.gray_700, backgroundColor: darkPalette.contrast_400,
},
bg_positive: {
backgroundColor: tokens.color.green_400,
},
bg_negative: {
backgroundColor: tokens.color.red_400,
}, },
border: { border: {
borderColor: tokens.color.gray_800, borderColor: darkPalette.contrast_100,
}, },
border_contrast_500: { border_contrast: {
borderColor: tokens.color.gray_500, borderColor: darkPalette.contrast_300,
},
shadow_sm: {
...atoms.shadow_sm,
shadowOpacity: 0.7,
shadowColor: tokens.color.trueBlack,
},
shadow_md: {
...atoms.shadow_md,
shadowOpacity: 0.7,
shadowColor: tokens.color.trueBlack,
},
shadow_lg: {
...atoms.shadow_lg,
shadowOpacity: 0.7,
shadowColor: tokens.color.trueBlack,
}, },
}, },
} }
+121 -53
View File
@@ -1,79 +1,95 @@
const BLUE_HUE = 211 const BLUE_HUE = 211
const GRAYSCALE_SATURATION = 22 const RED_HUE = 346
const GREEN_HUE = 152
export const color = { export const color = {
white: '#FFFFFF', trueBlack: '#000000',
gray_0: `hsl(${BLUE_HUE}, ${GRAYSCALE_SATURATION}%, 100%)`, gray_0: `hsl(${BLUE_HUE}, 20%, 100%)`,
gray_100: `hsl(${BLUE_HUE}, ${GRAYSCALE_SATURATION}%, 95%)`, gray_25: `hsl(${BLUE_HUE}, 20%, 97%)`,
gray_200: `hsl(${BLUE_HUE}, ${GRAYSCALE_SATURATION}%, 85%)`, gray_50: `hsl(${BLUE_HUE}, 20%, 95%)`,
gray_300: `hsl(${BLUE_HUE}, ${GRAYSCALE_SATURATION}%, 75%)`, gray_100: `hsl(${BLUE_HUE}, 20%, 90%)`,
gray_400: `hsl(${BLUE_HUE}, ${GRAYSCALE_SATURATION}%, 65%)`, gray_200: `hsl(${BLUE_HUE}, 20%, 80%)`,
gray_500: `hsl(${BLUE_HUE}, ${GRAYSCALE_SATURATION}%, 55%)`, gray_300: `hsl(${BLUE_HUE}, 20%, 70%)`,
gray_600: `hsl(${BLUE_HUE}, ${GRAYSCALE_SATURATION}%, 45%)`, gray_400: `hsl(${BLUE_HUE}, 20%, 60%)`,
gray_700: `hsl(${BLUE_HUE}, ${GRAYSCALE_SATURATION}%, 35%)`, gray_500: `hsl(${BLUE_HUE}, 20%, 50%)`,
gray_800: `hsl(${BLUE_HUE}, ${GRAYSCALE_SATURATION}%, 25%)`, gray_600: `hsl(${BLUE_HUE}, 20%, 42%)`,
gray_900: `hsl(${BLUE_HUE}, ${GRAYSCALE_SATURATION}%, 15%)`, gray_700: `hsl(${BLUE_HUE}, 20%, 34%)`,
gray_1000: `hsl(${BLUE_HUE}, ${GRAYSCALE_SATURATION}%, 5%)`, gray_800: `hsl(${BLUE_HUE}, 20%, 26%)`,
gray_900: `hsl(${BLUE_HUE}, 20%, 18%)`,
gray_950: `hsl(${BLUE_HUE}, 20%, 10%)`,
gray_975: `hsl(${BLUE_HUE}, 20%, 7%)`,
gray_1000: `hsl(${BLUE_HUE}, 20%, 4%)`,
blue_0: `hsl(${BLUE_HUE}, 99%, 100%)`, blue_25: `hsl(${BLUE_HUE}, 99%, 97%)`,
blue_100: `hsl(${BLUE_HUE}, 99%, 93%)`, blue_50: `hsl(${BLUE_HUE}, 99%, 95%)`,
blue_200: `hsl(${BLUE_HUE}, 99%, 83%)`, blue_100: `hsl(${BLUE_HUE}, 99%, 90%)`,
blue_300: `hsl(${BLUE_HUE}, 99%, 73%)`, blue_200: `hsl(${BLUE_HUE}, 99%, 80%)`,
blue_400: `hsl(${BLUE_HUE}, 99%, 63%)`, blue_300: `hsl(${BLUE_HUE}, 99%, 70%)`,
blue_400: `hsl(${BLUE_HUE}, 99%, 60%)`,
blue_500: `hsl(${BLUE_HUE}, 99%, 53%)`, blue_500: `hsl(${BLUE_HUE}, 99%, 53%)`,
blue_600: `hsl(${BLUE_HUE}, 99%, 43%)`, blue_600: `hsl(${BLUE_HUE}, 99%, 42%)`,
blue_700: `hsl(${BLUE_HUE}, 99%, 33%)`, blue_700: `hsl(${BLUE_HUE}, 99%, 34%)`,
blue_800: `hsl(${BLUE_HUE}, 99%, 23%)`, blue_800: `hsl(${BLUE_HUE}, 99%, 26%)`,
blue_900: `hsl(${BLUE_HUE}, 99%, 13%)`, blue_900: `hsl(${BLUE_HUE}, 99%, 18%)`,
blue_1000: `hsl(${BLUE_HUE}, 99%, 8%)`, blue_950: `hsl(${BLUE_HUE}, 99%, 10%)`,
blue_975: `hsl(${BLUE_HUE}, 99%, 7%)`,
green_0: `hsl(130, 60%, 100%)`, green_25: `hsl(${GREEN_HUE}, 82%, 97%)`,
green_100: `hsl(130, 60%, 95%)`, green_50: `hsl(${GREEN_HUE}, 82%, 95%)`,
green_200: `hsl(130, 60%, 85%)`, green_100: `hsl(${GREEN_HUE}, 82%, 90%)`,
green_300: `hsl(130, 60%, 75%)`, green_200: `hsl(${GREEN_HUE}, 82%, 80%)`,
green_400: `hsl(130, 60%, 65%)`, green_300: `hsl(${GREEN_HUE}, 82%, 70%)`,
green_500: `hsl(130, 60%, 55%)`, green_400: `hsl(${GREEN_HUE}, 82%, 60%)`,
green_600: `hsl(130, 60%, 45%)`, green_500: `hsl(${GREEN_HUE}, 82%, 50%)`,
green_700: `hsl(130, 60%, 35%)`, green_600: `hsl(${GREEN_HUE}, 82%, 42%)`,
green_800: `hsl(130, 60%, 25%)`, green_700: `hsl(${GREEN_HUE}, 82%, 34%)`,
green_900: `hsl(130, 60%, 15%)`, green_800: `hsl(${GREEN_HUE}, 82%, 26%)`,
green_1000: `hsl(130, 60%, 5%)`, green_900: `hsl(${GREEN_HUE}, 82%, 18%)`,
green_950: `hsl(${GREEN_HUE}, 82%, 10%)`,
green_975: `hsl(${GREEN_HUE}, 82%, 7%)`,
red_0: `hsl(349, 96%, 100%)`, red_25: `hsl(${RED_HUE}, 91%, 97%)`,
red_100: `hsl(349, 96%, 95%)`, red_50: `hsl(${RED_HUE}, 91%, 95%)`,
red_200: `hsl(349, 96%, 85%)`, red_100: `hsl(${RED_HUE}, 91%, 90%)`,
red_300: `hsl(349, 96%, 75%)`, red_200: `hsl(${RED_HUE}, 91%, 80%)`,
red_400: `hsl(349, 96%, 65%)`, red_300: `hsl(${RED_HUE}, 91%, 70%)`,
red_500: `hsl(349, 96%, 55%)`, red_400: `hsl(${RED_HUE}, 91%, 60%)`,
red_600: `hsl(349, 96%, 45%)`, red_500: `hsl(${RED_HUE}, 91%, 50%)`,
red_700: `hsl(349, 96%, 35%)`, red_600: `hsl(${RED_HUE}, 91%, 42%)`,
red_800: `hsl(349, 96%, 25%)`, red_700: `hsl(${RED_HUE}, 91%, 34%)`,
red_900: `hsl(349, 96%, 15%)`, red_800: `hsl(${RED_HUE}, 91%, 26%)`,
red_1000: `hsl(349, 96%, 5%)`, red_900: `hsl(${RED_HUE}, 91%, 18%)`,
red_950: `hsl(${RED_HUE}, 91%, 10%)`,
red_975: `hsl(${RED_HUE}, 91%, 7%)`,
} as const } as const
export const space = { export const space = {
xxs: 2, _2xs: 2,
xs: 4, xs: 4,
sm: 8, sm: 8,
md: 12, md: 12,
lg: 18, lg: 16,
xl: 24, xl: 20,
xxl: 32, _2xl: 24,
_3xl: 28,
_4xl: 32,
_5xl: 40,
} as const } as const
export const fontSize = { export const fontSize = {
xxs: 10, _2xs: 10,
xs: 12, xs: 12,
sm: 14, sm: 14,
md: 16, md: 16,
lg: 18, lg: 18,
xl: 22, xl: 20,
xxl: 26, _2xl: 22,
_3xl: 26,
_4xl: 32,
_5xl: 40,
} as const } as const
// TODO test
export const lineHeight = { export const lineHeight = {
none: 1, none: 1,
normal: 1.5, normal: 1.5,
@@ -81,6 +97,8 @@ export const lineHeight = {
} as const } as const
export const borderRadius = { export const borderRadius = {
_2xs: 2,
xs: 4,
sm: 8, sm: 8,
md: 12, md: 12,
full: 999, full: 999,
@@ -92,6 +110,56 @@ export const fontWeight = {
bold: '900', bold: '900',
} as const } as const
export const gradients = {
sky: {
values: [
[0, '#0A7AFF'],
[1, '#59B9FF'],
],
hover_value: '#0A7AFF',
},
midnight: {
values: [
[0, '#022C5E'],
[1, '#4079BC'],
],
hover_value: '#022C5E',
},
sunrise: {
values: [
[0, '#4E90AE'],
[0.4, '#AEA3AB'],
[0.8, '#E6A98F'],
[1, '#F3A84C'],
],
hover_value: '#AEA3AB',
},
sunset: {
values: [
[0, '#6772AF'],
[0.6, '#B88BB6'],
[1, '#FFA6AC'],
],
hover_value: '#B88BB6',
},
nordic: {
values: [
[0, '#083367'],
[1, '#9EE8C1'],
],
hover_value: '#3A7085',
},
bonfire: {
values: [
[0, '#203E4E'],
[0.4, '#755B62'],
[0.8, '#CD7765'],
[1, '#EF956E'],
],
hover_value: '#755B62',
},
} as const
export type Color = keyof typeof color export type Color = keyof typeof color
export type Space = keyof typeof space export type Space = keyof typeof space
export type FontSize = keyof typeof fontSize export type FontSize = keyof typeof fontSize
+3
View File
@@ -0,0 +1,3 @@
import {StyleSheet} from 'react-native'
export const flatten = StyleSheet.flatten
+507
View File
@@ -0,0 +1,507 @@
import React from 'react'
import {
Pressable,
Text,
PressableProps,
TextProps,
ViewStyle,
AccessibilityProps,
View,
TextStyle,
StyleSheet,
} from 'react-native'
import LinearGradient from 'react-native-linear-gradient'
import {useTheme, atoms as a, tokens, web, native} from '#/alf'
import {Props as SVGIconProps} from '#/components/icons/common'
export type ButtonVariant = 'solid' | 'outline' | 'ghost' | 'gradient'
export type ButtonColor =
| 'primary'
| 'secondary'
| 'negative'
| 'gradient_sky'
| 'gradient_midnight'
| 'gradient_sunrise'
| 'gradient_sunset'
| 'gradient_nordic'
| 'gradient_bonfire'
export type ButtonSize = 'small' | 'large'
export type VariantProps = {
/**
* The style variation of the button
*/
variant?: ButtonVariant
/**
* The color of the button
*/
color?: ButtonColor
/**
* The size of the button
*/
size?: ButtonSize
}
export type ButtonProps = React.PropsWithChildren<
Pick<PressableProps, 'disabled' | 'onPress'> &
AccessibilityProps &
VariantProps & {
label: string
}
>
export type ButtonTextProps = TextProps & VariantProps & {disabled?: boolean}
const Context = React.createContext<
VariantProps & {
hovered: boolean
focused: boolean
pressed: boolean
disabled: boolean
}
>({
hovered: false,
focused: false,
pressed: false,
disabled: false,
})
export function useButtonContext() {
return React.useContext(Context)
}
export function Button({
children,
variant,
color,
size,
label,
disabled = false,
...rest
}: ButtonProps) {
const t = useTheme()
const [state, setState] = React.useState({
pressed: false,
hovered: false,
focused: false,
})
const onPressIn = React.useCallback(() => {
setState(s => ({
...s,
pressed: true,
}))
}, [setState])
const onPressOut = React.useCallback(() => {
setState(s => ({
...s,
pressed: false,
}))
}, [setState])
const onHoverIn = React.useCallback(() => {
setState(s => ({
...s,
hovered: true,
}))
}, [setState])
const onHoverOut = React.useCallback(() => {
setState(s => ({
...s,
hovered: false,
}))
}, [setState])
const onFocus = React.useCallback(() => {
setState(s => ({
...s,
focused: true,
}))
}, [setState])
const onBlur = React.useCallback(() => {
setState(s => ({
...s,
focused: false,
}))
}, [setState])
const {baseStyles, hoverStyles, focusStyles} = React.useMemo(() => {
const baseStyles: ViewStyle[] = []
const hoverStyles: ViewStyle[] = []
const light = t.name === 'light'
if (color === 'primary') {
if (variant === 'solid') {
if (!disabled) {
baseStyles.push({
backgroundColor: t.palette.primary_500,
})
hoverStyles.push({
backgroundColor: t.palette.primary_600,
})
} else {
baseStyles.push({
backgroundColor: t.palette.primary_700,
})
}
} else if (variant === 'outline') {
baseStyles.push(a.border, t.atoms.bg, {
borderWidth: 1,
})
if (!disabled) {
baseStyles.push(a.border, {
borderColor: tokens.color.blue_500,
})
hoverStyles.push(a.border, {
backgroundColor: light
? t.palette.primary_50
: t.palette.primary_950,
})
} else {
baseStyles.push(a.border, {
borderColor: light ? tokens.color.blue_200 : tokens.color.blue_900,
})
}
} else if (variant === 'ghost') {
if (!disabled) {
baseStyles.push(t.atoms.bg)
hoverStyles.push({
backgroundColor: light
? t.palette.primary_100
: t.palette.primary_900,
})
}
}
} else if (color === 'secondary') {
if (variant === 'solid') {
if (!disabled) {
baseStyles.push({
backgroundColor: light
? tokens.color.gray_100
: tokens.color.gray_900,
})
hoverStyles.push({
backgroundColor: light
? tokens.color.gray_200
: tokens.color.gray_950,
})
} else {
baseStyles.push({
backgroundColor: light
? tokens.color.gray_300
: tokens.color.gray_950,
})
}
} else if (variant === 'outline') {
baseStyles.push(a.border, t.atoms.bg, {
borderWidth: 1,
})
if (!disabled) {
baseStyles.push(a.border, {
borderColor: light ? tokens.color.gray_500 : tokens.color.gray_500,
})
hoverStyles.push(a.border, t.atoms.bg_contrast_50)
} else {
baseStyles.push(a.border, {
borderColor: light ? tokens.color.gray_200 : tokens.color.gray_800,
})
}
} else if (variant === 'ghost') {
if (!disabled) {
baseStyles.push(t.atoms.bg)
hoverStyles.push({
backgroundColor: light
? tokens.color.gray_100
: tokens.color.gray_900,
})
}
}
} else if (color === 'negative') {
if (variant === 'solid') {
if (!disabled) {
baseStyles.push({
backgroundColor: t.palette.negative_400,
})
hoverStyles.push({
backgroundColor: t.palette.negative_500,
})
} else {
baseStyles.push({
backgroundColor: t.palette.negative_600,
})
}
} else if (variant === 'outline') {
baseStyles.push(a.border, t.atoms.bg, {
borderWidth: 1,
})
if (!disabled) {
baseStyles.push(a.border, {
borderColor: t.palette.negative_400,
})
hoverStyles.push(a.border, {
backgroundColor: light
? t.palette.negative_50
: t.palette.negative_975,
})
} else {
baseStyles.push(a.border, {
borderColor: light
? t.palette.negative_200
: t.palette.negative_900,
})
}
} else if (variant === 'ghost') {
if (!disabled) {
baseStyles.push(t.atoms.bg)
hoverStyles.push({
backgroundColor: light
? t.palette.negative_100
: t.palette.negative_950,
})
}
}
}
if (size === 'large') {
baseStyles.push({paddingVertical: 15}, a.px_2xl, a.rounded_sm, a.gap_sm)
} else if (size === 'small') {
baseStyles.push({paddingVertical: 9}, a.px_md, a.rounded_sm, a.gap_sm)
}
return {
baseStyles,
hoverStyles,
focusStyles: [
...hoverStyles,
{
outline: 0,
} as ViewStyle,
],
}
}, [t, variant, color, size, disabled])
const {gradientColors, gradientHoverColors, gradientLocations} =
React.useMemo(() => {
const colors: string[] = []
const hoverColors: string[] = []
const locations: number[] = []
const gradient = {
primary: tokens.gradients.sky,
secondary: tokens.gradients.sky,
negative: tokens.gradients.sky,
gradient_sky: tokens.gradients.sky,
gradient_midnight: tokens.gradients.midnight,
gradient_sunrise: tokens.gradients.sunrise,
gradient_sunset: tokens.gradients.sunset,
gradient_nordic: tokens.gradients.nordic,
gradient_bonfire: tokens.gradients.bonfire,
}[color || 'primary']
if (variant === 'gradient') {
colors.push(...gradient.values.map(([_, color]) => color))
hoverColors.push(...gradient.values.map(_ => gradient.hover_value))
locations.push(...gradient.values.map(([location, _]) => location))
}
return {
gradientColors: colors,
gradientHoverColors: hoverColors,
gradientLocations: locations,
}
}, [variant, color])
const context = React.useMemo(
() => ({
...state,
variant,
color,
size,
disabled: disabled || false,
}),
[state, variant, color, size, disabled],
)
return (
<Pressable
role="button"
accessibilityHint={undefined} // optional
{...rest}
aria-label={label}
aria-pressed={state.pressed}
accessibilityLabel={label}
disabled={disabled || false}
accessibilityState={{
disabled: disabled || false,
}}
style={[
a.flex_row,
a.align_center,
a.overflow_hidden,
...baseStyles,
...(state.hovered || state.pressed ? hoverStyles : []),
...(state.focused ? focusStyles : []),
]}
onPressIn={onPressIn}
onPressOut={onPressOut}
onHoverIn={onHoverIn}
onHoverOut={onHoverOut}
onFocus={onFocus}
onBlur={onBlur}>
{variant === 'gradient' && (
<LinearGradient
colors={
state.hovered || state.pressed || state.focused
? gradientHoverColors
: gradientColors
}
locations={gradientLocations}
start={{x: 0, y: 0}}
end={{x: 1, y: 1}}
style={[a.absolute, a.inset_0]}
/>
)}
<Context.Provider value={context}>
{typeof children === 'string' ? (
<ButtonText>{children}</ButtonText>
) : (
children
)}
</Context.Provider>
</Pressable>
)
}
export function useSharedButtonTextStyles() {
const t = useTheme()
const {color, variant, disabled, size} = useButtonContext()
return React.useMemo(() => {
const baseStyles: TextStyle[] = []
const light = t.name === 'light'
if (color === 'primary') {
if (variant === 'solid') {
if (!disabled) {
baseStyles.push({color: t.palette.white})
} else {
baseStyles.push({color: t.palette.white, opacity: 0.5})
}
} else if (variant === 'outline') {
if (!disabled) {
baseStyles.push({
color: light ? t.palette.primary_600 : t.palette.primary_500,
})
} else {
baseStyles.push({color: t.palette.primary_600, opacity: 0.5})
}
} else if (variant === 'ghost') {
if (!disabled) {
baseStyles.push({color: t.palette.primary_600})
} else {
baseStyles.push({color: t.palette.primary_600, opacity: 0.5})
}
}
} else if (color === 'secondary') {
if (variant === 'solid' || variant === 'gradient') {
if (!disabled) {
baseStyles.push({
color: light ? tokens.color.gray_700 : tokens.color.gray_100,
})
} else {
baseStyles.push({
color: light ? tokens.color.gray_400 : tokens.color.gray_700,
})
}
} else if (variant === 'outline') {
if (!disabled) {
baseStyles.push({
color: light ? tokens.color.gray_600 : tokens.color.gray_300,
})
} else {
baseStyles.push({
color: light ? tokens.color.gray_400 : tokens.color.gray_700,
})
}
} else if (variant === 'ghost') {
if (!disabled) {
baseStyles.push({
color: light ? tokens.color.gray_600 : tokens.color.gray_300,
})
} else {
baseStyles.push({
color: light ? tokens.color.gray_400 : tokens.color.gray_600,
})
}
}
} else if (color === 'negative') {
if (variant === 'solid' || variant === 'gradient') {
if (!disabled) {
baseStyles.push({color: t.palette.white})
} else {
baseStyles.push({color: t.palette.white, opacity: 0.5})
}
} else if (variant === 'outline') {
if (!disabled) {
baseStyles.push({color: t.palette.negative_400})
} else {
baseStyles.push({color: t.palette.negative_400, opacity: 0.5})
}
} else if (variant === 'ghost') {
if (!disabled) {
baseStyles.push({color: t.palette.negative_400})
} else {
baseStyles.push({color: t.palette.negative_400, opacity: 0.5})
}
}
} else {
if (!disabled) {
baseStyles.push({color: t.palette.white})
} else {
baseStyles.push({color: t.palette.white, opacity: 0.5})
}
}
if (size === 'large') {
baseStyles.push(
a.text_md,
web({paddingBottom: 1}),
native({marginTop: 2}),
)
} else {
baseStyles.push(
a.text_md,
web({paddingBottom: 1}),
native({marginTop: 2}),
)
}
return StyleSheet.flatten(baseStyles)
}, [t, variant, color, size, disabled])
}
export function ButtonText({children, style, ...rest}: ButtonTextProps) {
const textStyles = useSharedButtonTextStyles()
return (
<Text {...rest} style={[a.font_bold, a.text_center, textStyles, style]}>
{children}
</Text>
)
}
export function ButtonIcon({
icon: Comp,
}: {
icon: React.ComponentType<SVGIconProps>
}) {
const {size} = useButtonContext()
const textStyles = useSharedButtonTextStyles()
return (
<View style={[a.z_20]}>
<Comp
size={size === 'large' ? 'md' : 'sm'}
style={[{color: textStyles.color, pointerEvents: 'none'}]}
/>
</View>
)
}
+35
View File
@@ -0,0 +1,35 @@
import React from 'react'
import {useDialogStateContext} from '#/state/dialogs'
import {DialogContextProps, DialogControlProps} from '#/components/Dialog/types'
export const Context = React.createContext<DialogContextProps>({
close: () => {},
})
export function useDialogContext() {
return React.useContext(Context)
}
export function useDialogControl() {
const id = React.useId()
const control = React.useRef<DialogControlProps>({
open: () => {},
close: () => {},
})
const {activeDialogs} = useDialogStateContext()
React.useEffect(() => {
activeDialogs.current.set(id, control)
return () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
activeDialogs.current.delete(id)
}
}, [id, activeDialogs])
return {
ref: control,
open: () => control.current.open(),
close: () => control.current.close(),
}
}
+162
View File
@@ -0,0 +1,162 @@
import React, {useImperativeHandle} from 'react'
import {View, Dimensions} from 'react-native'
import BottomSheet, {
BottomSheetBackdrop,
BottomSheetScrollView,
BottomSheetTextInput,
BottomSheetView,
} from '@gorhom/bottom-sheet'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {useTheme, atoms as a} from '#/alf'
import {Portal} from '#/components/Portal'
import {createInput} from '#/components/forms/TextField'
import {
DialogOuterProps,
DialogControlProps,
DialogInnerProps,
} from '#/components/Dialog/types'
import {Context} from '#/components/Dialog/context'
export {useDialogControl, useDialogContext} from '#/components/Dialog/context'
export * from '#/components/Dialog/types'
// @ts-ignore
export const Input = createInput(BottomSheetTextInput)
export function Outer({
children,
control,
onClose,
nativeOptions,
}: React.PropsWithChildren<DialogOuterProps>) {
const t = useTheme()
const sheet = React.useRef<BottomSheet>(null)
const sheetOptions = nativeOptions?.sheet || {}
const hasSnapPoints = !!sheetOptions.snapPoints
const open = React.useCallback<DialogControlProps['open']>((i = 0) => {
sheet.current?.snapToIndex(i)
}, [])
const close = React.useCallback(() => {
sheet.current?.close()
onClose?.()
}, [onClose])
useImperativeHandle(
control.ref,
() => ({
open,
close,
}),
[open, close],
)
const context = React.useMemo(() => ({close}), [close])
return (
<Portal>
<BottomSheet
enableDynamicSizing={!hasSnapPoints}
enablePanDownToClose
keyboardBehavior="interactive"
android_keyboardInputMode="adjustResize"
keyboardBlurBehavior="restore"
{...sheetOptions}
ref={sheet}
index={-1}
backgroundStyle={{backgroundColor: 'transparent'}}
backdropComponent={props => (
<BottomSheetBackdrop
opacity={0.4}
appearsOnIndex={0}
disappearsOnIndex={-1}
{...props}
/>
)}
handleIndicatorStyle={{backgroundColor: t.palette.primary_500}}
handleStyle={{display: 'none'}}
onClose={onClose}>
<Context.Provider value={context}>
<View
style={[
a.absolute,
a.inset_0,
t.atoms.bg,
{
borderTopLeftRadius: 40,
borderTopRightRadius: 40,
height: Dimensions.get('window').height * 2,
},
]}
/>
{children}
</Context.Provider>
</BottomSheet>
</Portal>
)
}
// TODO a11y props here, or is that handled by the sheet?
export function Inner(props: DialogInnerProps) {
const insets = useSafeAreaInsets()
return (
<BottomSheetView
style={[
a.p_lg,
a.pt_3xl,
{
borderTopLeftRadius: 40,
borderTopRightRadius: 40,
paddingBottom: insets.bottom + a.pb_5xl.paddingBottom,
},
]}>
{props.children}
</BottomSheetView>
)
}
export function ScrollableInner(props: DialogInnerProps) {
const insets = useSafeAreaInsets()
return (
<BottomSheetScrollView
style={[
a.flex_1, // main diff is this
a.p_lg,
a.pt_3xl,
{
borderTopLeftRadius: 40,
borderTopRightRadius: 40,
},
]}>
{props.children}
<View style={{height: insets.bottom + a.pt_5xl.paddingTop}} />
</BottomSheetScrollView>
)
}
export function Handle() {
const t = useTheme()
return (
<View
style={[
a.absolute,
a.rounded_sm,
a.z_10,
{
top: a.pt_lg.paddingTop,
width: 35,
height: 4,
alignSelf: 'center',
backgroundColor: t.palette.contrast_900,
opacity: 0.5,
},
]}
/>
)
}
export function Close() {
return null
}
+194
View File
@@ -0,0 +1,194 @@
import React, {useImperativeHandle} from 'react'
import {View, TouchableWithoutFeedback} from 'react-native'
import {FocusScope} from '@tamagui/focus-scope'
import Animated, {FadeInDown, FadeIn} from 'react-native-reanimated'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useTheme, atoms as a, useBreakpoints, web} from '#/alf'
import {Portal} from '#/components/Portal'
import {DialogOuterProps, DialogInnerProps} from '#/components/Dialog/types'
import {Context} from '#/components/Dialog/context'
export {useDialogControl, useDialogContext} from '#/components/Dialog/context'
export * from '#/components/Dialog/types'
export {Input} from '#/components/forms/TextField'
const stopPropagation = (e: any) => e.stopPropagation()
export function Outer({
control,
onClose,
children,
}: React.PropsWithChildren<DialogOuterProps>) {
const {_} = useLingui()
const t = useTheme()
const {gtMobile} = useBreakpoints()
const [isOpen, setIsOpen] = React.useState(false)
const [isVisible, setIsVisible] = React.useState(true)
const open = React.useCallback(() => {
setIsOpen(true)
}, [setIsOpen])
const close = React.useCallback(async () => {
setIsVisible(false)
await new Promise(resolve => setTimeout(resolve, 150))
setIsOpen(false)
setIsVisible(true)
onClose?.()
}, [onClose, setIsOpen])
useImperativeHandle(
control.ref,
() => ({
open,
close,
}),
[open, close],
)
React.useEffect(() => {
if (!isOpen) return
function handler(e: KeyboardEvent) {
if (e.key === 'Escape') close()
}
document.addEventListener('keydown', handler)
return () => document.removeEventListener('keydown', handler)
}, [isOpen, close])
const context = React.useMemo(
() => ({
close,
}),
[close],
)
return (
<>
{isOpen && (
<Portal>
<Context.Provider value={context}>
<TouchableWithoutFeedback
accessibilityHint={undefined}
accessibilityLabel={_(msg`Close active dialog`)}
onPress={close}>
<View
style={[
web(a.fixed),
a.inset_0,
a.z_10,
a.align_center,
gtMobile ? a.p_lg : a.p_md,
{overflowY: 'auto'},
]}>
{isVisible && (
<Animated.View
entering={FadeIn.duration(150)}
// exiting={FadeOut.duration(150)}
style={[
web(a.fixed),
a.inset_0,
{opacity: 0.5, backgroundColor: t.palette.black},
]}
/>
)}
<View
style={[
a.w_full,
a.z_20,
a.justify_center,
a.align_center,
{
minHeight: web('calc(90vh - 36px)') || undefined,
},
]}>
{isVisible ? children : null}
</View>
</View>
</TouchableWithoutFeedback>
</Context.Provider>
</Portal>
)}
</>
)
}
export function Inner({
children,
style,
label,
accessibilityLabelledBy,
accessibilityDescribedBy,
}: DialogInnerProps) {
const t = useTheme()
const {gtMobile} = useBreakpoints()
return (
<FocusScope loop enabled trapped>
<Animated.View
role="dialog"
aria-role="dialog"
aria-label={label}
aria-labelledby={accessibilityLabelledBy}
aria-describedby={accessibilityDescribedBy}
// @ts-ignore web only -prf
onClick={stopPropagation}
onStartShouldSetResponder={_ => true}
onTouchEnd={stopPropagation}
entering={FadeInDown.duration(100)}
// exiting={FadeOut.duration(100)}
style={[
a.relative,
a.rounded_md,
a.w_full,
a.border,
gtMobile ? a.p_xl : a.p_lg,
t.atoms.bg,
{
maxWidth: 600,
borderColor: t.palette.contrast_200,
shadowColor: t.palette.black,
shadowOpacity: t.name === 'light' ? 0.1 : 0.4,
shadowRadius: 30,
},
...(Array.isArray(style) ? style : [style || {}]),
]}>
{children}
</Animated.View>
</FocusScope>
)
}
export const ScrollableInner = Inner
export function Handle() {
return null
}
/**
* TODO(eric) unused rn
*/
// export function Close() {
// const {_} = useLingui()
// const t = useTheme()
// const {close} = useDialogContext()
// return (
// <View
// style={[
// a.absolute,
// a.z_10,
// {
// top: a.pt_lg.paddingTop,
// right: a.pr_lg.paddingRight,
// },
// ]}>
// <Button onPress={close} label={_(msg`Close active dialog`)}>
// </Button>
// </View>
// )
// }
+43
View File
@@ -0,0 +1,43 @@
import React from 'react'
import type {ViewStyle, AccessibilityProps} from 'react-native'
import {BottomSheetProps} from '@gorhom/bottom-sheet'
type A11yProps = Required<AccessibilityProps>
export type DialogContextProps = {
close: () => void
}
export type DialogControlProps = {
open: (index?: number) => void
close: () => void
}
export type DialogOuterProps = {
control: {
ref: React.RefObject<DialogControlProps>
open: (index?: number) => void
close: () => void
}
onClose?: () => void
nativeOptions?: {
sheet?: Omit<BottomSheetProps, 'children'>
}
webOptions?: {}
}
type DialogInnerPropsBase<T> = React.PropsWithChildren<{
style?: ViewStyle
}> &
T
export type DialogInnerProps =
| DialogInnerPropsBase<{
label?: undefined
accessibilityLabelledBy: A11yProps['aria-labelledby']
accessibilityDescribedBy: string
}>
| DialogInnerPropsBase<{
label: string
accessibilityLabelledBy?: undefined
accessibilityDescribedBy?: undefined
}>
+191
View File
@@ -0,0 +1,191 @@
import React from 'react'
import {
Text,
TextStyle,
StyleProp,
GestureResponderEvent,
Linking,
} from 'react-native'
import {
useLinkProps,
useNavigation,
StackActions,
} from '@react-navigation/native'
import {sanitizeUrl} from '@braintree/sanitize-url'
import {isWeb} from '#/platform/detection'
import {useTheme, web, flatten} from '#/alf'
import {Button, ButtonProps, useButtonContext} from '#/components/Button'
import {AllNavigatorParams, NavigationProp} from '#/lib/routes/types'
import {
convertBskyAppUrlIfNeeded,
isExternalUrl,
linkRequiresWarning,
} from '#/lib/strings/url-helpers'
import {useModalControls} from '#/state/modals'
import {router} from '#/routes'
export type LinkProps = Omit<
ButtonProps,
'style' | 'onPress' | 'disabled' | 'label'
> & {
/**
* `TextStyle` to apply to the anchor element itself. Does not apply to any children.
*/
style?: StyleProp<TextStyle>
/**
* The React Navigation `StackAction` to perform when the link is pressed.
*/
action?: 'push' | 'replace' | 'navigate'
/**
* If true, will warn the user if the link text does not match the href. Only
* works for Links with children that are strings i.e. text links.
*/
warnOnMismatchingTextChild?: boolean
label?: ButtonProps['label']
} & Pick<Parameters<typeof useLinkProps<AllNavigatorParams>>[0], 'to'>
/**
* A interactive element that renders as a `<a>` tag on the web. On mobile it
* will translate the `href` to navigator screens and params and dispatch a
* navigation action.
*
* Intended to behave as a web anchor tag. For more complex routing, use a
* `Button`.
*/
export function Link({
children,
to,
action = 'push',
warnOnMismatchingTextChild,
style,
...rest
}: LinkProps) {
const navigation = useNavigation<NavigationProp>()
const {href} = useLinkProps<AllNavigatorParams>({
to:
typeof to === 'string' ? convertBskyAppUrlIfNeeded(sanitizeUrl(to)) : to,
})
const isExternal = isExternalUrl(href)
const {openModal, closeModal} = useModalControls()
const onPress = React.useCallback(
(e: GestureResponderEvent) => {
const stringChildren = typeof children === 'string' ? children : ''
const requiresWarning = Boolean(
warnOnMismatchingTextChild &&
stringChildren &&
isExternal &&
linkRequiresWarning(href, stringChildren),
)
if (requiresWarning) {
e.preventDefault()
openModal({
name: 'link-warning',
text: stringChildren,
href: href,
})
} else {
e.preventDefault()
if (isExternal) {
Linking.openURL(href)
} else {
/**
* A `GestureResponderEvent`, but cast to `any` to avoid using a bunch
* of @ts-ignore below.
*/
const event = e as any
const isMiddleClick = isWeb && event.button === 1
const isMetaKey =
isWeb &&
(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey)
const shouldOpenInNewTab = isMetaKey || isMiddleClick
if (
shouldOpenInNewTab ||
href.startsWith('http') ||
href.startsWith('mailto')
) {
Linking.openURL(href)
} else {
closeModal() // close any active modals
if (action === 'push') {
navigation.dispatch(StackActions.push(...router.matchPath(href)))
} else if (action === 'replace') {
navigation.dispatch(
StackActions.replace(...router.matchPath(href)),
)
} else if (action === 'navigate') {
// @ts-ignore
navigation.navigate(...router.matchPath(href))
} else {
throw Error('Unsupported navigator action.')
}
}
}
}
},
[
href,
isExternal,
warnOnMismatchingTextChild,
navigation,
action,
children,
closeModal,
openModal,
],
)
return (
<Button
label={href}
{...rest}
role="link"
accessibilityRole="link"
href={href}
onPress={onPress}
{...web({
hrefAttrs: {
target: isExternal ? 'blank' : undefined,
rel: isExternal ? 'noopener noreferrer' : undefined,
},
dataSet: {
// default to no underline, apply this ourselves
noUnderline: '1',
},
})}>
{typeof children === 'string' ? (
<LinkText style={style}>{children}</LinkText>
) : (
children
)}
</Button>
)
}
function LinkText({
children,
style,
}: React.PropsWithChildren<{
style?: StyleProp<TextStyle>
}>) {
const t = useTheme()
const {hovered} = useButtonContext()
return (
<Text
style={[
{color: t.palette.primary_500},
hovered && {
textDecorationLine: 'underline',
textDecorationColor: t.palette.primary_500,
},
flatten(style),
]}>
{children as string}
</Text>
)
}
+56
View File
@@ -0,0 +1,56 @@
import React from 'react'
type Component = React.ReactElement
type ContextType = {
outlet: Component | null
append(id: string, component: Component): void
remove(id: string): void
}
type ComponentMap = {
[id: string]: Component
}
export const Context = React.createContext<ContextType>({
outlet: null,
append: () => {},
remove: () => {},
})
export function Provider(props: React.PropsWithChildren<{}>) {
const map = React.useRef<ComponentMap>({})
const [outlet, setOutlet] = React.useState<ContextType['outlet']>(null)
const append = React.useCallback<ContextType['append']>((id, component) => {
if (map.current[id]) return
map.current[id] = <React.Fragment key={id}>{component}</React.Fragment>
setOutlet(<>{Object.values(map.current)}</>)
}, [])
const remove = React.useCallback<ContextType['remove']>(id => {
delete map.current[id]
setOutlet(<>{Object.values(map.current)}</>)
}, [])
return (
<Context.Provider value={{outlet, append, remove}}>
{props.children}
</Context.Provider>
)
}
export function Outlet() {
const ctx = React.useContext(Context)
return ctx.outlet
}
export function Portal({children}: React.PropsWithChildren<{}>) {
const {append, remove} = React.useContext(Context)
const id = React.useId()
React.useEffect(() => {
append(id, children as Component)
return () => remove(id)
}, [id, children, append, remove])
return null
}
+119
View File
@@ -0,0 +1,119 @@
import React from 'react'
import {View, PressableProps} from 'react-native'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useTheme, atoms as a} from '#/alf'
import {H4, P} from '#/components/Typography'
import {Button} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
export {useDialogControl as usePromptControl} from '#/components/Dialog'
const Context = React.createContext<{
titleId: string
descriptionId: string
}>({
titleId: '',
descriptionId: '',
})
export function Outer({
children,
control,
}: React.PropsWithChildren<{
control: Dialog.DialogOuterProps['control']
}>) {
const titleId = React.useId()
const descriptionId = React.useId()
const context = React.useMemo(
() => ({titleId, descriptionId}),
[titleId, descriptionId],
)
return (
<Dialog.Outer control={control}>
<Context.Provider value={context}>
<Dialog.Handle />
<Dialog.Inner
accessibilityLabelledBy={titleId}
accessibilityDescribedBy={descriptionId}
style={{width: 'auto', maxWidth: 400}}>
{children}
</Dialog.Inner>
</Context.Provider>
</Dialog.Outer>
)
}
export function Title({children}: React.PropsWithChildren<{}>) {
const t = useTheme()
const {titleId} = React.useContext(Context)
return (
<H4
nativeID={titleId}
style={[a.font_bold, t.atoms.text_contrast_700, a.pb_sm]}>
{children}
</H4>
)
}
export function Description({children}: React.PropsWithChildren<{}>) {
const t = useTheme()
const {descriptionId} = React.useContext(Context)
return (
<P nativeID={descriptionId} style={[t.atoms.text, a.pb_lg]}>
{children}
</P>
)
}
export function Actions({children}: React.PropsWithChildren<{}>) {
return (
<View style={[a.w_full, a.flex_row, a.gap_sm, a.justify_end]}>
{children}
</View>
)
}
export function Cancel({
children,
}: React.PropsWithChildren<{onPress?: PressableProps['onPress']}>) {
const {_} = useLingui()
const {close} = Dialog.useDialogContext()
return (
<Button
variant="solid"
color="secondary"
size="small"
label={_(msg`Cancel`)}
onPress={close}>
{children}
</Button>
)
}
export function Action({
children,
onPress,
}: React.PropsWithChildren<{onPress?: () => void}>) {
const {_} = useLingui()
const {close} = Dialog.useDialogContext()
const handleOnPress = React.useCallback(() => {
close()
onPress?.()
}, [close, onPress])
return (
<Button
variant="solid"
color="primary"
size="small"
label={_(msg`Confirm`)}
onPress={handleOnPress}>
{children}
</Button>
)
}
@@ -1,6 +1,7 @@
import React from 'react' import React from 'react'
import {Text as RNText, TextProps} from 'react-native' import {Text as RNText, TextProps} from 'react-native'
import {useTheme, atoms, web} from '#/alf'
import {useTheme, atoms, web, flatten} from '#/alf'
export function Text({style, ...rest}: TextProps) { export function Text({style, ...rest}: TextProps) {
const t = useTheme() const t = useTheme()
@@ -18,7 +19,7 @@ export function H1({style, ...rest}: TextProps) {
<RNText <RNText
{...attr} {...attr}
{...rest} {...rest}
style={[atoms.text_xl, atoms.font_bold, t.atoms.text, style]} style={[atoms.text_5xl, atoms.font_bold, t.atoms.text, flatten(style)]}
/> />
) )
} }
@@ -34,7 +35,7 @@ export function H2({style, ...rest}: TextProps) {
<RNText <RNText
{...attr} {...attr}
{...rest} {...rest}
style={[atoms.text_lg, atoms.font_bold, t.atoms.text, style]} style={[atoms.text_4xl, atoms.font_bold, t.atoms.text, flatten(style)]}
/> />
) )
} }
@@ -50,7 +51,7 @@ export function H3({style, ...rest}: TextProps) {
<RNText <RNText
{...attr} {...attr}
{...rest} {...rest}
style={[atoms.text_md, atoms.font_bold, t.atoms.text, style]} style={[atoms.text_3xl, atoms.font_bold, t.atoms.text, flatten(style)]}
/> />
) )
} }
@@ -66,7 +67,7 @@ export function H4({style, ...rest}: TextProps) {
<RNText <RNText
{...attr} {...attr}
{...rest} {...rest}
style={[atoms.text_sm, atoms.font_bold, t.atoms.text, style]} style={[atoms.text_2xl, atoms.font_bold, t.atoms.text, flatten(style)]}
/> />
) )
} }
@@ -82,7 +83,7 @@ export function H5({style, ...rest}: TextProps) {
<RNText <RNText
{...attr} {...attr}
{...rest} {...rest}
style={[atoms.text_xs, atoms.font_bold, t.atoms.text, style]} style={[atoms.text_xl, atoms.font_bold, t.atoms.text, flatten(style)]}
/> />
) )
} }
@@ -98,7 +99,26 @@ export function H6({style, ...rest}: TextProps) {
<RNText <RNText
{...attr} {...attr}
{...rest} {...rest}
style={[atoms.text_xxs, atoms.font_bold, t.atoms.text, style]} style={[atoms.text_lg, atoms.font_bold, t.atoms.text, flatten(style)]}
/>
)
}
export function P({style, ...rest}: TextProps) {
const t = useTheme()
const attr =
web({
role: 'paragraph',
}) || {}
const _style = flatten(style)
const lineHeight =
(_style?.lineHeight || atoms.text_md.lineHeight) *
atoms.leading_normal.lineHeight
return (
<RNText
{...attr}
{...rest}
style={[atoms.text_md, t.atoms.text, _style, {lineHeight}]}
/> />
) )
} }
@@ -0,0 +1,108 @@
import React from 'react'
import {View, Pressable} from 'react-native'
import DateTimePicker, {
BaseProps as DateTimePickerProps,
} from '@react-native-community/datetimepicker'
import {useTheme, atoms} from '#/alf'
import {Text} from '#/components/Typography'
import {useInteractionState} from '#/components/hooks/useInteractionState'
import * as TextField from '#/components/forms/TextField'
import {CalendarDays_Stroke2_Corner0_Rounded as CalendarDays} from '#/components/icons/CalendarDays'
import {DateFieldProps} from '#/components/forms/DateField/types'
import {
localizeDate,
toSimpleDateString,
} from '#/components/forms/DateField/utils'
export * as utils from '#/components/forms/DateField/utils'
export const Label = TextField.Label
export function DateField({
value,
onChangeDate,
label,
isInvalid,
testID,
}: DateFieldProps) {
const t = useTheme()
const [open, setOpen] = React.useState(false)
const {
state: pressed,
onIn: onPressIn,
onOut: onPressOut,
} = useInteractionState()
const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
const {chromeFocus, chromeError, chromeErrorHover} =
TextField.useSharedInputStyles()
const onChangeInternal = React.useCallback<
Required<DateTimePickerProps>['onChange']
>(
(_event, date) => {
setOpen(false)
if (date) {
const formatted = toSimpleDateString(date)
onChangeDate(formatted)
}
},
[onChangeDate, setOpen],
)
return (
<View style={[atoms.relative, atoms.w_full]}>
<Pressable
aria-label={label}
accessibilityLabel={label}
accessibilityHint={undefined}
onPress={() => setOpen(true)}
onPressIn={onPressIn}
onPressOut={onPressOut}
onFocus={onFocus}
onBlur={onBlur}
style={[
{
paddingTop: 16,
paddingBottom: 16,
borderColor: 'transparent',
borderWidth: 2,
},
atoms.flex_row,
atoms.flex_1,
atoms.w_full,
atoms.px_lg,
atoms.rounded_sm,
t.atoms.bg_contrast_50,
focused || pressed ? chromeFocus : {},
isInvalid ? chromeError : {},
isInvalid && (focused || pressed) ? chromeErrorHover : {},
]}>
<TextField.Icon icon={CalendarDays} />
<Text
style={[atoms.text_md, atoms.pl_xs, t.atoms.text, {paddingTop: 3}]}>
{localizeDate(value)}
</Text>
</Pressable>
{open && (
<DateTimePicker
aria-label={label}
accessibilityLabel={label}
accessibilityHint={undefined}
testID={`${testID}-datepicker`}
mode="date"
timeZoneName={'Etc/UTC'}
display="spinner"
// @ts-ignore applies in iOS only -prf
themeVariant={t.name === 'dark' ? 'dark' : 'light'}
value={new Date(value)}
onChange={onChangeInternal}
/>
)}
</View>
)
}
+56
View File
@@ -0,0 +1,56 @@
import React from 'react'
import {View} from 'react-native'
import DateTimePicker, {
DateTimePickerEvent,
} from '@react-native-community/datetimepicker'
import {useTheme, atoms} from '#/alf'
import * as TextField from '#/components/forms/TextField'
import {toSimpleDateString} from '#/components/forms/DateField/utils'
import {DateFieldProps} from '#/components/forms/DateField/types'
export * as utils from '#/components/forms/DateField/utils'
export const Label = TextField.Label
/**
* Date-only input. Accepts a date in the format YYYY-MM-DD, and reports date
* changes in the same format.
*
* For dates of unknown format, convert with the
* `utils.toSimpleDateString(Date)` export of this file.
*/
export function DateField({
value,
onChangeDate,
testID,
label,
}: DateFieldProps) {
const t = useTheme()
const onChangeInternal = React.useCallback(
(event: DateTimePickerEvent, date: Date | undefined) => {
if (date) {
const formatted = toSimpleDateString(date)
onChangeDate(formatted)
}
},
[onChangeDate],
)
return (
<View style={[atoms.relative, atoms.w_full]}>
<DateTimePicker
aria-label={label}
accessibilityLabel={label}
accessibilityHint={undefined}
testID={`${testID}-datepicker`}
mode="date"
timeZoneName={'Etc/UTC'}
display="spinner"
themeVariant={t.name === 'dark' ? 'dark' : 'light'}
value={new Date(value)}
onChange={onChangeInternal}
/>
</View>
)
}
@@ -0,0 +1,64 @@
import React from 'react'
import {TextInput, TextInputProps, StyleSheet} from 'react-native'
// @ts-ignore
import {unstable_createElement} from 'react-native-web'
import * as TextField from '#/components/forms/TextField'
import {toSimpleDateString} from '#/components/forms/DateField/utils'
import {DateFieldProps} from '#/components/forms/DateField/types'
export * as utils from '#/components/forms/DateField/utils'
export const Label = TextField.Label
const InputBase = React.forwardRef<HTMLInputElement, TextInputProps>(
({style, ...props}, ref) => {
return unstable_createElement('input', {
...props,
ref,
type: 'date',
style: [
StyleSheet.flatten(style),
{
background: 'transparent',
border: 0,
},
],
})
},
)
InputBase.displayName = 'InputBase'
const Input = TextField.createInput(InputBase as unknown as typeof TextInput)
export function DateField({
value,
onChangeDate,
label,
isInvalid,
testID,
}: DateFieldProps) {
const handleOnChange = React.useCallback(
(e: any) => {
const date = e.target.valueAsDate || e.target.value
if (date) {
const formatted = toSimpleDateString(date)
onChangeDate(formatted)
}
},
[onChangeDate],
)
return (
<TextField.Root isInvalid={isInvalid}>
<Input
value={value}
label={label}
onChange={handleOnChange}
onChangeText={() => {}}
testID={testID}
/>
</TextField.Root>
)
}
+7
View File
@@ -0,0 +1,7 @@
export type DateFieldProps = {
value: string
onChangeDate: (date: string) => void
label: string
isInvalid?: boolean
testID?: string
}
+16
View File
@@ -0,0 +1,16 @@
import {getLocales} from 'expo-localization'
const LOCALE = getLocales()[0]
// we need the date in the form yyyy-MM-dd to pass to the input
export function toSimpleDateString(date: Date | string): string {
const _date = typeof date === 'string' ? new Date(date) : date
return _date.toISOString().split('T')[0]
}
export function localizeDate(date: Date | string): string {
const _date = typeof date === 'string' ? new Date(date) : date
return new Intl.DateTimeFormat(LOCALE.languageTag, {
timeZone: 'UTC',
}).format(_date)
}
+43
View File
@@ -0,0 +1,43 @@
import React from 'react'
import {View} from 'react-native'
import {atoms, useTheme} from '#/alf'
/**
* NOT FINISHED, just here as a reference
*/
export function InputGroup(props: React.PropsWithChildren<{}>) {
const t = useTheme()
const children = React.Children.toArray(props.children)
const total = children.length
return (
<View style={[atoms.w_full]}>
{children.map((child, i) => {
return React.isValidElement(child) ? (
<React.Fragment key={i}>
{i > 0 ? (
<View
style={[atoms.border_b, {borderColor: t.palette.contrast_500}]}
/>
) : null}
{React.cloneElement(child, {
// @ts-ignore
style: [
...(Array.isArray(child.props?.style)
? child.props.style
: [child.props.style || {}]),
{
borderTopLeftRadius: i > 0 ? 0 : undefined,
borderTopRightRadius: i > 0 ? 0 : undefined,
borderBottomLeftRadius: i < total - 1 ? 0 : undefined,
borderBottomRightRadius: i < total - 1 ? 0 : undefined,
borderBottomWidth: i < total - 1 ? 0 : undefined,
},
],
})}
</React.Fragment>
) : null
})}
</View>
)
}
+334
View File
@@ -0,0 +1,334 @@
import React from 'react'
import {
View,
TextInput,
TextInputProps,
TextStyle,
ViewStyle,
Pressable,
StyleSheet,
AccessibilityProps,
} from 'react-native'
import {HITSLOP_20} from 'lib/constants'
import {isWeb} from '#/platform/detection'
import {useTheme, atoms as a, web, tokens, android} from '#/alf'
import {Text} from '#/components/Typography'
import {useInteractionState} from '#/components/hooks/useInteractionState'
import {Props as SVGIconProps} from '#/components/icons/common'
const Context = React.createContext<{
inputRef: React.RefObject<TextInput> | null
isInvalid: boolean
hovered: boolean
onHoverIn: () => void
onHoverOut: () => void
focused: boolean
onFocus: () => void
onBlur: () => void
}>({
inputRef: null,
isInvalid: false,
hovered: false,
onHoverIn: () => {},
onHoverOut: () => {},
focused: false,
onFocus: () => {},
onBlur: () => {},
})
export type RootProps = React.PropsWithChildren<{isInvalid?: boolean}>
export function Root({children, isInvalid = false}: RootProps) {
const inputRef = React.useRef<TextInput>(null)
const rootRef = React.useRef<View>(null)
const {
state: hovered,
onIn: onHoverIn,
onOut: onHoverOut,
} = useInteractionState()
const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
const context = React.useMemo(
() => ({
inputRef,
hovered,
onHoverIn,
onHoverOut,
focused,
onFocus,
onBlur,
isInvalid,
}),
[
inputRef,
hovered,
onHoverIn,
onHoverOut,
focused,
onFocus,
onBlur,
isInvalid,
],
)
React.useLayoutEffect(() => {
const root = rootRef.current
if (!root || !isWeb) return
// @ts-ignore web only
root.tabIndex = -1
}, [])
return (
<Context.Provider value={context}>
<Pressable
accessibilityRole="button"
ref={rootRef}
role="none"
style={[
a.flex_row,
a.align_center,
a.relative,
a.w_full,
a.px_md,
{
paddingVertical: 14,
},
]}
// onPressIn/out don't work on android web
onPress={() => inputRef.current?.focus()}
onHoverIn={onHoverIn}
onHoverOut={onHoverOut}>
{children}
</Pressable>
</Context.Provider>
)
}
export function useSharedInputStyles() {
const t = useTheme()
return React.useMemo(() => {
const hover: ViewStyle[] = [
{
borderColor: t.palette.contrast_100,
},
]
const focus: ViewStyle[] = [
{
backgroundColor: t.palette.contrast_50,
borderColor: t.palette.primary_500,
},
]
const error: ViewStyle[] = [
{
backgroundColor:
t.name === 'light' ? t.palette.negative_25 : t.palette.negative_900,
borderColor:
t.name === 'light' ? t.palette.negative_300 : t.palette.negative_800,
},
]
const errorHover: ViewStyle[] = [
{
backgroundColor:
t.name === 'light' ? t.palette.negative_25 : t.palette.negative_900,
borderColor: tokens.color.red_500,
},
]
return {
chromeHover: StyleSheet.flatten(hover),
chromeFocus: StyleSheet.flatten(focus),
chromeError: StyleSheet.flatten(error),
chromeErrorHover: StyleSheet.flatten(errorHover),
}
}, [t])
}
export type InputProps = Omit<TextInputProps, 'value' | 'onChangeText'> & {
label: string
value: string
onChangeText: (value: string) => void
isInvalid?: boolean
}
export function createInput(Component: typeof TextInput) {
return function Input({
label,
placeholder,
value,
onChangeText,
isInvalid,
...rest
}: InputProps) {
const t = useTheme()
const ctx = React.useContext(Context)
const withinRoot = Boolean(ctx.inputRef)
const {chromeHover, chromeFocus, chromeError, chromeErrorHover} =
useSharedInputStyles()
if (!withinRoot) {
return (
<Root isInvalid={isInvalid}>
<Input
label={label}
placeholder={placeholder}
value={value}
onChangeText={onChangeText}
isInvalid={isInvalid}
{...rest}
/>
</Root>
)
}
return (
<>
<Component
accessibilityHint={undefined}
{...rest}
aria-label={label}
accessibilityLabel={label}
ref={ctx.inputRef}
value={value}
onChangeText={onChangeText}
onFocus={ctx.onFocus}
onBlur={ctx.onBlur}
placeholder={placeholder || label}
placeholderTextColor={t.palette.contrast_500}
hitSlop={HITSLOP_20}
style={[
a.relative,
a.z_20,
a.flex_1,
a.text_md,
t.atoms.text,
a.px_xs,
android({
paddingBottom: 2,
}),
{
lineHeight: a.text_md.lineHeight * 1.1875,
textAlignVertical: rest.multiline ? 'top' : undefined,
minHeight: rest.multiline ? 60 : undefined,
},
]}
/>
<View
style={[
a.z_10,
a.absolute,
a.inset_0,
a.rounded_sm,
t.atoms.bg_contrast_25,
{borderColor: 'transparent', borderWidth: 2},
ctx.hovered ? chromeHover : {},
ctx.focused ? chromeFocus : {},
ctx.isInvalid || isInvalid ? chromeError : {},
(ctx.isInvalid || isInvalid) && (ctx.hovered || ctx.focused)
? chromeErrorHover
: {},
]}
/>
</>
)
}
}
export const Input = createInput(TextInput)
export function Label({children}: React.PropsWithChildren<{}>) {
const t = useTheme()
return (
<Text style={[a.text_sm, a.font_bold, t.atoms.text_contrast_600, a.mb_sm]}>
{children}
</Text>
)
}
export function Icon({icon: Comp}: {icon: React.ComponentType<SVGIconProps>}) {
const t = useTheme()
const ctx = React.useContext(Context)
const {hover, focus, errorHover, errorFocus} = React.useMemo(() => {
const hover: TextStyle[] = [
{
color: t.palette.contrast_800,
},
]
const focus: TextStyle[] = [
{
color: t.palette.primary_500,
},
]
const errorHover: TextStyle[] = [
{
color: t.palette.negative_500,
},
]
const errorFocus: TextStyle[] = [
{
color: t.palette.negative_500,
},
]
return {
hover,
focus,
errorHover,
errorFocus,
}
}, [t])
return (
<View style={[a.z_20, a.pr_xs]}>
<Comp
size="md"
style={[
{color: t.palette.contrast_500, pointerEvents: 'none'},
ctx.hovered ? hover : {},
ctx.focused ? focus : {},
ctx.isInvalid && ctx.hovered ? errorHover : {},
ctx.isInvalid && ctx.focused ? errorFocus : {},
]}
/>
</View>
)
}
export function Suffix({
children,
label,
accessibilityHint,
}: React.PropsWithChildren<{
label: string
accessibilityHint?: AccessibilityProps['accessibilityHint']
}>) {
const t = useTheme()
const ctx = React.useContext(Context)
return (
<Text
aria-label={label}
accessibilityLabel={label}
accessibilityHint={accessibilityHint}
style={[
a.z_20,
a.pr_sm,
a.text_md,
t.atoms.text_contrast_400,
{
pointerEvents: 'none',
},
web({
marginTop: -2,
}),
ctx.hovered || ctx.focused
? {
color: t.palette.contrast_800,
}
: {},
]}>
{children}
</Text>
)
}
+473
View File
@@ -0,0 +1,473 @@
import React from 'react'
import {Pressable, View, ViewStyle} from 'react-native'
import {HITSLOP_10} from 'lib/constants'
import {useTheme, atoms as a, web, native} from '#/alf'
import {Text} from '#/components/Typography'
import {useInteractionState} from '#/components/hooks/useInteractionState'
export type ItemState = {
name: string
selected: boolean
disabled: boolean
isInvalid: boolean
hovered: boolean
pressed: boolean
focused: boolean
}
const ItemContext = React.createContext<ItemState>({
name: '',
selected: false,
disabled: false,
isInvalid: false,
hovered: false,
pressed: false,
focused: false,
})
const GroupContext = React.createContext<{
values: string[]
disabled: boolean
type: 'radio' | 'checkbox'
maxSelectionsReached: boolean
setFieldValue: (props: {name: string; value: boolean}) => void
}>({
type: 'checkbox',
values: [],
disabled: false,
maxSelectionsReached: false,
setFieldValue: () => {},
})
export type GroupProps = React.PropsWithChildren<{
type?: 'radio' | 'checkbox'
values: string[]
maxSelections?: number
disabled?: boolean
onChange: (value: string[]) => void
label: string
}>
export type ItemProps = {
type?: 'radio' | 'checkbox'
name: string
label: string
value?: boolean
disabled?: boolean
onChange?: (selected: boolean) => void
isInvalid?: boolean
style?: (state: ItemState) => ViewStyle
children: ((props: ItemState) => React.ReactNode) | React.ReactNode
}
export function useItemContext() {
return React.useContext(ItemContext)
}
export function Group({
children,
values: providedValues,
onChange,
disabled = false,
type = 'checkbox',
maxSelections,
label,
}: GroupProps) {
const groupRole = type === 'radio' ? 'radiogroup' : undefined
const values = type === 'radio' ? providedValues.slice(0, 1) : providedValues
const [maxReached, setMaxReached] = React.useState(false)
const setFieldValue = React.useCallback<
(props: {name: string; value: boolean}) => void
>(
({name, value}) => {
if (type === 'checkbox') {
const pruned = values.filter(v => v !== name)
const next = value ? pruned.concat(name) : pruned
onChange(next)
} else {
onChange([name])
}
},
[type, onChange, values],
)
React.useEffect(() => {
if (type === 'checkbox') {
if (
maxSelections &&
values.length >= maxSelections &&
maxReached === false
) {
setMaxReached(true)
} else if (
maxSelections &&
values.length < maxSelections &&
maxReached === true
) {
setMaxReached(false)
}
}
}, [type, values.length, maxSelections, maxReached, setMaxReached])
const context = React.useMemo(
() => ({
values,
type,
disabled,
maxSelectionsReached: maxReached,
setFieldValue,
}),
[values, disabled, type, maxReached, setFieldValue],
)
return (
<GroupContext.Provider value={context}>
<View
role={groupRole}
{...(groupRole === 'radiogroup'
? {
'aria-label': label,
accessibilityLabel: label,
accessibilityRole: groupRole,
}
: {})}>
{children}
</View>
</GroupContext.Provider>
)
}
export function Item({
children,
name,
value = false,
disabled: itemDisabled = false,
onChange,
isInvalid,
style,
type = 'checkbox',
label,
...rest
}: ItemProps) {
const {
values: selectedValues,
type: groupType,
disabled: groupDisabled,
setFieldValue,
maxSelectionsReached,
} = React.useContext(GroupContext)
const {
state: hovered,
onIn: onHoverIn,
onOut: onHoverOut,
} = useInteractionState()
const {
state: pressed,
onIn: onPressIn,
onOut: onPressOut,
} = useInteractionState()
const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
const role = groupType === 'radio' ? 'radio' : type
const selected = selectedValues.includes(name) || !!value
const disabled =
groupDisabled || itemDisabled || (!selected && maxSelectionsReached)
const onPress = React.useCallback(() => {
const next = !selected
setFieldValue({name, value: next})
onChange?.(next)
}, [name, selected, onChange, setFieldValue])
const state = React.useMemo(
() => ({
name,
selected,
disabled: disabled ?? false,
isInvalid: isInvalid ?? false,
hovered,
pressed,
focused,
}),
[name, selected, disabled, hovered, pressed, focused, isInvalid],
)
return (
<ItemContext.Provider value={state}>
<Pressable
accessibilityHint={undefined} // optional
hitSlop={HITSLOP_10}
{...rest}
disabled={disabled}
aria-disabled={disabled ?? false}
aria-checked={selected}
aria-invalid={isInvalid}
aria-label={label}
role={role}
accessibilityRole={role}
accessibilityState={{
disabled: disabled ?? false,
selected: selected,
}}
accessibilityLabel={label}
onPress={onPress}
onHoverIn={onHoverIn}
onHoverOut={onHoverOut}
onPressIn={onPressIn}
onPressOut={onPressOut}
onFocus={onFocus}
onBlur={onBlur}
style={[
a.flex_row,
a.align_center,
a.gap_sm,
focused ? web({outline: 'none'}) : {},
style?.(state),
]}>
{typeof children === 'function' ? children(state) : children}
</Pressable>
</ItemContext.Provider>
)
}
export function Label({children}: React.PropsWithChildren<{}>) {
const t = useTheme()
const {disabled} = useItemContext()
return (
<Text
style={[
a.font_bold,
{
userSelect: 'none',
color: disabled ? t.palette.contrast_400 : t.palette.contrast_600,
},
native({
paddingTop: 3,
}),
]}>
{children}
</Text>
)
}
// TODO(eric) refactor to memoize styles without knowledge of state
export function createSharedToggleStyles({
theme: t,
hovered,
focused,
selected,
disabled,
isInvalid,
}: {
theme: ReturnType<typeof useTheme>
selected: boolean
hovered: boolean
focused: boolean
disabled: boolean
isInvalid: boolean
}) {
const base: ViewStyle[] = []
const baseHover: ViewStyle[] = []
const indicator: ViewStyle[] = []
if (selected) {
base.push({
backgroundColor:
t.name === 'light' ? t.palette.primary_25 : t.palette.primary_900,
borderColor: t.palette.primary_500,
})
if (hovered || focused) {
baseHover.push({
backgroundColor:
t.name === 'light' ? t.palette.primary_100 : t.palette.primary_800,
borderColor:
t.name === 'light' ? t.palette.primary_600 : t.palette.primary_400,
})
}
} else {
if (hovered || focused) {
baseHover.push({
backgroundColor:
t.name === 'light' ? t.palette.contrast_50 : t.palette.contrast_100,
borderColor: t.palette.contrast_500,
})
}
}
if (isInvalid) {
base.push({
backgroundColor:
t.name === 'light' ? t.palette.negative_25 : t.palette.negative_900,
borderColor:
t.name === 'light' ? t.palette.negative_300 : t.palette.negative_800,
})
if (hovered || focused) {
baseHover.push({
backgroundColor:
t.name === 'light' ? t.palette.negative_25 : t.palette.negative_900,
borderColor: t.palette.negative_500,
})
}
}
if (disabled) {
base.push({
backgroundColor: t.palette.contrast_100,
borderColor: t.palette.contrast_400,
})
}
return {
baseStyles: base,
baseHoverStyles: disabled ? [] : baseHover,
indicatorStyles: indicator,
}
}
export function Checkbox() {
const t = useTheme()
const {selected, hovered, focused, disabled, isInvalid} = useItemContext()
const {baseStyles, baseHoverStyles, indicatorStyles} =
createSharedToggleStyles({
theme: t,
hovered,
focused,
selected,
disabled,
isInvalid,
})
return (
<View
style={[
a.justify_center,
a.align_center,
a.border,
a.rounded_xs,
t.atoms.border_contrast,
{
height: 20,
width: 20,
},
baseStyles,
hovered || focused ? baseHoverStyles : {},
]}>
{selected ? (
<View
style={[
a.absolute,
a.rounded_2xs,
{height: 12, width: 12},
selected
? {
backgroundColor: t.palette.primary_500,
}
: {},
indicatorStyles,
]}
/>
) : null}
</View>
)
}
export function Switch() {
const t = useTheme()
const {selected, hovered, focused, disabled, isInvalid} = useItemContext()
const {baseStyles, baseHoverStyles, indicatorStyles} =
createSharedToggleStyles({
theme: t,
hovered,
focused,
selected,
disabled,
isInvalid,
})
return (
<View
style={[
a.relative,
a.border,
a.rounded_full,
t.atoms.bg,
t.atoms.border_contrast,
{
height: 20,
width: 30,
},
baseStyles,
hovered || focused ? baseHoverStyles : {},
]}>
<View
style={[
a.absolute,
a.rounded_full,
{
height: 12,
width: 12,
top: 3,
left: 3,
backgroundColor: t.palette.contrast_400,
},
selected
? {
backgroundColor: t.palette.primary_500,
left: 13,
}
: {},
indicatorStyles,
]}
/>
</View>
)
}
export function Radio() {
const t = useTheme()
const {selected, hovered, focused, disabled, isInvalid} =
React.useContext(ItemContext)
const {baseStyles, baseHoverStyles, indicatorStyles} =
createSharedToggleStyles({
theme: t,
hovered,
focused,
selected,
disabled,
isInvalid,
})
return (
<View
style={[
a.justify_center,
a.align_center,
a.border,
a.rounded_full,
t.atoms.border_contrast,
{
height: 20,
width: 20,
},
baseStyles,
hovered || focused ? baseHoverStyles : {},
]}>
{selected ? (
<View
style={[
a.absolute,
a.rounded_full,
{height: 12, width: 12},
selected
? {
backgroundColor: t.palette.primary_500,
}
: {},
indicatorStyles,
]}
/>
) : null}
</View>
)
}
+124
View File
@@ -0,0 +1,124 @@
import React from 'react'
import {View, AccessibilityProps, TextStyle, ViewStyle} from 'react-native'
import {atoms as a, useTheme, native} from '#/alf'
import {Text} from '#/components/Typography'
import * as Toggle from '#/components/forms/Toggle'
export type ItemProps = Omit<Toggle.ItemProps, 'style' | 'role' | 'children'> &
AccessibilityProps &
React.PropsWithChildren<{}>
export type GroupProps = Omit<Toggle.GroupProps, 'style' | 'type'> & {
multiple?: boolean
}
export function Group({children, multiple, ...props}: GroupProps) {
const t = useTheme()
return (
<Toggle.Group type={multiple ? 'checkbox' : 'radio'} {...props}>
<View
style={[
a.flex_row,
a.border,
a.rounded_sm,
a.overflow_hidden,
t.atoms.border,
]}>
{children}
</View>
</Toggle.Group>
)
}
export function Button({children, ...props}: ItemProps) {
return (
<Toggle.Item {...props}>
<ButtonInner>{children}</ButtonInner>
</Toggle.Item>
)
}
function ButtonInner({children}: React.PropsWithChildren<{}>) {
const t = useTheme()
const state = Toggle.useItemContext()
const {baseStyles, hoverStyles, activeStyles, textStyles} =
React.useMemo(() => {
const base: ViewStyle[] = []
const hover: ViewStyle[] = []
const active: ViewStyle[] = []
const text: TextStyle[] = []
hover.push(
t.name === 'light' ? t.atoms.bg_contrast_100 : t.atoms.bg_contrast_25,
)
if (state.selected) {
active.push({
backgroundColor: t.palette.contrast_800,
})
text.push(t.atoms.text_inverted)
hover.push({
backgroundColor: t.palette.contrast_800,
})
if (state.disabled) {
active.push({
backgroundColor: t.palette.contrast_500,
})
}
}
if (state.disabled) {
base.push({
backgroundColor: t.palette.contrast_100,
})
text.push({
opacity: 0.5,
})
}
return {
baseStyles: base,
hoverStyles: hover,
activeStyles: active,
textStyles: text,
}
}, [t, state])
return (
<View
style={[
{
borderLeftWidth: 1,
marginLeft: -1,
},
a.px_lg,
a.py_md,
native({
paddingTop: 14,
}),
t.atoms.bg,
t.atoms.border,
baseStyles,
activeStyles,
(state.hovered || state.focused || state.pressed) && hoverStyles,
]}>
{typeof children === 'string' ? (
<Text
style={[
a.text_center,
a.font_bold,
t.atoms.text_contrast_500,
textStyles,
]}>
{children}
</Text>
) : (
children
)}
</View>
)
}
@@ -0,0 +1,21 @@
import React from 'react'
export function useInteractionState() {
const [state, setState] = React.useState(false)
const onIn = React.useCallback(() => {
setState(true)
}, [setState])
const onOut = React.useCallback(() => {
setState(false)
}, [setState])
return React.useMemo(
() => ({
state,
onIn,
onOut,
}),
[state, onIn, onOut],
)
}
+5
View File
@@ -0,0 +1,5 @@
import {createSinglePathSVG} from './TEMPLATE'
export const ArrowTopRight_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M8 6a1 1 0 0 1 1-1h9a1 1 0 0 1 1 1v9a1 1 0 1 1-2 0V8.414l-9.793 9.793a1 1 0 0 1-1.414-1.414L15.586 7H9a1 1 0 0 1-1-1Z',
})
+5
View File
@@ -0,0 +1,5 @@
import {createSinglePathSVG} from './TEMPLATE'
export const CalendarDays_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M4 3a1 1 0 0 0-1 1v16a1 1 0 0 0 1 1h16a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1H4Zm1 16V9h14v10H5ZM5 7h14V5H5v2Zm3 10.25a1.25 1.25 0 1 0 0-2.5 1.25 1.25 0 0 0 0 2.5ZM17.25 12a1.25 1.25 0 1 1-2.5 0 1.25 1.25 0 0 1 2.5 0ZM12 13.25a1.25 1.25 0 1 0 0-2.5 1.25 1.25 0 0 0 0 2.5ZM9.25 12a1.25 1.25 0 1 1-2.5 0 1.25 1.25 0 0 1 2.5 0ZM12 17.25a1.25 1.25 0 1 0 0-2.5 1.25 1.25 0 0 0 0 2.5Z',
})
+5
View File
@@ -0,0 +1,5 @@
import {createSinglePathSVG} from './TEMPLATE'
export const ColorPalette_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M4 12c0-4.09 3.527-7.5 8-7.5s8 3.41 8 7.5c0 1.579-.419 2.056-.708 2.236-.388.241-1.031.286-2.058.153-.33-.043-.652-.096-.991-.152a65.905 65.905 0 0 0-.531-.087c-.52-.081-1.077-.156-1.61-.164-1.065-.016-2.336.245-2.996 1.567-.418.834-.295 1.67-.078 2.314.18.534.47 1.055.683 1.437v.001l.097.175.01.018C7.432 19.407 4 16.033 4 12Zm8-9.5C6.532 2.5 2 6.7 2 12s4.532 9.5 10 9.5c.401 0 .812-.04 1.166-.193.41-.176.761-.517.866-1.028.085-.416-.03-.796-.118-1.029a5.981 5.981 0 0 0-.351-.73l-.12-.215c-.215-.392-.403-.73-.52-1.078-.13-.387-.111-.614-.029-.78.146-.291.404-.473 1.178-.461.385.005.825.06 1.329.14.15.023.308.05.47.077.36.059.742.122 1.105.17 1.021.132 2.325.213 3.373-.439C21.496 15.22 22 13.874 22 12c0-5.3-4.532-9.5-10-9.5Zm3.5 8.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3ZM9 12.25a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0Zm1.5-2.75a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z',
})
+5
View File
@@ -0,0 +1,5 @@
import {createSinglePathSVG} from './TEMPLATE'
export const Globe_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M4.062 11h2.961c.103-2.204.545-4.218 1.235-5.77.06-.136.123-.269.188-.399A8.007 8.007 0 0 0 4.062 11ZM12 2C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2Zm0 2c-.227 0-.518.1-.868.432-.354.337-.719.872-1.047 1.61-.561 1.263-.958 2.991-1.06 4.958h5.95c-.102-1.967-.499-3.695-1.06-4.958-.328-.738-.693-1.273-1.047-1.61C12.518 4.099 12.227 4 12 4Zm4.977 7c-.103-2.204-.545-4.218-1.235-5.77a9.78 9.78 0 0 0-.188-.399A8.006 8.006 0 0 1 19.938 11h-2.961Zm-2.003 2H9.026c.101 1.966.498 3.695 1.06 4.958.327.738.692 1.273 1.046 1.61.35.333.641.432.868.432.227 0 .518-.1.868-.432.354-.337.719-.872 1.047-1.61.561-1.263.958-2.991 1.06-4.958Zm.58 6.169c.065-.13.128-.263.188-.399.69-1.552 1.132-3.566 1.235-5.77h2.961a8.006 8.006 0 0 1-4.384 6.169Zm-7.108 0a9.877 9.877 0 0 1-.188-.399c-.69-1.552-1.132-3.566-1.235-5.77H4.062a8.006 8.006 0 0 0 4.384 6.169Z',
})
+48
View File
@@ -0,0 +1,48 @@
import React from 'react'
import Svg, {Path} from 'react-native-svg'
import {useCommonSVGProps, Props} from '#/components/icons/common'
export const IconTemplate_Stroke2_Corner0_Rounded = React.forwardRef(
function LogoImpl(props: Props, ref) {
const {fill, size, style, ...rest} = useCommonSVGProps(props)
return (
<Svg
fill="none"
{...rest}
// @ts-ignore it's fiiiiine
ref={ref}
viewBox="0 0 24 24"
width={size}
height={size}
style={[style]}>
<Path
fill={fill}
fillRule="evenodd"
clipRule="evenodd"
d="M4.062 11h2.961c.103-2.204.545-4.218 1.235-5.77.06-.136.123-.269.188-.399A8.007 8.007 0 0 0 4.062 11ZM12 2C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2Zm0 2c-.227 0-.518.1-.868.432-.354.337-.719.872-1.047 1.61-.561 1.263-.958 2.991-1.06 4.958h5.95c-.102-1.967-.499-3.695-1.06-4.958-.328-.738-.693-1.273-1.047-1.61C12.518 4.099 12.227 4 12 4Zm4.977 7c-.103-2.204-.545-4.218-1.235-5.77a9.78 9.78 0 0 0-.188-.399A8.006 8.006 0 0 1 19.938 11h-2.961Zm-2.003 2H9.026c.101 1.966.498 3.695 1.06 4.958.327.738.692 1.273 1.046 1.61.35.333.641.432.868.432.227 0 .518-.1.868-.432.354-.337.719-.872 1.047-1.61.561-1.263.958-2.991 1.06-4.958Zm.58 6.169c.065-.13.128-.263.188-.399.69-1.552 1.132-3.566 1.235-5.77h2.961a8.006 8.006 0 0 1-4.384 6.169Zm-7.108 0a9.877 9.877 0 0 1-.188-.399c-.69-1.552-1.132-3.566-1.235-5.77H4.062a8.006 8.006 0 0 0 4.384 6.169Z"
/>
</Svg>
)
},
)
export function createSinglePathSVG({path}: {path: string}) {
return React.forwardRef<Svg, Props>(function LogoImpl(props, ref) {
const {fill, size, style, ...rest} = useCommonSVGProps(props)
return (
<Svg
fill="none"
{...rest}
ref={ref}
viewBox="0 0 24 24"
width={size}
height={size}
style={[style]}>
<Path fill={fill} fillRule="evenodd" clipRule="evenodd" d={path} />
</Svg>
)
})
}
+32
View File
@@ -0,0 +1,32 @@
import {StyleSheet, TextProps} from 'react-native'
import type {SvgProps, PathProps} from 'react-native-svg'
import {tokens} from '#/alf'
export type Props = {
fill?: PathProps['fill']
style?: TextProps['style']
size?: keyof typeof sizes
} & Omit<SvgProps, 'style' | 'size'>
export const sizes = {
xs: 12,
sm: 16,
md: 20,
lg: 24,
xl: 28,
}
export function useCommonSVGProps(props: Props) {
const {fill, size, ...rest} = props
const style = StyleSheet.flatten(rest.style)
const _fill = fill || style?.color || tokens.color.blue_500
const _size = Number(size ? sizes[size] : rest.width || sizes.md)
return {
fill: _fill,
size: _size,
style,
...rest,
}
}
+18 -9
View File
@@ -4,15 +4,20 @@ import {
} from '@atproto/api' } from '@atproto/api'
import {FeedAPI, FeedAPIResponse} from './types' import {FeedAPI, FeedAPIResponse} from './types'
import {getAgent} from '#/state/session' import {getAgent} from '#/state/session'
import {getContentLanguages} from '#/state/preferences/languages'
export class CustomFeedAPI implements FeedAPI { export class CustomFeedAPI implements FeedAPI {
constructor(public params: GetCustomFeed.QueryParams) {} constructor(public params: GetCustomFeed.QueryParams) {}
async peekLatest(): Promise<AppBskyFeedDefs.FeedViewPost> { async peekLatest(): Promise<AppBskyFeedDefs.FeedViewPost> {
const res = await getAgent().app.bsky.feed.getFeed({ const contentLangs = getContentLanguages().join(',')
...this.params, const res = await getAgent().app.bsky.feed.getFeed(
limit: 1, {
}) ...this.params,
limit: 1,
},
{headers: {'Accept-Language': contentLangs}},
)
return res.data.feed[0] return res.data.feed[0]
} }
@@ -23,11 +28,15 @@ export class CustomFeedAPI implements FeedAPI {
cursor: string | undefined cursor: string | undefined
limit: number limit: number
}): Promise<FeedAPIResponse> { }): Promise<FeedAPIResponse> {
const res = await getAgent().app.bsky.feed.getFeed({ const contentLangs = getContentLanguages().join(',')
...this.params, const res = await getAgent().app.bsky.feed.getFeed(
cursor, {
limit, ...this.params,
}) cursor,
limit,
},
{headers: {'Accept-Language': contentLangs}},
)
if (res.success) { if (res.success) {
// NOTE // NOTE
// some custom feeds fail to enforce the pagination limit // some custom feeds fail to enforce the pagination limit
+89
View File
@@ -0,0 +1,89 @@
import {AppBskyFeedDefs} from '@atproto/api'
import {FeedAPI, FeedAPIResponse} from './types'
import {FollowingFeedAPI} from './following'
import {CustomFeedAPI} from './custom'
import {PROD_DEFAULT_FEED} from '#/lib/constants'
// HACK
// the feed API does not include any facilities for passing down
// non-post elements. adding that is a bit of a heavy lift, and we
// have just one temporary usecase for it: flagging when the home feed
// falls back to discover.
// we use this fallback marker post to drive this instead. see Feed.tsx
// for the usage.
// -prf
export const FALLBACK_MARKER_POST: AppBskyFeedDefs.FeedViewPost = {
post: {
uri: 'fallback-marker-post',
cid: 'fake',
record: {},
author: {
did: 'did:fake',
handle: 'fake.com',
},
indexedAt: new Date().toISOString(),
},
}
export class HomeFeedAPI implements FeedAPI {
following: FollowingFeedAPI
discover: CustomFeedAPI
usingDiscover = false
itemCursor = 0
constructor() {
this.following = new FollowingFeedAPI()
this.discover = new CustomFeedAPI({feed: PROD_DEFAULT_FEED('whats-hot')})
}
reset() {
this.following = new FollowingFeedAPI()
this.discover = new CustomFeedAPI({feed: PROD_DEFAULT_FEED('whats-hot')})
this.usingDiscover = false
this.itemCursor = 0
}
async peekLatest(): Promise<AppBskyFeedDefs.FeedViewPost> {
if (this.usingDiscover) {
return this.discover.peekLatest()
}
return this.following.peekLatest()
}
async fetch({
cursor,
limit,
}: {
cursor: string | undefined
limit: number
}): Promise<FeedAPIResponse> {
if (!cursor) {
this.reset()
}
let returnCursor
let posts: AppBskyFeedDefs.FeedViewPost[] = []
if (!this.usingDiscover) {
const res = await this.following.fetch({cursor, limit})
returnCursor = res.cursor
posts = posts.concat(res.feed)
if (!returnCursor) {
cursor = ''
posts.push(FALLBACK_MARKER_POST)
this.usingDiscover = true
}
}
if (this.usingDiscover) {
const res = await this.discover.fetch({cursor, limit})
returnCursor = res.cursor
posts = posts.concat(res.feed)
}
return {
cursor: returnCursor,
feed: posts,
}
}
}
+11 -6
View File
@@ -8,6 +8,7 @@ import {FeedAPI, FeedAPIResponse, ReasonFeedSource} from './types'
import {FeedParams} from '#/state/queries/post-feed' import {FeedParams} from '#/state/queries/post-feed'
import {FeedTunerFn} from '../feed-manip' import {FeedTunerFn} from '../feed-manip'
import {getAgent} from '#/state/session' import {getAgent} from '#/state/session'
import {getContentLanguages} from '#/state/preferences/languages'
const REQUEST_WAIT_MS = 500 // 500ms const REQUEST_WAIT_MS = 500 // 500ms
const POST_AGE_CUTOFF = 60e3 * 60 * 24 // 24hours const POST_AGE_CUTOFF = 60e3 * 60 * 24 // 24hours
@@ -25,7 +26,7 @@ export class MergeFeedAPI implements FeedAPI {
reset() { reset() {
this.following = new MergeFeedSource_Following(this.feedTuners) this.following = new MergeFeedSource_Following(this.feedTuners)
this.customFeeds = [] // just empty the array, they will be captured in _fetchNext() this.customFeeds = []
this.feedCursor = 0 this.feedCursor = 0
this.itemCursor = 0 this.itemCursor = 0
this.sampleCursor = 0 this.sampleCursor = 0
@@ -231,11 +232,15 @@ class MergeFeedSource_Custom extends MergeFeedSource {
limit: number, limit: number,
): Promise<AppBskyFeedGetTimeline.Response> { ): Promise<AppBskyFeedGetTimeline.Response> {
try { try {
const res = await getAgent().app.bsky.feed.getFeed({ const contentLangs = getContentLanguages().join(',')
cursor, const res = await getAgent().app.bsky.feed.getFeed(
limit, {
feed: this.feedUri, cursor,
}) limit,
feed: this.feedUri,
},
{headers: {'Accept-Language': contentLangs}},
)
// NOTE // NOTE
// some custom feeds fail to enforce the pagination limit // some custom feeds fail to enforce the pagination limit
// so we manually truncate here // so we manually truncate here
+4 -2
View File
@@ -68,11 +68,12 @@ export function parseEmbedPlayerFromUrl(
// youtube // youtube
if (urlp.hostname === 'youtu.be') { if (urlp.hostname === 'youtu.be') {
const videoId = urlp.pathname.split('/')[1] const videoId = urlp.pathname.split('/')[1]
const seek = encodeURIComponent(urlp.searchParams.get('t') ?? 0)
if (videoId) { if (videoId) {
return { return {
type: 'youtube_video', type: 'youtube_video',
source: 'youtube', source: 'youtube',
playerUri: `https://www.youtube.com/embed/${videoId}?autoplay=1&playsinline=1`, playerUri: `https://www.youtube.com/embed/${videoId}?autoplay=1&playsinline=1&start=${seek}`,
} }
} }
} }
@@ -84,13 +85,14 @@ export function parseEmbedPlayerFromUrl(
const [_, page, shortVideoId] = urlp.pathname.split('/') const [_, page, shortVideoId] = urlp.pathname.split('/')
const videoId = const videoId =
page === 'shorts' ? shortVideoId : (urlp.searchParams.get('v') as string) page === 'shorts' ? shortVideoId : (urlp.searchParams.get('v') as string)
const seek = encodeURIComponent(urlp.searchParams.get('t') ?? 0)
if (videoId) { if (videoId) {
return { return {
type: page === 'shorts' ? 'youtube_short' : 'youtube_video', type: page === 'shorts' ? 'youtube_short' : 'youtube_video',
source: page === 'shorts' ? 'youtubeShorts' : 'youtube', source: page === 'shorts' ? 'youtubeShorts' : 'youtube',
hideDetails: page === 'shorts' ? true : undefined, hideDetails: page === 'shorts' ? true : undefined,
playerUri: `https://www.youtube.com/embed/${videoId}?autoplay=1&playsinline=1`, playerUri: `https://www.youtube.com/embed/${videoId}?autoplay=1&playsinline=1&start=${seek}`,
} }
} }
} }
+56 -54
View File
@@ -2,30 +2,32 @@ import {Platform} from 'react-native'
import type {Theme} from './ThemeContext' import type {Theme} from './ThemeContext'
import {colors} from './styles' import {colors} from './styles'
import {darkPalette, lightPalette} from '#/alf/themes'
export const defaultTheme: Theme = { export const defaultTheme: Theme = {
colorScheme: 'light', colorScheme: 'light',
palette: { palette: {
default: { default: {
background: colors.white, background: lightPalette.white,
backgroundLight: colors.gray1, backgroundLight: lightPalette.contrast_50,
text: colors.black, text: lightPalette.black,
textLight: colors.gray5, textLight: lightPalette.contrast_700,
textInverted: colors.white, textInverted: lightPalette.white,
link: colors.blue3, link: lightPalette.primary_500,
border: '#f0e9e9', border: lightPalette.contrast_100,
borderDark: '#e0d9d9', borderDark: lightPalette.contrast_200,
icon: colors.gray4, icon: lightPalette.contrast_500,
// non-standard // non-standard
textVeryLight: colors.gray4, textVeryLight: lightPalette.contrast_400,
replyLine: colors.gray2, replyLine: lightPalette.contrast_100,
replyLineDot: colors.gray3, replyLineDot: lightPalette.contrast_200,
unreadNotifBg: '#ebf6ff', unreadNotifBg: lightPalette.primary_25,
unreadNotifBorder: colors.blue1, unreadNotifBorder: lightPalette.primary_100,
postCtrl: '#71768A', postCtrl: lightPalette.contrast_500,
brandText: '#0066FF', brandText: lightPalette.primary_500,
emptyStateIcon: '#B6B6C9', emptyStateIcon: lightPalette.contrast_300,
borderLinkHover: '#cac1c1', borderLinkHover: lightPalette.contrast_300,
}, },
primary: { primary: {
background: colors.blue3, background: colors.blue3,
@@ -50,15 +52,15 @@ export const defaultTheme: Theme = {
icon: colors.green4, icon: colors.green4,
}, },
inverted: { inverted: {
background: colors.black, background: darkPalette.black,
backgroundLight: colors.gray6, backgroundLight: darkPalette.contrast_50,
text: colors.white, text: darkPalette.white,
textLight: colors.gray3, textLight: darkPalette.contrast_700,
textInverted: colors.black, textInverted: darkPalette.black,
link: colors.blue2, link: darkPalette.primary_500,
border: colors.gray3, border: darkPalette.contrast_100,
borderDark: colors.gray2, borderDark: darkPalette.contrast_200,
icon: colors.gray5, icon: darkPalette.contrast_500,
}, },
error: { error: {
background: colors.red3, background: colors.red3,
@@ -292,26 +294,26 @@ export const darkTheme: Theme = {
palette: { palette: {
...defaultTheme.palette, ...defaultTheme.palette,
default: { default: {
background: colors.black, background: darkPalette.black,
backgroundLight: colors.gray7, backgroundLight: darkPalette.contrast_50,
text: colors.white, text: darkPalette.white,
textLight: colors.gray3, textLight: darkPalette.contrast_700,
textInverted: colors.black, textInverted: darkPalette.black,
link: colors.blue3, link: darkPalette.primary_500,
border: colors.gray7, border: darkPalette.contrast_100,
borderDark: colors.gray6, borderDark: darkPalette.contrast_200,
icon: colors.gray4, icon: darkPalette.contrast_500,
// non-standard // non-standard
textVeryLight: colors.gray4, textVeryLight: darkPalette.contrast_400,
replyLine: colors.gray5, replyLine: darkPalette.contrast_100,
replyLineDot: colors.gray6, replyLineDot: darkPalette.contrast_200,
unreadNotifBg: colors.blue7, unreadNotifBg: darkPalette.primary_975,
unreadNotifBorder: colors.blue6, unreadNotifBorder: darkPalette.primary_900,
postCtrl: '#707489', postCtrl: darkPalette.contrast_500,
brandText: '#0085ff', brandText: darkPalette.primary_500,
emptyStateIcon: colors.gray4, emptyStateIcon: darkPalette.contrast_300,
borderLinkHover: colors.gray5, borderLinkHover: darkPalette.contrast_300,
}, },
primary: { primary: {
...defaultTheme.palette.primary, ...defaultTheme.palette.primary,
@@ -322,15 +324,15 @@ export const darkTheme: Theme = {
textInverted: colors.green2, textInverted: colors.green2,
}, },
inverted: { inverted: {
background: colors.white, background: lightPalette.white,
backgroundLight: colors.gray2, backgroundLight: lightPalette.contrast_50,
text: colors.black, text: lightPalette.black,
textLight: colors.gray5, textLight: lightPalette.contrast_700,
textInverted: colors.white, textInverted: lightPalette.white,
link: colors.blue3, link: lightPalette.primary_500,
border: colors.gray3, border: lightPalette.contrast_100,
borderDark: colors.gray4, borderDark: lightPalette.contrast_200,
icon: colors.gray1, icon: lightPalette.contrast_500,
}, },
}, },
} }
+2
View File
@@ -137,6 +137,8 @@ export function sanitizeAppLanguageSetting(appLanguage: string): AppLanguage {
return AppLanguage.pt_BR return AppLanguage.pt_BR
case 'uk': case 'uk':
return AppLanguage.uk return AppLanguage.uk
case 'ca':
return AppLanguage.ca
default: default:
continue continue
} }
+5
View File
@@ -13,6 +13,7 @@ import {messages as messagesJa} from '#/locale/locales/ja/messages'
import {messages as messagesKo} from '#/locale/locales/ko/messages' import {messages as messagesKo} from '#/locale/locales/ko/messages'
import {messages as messagesPt_BR} from '#/locale/locales/pt-BR/messages' import {messages as messagesPt_BR} from '#/locale/locales/pt-BR/messages'
import {messages as messagesUk} from '#/locale/locales/uk/messages' import {messages as messagesUk} from '#/locale/locales/uk/messages'
import {messages as messagesCa} from '#/locale/locales/ca/messages'
import {sanitizeAppLanguageSetting} from '#/locale/helpers' import {sanitizeAppLanguageSetting} from '#/locale/helpers'
import {AppLanguage} from '#/locale/languages' import {AppLanguage} from '#/locale/languages'
@@ -59,6 +60,10 @@ export async function dynamicActivate(locale: AppLanguage) {
i18n.loadAndActivate({locale, messages: messagesUk}) i18n.loadAndActivate({locale, messages: messagesUk})
break break
} }
case AppLanguage.ca: {
i18n.loadAndActivate({locale, messages: messagesCa})
break
}
default: { default: {
i18n.loadAndActivate({locale, messages: messagesEn}) i18n.loadAndActivate({locale, messages: messagesEn})
break break
+4
View File
@@ -49,6 +49,10 @@ export async function dynamicActivate(locale: AppLanguage) {
mod = await import(`./locales/uk/messages`) mod = await import(`./locales/uk/messages`)
break break
} }
case AppLanguage.ca: {
mod = await import(`./locales/ca/messages`)
break
}
default: { default: {
mod = await import(`./locales/en/messages`) mod = await import(`./locales/en/messages`)
break break
+2
View File
@@ -16,6 +16,7 @@ export enum AppLanguage {
ko = 'ko', ko = 'ko',
pt_BR = 'pt-BR', pt_BR = 'pt-BR',
uk = 'uk', uk = 'uk',
ca = 'ca',
} }
interface AppLanguageConfig { interface AppLanguageConfig {
@@ -35,6 +36,7 @@ export const APP_LANGUAGES: AppLanguageConfig[] = [
{code2: AppLanguage.ko, name: '한국어'}, {code2: AppLanguage.ko, name: '한국어'},
{code2: AppLanguage.pt_BR, name: 'Português (BR)'}, {code2: AppLanguage.pt_BR, name: 'Português (BR)'},
{code2: AppLanguage.uk, name: 'Українська'}, {code2: AppLanguage.uk, name: 'Українська'},
{code2: AppLanguage.ca, name: 'Catalan'},
] ]
export const LANGUAGES: Language[] = [ export const LANGUAGES: Language[] = [
File diff suppressed because it is too large Load Diff
+48 -39
View File
@@ -345,11 +345,6 @@ msgstr ""
msgid "Artistic or non-erotic nudity." msgid "Artistic or non-erotic nudity."
msgstr "Künstlerische oder nicht-erotische Nacktheit." msgstr "Künstlerische oder nicht-erotische Nacktheit."
#: src/view/com/post-thread/PostThread.tsx:400
msgctxt "action"
msgid "Back"
msgstr ""
#: src/view/com/auth/create/CreateAccount.tsx:141 #: src/view/com/auth/create/CreateAccount.tsx:141
#: src/view/com/auth/login/ChooseAccountForm.tsx:151 #: src/view/com/auth/login/ChooseAccountForm.tsx:151
#: src/view/com/auth/login/ForgotPasswordForm.tsx:170 #: src/view/com/auth/login/ForgotPasswordForm.tsx:170
@@ -364,6 +359,11 @@ msgstr ""
msgid "Back" msgid "Back"
msgstr "Zurück" msgstr "Zurück"
#: src/view/com/post-thread/PostThread.tsx:400
msgctxt "action"
msgid "Back"
msgstr ""
#: src/view/screens/Settings.tsx:489 #: src/view/screens/Settings.tsx:489
msgid "Basics" msgid "Basics"
msgstr "Grundlagen" msgstr "Grundlagen"
@@ -398,6 +398,7 @@ msgstr "Diese Kontos sperren?"
msgid "Block this List" msgid "Block this List"
msgstr "" msgstr ""
#: src/view/com/lists/ListCard.tsx:109
#: src/view/com/util/post-embeds/QuoteEmbed.tsx:57 #: src/view/com/util/post-embeds/QuoteEmbed.tsx:57
msgid "Blocked" msgid "Blocked"
msgstr "" msgstr ""
@@ -500,15 +501,6 @@ msgstr "Kamera"
msgid "Can only contain letters, numbers, spaces, dashes, and underscores. Must be at least 4 characters long, but no more than 32 characters long." msgid "Can only contain letters, numbers, spaces, dashes, and underscores. Must be at least 4 characters long, but no more than 32 characters long."
msgstr "Darf nur Buchstaben, Zahlen, Leerzeichen, Bindestriche und Unterstriche enthalten. Muss mindestens 4 Zeichen lang sein, darf aber nicht länger als 32 Zeichen sein." msgstr "Darf nur Buchstaben, Zahlen, Leerzeichen, Bindestriche und Unterstriche enthalten. Muss mindestens 4 Zeichen lang sein, darf aber nicht länger als 32 Zeichen sein."
#: src/view/com/modals/Confirm.tsx:88
#: src/view/com/modals/Confirm.tsx:91
#: src/view/com/modals/CreateOrEditList.tsx:293
#: src/view/com/modals/DeleteAccount.tsx:152
#: src/view/com/modals/DeleteAccount.tsx:227
msgctxt "action"
msgid "Cancel"
msgstr ""
#: src/view/com/composer/Composer.tsx:300 #: src/view/com/composer/Composer.tsx:300
#: src/view/com/composer/Composer.tsx:305 #: src/view/com/composer/Composer.tsx:305
#: src/view/com/modals/ChangeEmail.tsx:218 #: src/view/com/modals/ChangeEmail.tsx:218
@@ -527,8 +519,17 @@ msgstr ""
msgid "Cancel" msgid "Cancel"
msgstr "Abbrechen" msgstr "Abbrechen"
#: src/view/com/modals/Confirm.tsx:88
#: src/view/com/modals/Confirm.tsx:91
#: src/view/com/modals/CreateOrEditList.tsx:293
#: src/view/com/modals/DeleteAccount.tsx:152
#: src/view/com/modals/DeleteAccount.tsx:230
msgctxt "action"
msgid "Cancel"
msgstr ""
#: src/view/com/modals/DeleteAccount.tsx:148 #: src/view/com/modals/DeleteAccount.tsx:148
#: src/view/com/modals/DeleteAccount.tsx:223 #: src/view/com/modals/DeleteAccount.tsx:226
msgid "Cancel account deletion" msgid "Cancel account deletion"
msgstr "Kontolöschung abbrechen" msgstr "Kontolöschung abbrechen"
@@ -704,12 +705,6 @@ msgstr ""
msgid "Compose reply" msgid "Compose reply"
msgstr "Antwort zusammenstellen" msgstr "Antwort zusammenstellen"
#: src/view/com/modals/Confirm.tsx:75
#: src/view/com/modals/Confirm.tsx:78
msgctxt "action"
msgid "Confirm"
msgstr ""
#: src/view/com/modals/AppealLabel.tsx:98 #: src/view/com/modals/AppealLabel.tsx:98
#: src/view/com/modals/SelfLabel.tsx:154 #: src/view/com/modals/SelfLabel.tsx:154
#: src/view/com/modals/VerifyEmail.tsx:231 #: src/view/com/modals/VerifyEmail.tsx:231
@@ -719,6 +714,12 @@ msgstr ""
msgid "Confirm" msgid "Confirm"
msgstr "Bestätigen" msgstr "Bestätigen"
#: src/view/com/modals/Confirm.tsx:75
#: src/view/com/modals/Confirm.tsx:78
msgctxt "action"
msgid "Confirm"
msgstr ""
#: src/view/com/modals/ChangeEmail.tsx:193 #: src/view/com/modals/ChangeEmail.tsx:193
#: src/view/com/modals/ChangeEmail.tsx:195 #: src/view/com/modals/ChangeEmail.tsx:195
msgid "Confirm Change" msgid "Confirm Change"
@@ -728,7 +729,7 @@ msgstr "Änderung bestätigen"
msgid "Confirm content language settings" msgid "Confirm content language settings"
msgstr "Bestätigen Sie die Spracheinstellungen für den Inhalt" msgstr "Bestätigen Sie die Spracheinstellungen für den Inhalt"
#: src/view/com/modals/DeleteAccount.tsx:213 #: src/view/com/modals/DeleteAccount.tsx:216
msgid "Confirm delete account" msgid "Confirm delete account"
msgstr "Bestätigen Sie Konto löschen" msgstr "Bestätigen Sie Konto löschen"
@@ -920,7 +921,7 @@ msgstr "App-Passwort löschen"
msgid "Delete List" msgid "Delete List"
msgstr "Liste löschen" msgstr "Liste löschen"
#: src/view/com/modals/DeleteAccount.tsx:216 #: src/view/com/modals/DeleteAccount.tsx:219
msgid "Delete my account" msgid "Delete my account"
msgstr "Mein Konto löschen" msgstr "Mein Konto löschen"
@@ -1624,7 +1625,7 @@ msgstr ""
msgid "Input new password" msgid "Input new password"
msgstr "" msgstr ""
#: src/view/com/modals/DeleteAccount.tsx:196 #: src/view/com/modals/DeleteAccount.tsx:199
msgid "Input password for account deletion" msgid "Input password for account deletion"
msgstr "" msgstr ""
@@ -2014,6 +2015,10 @@ msgstr ""
msgid "Mute thread" msgid "Mute thread"
msgstr "Thema stummschalten" msgstr "Thema stummschalten"
#: src/view/com/lists/ListCard.tsx:101
msgid "Muted"
msgstr ""
#: src/view/screens/Moderation.tsx:109 #: src/view/screens/Moderation.tsx:109
msgid "Muted accounts" msgid "Muted accounts"
msgstr "Stumme Kontos" msgstr "Stumme Kontos"
@@ -2124,11 +2129,6 @@ msgstr ""
msgid "Newest replies first" msgid "Newest replies first"
msgstr "" msgstr ""
#: src/view/com/auth/onboarding/WelcomeDesktop.tsx:103
msgctxt "action"
msgid "Next"
msgstr ""
#: src/view/com/auth/create/CreateAccount.tsx:154 #: src/view/com/auth/create/CreateAccount.tsx:154
#: src/view/com/auth/login/ForgotPasswordForm.tsx:178 #: src/view/com/auth/login/ForgotPasswordForm.tsx:178
#: src/view/com/auth/login/ForgotPasswordForm.tsx:188 #: src/view/com/auth/login/ForgotPasswordForm.tsx:188
@@ -2139,6 +2139,11 @@ msgstr ""
msgid "Next" msgid "Next"
msgstr "Nächster" msgstr "Nächster"
#: src/view/com/auth/onboarding/WelcomeDesktop.tsx:103
msgctxt "action"
msgid "Next"
msgstr ""
#: src/view/com/lightbox/Lightbox.web.tsx:142 #: src/view/com/lightbox/Lightbox.web.tsx:142
msgid "Next image" msgid "Next image"
msgstr "Nächstes Bild " msgstr "Nächstes Bild "
@@ -2395,7 +2400,7 @@ msgstr "Seite nicht gefunden"
#: src/view/com/auth/create/Step2.tsx:132 #: src/view/com/auth/create/Step2.tsx:132
#: src/view/com/auth/login/LoginForm.tsx:223 #: src/view/com/auth/login/LoginForm.tsx:223
#: src/view/com/auth/login/SetNewPasswordForm.tsx:132 #: src/view/com/auth/login/SetNewPasswordForm.tsx:132
#: src/view/com/modals/DeleteAccount.tsx:195 #: src/view/com/modals/DeleteAccount.tsx:198
msgid "Password" msgid "Password"
msgstr "Passwort" msgstr "Passwort"
@@ -2473,7 +2478,7 @@ msgstr "Bitte geben Sie einen eindeutigen Namen für dieses App-Passwort ein ode
msgid "Please enter your email." msgid "Please enter your email."
msgstr "Bitte geben Sie Ihre E-Mail-Adresse ein." msgstr "Bitte geben Sie Ihre E-Mail-Adresse ein."
#: src/view/com/modals/DeleteAccount.tsx:184 #: src/view/com/modals/DeleteAccount.tsx:187
msgid "Please enter your password as well:" msgid "Please enter your password as well:"
msgstr "Bitte geben Sie auch Ihr Passwort ein:" msgstr "Bitte geben Sie auch Ihr Passwort ein:"
@@ -3315,8 +3320,8 @@ msgid "Subscribe to this list"
msgstr "Abonnieren Sie diese Liste" msgstr "Abonnieren Sie diese Liste"
#: src/view/com/lists/ListCard.tsx:101 #: src/view/com/lists/ListCard.tsx:101
msgid "Subscribed" #~ msgid "Subscribed"
msgstr "" #~ msgstr ""
#: src/view/screens/Search/Search.tsx:362 #: src/view/screens/Search/Search.tsx:362
msgid "Suggested Follows" msgid "Suggested Follows"
@@ -3450,7 +3455,7 @@ msgstr ""
msgid "There was an issue fetching notifications. Tap here to try again." msgid "There was an issue fetching notifications. Tap here to try again."
msgstr "" msgstr ""
#: src/view/com/posts/Feed.tsx:261 #: src/view/com/posts/Feed.tsx:263
msgid "There was an issue fetching posts. Tap here to try again." msgid "There was an issue fetching posts. Tap here to try again."
msgstr "" msgstr ""
@@ -3628,16 +3633,16 @@ msgstr "Stummschaltliste aufheben"
msgid "Unable to contact your service. Please check your Internet connection." msgid "Unable to contact your service. Please check your Internet connection."
msgstr "Sie können Ihren Dienst nicht kontaktieren. Bitte überprüfen Sie Ihre Internetverbindung." msgstr "Sie können Ihren Dienst nicht kontaktieren. Bitte überprüfen Sie Ihre Internetverbindung."
#: src/view/com/profile/ProfileHeader.tsx:475
msgctxt "action"
msgid "Unblock"
msgstr ""
#: src/view/com/profile/ProfileHeader.tsx:472 #: src/view/com/profile/ProfileHeader.tsx:472
#: src/view/screens/ProfileList.tsx:568 #: src/view/screens/ProfileList.tsx:568
msgid "Unblock" msgid "Unblock"
msgstr "Freischalten" msgstr "Freischalten"
#: src/view/com/profile/ProfileHeader.tsx:475
msgctxt "action"
msgid "Unblock"
msgstr ""
#: src/view/com/profile/ProfileHeader.tsx:308 #: src/view/com/profile/ProfileHeader.tsx:308
#: src/view/com/profile/ProfileHeader.tsx:392 #: src/view/com/profile/ProfileHeader.tsx:392
msgid "Unblock Account" msgid "Unblock Account"
@@ -3847,6 +3852,10 @@ msgstr "Seite besuchen"
msgid "Warn" msgid "Warn"
msgstr "" msgstr ""
#: src/view/com/posts/DiscoverFallbackHeader.tsx:29
msgid "We ran out of posts from your follows. Here's the latest from"
msgstr ""
#: src/view/com/modals/AppealLabel.tsx:48 #: src/view/com/modals/AppealLabel.tsx:48
msgid "We'll look into your appeal promptly." msgid "We'll look into your appeal promptly."
msgstr "" msgstr ""
+47 -38
View File
@@ -373,11 +373,6 @@ msgstr ""
#~ msgid "Ask apps to limit the visibility of my account" #~ msgid "Ask apps to limit the visibility of my account"
#~ msgstr "" #~ msgstr ""
#: src/view/com/post-thread/PostThread.tsx:400
msgctxt "action"
msgid "Back"
msgstr ""
#: src/view/com/auth/create/CreateAccount.tsx:141 #: src/view/com/auth/create/CreateAccount.tsx:141
#: src/view/com/auth/login/ChooseAccountForm.tsx:151 #: src/view/com/auth/login/ChooseAccountForm.tsx:151
#: src/view/com/auth/login/ForgotPasswordForm.tsx:170 #: src/view/com/auth/login/ForgotPasswordForm.tsx:170
@@ -392,6 +387,11 @@ msgstr ""
msgid "Back" msgid "Back"
msgstr "" msgstr ""
#: src/view/com/post-thread/PostThread.tsx:400
msgctxt "action"
msgid "Back"
msgstr ""
#: src/view/screens/Settings.tsx:489 #: src/view/screens/Settings.tsx:489
msgid "Basics" msgid "Basics"
msgstr "" msgstr ""
@@ -426,6 +426,7 @@ msgstr ""
msgid "Block this List" msgid "Block this List"
msgstr "" msgstr ""
#: src/view/com/lists/ListCard.tsx:109
#: src/view/com/util/post-embeds/QuoteEmbed.tsx:57 #: src/view/com/util/post-embeds/QuoteEmbed.tsx:57
msgid "Blocked" msgid "Blocked"
msgstr "" msgstr ""
@@ -528,15 +529,6 @@ msgstr ""
msgid "Can only contain letters, numbers, spaces, dashes, and underscores. Must be at least 4 characters long, but no more than 32 characters long." msgid "Can only contain letters, numbers, spaces, dashes, and underscores. Must be at least 4 characters long, but no more than 32 characters long."
msgstr "" msgstr ""
#: src/view/com/modals/Confirm.tsx:88
#: src/view/com/modals/Confirm.tsx:91
#: src/view/com/modals/CreateOrEditList.tsx:293
#: src/view/com/modals/DeleteAccount.tsx:152
#: src/view/com/modals/DeleteAccount.tsx:227
msgctxt "action"
msgid "Cancel"
msgstr ""
#: src/view/com/composer/Composer.tsx:300 #: src/view/com/composer/Composer.tsx:300
#: src/view/com/composer/Composer.tsx:305 #: src/view/com/composer/Composer.tsx:305
#: src/view/com/modals/ChangeEmail.tsx:218 #: src/view/com/modals/ChangeEmail.tsx:218
@@ -555,8 +547,17 @@ msgstr ""
msgid "Cancel" msgid "Cancel"
msgstr "" msgstr ""
#: src/view/com/modals/Confirm.tsx:88
#: src/view/com/modals/Confirm.tsx:91
#: src/view/com/modals/CreateOrEditList.tsx:293
#: src/view/com/modals/DeleteAccount.tsx:152
#: src/view/com/modals/DeleteAccount.tsx:230
msgctxt "action"
msgid "Cancel"
msgstr ""
#: src/view/com/modals/DeleteAccount.tsx:148 #: src/view/com/modals/DeleteAccount.tsx:148
#: src/view/com/modals/DeleteAccount.tsx:223 #: src/view/com/modals/DeleteAccount.tsx:226
msgid "Cancel account deletion" msgid "Cancel account deletion"
msgstr "" msgstr ""
@@ -736,12 +737,6 @@ msgstr ""
msgid "Compose reply" msgid "Compose reply"
msgstr "" msgstr ""
#: src/view/com/modals/Confirm.tsx:75
#: src/view/com/modals/Confirm.tsx:78
msgctxt "action"
msgid "Confirm"
msgstr ""
#: src/view/com/modals/AppealLabel.tsx:98 #: src/view/com/modals/AppealLabel.tsx:98
#: src/view/com/modals/SelfLabel.tsx:154 #: src/view/com/modals/SelfLabel.tsx:154
#: src/view/com/modals/VerifyEmail.tsx:231 #: src/view/com/modals/VerifyEmail.tsx:231
@@ -751,6 +746,12 @@ msgstr ""
msgid "Confirm" msgid "Confirm"
msgstr "" msgstr ""
#: src/view/com/modals/Confirm.tsx:75
#: src/view/com/modals/Confirm.tsx:78
msgctxt "action"
msgid "Confirm"
msgstr ""
#: src/view/com/modals/ChangeEmail.tsx:193 #: src/view/com/modals/ChangeEmail.tsx:193
#: src/view/com/modals/ChangeEmail.tsx:195 #: src/view/com/modals/ChangeEmail.tsx:195
msgid "Confirm Change" msgid "Confirm Change"
@@ -760,7 +761,7 @@ msgstr ""
msgid "Confirm content language settings" msgid "Confirm content language settings"
msgstr "" msgstr ""
#: src/view/com/modals/DeleteAccount.tsx:213 #: src/view/com/modals/DeleteAccount.tsx:216
msgid "Confirm delete account" msgid "Confirm delete account"
msgstr "" msgstr ""
@@ -952,7 +953,7 @@ msgstr ""
msgid "Delete List" msgid "Delete List"
msgstr "" msgstr ""
#: src/view/com/modals/DeleteAccount.tsx:216 #: src/view/com/modals/DeleteAccount.tsx:219
msgid "Delete my account" msgid "Delete my account"
msgstr "" msgstr ""
@@ -1673,7 +1674,7 @@ msgstr ""
msgid "Input new password" msgid "Input new password"
msgstr "" msgstr ""
#: src/view/com/modals/DeleteAccount.tsx:196 #: src/view/com/modals/DeleteAccount.tsx:199
msgid "Input password for account deletion" msgid "Input password for account deletion"
msgstr "" msgstr ""
@@ -2075,6 +2076,10 @@ msgstr ""
msgid "Mute thread" msgid "Mute thread"
msgstr "" msgstr ""
#: src/view/com/lists/ListCard.tsx:101
msgid "Muted"
msgstr ""
#: src/view/screens/Moderation.tsx:109 #: src/view/screens/Moderation.tsx:109
msgid "Muted accounts" msgid "Muted accounts"
msgstr "" msgstr ""
@@ -2189,11 +2194,6 @@ msgstr ""
msgid "Newest replies first" msgid "Newest replies first"
msgstr "" msgstr ""
#: src/view/com/auth/onboarding/WelcomeDesktop.tsx:103
msgctxt "action"
msgid "Next"
msgstr ""
#: src/view/com/auth/create/CreateAccount.tsx:154 #: src/view/com/auth/create/CreateAccount.tsx:154
#: src/view/com/auth/login/ForgotPasswordForm.tsx:178 #: src/view/com/auth/login/ForgotPasswordForm.tsx:178
#: src/view/com/auth/login/ForgotPasswordForm.tsx:188 #: src/view/com/auth/login/ForgotPasswordForm.tsx:188
@@ -2204,6 +2204,11 @@ msgstr ""
msgid "Next" msgid "Next"
msgstr "" msgstr ""
#: src/view/com/auth/onboarding/WelcomeDesktop.tsx:103
msgctxt "action"
msgid "Next"
msgstr ""
#: src/view/com/lightbox/Lightbox.web.tsx:142 #: src/view/com/lightbox/Lightbox.web.tsx:142
msgid "Next image" msgid "Next image"
msgstr "" msgstr ""
@@ -2477,7 +2482,7 @@ msgstr ""
#: src/view/com/auth/create/Step2.tsx:132 #: src/view/com/auth/create/Step2.tsx:132
#: src/view/com/auth/login/LoginForm.tsx:223 #: src/view/com/auth/login/LoginForm.tsx:223
#: src/view/com/auth/login/SetNewPasswordForm.tsx:132 #: src/view/com/auth/login/SetNewPasswordForm.tsx:132
#: src/view/com/modals/DeleteAccount.tsx:195 #: src/view/com/modals/DeleteAccount.tsx:198
msgid "Password" msgid "Password"
msgstr "" msgstr ""
@@ -2555,7 +2560,7 @@ msgstr ""
msgid "Please enter your email." msgid "Please enter your email."
msgstr "" msgstr ""
#: src/view/com/modals/DeleteAccount.tsx:184 #: src/view/com/modals/DeleteAccount.tsx:187
msgid "Please enter your password as well:" msgid "Please enter your password as well:"
msgstr "" msgstr ""
@@ -3422,8 +3427,8 @@ msgid "Subscribe to this list"
msgstr "" msgstr ""
#: src/view/com/lists/ListCard.tsx:101 #: src/view/com/lists/ListCard.tsx:101
msgid "Subscribed" #~ msgid "Subscribed"
msgstr "" #~ msgstr ""
#: src/view/screens/Search/Search.tsx:362 #: src/view/screens/Search/Search.tsx:362
msgid "Suggested Follows" msgid "Suggested Follows"
@@ -3557,7 +3562,7 @@ msgstr ""
msgid "There was an issue fetching notifications. Tap here to try again." msgid "There was an issue fetching notifications. Tap here to try again."
msgstr "" msgstr ""
#: src/view/com/posts/Feed.tsx:261 #: src/view/com/posts/Feed.tsx:263
msgid "There was an issue fetching posts. Tap here to try again." msgid "There was an issue fetching posts. Tap here to try again."
msgstr "" msgstr ""
@@ -3735,13 +3740,13 @@ msgstr ""
msgid "Unable to contact your service. Please check your Internet connection." msgid "Unable to contact your service. Please check your Internet connection."
msgstr "" msgstr ""
#: src/view/com/profile/ProfileHeader.tsx:475 #: src/view/com/profile/ProfileHeader.tsx:472
msgctxt "action" #: src/view/screens/ProfileList.tsx:568
msgid "Unblock" msgid "Unblock"
msgstr "" msgstr ""
#: src/view/com/profile/ProfileHeader.tsx:472 #: src/view/com/profile/ProfileHeader.tsx:475
#: src/view/screens/ProfileList.tsx:568 msgctxt "action"
msgid "Unblock" msgid "Unblock"
msgstr "" msgstr ""
@@ -3958,6 +3963,10 @@ msgstr ""
msgid "Warn" msgid "Warn"
msgstr "" msgstr ""
#: src/view/com/posts/DiscoverFallbackHeader.tsx:29
msgid "We ran out of posts from your follows. Here's the latest from"
msgstr ""
#: src/view/com/modals/AppealLabel.tsx:48 #: src/view/com/modals/AppealLabel.tsx:48
msgid "We'll look into your appeal promptly." msgid "We'll look into your appeal promptly."
msgstr "" msgstr ""
+48 -39
View File
@@ -341,11 +341,6 @@ msgstr ""
msgid "Artistic or non-erotic nudity." msgid "Artistic or non-erotic nudity."
msgstr "Desnudez artística o no erótica." msgstr "Desnudez artística o no erótica."
#: src/view/com/post-thread/PostThread.tsx:400
msgctxt "action"
msgid "Back"
msgstr ""
#: src/view/com/auth/create/CreateAccount.tsx:141 #: src/view/com/auth/create/CreateAccount.tsx:141
#: src/view/com/auth/login/ChooseAccountForm.tsx:151 #: src/view/com/auth/login/ChooseAccountForm.tsx:151
#: src/view/com/auth/login/ForgotPasswordForm.tsx:170 #: src/view/com/auth/login/ForgotPasswordForm.tsx:170
@@ -360,6 +355,11 @@ msgstr ""
msgid "Back" msgid "Back"
msgstr "Regresar" msgstr "Regresar"
#: src/view/com/post-thread/PostThread.tsx:400
msgctxt "action"
msgid "Back"
msgstr ""
#: src/view/screens/Settings.tsx:489 #: src/view/screens/Settings.tsx:489
msgid "Basics" msgid "Basics"
msgstr "Conceptos básicos" msgstr "Conceptos básicos"
@@ -394,6 +394,7 @@ msgstr "¿Bloquear estas cuentas?"
msgid "Block this List" msgid "Block this List"
msgstr "" msgstr ""
#: src/view/com/lists/ListCard.tsx:109
#: src/view/com/util/post-embeds/QuoteEmbed.tsx:57 #: src/view/com/util/post-embeds/QuoteEmbed.tsx:57
msgid "Blocked" msgid "Blocked"
msgstr "" msgstr ""
@@ -496,15 +497,6 @@ msgstr "Cámara"
msgid "Can only contain letters, numbers, spaces, dashes, and underscores. Must be at least 4 characters long, but no more than 32 characters long." msgid "Can only contain letters, numbers, spaces, dashes, and underscores. Must be at least 4 characters long, but no more than 32 characters long."
msgstr "Sólo puede contener letras, números, espacios, guiones y guiones bajos. Debe tener al menos 4 caracteres, pero no más de 32." msgstr "Sólo puede contener letras, números, espacios, guiones y guiones bajos. Debe tener al menos 4 caracteres, pero no más de 32."
#: src/view/com/modals/Confirm.tsx:88
#: src/view/com/modals/Confirm.tsx:91
#: src/view/com/modals/CreateOrEditList.tsx:293
#: src/view/com/modals/DeleteAccount.tsx:152
#: src/view/com/modals/DeleteAccount.tsx:227
msgctxt "action"
msgid "Cancel"
msgstr ""
#: src/view/com/composer/Composer.tsx:300 #: src/view/com/composer/Composer.tsx:300
#: src/view/com/composer/Composer.tsx:305 #: src/view/com/composer/Composer.tsx:305
#: src/view/com/modals/ChangeEmail.tsx:218 #: src/view/com/modals/ChangeEmail.tsx:218
@@ -523,8 +515,17 @@ msgstr ""
msgid "Cancel" msgid "Cancel"
msgstr "Cancelar" msgstr "Cancelar"
#: src/view/com/modals/Confirm.tsx:88
#: src/view/com/modals/Confirm.tsx:91
#: src/view/com/modals/CreateOrEditList.tsx:293
#: src/view/com/modals/DeleteAccount.tsx:152
#: src/view/com/modals/DeleteAccount.tsx:230
msgctxt "action"
msgid "Cancel"
msgstr ""
#: src/view/com/modals/DeleteAccount.tsx:148 #: src/view/com/modals/DeleteAccount.tsx:148
#: src/view/com/modals/DeleteAccount.tsx:223 #: src/view/com/modals/DeleteAccount.tsx:226
msgid "Cancel account deletion" msgid "Cancel account deletion"
msgstr "Cancelar la eliminación de la cuenta" msgstr "Cancelar la eliminación de la cuenta"
@@ -700,12 +701,6 @@ msgstr ""
msgid "Compose reply" msgid "Compose reply"
msgstr "Redactar la respuesta" msgstr "Redactar la respuesta"
#: src/view/com/modals/Confirm.tsx:75
#: src/view/com/modals/Confirm.tsx:78
msgctxt "action"
msgid "Confirm"
msgstr ""
#: src/view/com/modals/AppealLabel.tsx:98 #: src/view/com/modals/AppealLabel.tsx:98
#: src/view/com/modals/SelfLabel.tsx:154 #: src/view/com/modals/SelfLabel.tsx:154
#: src/view/com/modals/VerifyEmail.tsx:231 #: src/view/com/modals/VerifyEmail.tsx:231
@@ -715,6 +710,12 @@ msgstr ""
msgid "Confirm" msgid "Confirm"
msgstr "Confirmar" msgstr "Confirmar"
#: src/view/com/modals/Confirm.tsx:75
#: src/view/com/modals/Confirm.tsx:78
msgctxt "action"
msgid "Confirm"
msgstr ""
#: src/view/com/modals/ChangeEmail.tsx:193 #: src/view/com/modals/ChangeEmail.tsx:193
#: src/view/com/modals/ChangeEmail.tsx:195 #: src/view/com/modals/ChangeEmail.tsx:195
msgid "Confirm Change" msgid "Confirm Change"
@@ -724,7 +725,7 @@ msgstr "Confirmar el cambio"
msgid "Confirm content language settings" msgid "Confirm content language settings"
msgstr "Confirmar la configuración del idioma del contenido" msgstr "Confirmar la configuración del idioma del contenido"
#: src/view/com/modals/DeleteAccount.tsx:213 #: src/view/com/modals/DeleteAccount.tsx:216
msgid "Confirm delete account" msgid "Confirm delete account"
msgstr "Confirmar eliminación de cuenta" msgstr "Confirmar eliminación de cuenta"
@@ -916,7 +917,7 @@ msgstr "Borrar la contraseña de la app"
msgid "Delete List" msgid "Delete List"
msgstr "Borrar la lista" msgstr "Borrar la lista"
#: src/view/com/modals/DeleteAccount.tsx:216 #: src/view/com/modals/DeleteAccount.tsx:219
msgid "Delete my account" msgid "Delete my account"
msgstr "Borrar mi cuenta" msgstr "Borrar mi cuenta"
@@ -1620,7 +1621,7 @@ msgstr ""
msgid "Input new password" msgid "Input new password"
msgstr "" msgstr ""
#: src/view/com/modals/DeleteAccount.tsx:196 #: src/view/com/modals/DeleteAccount.tsx:199
msgid "Input password for account deletion" msgid "Input password for account deletion"
msgstr "" msgstr ""
@@ -2006,6 +2007,10 @@ msgstr ""
msgid "Mute thread" msgid "Mute thread"
msgstr "Silenciar el hilo" msgstr "Silenciar el hilo"
#: src/view/com/lists/ListCard.tsx:101
msgid "Muted"
msgstr ""
#: src/view/screens/Moderation.tsx:109 #: src/view/screens/Moderation.tsx:109
msgid "Muted accounts" msgid "Muted accounts"
msgstr "Cuentas silenciadas" msgstr "Cuentas silenciadas"
@@ -2116,11 +2121,6 @@ msgstr ""
msgid "Newest replies first" msgid "Newest replies first"
msgstr "" msgstr ""
#: src/view/com/auth/onboarding/WelcomeDesktop.tsx:103
msgctxt "action"
msgid "Next"
msgstr ""
#: src/view/com/auth/create/CreateAccount.tsx:154 #: src/view/com/auth/create/CreateAccount.tsx:154
#: src/view/com/auth/login/ForgotPasswordForm.tsx:178 #: src/view/com/auth/login/ForgotPasswordForm.tsx:178
#: src/view/com/auth/login/ForgotPasswordForm.tsx:188 #: src/view/com/auth/login/ForgotPasswordForm.tsx:188
@@ -2131,6 +2131,11 @@ msgstr ""
msgid "Next" msgid "Next"
msgstr "Siguiente" msgstr "Siguiente"
#: src/view/com/auth/onboarding/WelcomeDesktop.tsx:103
msgctxt "action"
msgid "Next"
msgstr ""
#: src/view/com/lightbox/Lightbox.web.tsx:142 #: src/view/com/lightbox/Lightbox.web.tsx:142
msgid "Next image" msgid "Next image"
msgstr "Imagen nueva" msgstr "Imagen nueva"
@@ -2387,7 +2392,7 @@ msgstr "Página no encontrada"
#: src/view/com/auth/create/Step2.tsx:132 #: src/view/com/auth/create/Step2.tsx:132
#: src/view/com/auth/login/LoginForm.tsx:223 #: src/view/com/auth/login/LoginForm.tsx:223
#: src/view/com/auth/login/SetNewPasswordForm.tsx:132 #: src/view/com/auth/login/SetNewPasswordForm.tsx:132
#: src/view/com/modals/DeleteAccount.tsx:195 #: src/view/com/modals/DeleteAccount.tsx:198
msgid "Password" msgid "Password"
msgstr "Contraseña" msgstr "Contraseña"
@@ -2465,7 +2470,7 @@ msgstr "Introduce un nombre único para la contraseña de esta app o utiliza una
msgid "Please enter your email." msgid "Please enter your email."
msgstr "Introduce tu correo electrónico." msgstr "Introduce tu correo electrónico."
#: src/view/com/modals/DeleteAccount.tsx:184 #: src/view/com/modals/DeleteAccount.tsx:187
msgid "Please enter your password as well:" msgid "Please enter your password as well:"
msgstr "Introduce tu contraseña, también:" msgstr "Introduce tu contraseña, también:"
@@ -3302,8 +3307,8 @@ msgid "Subscribe to this list"
msgstr "Suscribirse a esta lista" msgstr "Suscribirse a esta lista"
#: src/view/com/lists/ListCard.tsx:101 #: src/view/com/lists/ListCard.tsx:101
msgid "Subscribed" #~ msgid "Subscribed"
msgstr "" #~ msgstr ""
#: src/view/screens/Search/Search.tsx:362 #: src/view/screens/Search/Search.tsx:362
msgid "Suggested Follows" msgid "Suggested Follows"
@@ -3437,7 +3442,7 @@ msgstr ""
msgid "There was an issue fetching notifications. Tap here to try again." msgid "There was an issue fetching notifications. Tap here to try again."
msgstr "" msgstr ""
#: src/view/com/posts/Feed.tsx:261 #: src/view/com/posts/Feed.tsx:263
msgid "There was an issue fetching posts. Tap here to try again." msgid "There was an issue fetching posts. Tap here to try again."
msgstr "" msgstr ""
@@ -3611,16 +3616,16 @@ msgstr "Desactivar la opción de silenciar la lista"
msgid "Unable to contact your service. Please check your Internet connection." msgid "Unable to contact your service. Please check your Internet connection."
msgstr "No se puede contactar con tu servicio. Comprueba tu conexión a Internet." msgstr "No se puede contactar con tu servicio. Comprueba tu conexión a Internet."
#: src/view/com/profile/ProfileHeader.tsx:475
msgctxt "action"
msgid "Unblock"
msgstr ""
#: src/view/com/profile/ProfileHeader.tsx:472 #: src/view/com/profile/ProfileHeader.tsx:472
#: src/view/screens/ProfileList.tsx:568 #: src/view/screens/ProfileList.tsx:568
msgid "Unblock" msgid "Unblock"
msgstr "Desbloquear" msgstr "Desbloquear"
#: src/view/com/profile/ProfileHeader.tsx:475
msgctxt "action"
msgid "Unblock"
msgstr ""
#: src/view/com/profile/ProfileHeader.tsx:308 #: src/view/com/profile/ProfileHeader.tsx:308
#: src/view/com/profile/ProfileHeader.tsx:392 #: src/view/com/profile/ProfileHeader.tsx:392
msgid "Unblock Account" msgid "Unblock Account"
@@ -3830,6 +3835,10 @@ msgstr "Visitar el sitio"
msgid "Warn" msgid "Warn"
msgstr "" msgstr ""
#: src/view/com/posts/DiscoverFallbackHeader.tsx:29
msgid "We ran out of posts from your follows. Here's the latest from"
msgstr ""
#: src/view/com/modals/AppealLabel.tsx:48 #: src/view/com/modals/AppealLabel.tsx:48
msgid "We'll look into your appeal promptly." msgid "We'll look into your appeal promptly."
msgstr "" msgstr ""
File diff suppressed because it is too large Load Diff
+48 -39
View File
@@ -373,11 +373,6 @@ msgstr "कलात्मक या गैर-कामुक नग्नत
#~ msgid "Ask apps to limit the visibility of my account" #~ msgid "Ask apps to limit the visibility of my account"
#~ msgstr "" #~ msgstr ""
#: src/view/com/post-thread/PostThread.tsx:400
msgctxt "action"
msgid "Back"
msgstr ""
#: src/view/com/auth/create/CreateAccount.tsx:141 #: src/view/com/auth/create/CreateAccount.tsx:141
#: src/view/com/auth/login/ChooseAccountForm.tsx:151 #: src/view/com/auth/login/ChooseAccountForm.tsx:151
#: src/view/com/auth/login/ForgotPasswordForm.tsx:170 #: src/view/com/auth/login/ForgotPasswordForm.tsx:170
@@ -392,6 +387,11 @@ msgstr ""
msgid "Back" msgid "Back"
msgstr "वापस" msgstr "वापस"
#: src/view/com/post-thread/PostThread.tsx:400
msgctxt "action"
msgid "Back"
msgstr ""
#: src/view/screens/Settings.tsx:489 #: src/view/screens/Settings.tsx:489
msgid "Basics" msgid "Basics"
msgstr "मूल बातें" msgstr "मूल बातें"
@@ -426,6 +426,7 @@ msgstr "खाता ब्लॉक करें?"
msgid "Block this List" msgid "Block this List"
msgstr "" msgstr ""
#: src/view/com/lists/ListCard.tsx:109
#: src/view/com/util/post-embeds/QuoteEmbed.tsx:57 #: src/view/com/util/post-embeds/QuoteEmbed.tsx:57
msgid "Blocked" msgid "Blocked"
msgstr "" msgstr ""
@@ -528,15 +529,6 @@ msgstr "कैमरा"
msgid "Can only contain letters, numbers, spaces, dashes, and underscores. Must be at least 4 characters long, but no more than 32 characters long." msgid "Can only contain letters, numbers, spaces, dashes, and underscores. Must be at least 4 characters long, but no more than 32 characters long."
msgstr "केवल अक्षर, संख्या, रिक्त स्थान, डैश और अंडरस्कोर हो सकते हैं। कम से कम 4 अक्षर लंबा होना चाहिए, लेकिन 32 अक्षरों से अधिक लंबा नहीं होना चाहिए।।" msgstr "केवल अक्षर, संख्या, रिक्त स्थान, डैश और अंडरस्कोर हो सकते हैं। कम से कम 4 अक्षर लंबा होना चाहिए, लेकिन 32 अक्षरों से अधिक लंबा नहीं होना चाहिए।।"
#: src/view/com/modals/Confirm.tsx:88
#: src/view/com/modals/Confirm.tsx:91
#: src/view/com/modals/CreateOrEditList.tsx:293
#: src/view/com/modals/DeleteAccount.tsx:152
#: src/view/com/modals/DeleteAccount.tsx:227
msgctxt "action"
msgid "Cancel"
msgstr ""
#: src/view/com/composer/Composer.tsx:300 #: src/view/com/composer/Composer.tsx:300
#: src/view/com/composer/Composer.tsx:305 #: src/view/com/composer/Composer.tsx:305
#: src/view/com/modals/ChangeEmail.tsx:218 #: src/view/com/modals/ChangeEmail.tsx:218
@@ -555,8 +547,17 @@ msgstr ""
msgid "Cancel" msgid "Cancel"
msgstr "कैंसिल" msgstr "कैंसिल"
#: src/view/com/modals/Confirm.tsx:88
#: src/view/com/modals/Confirm.tsx:91
#: src/view/com/modals/CreateOrEditList.tsx:293
#: src/view/com/modals/DeleteAccount.tsx:152
#: src/view/com/modals/DeleteAccount.tsx:230
msgctxt "action"
msgid "Cancel"
msgstr ""
#: src/view/com/modals/DeleteAccount.tsx:148 #: src/view/com/modals/DeleteAccount.tsx:148
#: src/view/com/modals/DeleteAccount.tsx:223 #: src/view/com/modals/DeleteAccount.tsx:226
msgid "Cancel account deletion" msgid "Cancel account deletion"
msgstr "अकाउंट बंद मत करो" msgstr "अकाउंट बंद मत करो"
@@ -732,12 +733,6 @@ msgstr ""
msgid "Compose reply" msgid "Compose reply"
msgstr "जवाब लिखो" msgstr "जवाब लिखो"
#: src/view/com/modals/Confirm.tsx:75
#: src/view/com/modals/Confirm.tsx:78
msgctxt "action"
msgid "Confirm"
msgstr ""
#: src/view/com/modals/AppealLabel.tsx:98 #: src/view/com/modals/AppealLabel.tsx:98
#: src/view/com/modals/SelfLabel.tsx:154 #: src/view/com/modals/SelfLabel.tsx:154
#: src/view/com/modals/VerifyEmail.tsx:231 #: src/view/com/modals/VerifyEmail.tsx:231
@@ -747,6 +742,12 @@ msgstr ""
msgid "Confirm" msgid "Confirm"
msgstr "हो गया" msgstr "हो गया"
#: src/view/com/modals/Confirm.tsx:75
#: src/view/com/modals/Confirm.tsx:78
msgctxt "action"
msgid "Confirm"
msgstr ""
#: src/view/com/modals/ChangeEmail.tsx:193 #: src/view/com/modals/ChangeEmail.tsx:193
#: src/view/com/modals/ChangeEmail.tsx:195 #: src/view/com/modals/ChangeEmail.tsx:195
msgid "Confirm Change" msgid "Confirm Change"
@@ -756,7 +757,7 @@ msgstr "बदलाव की पुष्टि करें"
msgid "Confirm content language settings" msgid "Confirm content language settings"
msgstr "सामग्री भाषा सेटिंग्स की पुष्टि करें" msgstr "सामग्री भाषा सेटिंग्स की पुष्टि करें"
#: src/view/com/modals/DeleteAccount.tsx:213 #: src/view/com/modals/DeleteAccount.tsx:216
msgid "Confirm delete account" msgid "Confirm delete account"
msgstr "खाते को हटा दें" msgstr "खाते को हटा दें"
@@ -948,7 +949,7 @@ msgstr "अप्प पासवर्ड हटाएं"
msgid "Delete List" msgid "Delete List"
msgstr "सूची हटाएँ" msgstr "सूची हटाएँ"
#: src/view/com/modals/DeleteAccount.tsx:216 #: src/view/com/modals/DeleteAccount.tsx:219
msgid "Delete my account" msgid "Delete my account"
msgstr "मेरा खाता हटाएं" msgstr "मेरा खाता हटाएं"
@@ -1665,7 +1666,7 @@ msgstr ""
msgid "Input new password" msgid "Input new password"
msgstr "" msgstr ""
#: src/view/com/modals/DeleteAccount.tsx:196 #: src/view/com/modals/DeleteAccount.tsx:199
msgid "Input password for account deletion" msgid "Input password for account deletion"
msgstr "" msgstr ""
@@ -2067,6 +2068,10 @@ msgstr ""
msgid "Mute thread" msgid "Mute thread"
msgstr "थ्रेड म्यूट करें" msgstr "थ्रेड म्यूट करें"
#: src/view/com/lists/ListCard.tsx:101
msgid "Muted"
msgstr ""
#: src/view/screens/Moderation.tsx:109 #: src/view/screens/Moderation.tsx:109
msgid "Muted accounts" msgid "Muted accounts"
msgstr "म्यूट किए गए खाते" msgstr "म्यूट किए गए खाते"
@@ -2181,11 +2186,6 @@ msgstr ""
msgid "Newest replies first" msgid "Newest replies first"
msgstr "" msgstr ""
#: src/view/com/auth/onboarding/WelcomeDesktop.tsx:103
msgctxt "action"
msgid "Next"
msgstr ""
#: src/view/com/auth/create/CreateAccount.tsx:154 #: src/view/com/auth/create/CreateAccount.tsx:154
#: src/view/com/auth/login/ForgotPasswordForm.tsx:178 #: src/view/com/auth/login/ForgotPasswordForm.tsx:178
#: src/view/com/auth/login/ForgotPasswordForm.tsx:188 #: src/view/com/auth/login/ForgotPasswordForm.tsx:188
@@ -2196,6 +2196,11 @@ msgstr ""
msgid "Next" msgid "Next"
msgstr "अगला" msgstr "अगला"
#: src/view/com/auth/onboarding/WelcomeDesktop.tsx:103
msgctxt "action"
msgid "Next"
msgstr ""
#: src/view/com/lightbox/Lightbox.web.tsx:142 #: src/view/com/lightbox/Lightbox.web.tsx:142
msgid "Next image" msgid "Next image"
msgstr "अगली फोटो" msgstr "अगली फोटो"
@@ -2469,7 +2474,7 @@ msgstr "पृष्ठ नहीं मिला"
#: src/view/com/auth/create/Step2.tsx:132 #: src/view/com/auth/create/Step2.tsx:132
#: src/view/com/auth/login/LoginForm.tsx:223 #: src/view/com/auth/login/LoginForm.tsx:223
#: src/view/com/auth/login/SetNewPasswordForm.tsx:132 #: src/view/com/auth/login/SetNewPasswordForm.tsx:132
#: src/view/com/modals/DeleteAccount.tsx:195 #: src/view/com/modals/DeleteAccount.tsx:198
msgid "Password" msgid "Password"
msgstr "पासवर्ड" msgstr "पासवर्ड"
@@ -2547,7 +2552,7 @@ msgstr "कृपया इस ऐप पासवर्ड के लिए ए
msgid "Please enter your email." msgid "Please enter your email."
msgstr "" msgstr ""
#: src/view/com/modals/DeleteAccount.tsx:184 #: src/view/com/modals/DeleteAccount.tsx:187
msgid "Please enter your password as well:" msgid "Please enter your password as well:"
msgstr "कृपया अपना पासवर्ड भी दर्ज करें:" msgstr "कृपया अपना पासवर्ड भी दर्ज करें:"
@@ -3414,8 +3419,8 @@ msgid "Subscribe to this list"
msgstr "इस सूची को सब्सक्राइब करें" msgstr "इस सूची को सब्सक्राइब करें"
#: src/view/com/lists/ListCard.tsx:101 #: src/view/com/lists/ListCard.tsx:101
msgid "Subscribed" #~ msgid "Subscribed"
msgstr "" #~ msgstr ""
#: src/view/screens/Search/Search.tsx:362 #: src/view/screens/Search/Search.tsx:362
msgid "Suggested Follows" msgid "Suggested Follows"
@@ -3549,7 +3554,7 @@ msgstr ""
msgid "There was an issue fetching notifications. Tap here to try again." msgid "There was an issue fetching notifications. Tap here to try again."
msgstr "" msgstr ""
#: src/view/com/posts/Feed.tsx:261 #: src/view/com/posts/Feed.tsx:263
msgid "There was an issue fetching posts. Tap here to try again." msgid "There was an issue fetching posts. Tap here to try again."
msgstr "" msgstr ""
@@ -3727,16 +3732,16 @@ msgstr ""
msgid "Unable to contact your service. Please check your Internet connection." msgid "Unable to contact your service. Please check your Internet connection."
msgstr "आपकी सेवा से संपर्क करने में असमर्थ। कृपया अपने इंटरनेट कनेक्शन की जांच करें।।" msgstr "आपकी सेवा से संपर्क करने में असमर्थ। कृपया अपने इंटरनेट कनेक्शन की जांच करें।।"
#: src/view/com/profile/ProfileHeader.tsx:475
msgctxt "action"
msgid "Unblock"
msgstr ""
#: src/view/com/profile/ProfileHeader.tsx:472 #: src/view/com/profile/ProfileHeader.tsx:472
#: src/view/screens/ProfileList.tsx:568 #: src/view/screens/ProfileList.tsx:568
msgid "Unblock" msgid "Unblock"
msgstr "अनब्लॉक" msgstr "अनब्लॉक"
#: src/view/com/profile/ProfileHeader.tsx:475
msgctxt "action"
msgid "Unblock"
msgstr ""
#: src/view/com/profile/ProfileHeader.tsx:308 #: src/view/com/profile/ProfileHeader.tsx:308
#: src/view/com/profile/ProfileHeader.tsx:392 #: src/view/com/profile/ProfileHeader.tsx:392
msgid "Unblock Account" msgid "Unblock Account"
@@ -3950,6 +3955,10 @@ msgstr "साइट पर जाएं"
msgid "Warn" msgid "Warn"
msgstr "" msgstr ""
#: src/view/com/posts/DiscoverFallbackHeader.tsx:29
msgid "We ran out of posts from your follows. Here's the latest from"
msgstr ""
#: src/view/com/modals/AppealLabel.tsx:48 #: src/view/com/modals/AppealLabel.tsx:48
msgid "We'll look into your appeal promptly." msgid "We'll look into your appeal promptly."
msgstr "" msgstr ""
+48 -39
View File
@@ -345,11 +345,6 @@ msgstr ""
msgid "Artistic or non-erotic nudity." msgid "Artistic or non-erotic nudity."
msgstr "Ketelanjangan artistik atau non-erotis." msgstr "Ketelanjangan artistik atau non-erotis."
#: src/view/com/post-thread/PostThread.tsx:400
msgctxt "action"
msgid "Back"
msgstr ""
#: src/view/com/auth/create/CreateAccount.tsx:141 #: src/view/com/auth/create/CreateAccount.tsx:141
#: src/view/com/auth/login/ChooseAccountForm.tsx:151 #: src/view/com/auth/login/ChooseAccountForm.tsx:151
#: src/view/com/auth/login/ForgotPasswordForm.tsx:170 #: src/view/com/auth/login/ForgotPasswordForm.tsx:170
@@ -364,6 +359,11 @@ msgstr ""
msgid "Back" msgid "Back"
msgstr "Kembali" msgstr "Kembali"
#: src/view/com/post-thread/PostThread.tsx:400
msgctxt "action"
msgid "Back"
msgstr ""
#: src/view/screens/Settings.tsx:489 #: src/view/screens/Settings.tsx:489
msgid "Basics" msgid "Basics"
msgstr "Dasar" msgstr "Dasar"
@@ -398,6 +398,7 @@ msgstr "Blokir akun ini?"
msgid "Block this List" msgid "Block this List"
msgstr "" msgstr ""
#: src/view/com/lists/ListCard.tsx:109
#: src/view/com/util/post-embeds/QuoteEmbed.tsx:57 #: src/view/com/util/post-embeds/QuoteEmbed.tsx:57
msgid "Blocked" msgid "Blocked"
msgstr "" msgstr ""
@@ -500,15 +501,6 @@ msgstr "Kamera"
msgid "Can only contain letters, numbers, spaces, dashes, and underscores. Must be at least 4 characters long, but no more than 32 characters long." msgid "Can only contain letters, numbers, spaces, dashes, and underscores. Must be at least 4 characters long, but no more than 32 characters long."
msgstr "Hanya dapat terdiri dari huruf, angka, spasi, tanda hubung dan garis bawah. Minimal 4 karakter, namun tidak boleh lebih dari 32 karakter." msgstr "Hanya dapat terdiri dari huruf, angka, spasi, tanda hubung dan garis bawah. Minimal 4 karakter, namun tidak boleh lebih dari 32 karakter."
#: src/view/com/modals/Confirm.tsx:88
#: src/view/com/modals/Confirm.tsx:91
#: src/view/com/modals/CreateOrEditList.tsx:293
#: src/view/com/modals/DeleteAccount.tsx:152
#: src/view/com/modals/DeleteAccount.tsx:227
msgctxt "action"
msgid "Cancel"
msgstr ""
#: src/view/com/composer/Composer.tsx:300 #: src/view/com/composer/Composer.tsx:300
#: src/view/com/composer/Composer.tsx:305 #: src/view/com/composer/Composer.tsx:305
#: src/view/com/modals/ChangeEmail.tsx:218 #: src/view/com/modals/ChangeEmail.tsx:218
@@ -527,8 +519,17 @@ msgstr ""
msgid "Cancel" msgid "Cancel"
msgstr "Batal" msgstr "Batal"
#: src/view/com/modals/Confirm.tsx:88
#: src/view/com/modals/Confirm.tsx:91
#: src/view/com/modals/CreateOrEditList.tsx:293
#: src/view/com/modals/DeleteAccount.tsx:152
#: src/view/com/modals/DeleteAccount.tsx:230
msgctxt "action"
msgid "Cancel"
msgstr ""
#: src/view/com/modals/DeleteAccount.tsx:148 #: src/view/com/modals/DeleteAccount.tsx:148
#: src/view/com/modals/DeleteAccount.tsx:223 #: src/view/com/modals/DeleteAccount.tsx:226
msgid "Cancel account deletion" msgid "Cancel account deletion"
msgstr "Batal menghapus akun" msgstr "Batal menghapus akun"
@@ -704,12 +705,6 @@ msgstr ""
msgid "Compose reply" msgid "Compose reply"
msgstr "Tulis balasan" msgstr "Tulis balasan"
#: src/view/com/modals/Confirm.tsx:75
#: src/view/com/modals/Confirm.tsx:78
msgctxt "action"
msgid "Confirm"
msgstr ""
#: src/view/com/modals/AppealLabel.tsx:98 #: src/view/com/modals/AppealLabel.tsx:98
#: src/view/com/modals/SelfLabel.tsx:154 #: src/view/com/modals/SelfLabel.tsx:154
#: src/view/com/modals/VerifyEmail.tsx:231 #: src/view/com/modals/VerifyEmail.tsx:231
@@ -719,6 +714,12 @@ msgstr ""
msgid "Confirm" msgid "Confirm"
msgstr "Konfirmasi" msgstr "Konfirmasi"
#: src/view/com/modals/Confirm.tsx:75
#: src/view/com/modals/Confirm.tsx:78
msgctxt "action"
msgid "Confirm"
msgstr ""
#: src/view/com/modals/ChangeEmail.tsx:193 #: src/view/com/modals/ChangeEmail.tsx:193
#: src/view/com/modals/ChangeEmail.tsx:195 #: src/view/com/modals/ChangeEmail.tsx:195
msgid "Confirm Change" msgid "Confirm Change"
@@ -728,7 +729,7 @@ msgstr "Konfirmasi Perubahan"
msgid "Confirm content language settings" msgid "Confirm content language settings"
msgstr "Konfirmasi pengaturan bahasa konten" msgstr "Konfirmasi pengaturan bahasa konten"
#: src/view/com/modals/DeleteAccount.tsx:213 #: src/view/com/modals/DeleteAccount.tsx:216
msgid "Confirm delete account" msgid "Confirm delete account"
msgstr "Konfirmasi hapus akun" msgstr "Konfirmasi hapus akun"
@@ -920,7 +921,7 @@ msgstr "Hapus kata sandi aplikasi"
msgid "Delete List" msgid "Delete List"
msgstr "Hapus Daftar " msgstr "Hapus Daftar "
#: src/view/com/modals/DeleteAccount.tsx:216 #: src/view/com/modals/DeleteAccount.tsx:219
msgid "Delete my account" msgid "Delete my account"
msgstr "Hapus akun saya" msgstr "Hapus akun saya"
@@ -1624,7 +1625,7 @@ msgstr ""
msgid "Input new password" msgid "Input new password"
msgstr "" msgstr ""
#: src/view/com/modals/DeleteAccount.tsx:196 #: src/view/com/modals/DeleteAccount.tsx:199
msgid "Input password for account deletion" msgid "Input password for account deletion"
msgstr "" msgstr ""
@@ -2014,6 +2015,10 @@ msgstr ""
msgid "Mute thread" msgid "Mute thread"
msgstr "Bisukan utasan" msgstr "Bisukan utasan"
#: src/view/com/lists/ListCard.tsx:101
msgid "Muted"
msgstr ""
#: src/view/screens/Moderation.tsx:109 #: src/view/screens/Moderation.tsx:109
msgid "Muted accounts" msgid "Muted accounts"
msgstr "Akun yang dibisukan" msgstr "Akun yang dibisukan"
@@ -2124,11 +2129,6 @@ msgstr ""
msgid "Newest replies first" msgid "Newest replies first"
msgstr "" msgstr ""
#: src/view/com/auth/onboarding/WelcomeDesktop.tsx:103
msgctxt "action"
msgid "Next"
msgstr ""
#: src/view/com/auth/create/CreateAccount.tsx:154 #: src/view/com/auth/create/CreateAccount.tsx:154
#: src/view/com/auth/login/ForgotPasswordForm.tsx:178 #: src/view/com/auth/login/ForgotPasswordForm.tsx:178
#: src/view/com/auth/login/ForgotPasswordForm.tsx:188 #: src/view/com/auth/login/ForgotPasswordForm.tsx:188
@@ -2139,6 +2139,11 @@ msgstr ""
msgid "Next" msgid "Next"
msgstr "Berikutnya" msgstr "Berikutnya"
#: src/view/com/auth/onboarding/WelcomeDesktop.tsx:103
msgctxt "action"
msgid "Next"
msgstr ""
#: src/view/com/lightbox/Lightbox.web.tsx:142 #: src/view/com/lightbox/Lightbox.web.tsx:142
msgid "Next image" msgid "Next image"
msgstr "Gambar berikutnya" msgstr "Gambar berikutnya"
@@ -2395,7 +2400,7 @@ msgstr "Halaman tidak ditemukan"
#: src/view/com/auth/create/Step2.tsx:132 #: src/view/com/auth/create/Step2.tsx:132
#: src/view/com/auth/login/LoginForm.tsx:223 #: src/view/com/auth/login/LoginForm.tsx:223
#: src/view/com/auth/login/SetNewPasswordForm.tsx:132 #: src/view/com/auth/login/SetNewPasswordForm.tsx:132
#: src/view/com/modals/DeleteAccount.tsx:195 #: src/view/com/modals/DeleteAccount.tsx:198
msgid "Password" msgid "Password"
msgstr "Kata sandi" msgstr "Kata sandi"
@@ -2473,7 +2478,7 @@ msgstr "Mohon masukkan nama unik untuk Kata Sandi Aplikasi ini atau gunakan nama
msgid "Please enter your email." msgid "Please enter your email."
msgstr "Masukkan email Anda." msgstr "Masukkan email Anda."
#: src/view/com/modals/DeleteAccount.tsx:184 #: src/view/com/modals/DeleteAccount.tsx:187
msgid "Please enter your password as well:" msgid "Please enter your password as well:"
msgstr "Masukkan juga kata sandi Anda:" msgstr "Masukkan juga kata sandi Anda:"
@@ -3315,8 +3320,8 @@ msgid "Subscribe to this list"
msgstr "Langganan ke daftar ini" msgstr "Langganan ke daftar ini"
#: src/view/com/lists/ListCard.tsx:101 #: src/view/com/lists/ListCard.tsx:101
msgid "Subscribed" #~ msgid "Subscribed"
msgstr "" #~ msgstr ""
#: src/view/screens/Search/Search.tsx:362 #: src/view/screens/Search/Search.tsx:362
msgid "Suggested Follows" msgid "Suggested Follows"
@@ -3450,7 +3455,7 @@ msgstr ""
msgid "There was an issue fetching notifications. Tap here to try again." msgid "There was an issue fetching notifications. Tap here to try again."
msgstr "" msgstr ""
#: src/view/com/posts/Feed.tsx:261 #: src/view/com/posts/Feed.tsx:263
msgid "There was an issue fetching posts. Tap here to try again." msgid "There was an issue fetching posts. Tap here to try again."
msgstr "" msgstr ""
@@ -3628,16 +3633,16 @@ msgstr "Bunyikan daftar"
msgid "Unable to contact your service. Please check your Internet connection." msgid "Unable to contact your service. Please check your Internet connection."
msgstr "Tidak dapat terhubung ke layanan. Mohon periksa koneksi internet Anda." msgstr "Tidak dapat terhubung ke layanan. Mohon periksa koneksi internet Anda."
#: src/view/com/profile/ProfileHeader.tsx:475
msgctxt "action"
msgid "Unblock"
msgstr ""
#: src/view/com/profile/ProfileHeader.tsx:472 #: src/view/com/profile/ProfileHeader.tsx:472
#: src/view/screens/ProfileList.tsx:568 #: src/view/screens/ProfileList.tsx:568
msgid "Unblock" msgid "Unblock"
msgstr "Buka blokir" msgstr "Buka blokir"
#: src/view/com/profile/ProfileHeader.tsx:475
msgctxt "action"
msgid "Unblock"
msgstr ""
#: src/view/com/profile/ProfileHeader.tsx:308 #: src/view/com/profile/ProfileHeader.tsx:308
#: src/view/com/profile/ProfileHeader.tsx:392 #: src/view/com/profile/ProfileHeader.tsx:392
msgid "Unblock Account" msgid "Unblock Account"
@@ -3847,6 +3852,10 @@ msgstr "Kunjungi Halaman"
msgid "Warn" msgid "Warn"
msgstr "" msgstr ""
#: src/view/com/posts/DiscoverFallbackHeader.tsx:29
msgid "We ran out of posts from your follows. Here's the latest from"
msgstr ""
#: src/view/com/modals/AppealLabel.tsx:48 #: src/view/com/modals/AppealLabel.tsx:48
msgid "We'll look into your appeal promptly." msgid "We'll look into your appeal promptly."
msgstr "" msgstr ""
+48 -39
View File
@@ -345,11 +345,6 @@ msgstr ""
msgid "Artistic or non-erotic nudity." msgid "Artistic or non-erotic nudity."
msgstr "芸術的または性的ではないヌード。" msgstr "芸術的または性的ではないヌード。"
#: src/view/com/post-thread/PostThread.tsx:400
msgctxt "action"
msgid "Back"
msgstr ""
#: src/view/com/auth/create/CreateAccount.tsx:141 #: src/view/com/auth/create/CreateAccount.tsx:141
#: src/view/com/auth/login/ChooseAccountForm.tsx:151 #: src/view/com/auth/login/ChooseAccountForm.tsx:151
#: src/view/com/auth/login/ForgotPasswordForm.tsx:170 #: src/view/com/auth/login/ForgotPasswordForm.tsx:170
@@ -364,6 +359,11 @@ msgstr ""
msgid "Back" msgid "Back"
msgstr "戻る" msgstr "戻る"
#: src/view/com/post-thread/PostThread.tsx:400
msgctxt "action"
msgid "Back"
msgstr ""
#: src/view/screens/Settings.tsx:489 #: src/view/screens/Settings.tsx:489
msgid "Basics" msgid "Basics"
msgstr "基本" msgstr "基本"
@@ -398,6 +398,7 @@ msgstr "これらのアカウントをブロックしますか?"
msgid "Block this List" msgid "Block this List"
msgstr "" msgstr ""
#: src/view/com/lists/ListCard.tsx:109
#: src/view/com/util/post-embeds/QuoteEmbed.tsx:57 #: src/view/com/util/post-embeds/QuoteEmbed.tsx:57
msgid "Blocked" msgid "Blocked"
msgstr "" msgstr ""
@@ -500,15 +501,6 @@ msgstr "カメラ"
msgid "Can only contain letters, numbers, spaces, dashes, and underscores. Must be at least 4 characters long, but no more than 32 characters long." msgid "Can only contain letters, numbers, spaces, dashes, and underscores. Must be at least 4 characters long, but no more than 32 characters long."
msgstr "文字、数字、スペース、ハイフン、およびアンダースコアのみが使用可能です。長さは4文字以上32文字以下である必要があります。" msgstr "文字、数字、スペース、ハイフン、およびアンダースコアのみが使用可能です。長さは4文字以上32文字以下である必要があります。"
#: src/view/com/modals/Confirm.tsx:88
#: src/view/com/modals/Confirm.tsx:91
#: src/view/com/modals/CreateOrEditList.tsx:293
#: src/view/com/modals/DeleteAccount.tsx:152
#: src/view/com/modals/DeleteAccount.tsx:227
msgctxt "action"
msgid "Cancel"
msgstr ""
#: src/view/com/composer/Composer.tsx:300 #: src/view/com/composer/Composer.tsx:300
#: src/view/com/composer/Composer.tsx:305 #: src/view/com/composer/Composer.tsx:305
#: src/view/com/modals/ChangeEmail.tsx:218 #: src/view/com/modals/ChangeEmail.tsx:218
@@ -527,8 +519,17 @@ msgstr ""
msgid "Cancel" msgid "Cancel"
msgstr "キャンセル" msgstr "キャンセル"
#: src/view/com/modals/Confirm.tsx:88
#: src/view/com/modals/Confirm.tsx:91
#: src/view/com/modals/CreateOrEditList.tsx:293
#: src/view/com/modals/DeleteAccount.tsx:152
#: src/view/com/modals/DeleteAccount.tsx:230
msgctxt "action"
msgid "Cancel"
msgstr ""
#: src/view/com/modals/DeleteAccount.tsx:148 #: src/view/com/modals/DeleteAccount.tsx:148
#: src/view/com/modals/DeleteAccount.tsx:223 #: src/view/com/modals/DeleteAccount.tsx:226
msgid "Cancel account deletion" msgid "Cancel account deletion"
msgstr "アカウントの削除をキャンセル" msgstr "アカウントの削除をキャンセル"
@@ -704,12 +705,6 @@ msgstr ""
msgid "Compose reply" msgid "Compose reply"
msgstr "返信を作成" msgstr "返信を作成"
#: src/view/com/modals/Confirm.tsx:75
#: src/view/com/modals/Confirm.tsx:78
msgctxt "action"
msgid "Confirm"
msgstr ""
#: src/view/com/modals/AppealLabel.tsx:98 #: src/view/com/modals/AppealLabel.tsx:98
#: src/view/com/modals/SelfLabel.tsx:154 #: src/view/com/modals/SelfLabel.tsx:154
#: src/view/com/modals/VerifyEmail.tsx:231 #: src/view/com/modals/VerifyEmail.tsx:231
@@ -719,6 +714,12 @@ msgstr ""
msgid "Confirm" msgid "Confirm"
msgstr "確認" msgstr "確認"
#: src/view/com/modals/Confirm.tsx:75
#: src/view/com/modals/Confirm.tsx:78
msgctxt "action"
msgid "Confirm"
msgstr ""
#: src/view/com/modals/ChangeEmail.tsx:193 #: src/view/com/modals/ChangeEmail.tsx:193
#: src/view/com/modals/ChangeEmail.tsx:195 #: src/view/com/modals/ChangeEmail.tsx:195
msgid "Confirm Change" msgid "Confirm Change"
@@ -728,7 +729,7 @@ msgstr "変更を確認"
msgid "Confirm content language settings" msgid "Confirm content language settings"
msgstr "コンテンツの言語設定を確認" msgstr "コンテンツの言語設定を確認"
#: src/view/com/modals/DeleteAccount.tsx:213 #: src/view/com/modals/DeleteAccount.tsx:216
msgid "Confirm delete account" msgid "Confirm delete account"
msgstr "アカウントの削除を確認" msgstr "アカウントの削除を確認"
@@ -920,7 +921,7 @@ msgstr "アプリパスワードを削除"
msgid "Delete List" msgid "Delete List"
msgstr "リストを削除" msgstr "リストを削除"
#: src/view/com/modals/DeleteAccount.tsx:216 #: src/view/com/modals/DeleteAccount.tsx:219
msgid "Delete my account" msgid "Delete my account"
msgstr "マイアカウントを削除" msgstr "マイアカウントを削除"
@@ -1624,7 +1625,7 @@ msgstr ""
msgid "Input new password" msgid "Input new password"
msgstr "" msgstr ""
#: src/view/com/modals/DeleteAccount.tsx:196 #: src/view/com/modals/DeleteAccount.tsx:199
msgid "Input password for account deletion" msgid "Input password for account deletion"
msgstr "" msgstr ""
@@ -2022,6 +2023,10 @@ msgstr ""
msgid "Mute thread" msgid "Mute thread"
msgstr "スレッドをミュート" msgstr "スレッドをミュート"
#: src/view/com/lists/ListCard.tsx:101
msgid "Muted"
msgstr ""
#: src/view/screens/Moderation.tsx:109 #: src/view/screens/Moderation.tsx:109
msgid "Muted accounts" msgid "Muted accounts"
msgstr "ミュート中のアカウント" msgstr "ミュート中のアカウント"
@@ -2132,11 +2137,6 @@ msgstr ""
msgid "Newest replies first" msgid "Newest replies first"
msgstr "新しい順に返信を表示" msgstr "新しい順に返信を表示"
#: src/view/com/auth/onboarding/WelcomeDesktop.tsx:103
msgctxt "action"
msgid "Next"
msgstr ""
#: src/view/com/auth/create/CreateAccount.tsx:154 #: src/view/com/auth/create/CreateAccount.tsx:154
#: src/view/com/auth/login/ForgotPasswordForm.tsx:178 #: src/view/com/auth/login/ForgotPasswordForm.tsx:178
#: src/view/com/auth/login/ForgotPasswordForm.tsx:188 #: src/view/com/auth/login/ForgotPasswordForm.tsx:188
@@ -2147,6 +2147,11 @@ msgstr ""
msgid "Next" msgid "Next"
msgstr "次へ" msgstr "次へ"
#: src/view/com/auth/onboarding/WelcomeDesktop.tsx:103
msgctxt "action"
msgid "Next"
msgstr ""
#: src/view/com/lightbox/Lightbox.web.tsx:142 #: src/view/com/lightbox/Lightbox.web.tsx:142
msgid "Next image" msgid "Next image"
msgstr "次の画像" msgstr "次の画像"
@@ -2407,7 +2412,7 @@ msgstr "ページが見つかりません"
#: src/view/com/auth/create/Step2.tsx:132 #: src/view/com/auth/create/Step2.tsx:132
#: src/view/com/auth/login/LoginForm.tsx:223 #: src/view/com/auth/login/LoginForm.tsx:223
#: src/view/com/auth/login/SetNewPasswordForm.tsx:132 #: src/view/com/auth/login/SetNewPasswordForm.tsx:132
#: src/view/com/modals/DeleteAccount.tsx:195 #: src/view/com/modals/DeleteAccount.tsx:198
msgid "Password" msgid "Password"
msgstr "パスワード" msgstr "パスワード"
@@ -2485,7 +2490,7 @@ msgstr "このアプリパスワードに固有の名前を入力するか、ラ
msgid "Please enter your email." msgid "Please enter your email."
msgstr "メールアドレスを入力してください。" msgstr "メールアドレスを入力してください。"
#: src/view/com/modals/DeleteAccount.tsx:184 #: src/view/com/modals/DeleteAccount.tsx:187
msgid "Please enter your password as well:" msgid "Please enter your password as well:"
msgstr "パスワードも入力してください:" msgstr "パスワードも入力してください:"
@@ -3331,8 +3336,8 @@ msgid "Subscribe to this list"
msgstr "このリストに登録" msgstr "このリストに登録"
#: src/view/com/lists/ListCard.tsx:101 #: src/view/com/lists/ListCard.tsx:101
msgid "Subscribed" #~ msgid "Subscribed"
msgstr "" #~ msgstr ""
#: src/view/screens/Search/Search.tsx:362 #: src/view/screens/Search/Search.tsx:362
msgid "Suggested Follows" msgid "Suggested Follows"
@@ -3466,7 +3471,7 @@ msgstr ""
msgid "There was an issue fetching notifications. Tap here to try again." msgid "There was an issue fetching notifications. Tap here to try again."
msgstr "" msgstr ""
#: src/view/com/posts/Feed.tsx:261 #: src/view/com/posts/Feed.tsx:263
msgid "There was an issue fetching posts. Tap here to try again." msgid "There was an issue fetching posts. Tap here to try again."
msgstr "" msgstr ""
@@ -3644,16 +3649,16 @@ msgstr "リストでのミュートを解除"
msgid "Unable to contact your service. Please check your Internet connection." msgid "Unable to contact your service. Please check your Internet connection."
msgstr "あなたのサービスに接続できません。インターネットの接続を確認してください。" msgstr "あなたのサービスに接続できません。インターネットの接続を確認してください。"
#: src/view/com/profile/ProfileHeader.tsx:475
msgctxt "action"
msgid "Unblock"
msgstr ""
#: src/view/com/profile/ProfileHeader.tsx:472 #: src/view/com/profile/ProfileHeader.tsx:472
#: src/view/screens/ProfileList.tsx:568 #: src/view/screens/ProfileList.tsx:568
msgid "Unblock" msgid "Unblock"
msgstr "ブロックを解除" msgstr "ブロックを解除"
#: src/view/com/profile/ProfileHeader.tsx:475
msgctxt "action"
msgid "Unblock"
msgstr ""
#: src/view/com/profile/ProfileHeader.tsx:308 #: src/view/com/profile/ProfileHeader.tsx:308
#: src/view/com/profile/ProfileHeader.tsx:392 #: src/view/com/profile/ProfileHeader.tsx:392
msgid "Unblock Account" msgid "Unblock Account"
@@ -3863,6 +3868,10 @@ msgstr "サイトへアクセス"
msgid "Warn" msgid "Warn"
msgstr "" msgstr ""
#: src/view/com/posts/DiscoverFallbackHeader.tsx:29
msgid "We ran out of posts from your follows. Here's the latest from"
msgstr ""
#: src/view/com/modals/AppealLabel.tsx:48 #: src/view/com/modals/AppealLabel.tsx:48
msgid "We'll look into your appeal promptly." msgid "We'll look into your appeal promptly."
msgstr "" msgstr ""
+48 -39
View File
@@ -421,11 +421,6 @@ msgstr "선정적이지 않거나 예술적인 노출."
#~ msgid "Ask apps to limit the visibility of my account" #~ msgid "Ask apps to limit the visibility of my account"
#~ msgstr "내 계정의 공개 범위를 제한하도록 앱에 요청하기" #~ msgstr "내 계정의 공개 범위를 제한하도록 앱에 요청하기"
#: src/view/com/post-thread/PostThread.tsx:400
msgctxt "action"
msgid "Back"
msgstr ""
#: src/view/com/auth/create/CreateAccount.tsx:141 #: src/view/com/auth/create/CreateAccount.tsx:141
#: src/view/com/auth/login/ChooseAccountForm.tsx:151 #: src/view/com/auth/login/ChooseAccountForm.tsx:151
#: src/view/com/auth/login/ForgotPasswordForm.tsx:170 #: src/view/com/auth/login/ForgotPasswordForm.tsx:170
@@ -440,6 +435,11 @@ msgstr ""
msgid "Back" msgid "Back"
msgstr "뒤로" msgstr "뒤로"
#: src/view/com/post-thread/PostThread.tsx:400
msgctxt "action"
msgid "Back"
msgstr ""
#: src/view/screens/Settings.tsx:489 #: src/view/screens/Settings.tsx:489
msgid "Basics" msgid "Basics"
msgstr "기본" msgstr "기본"
@@ -474,6 +474,7 @@ msgstr "이 계정들을 차단하시겠습니까?"
msgid "Block this List" msgid "Block this List"
msgstr "" msgstr ""
#: src/view/com/lists/ListCard.tsx:109
#: src/view/com/util/post-embeds/QuoteEmbed.tsx:57 #: src/view/com/util/post-embeds/QuoteEmbed.tsx:57
msgid "Blocked" msgid "Blocked"
msgstr "" msgstr ""
@@ -580,15 +581,6 @@ msgstr "카메라"
msgid "Can only contain letters, numbers, spaces, dashes, and underscores. Must be at least 4 characters long, but no more than 32 characters long." msgid "Can only contain letters, numbers, spaces, dashes, and underscores. Must be at least 4 characters long, but no more than 32 characters long."
msgstr "글자, 숫자, 공백, 대시, 밑줄만 포함할 수 있습니다. 길이는 4자 이상이어야 하고 32자를 넘지 않아야 합니다." msgstr "글자, 숫자, 공백, 대시, 밑줄만 포함할 수 있습니다. 길이는 4자 이상이어야 하고 32자를 넘지 않아야 합니다."
#: src/view/com/modals/Confirm.tsx:88
#: src/view/com/modals/Confirm.tsx:91
#: src/view/com/modals/CreateOrEditList.tsx:293
#: src/view/com/modals/DeleteAccount.tsx:152
#: src/view/com/modals/DeleteAccount.tsx:227
msgctxt "action"
msgid "Cancel"
msgstr ""
#: src/view/com/composer/Composer.tsx:300 #: src/view/com/composer/Composer.tsx:300
#: src/view/com/composer/Composer.tsx:305 #: src/view/com/composer/Composer.tsx:305
#: src/view/com/modals/ChangeEmail.tsx:218 #: src/view/com/modals/ChangeEmail.tsx:218
@@ -607,8 +599,17 @@ msgstr ""
msgid "Cancel" msgid "Cancel"
msgstr "취소" msgstr "취소"
#: src/view/com/modals/Confirm.tsx:88
#: src/view/com/modals/Confirm.tsx:91
#: src/view/com/modals/CreateOrEditList.tsx:293
#: src/view/com/modals/DeleteAccount.tsx:152
#: src/view/com/modals/DeleteAccount.tsx:230
msgctxt "action"
msgid "Cancel"
msgstr ""
#: src/view/com/modals/DeleteAccount.tsx:148 #: src/view/com/modals/DeleteAccount.tsx:148
#: src/view/com/modals/DeleteAccount.tsx:223 #: src/view/com/modals/DeleteAccount.tsx:226
msgid "Cancel account deletion" msgid "Cancel account deletion"
msgstr "계정 삭제 취소" msgstr "계정 삭제 취소"
@@ -788,12 +789,6 @@ msgstr ""
msgid "Compose reply" msgid "Compose reply"
msgstr "답글 작성하기" msgstr "답글 작성하기"
#: src/view/com/modals/Confirm.tsx:75
#: src/view/com/modals/Confirm.tsx:78
msgctxt "action"
msgid "Confirm"
msgstr ""
#: src/view/com/modals/AppealLabel.tsx:98 #: src/view/com/modals/AppealLabel.tsx:98
#: src/view/com/modals/SelfLabel.tsx:154 #: src/view/com/modals/SelfLabel.tsx:154
#: src/view/com/modals/VerifyEmail.tsx:231 #: src/view/com/modals/VerifyEmail.tsx:231
@@ -803,6 +798,12 @@ msgstr ""
msgid "Confirm" msgid "Confirm"
msgstr "확인" msgstr "확인"
#: src/view/com/modals/Confirm.tsx:75
#: src/view/com/modals/Confirm.tsx:78
msgctxt "action"
msgid "Confirm"
msgstr ""
#: src/view/com/modals/ChangeEmail.tsx:193 #: src/view/com/modals/ChangeEmail.tsx:193
#: src/view/com/modals/ChangeEmail.tsx:195 #: src/view/com/modals/ChangeEmail.tsx:195
msgid "Confirm Change" msgid "Confirm Change"
@@ -812,7 +813,7 @@ msgstr "변경 확인"
msgid "Confirm content language settings" msgid "Confirm content language settings"
msgstr "콘텐츠 언어 설정 확인" msgstr "콘텐츠 언어 설정 확인"
#: src/view/com/modals/DeleteAccount.tsx:213 #: src/view/com/modals/DeleteAccount.tsx:216
msgid "Confirm delete account" msgid "Confirm delete account"
msgstr "계정 삭제 확인" msgstr "계정 삭제 확인"
@@ -1020,7 +1021,7 @@ msgstr "앱 비밀번호 삭제"
msgid "Delete List" msgid "Delete List"
msgstr "리스트 삭제" msgstr "리스트 삭제"
#: src/view/com/modals/DeleteAccount.tsx:216 #: src/view/com/modals/DeleteAccount.tsx:219
msgid "Delete my account" msgid "Delete my account"
msgstr "내 계정 삭제" msgstr "내 계정 삭제"
@@ -1850,7 +1851,7 @@ msgstr ""
msgid "Input new password" msgid "Input new password"
msgstr "" msgstr ""
#: src/view/com/modals/DeleteAccount.tsx:196 #: src/view/com/modals/DeleteAccount.tsx:199
msgid "Input password for account deletion" msgid "Input password for account deletion"
msgstr "" msgstr ""
@@ -2256,6 +2257,10 @@ msgstr ""
msgid "Mute thread" msgid "Mute thread"
msgstr "스레드 뮤트" msgstr "스레드 뮤트"
#: src/view/com/lists/ListCard.tsx:101
msgid "Muted"
msgstr ""
#: src/view/screens/Moderation.tsx:109 #: src/view/screens/Moderation.tsx:109
msgid "Muted accounts" msgid "Muted accounts"
msgstr "뮤트한 계정" msgstr "뮤트한 계정"
@@ -2379,11 +2384,6 @@ msgstr ""
msgid "Newest replies first" msgid "Newest replies first"
msgstr "새로운 순" msgstr "새로운 순"
#: src/view/com/auth/onboarding/WelcomeDesktop.tsx:103
msgctxt "action"
msgid "Next"
msgstr ""
#: src/view/com/auth/create/CreateAccount.tsx:154 #: src/view/com/auth/create/CreateAccount.tsx:154
#: src/view/com/auth/login/ForgotPasswordForm.tsx:178 #: src/view/com/auth/login/ForgotPasswordForm.tsx:178
#: src/view/com/auth/login/ForgotPasswordForm.tsx:188 #: src/view/com/auth/login/ForgotPasswordForm.tsx:188
@@ -2394,6 +2394,11 @@ msgstr ""
msgid "Next" msgid "Next"
msgstr "다음" msgstr "다음"
#: src/view/com/auth/onboarding/WelcomeDesktop.tsx:103
msgctxt "action"
msgid "Next"
msgstr ""
#: src/view/com/lightbox/Lightbox.web.tsx:142 #: src/view/com/lightbox/Lightbox.web.tsx:142
msgid "Next image" msgid "Next image"
msgstr "다음 이미지" msgstr "다음 이미지"
@@ -2688,7 +2693,7 @@ msgstr "페이지를 찾을 수 없음"
#: src/view/com/auth/create/Step2.tsx:132 #: src/view/com/auth/create/Step2.tsx:132
#: src/view/com/auth/login/LoginForm.tsx:223 #: src/view/com/auth/login/LoginForm.tsx:223
#: src/view/com/auth/login/SetNewPasswordForm.tsx:132 #: src/view/com/auth/login/SetNewPasswordForm.tsx:132
#: src/view/com/modals/DeleteAccount.tsx:195 #: src/view/com/modals/DeleteAccount.tsx:198
msgid "Password" msgid "Password"
msgstr "비밀번호" msgstr "비밀번호"
@@ -2766,7 +2771,7 @@ msgstr "이 앱 비밀번호에 대해 고유한 이름을 입력하거나 무
msgid "Please enter your email." msgid "Please enter your email."
msgstr "이메일을 입력하세요." msgstr "이메일을 입력하세요."
#: src/view/com/modals/DeleteAccount.tsx:184 #: src/view/com/modals/DeleteAccount.tsx:187
msgid "Please enter your password as well:" msgid "Please enter your password as well:"
msgstr "비밀번호도 입력해 주세요:" msgstr "비밀번호도 입력해 주세요:"
@@ -3683,8 +3688,8 @@ msgid "Subscribe to this list"
msgstr "이 리스트로 구독" msgstr "이 리스트로 구독"
#: src/view/com/lists/ListCard.tsx:101 #: src/view/com/lists/ListCard.tsx:101
msgid "Subscribed" #~ msgid "Subscribed"
msgstr "" #~ msgstr ""
#: src/view/screens/Search/Search.tsx:362 #: src/view/screens/Search/Search.tsx:362
msgid "Suggested Follows" msgid "Suggested Follows"
@@ -3839,7 +3844,7 @@ msgstr "서버에 연결하는 동안 문제가 발생했습니다."
msgid "There was an issue fetching notifications. Tap here to try again." msgid "There was an issue fetching notifications. Tap here to try again."
msgstr "" msgstr ""
#: src/view/com/posts/Feed.tsx:261 #: src/view/com/posts/Feed.tsx:263
msgid "There was an issue fetching posts. Tap here to try again." msgid "There was an issue fetching posts. Tap here to try again."
msgstr "" msgstr ""
@@ -4029,16 +4034,16 @@ msgstr "리스트 언뮤트"
msgid "Unable to contact your service. Please check your Internet connection." msgid "Unable to contact your service. Please check your Internet connection."
msgstr "서비스에 연결할 수 없습니다. 인터넷 연결을 확인하세요." msgstr "서비스에 연결할 수 없습니다. 인터넷 연결을 확인하세요."
#: src/view/com/profile/ProfileHeader.tsx:475
msgctxt "action"
msgid "Unblock"
msgstr ""
#: src/view/com/profile/ProfileHeader.tsx:472 #: src/view/com/profile/ProfileHeader.tsx:472
#: src/view/screens/ProfileList.tsx:568 #: src/view/screens/ProfileList.tsx:568
msgid "Unblock" msgid "Unblock"
msgstr "차단 해제" msgstr "차단 해제"
#: src/view/com/profile/ProfileHeader.tsx:475
msgctxt "action"
msgid "Unblock"
msgstr ""
#: src/view/com/profile/ProfileHeader.tsx:308 #: src/view/com/profile/ProfileHeader.tsx:308
#: src/view/com/profile/ProfileHeader.tsx:392 #: src/view/com/profile/ProfileHeader.tsx:392
msgid "Unblock Account" msgid "Unblock Account"
@@ -4290,6 +4295,10 @@ msgstr "사이트 방문"
msgid "Warn" msgid "Warn"
msgstr "경고" msgstr "경고"
#: src/view/com/posts/DiscoverFallbackHeader.tsx:29
msgid "We ran out of posts from your follows. Here's the latest from"
msgstr ""
#: src/view/com/modals/AppealLabel.tsx:48 #: src/view/com/modals/AppealLabel.tsx:48
msgid "We'll look into your appeal promptly." msgid "We'll look into your appeal promptly."
msgstr "이의신청을 즉시 검토하겠습니다." msgstr "이의신청을 즉시 검토하겠습니다."
+48 -39
View File
@@ -373,11 +373,6 @@ msgstr "Nudez artística ou não erótica."
#~ msgid "Ask apps to limit the visibility of my account" #~ msgid "Ask apps to limit the visibility of my account"
#~ msgstr "" #~ msgstr ""
#: src/view/com/post-thread/PostThread.tsx:400
msgctxt "action"
msgid "Back"
msgstr ""
#: src/view/com/auth/create/CreateAccount.tsx:141 #: src/view/com/auth/create/CreateAccount.tsx:141
#: src/view/com/auth/login/ChooseAccountForm.tsx:151 #: src/view/com/auth/login/ChooseAccountForm.tsx:151
#: src/view/com/auth/login/ForgotPasswordForm.tsx:170 #: src/view/com/auth/login/ForgotPasswordForm.tsx:170
@@ -392,6 +387,11 @@ msgstr ""
msgid "Back" msgid "Back"
msgstr "Voltar" msgstr "Voltar"
#: src/view/com/post-thread/PostThread.tsx:400
msgctxt "action"
msgid "Back"
msgstr ""
#: src/view/screens/Settings.tsx:489 #: src/view/screens/Settings.tsx:489
msgid "Basics" msgid "Basics"
msgstr "Básicos" msgstr "Básicos"
@@ -426,6 +426,7 @@ msgstr "Bloquear esta conta?"
msgid "Block this List" msgid "Block this List"
msgstr "" msgstr ""
#: src/view/com/lists/ListCard.tsx:109
#: src/view/com/util/post-embeds/QuoteEmbed.tsx:57 #: src/view/com/util/post-embeds/QuoteEmbed.tsx:57
msgid "Blocked" msgid "Blocked"
msgstr "" msgstr ""
@@ -528,15 +529,6 @@ msgstr "Câmera"
msgid "Can only contain letters, numbers, spaces, dashes, and underscores. Must be at least 4 characters long, but no more than 32 characters long." msgid "Can only contain letters, numbers, spaces, dashes, and underscores. Must be at least 4 characters long, but no more than 32 characters long."
msgstr "Só pode conter letras, números, espaços, traços e sublinhados. Deve ter pelo menos 4 caracteres, mas não mais de 32 caracteres." msgstr "Só pode conter letras, números, espaços, traços e sublinhados. Deve ter pelo menos 4 caracteres, mas não mais de 32 caracteres."
#: src/view/com/modals/Confirm.tsx:88
#: src/view/com/modals/Confirm.tsx:91
#: src/view/com/modals/CreateOrEditList.tsx:293
#: src/view/com/modals/DeleteAccount.tsx:152
#: src/view/com/modals/DeleteAccount.tsx:227
msgctxt "action"
msgid "Cancel"
msgstr ""
#: src/view/com/composer/Composer.tsx:300 #: src/view/com/composer/Composer.tsx:300
#: src/view/com/composer/Composer.tsx:305 #: src/view/com/composer/Composer.tsx:305
#: src/view/com/modals/ChangeEmail.tsx:218 #: src/view/com/modals/ChangeEmail.tsx:218
@@ -555,8 +547,17 @@ msgstr ""
msgid "Cancel" msgid "Cancel"
msgstr "Cancelar" msgstr "Cancelar"
#: src/view/com/modals/Confirm.tsx:88
#: src/view/com/modals/Confirm.tsx:91
#: src/view/com/modals/CreateOrEditList.tsx:293
#: src/view/com/modals/DeleteAccount.tsx:152
#: src/view/com/modals/DeleteAccount.tsx:230
msgctxt "action"
msgid "Cancel"
msgstr ""
#: src/view/com/modals/DeleteAccount.tsx:148 #: src/view/com/modals/DeleteAccount.tsx:148
#: src/view/com/modals/DeleteAccount.tsx:223 #: src/view/com/modals/DeleteAccount.tsx:226
msgid "Cancel account deletion" msgid "Cancel account deletion"
msgstr "Cancelar exclusão da conta" msgstr "Cancelar exclusão da conta"
@@ -736,12 +737,6 @@ msgstr ""
msgid "Compose reply" msgid "Compose reply"
msgstr "Escrever resposta" msgstr "Escrever resposta"
#: src/view/com/modals/Confirm.tsx:75
#: src/view/com/modals/Confirm.tsx:78
msgctxt "action"
msgid "Confirm"
msgstr ""
#: src/view/com/modals/AppealLabel.tsx:98 #: src/view/com/modals/AppealLabel.tsx:98
#: src/view/com/modals/SelfLabel.tsx:154 #: src/view/com/modals/SelfLabel.tsx:154
#: src/view/com/modals/VerifyEmail.tsx:231 #: src/view/com/modals/VerifyEmail.tsx:231
@@ -751,6 +746,12 @@ msgstr ""
msgid "Confirm" msgid "Confirm"
msgstr "Confirme" msgstr "Confirme"
#: src/view/com/modals/Confirm.tsx:75
#: src/view/com/modals/Confirm.tsx:78
msgctxt "action"
msgid "Confirm"
msgstr ""
#: src/view/com/modals/ChangeEmail.tsx:193 #: src/view/com/modals/ChangeEmail.tsx:193
#: src/view/com/modals/ChangeEmail.tsx:195 #: src/view/com/modals/ChangeEmail.tsx:195
msgid "Confirm Change" msgid "Confirm Change"
@@ -760,7 +761,7 @@ msgstr "Confirmar Alterações"
msgid "Confirm content language settings" msgid "Confirm content language settings"
msgstr "Confirmar configurações de idioma de conteúdo" msgstr "Confirmar configurações de idioma de conteúdo"
#: src/view/com/modals/DeleteAccount.tsx:213 #: src/view/com/modals/DeleteAccount.tsx:216
msgid "Confirm delete account" msgid "Confirm delete account"
msgstr "Confirmar a exclusão da conta" msgstr "Confirmar a exclusão da conta"
@@ -952,7 +953,7 @@ msgstr "Excluir senha do aplicativo"
msgid "Delete List" msgid "Delete List"
msgstr "Excluir Lista" msgstr "Excluir Lista"
#: src/view/com/modals/DeleteAccount.tsx:216 #: src/view/com/modals/DeleteAccount.tsx:219
msgid "Delete my account" msgid "Delete my account"
msgstr "Excluir minha conta" msgstr "Excluir minha conta"
@@ -1673,7 +1674,7 @@ msgstr ""
msgid "Input new password" msgid "Input new password"
msgstr "" msgstr ""
#: src/view/com/modals/DeleteAccount.tsx:196 #: src/view/com/modals/DeleteAccount.tsx:199
msgid "Input password for account deletion" msgid "Input password for account deletion"
msgstr "" msgstr ""
@@ -2075,6 +2076,10 @@ msgstr ""
msgid "Mute thread" msgid "Mute thread"
msgstr "Silenciar tópico" msgstr "Silenciar tópico"
#: src/view/com/lists/ListCard.tsx:101
msgid "Muted"
msgstr ""
#: src/view/screens/Moderation.tsx:109 #: src/view/screens/Moderation.tsx:109
msgid "Muted accounts" msgid "Muted accounts"
msgstr "Contas silenciadas" msgstr "Contas silenciadas"
@@ -2189,11 +2194,6 @@ msgstr ""
msgid "Newest replies first" msgid "Newest replies first"
msgstr "Respostas mais recentes primeiro" msgstr "Respostas mais recentes primeiro"
#: src/view/com/auth/onboarding/WelcomeDesktop.tsx:103
msgctxt "action"
msgid "Next"
msgstr ""
#: src/view/com/auth/create/CreateAccount.tsx:154 #: src/view/com/auth/create/CreateAccount.tsx:154
#: src/view/com/auth/login/ForgotPasswordForm.tsx:178 #: src/view/com/auth/login/ForgotPasswordForm.tsx:178
#: src/view/com/auth/login/ForgotPasswordForm.tsx:188 #: src/view/com/auth/login/ForgotPasswordForm.tsx:188
@@ -2204,6 +2204,11 @@ msgstr ""
msgid "Next" msgid "Next"
msgstr "Próximo" msgstr "Próximo"
#: src/view/com/auth/onboarding/WelcomeDesktop.tsx:103
msgctxt "action"
msgid "Next"
msgstr ""
#: src/view/com/lightbox/Lightbox.web.tsx:142 #: src/view/com/lightbox/Lightbox.web.tsx:142
msgid "Next image" msgid "Next image"
msgstr "Próxima imagem" msgstr "Próxima imagem"
@@ -2477,7 +2482,7 @@ msgstr "Página não encontrada"
#: src/view/com/auth/create/Step2.tsx:132 #: src/view/com/auth/create/Step2.tsx:132
#: src/view/com/auth/login/LoginForm.tsx:223 #: src/view/com/auth/login/LoginForm.tsx:223
#: src/view/com/auth/login/SetNewPasswordForm.tsx:132 #: src/view/com/auth/login/SetNewPasswordForm.tsx:132
#: src/view/com/modals/DeleteAccount.tsx:195 #: src/view/com/modals/DeleteAccount.tsx:198
msgid "Password" msgid "Password"
msgstr "Senha" msgstr "Senha"
@@ -2555,7 +2560,7 @@ msgstr "Por favor, insira um nome único para esta Senha do Aplicativo ou use no
msgid "Please enter your email." msgid "Please enter your email."
msgstr "Por favor, digite o seu email." msgstr "Por favor, digite o seu email."
#: src/view/com/modals/DeleteAccount.tsx:184 #: src/view/com/modals/DeleteAccount.tsx:187
msgid "Please enter your password as well:" msgid "Please enter your password as well:"
msgstr "Por favor, digite sua senha também:" msgstr "Por favor, digite sua senha também:"
@@ -3422,8 +3427,8 @@ msgid "Subscribe to this list"
msgstr "Assinar esta lista" msgstr "Assinar esta lista"
#: src/view/com/lists/ListCard.tsx:101 #: src/view/com/lists/ListCard.tsx:101
msgid "Subscribed" #~ msgid "Subscribed"
msgstr "" #~ msgstr ""
#: src/view/screens/Search/Search.tsx:362 #: src/view/screens/Search/Search.tsx:362
msgid "Suggested Follows" msgid "Suggested Follows"
@@ -3557,7 +3562,7 @@ msgstr ""
msgid "There was an issue fetching notifications. Tap here to try again." msgid "There was an issue fetching notifications. Tap here to try again."
msgstr "" msgstr ""
#: src/view/com/posts/Feed.tsx:261 #: src/view/com/posts/Feed.tsx:263
msgid "There was an issue fetching posts. Tap here to try again." msgid "There was an issue fetching posts. Tap here to try again."
msgstr "" msgstr ""
@@ -3735,16 +3740,16 @@ msgstr "Lista de não silenciados"
msgid "Unable to contact your service. Please check your Internet connection." msgid "Unable to contact your service. Please check your Internet connection."
msgstr "Não foi possível entrar em contato com seu serviço. Por favor, verifique sua conexão à internet." msgstr "Não foi possível entrar em contato com seu serviço. Por favor, verifique sua conexão à internet."
#: src/view/com/profile/ProfileHeader.tsx:475
msgctxt "action"
msgid "Unblock"
msgstr ""
#: src/view/com/profile/ProfileHeader.tsx:472 #: src/view/com/profile/ProfileHeader.tsx:472
#: src/view/screens/ProfileList.tsx:568 #: src/view/screens/ProfileList.tsx:568
msgid "Unblock" msgid "Unblock"
msgstr "Desbloquear" msgstr "Desbloquear"
#: src/view/com/profile/ProfileHeader.tsx:475
msgctxt "action"
msgid "Unblock"
msgstr ""
#: src/view/com/profile/ProfileHeader.tsx:308 #: src/view/com/profile/ProfileHeader.tsx:308
#: src/view/com/profile/ProfileHeader.tsx:392 #: src/view/com/profile/ProfileHeader.tsx:392
msgid "Unblock Account" msgid "Unblock Account"
@@ -3958,6 +3963,10 @@ msgstr "Visitar Site"
msgid "Warn" msgid "Warn"
msgstr "" msgstr ""
#: src/view/com/posts/DiscoverFallbackHeader.tsx:29
msgid "We ran out of posts from your follows. Here's the latest from"
msgstr ""
#: src/view/com/modals/AppealLabel.tsx:48 #: src/view/com/modals/AppealLabel.tsx:48
msgid "We'll look into your appeal promptly." msgid "We'll look into your appeal promptly."
msgstr "" msgstr ""
+48 -39
View File
@@ -378,11 +378,6 @@ msgstr "Художня або нееротична оголеність."
#~ msgid "Ask apps to limit the visibility of my account" #~ msgid "Ask apps to limit the visibility of my account"
#~ msgstr "" #~ msgstr ""
#: src/view/com/post-thread/PostThread.tsx:400
msgctxt "action"
msgid "Back"
msgstr ""
#: src/view/com/auth/create/CreateAccount.tsx:141 #: src/view/com/auth/create/CreateAccount.tsx:141
#: src/view/com/auth/login/ChooseAccountForm.tsx:151 #: src/view/com/auth/login/ChooseAccountForm.tsx:151
#: src/view/com/auth/login/ForgotPasswordForm.tsx:170 #: src/view/com/auth/login/ForgotPasswordForm.tsx:170
@@ -397,6 +392,11 @@ msgstr ""
msgid "Back" msgid "Back"
msgstr "Назад" msgstr "Назад"
#: src/view/com/post-thread/PostThread.tsx:400
msgctxt "action"
msgid "Back"
msgstr ""
#: src/view/screens/Settings.tsx:489 #: src/view/screens/Settings.tsx:489
msgid "Basics" msgid "Basics"
msgstr "Основні" msgstr "Основні"
@@ -431,6 +431,7 @@ msgstr "Заблокувати ці облікові записи?"
msgid "Block this List" msgid "Block this List"
msgstr "" msgstr ""
#: src/view/com/lists/ListCard.tsx:109
#: src/view/com/util/post-embeds/QuoteEmbed.tsx:57 #: src/view/com/util/post-embeds/QuoteEmbed.tsx:57
msgid "Blocked" msgid "Blocked"
msgstr "" msgstr ""
@@ -533,15 +534,6 @@ msgstr "Камера"
msgid "Can only contain letters, numbers, spaces, dashes, and underscores. Must be at least 4 characters long, but no more than 32 characters long." msgid "Can only contain letters, numbers, spaces, dashes, and underscores. Must be at least 4 characters long, but no more than 32 characters long."
msgstr "Може містити лише літери, цифри, пробіли, дефіси та знаки підкреслення, і мати довжину від 4 до 32 символів." msgstr "Може містити лише літери, цифри, пробіли, дефіси та знаки підкреслення, і мати довжину від 4 до 32 символів."
#: src/view/com/modals/Confirm.tsx:88
#: src/view/com/modals/Confirm.tsx:91
#: src/view/com/modals/CreateOrEditList.tsx:293
#: src/view/com/modals/DeleteAccount.tsx:152
#: src/view/com/modals/DeleteAccount.tsx:227
msgctxt "action"
msgid "Cancel"
msgstr ""
#: src/view/com/composer/Composer.tsx:300 #: src/view/com/composer/Composer.tsx:300
#: src/view/com/composer/Composer.tsx:305 #: src/view/com/composer/Composer.tsx:305
#: src/view/com/modals/ChangeEmail.tsx:218 #: src/view/com/modals/ChangeEmail.tsx:218
@@ -560,8 +552,17 @@ msgstr ""
msgid "Cancel" msgid "Cancel"
msgstr "Скасувати" msgstr "Скасувати"
#: src/view/com/modals/Confirm.tsx:88
#: src/view/com/modals/Confirm.tsx:91
#: src/view/com/modals/CreateOrEditList.tsx:293
#: src/view/com/modals/DeleteAccount.tsx:152
#: src/view/com/modals/DeleteAccount.tsx:230
msgctxt "action"
msgid "Cancel"
msgstr ""
#: src/view/com/modals/DeleteAccount.tsx:148 #: src/view/com/modals/DeleteAccount.tsx:148
#: src/view/com/modals/DeleteAccount.tsx:223 #: src/view/com/modals/DeleteAccount.tsx:226
msgid "Cancel account deletion" msgid "Cancel account deletion"
msgstr "Скасувати видалення облікового запису" msgstr "Скасувати видалення облікового запису"
@@ -741,12 +742,6 @@ msgstr ""
msgid "Compose reply" msgid "Compose reply"
msgstr "Відповісти" msgstr "Відповісти"
#: src/view/com/modals/Confirm.tsx:75
#: src/view/com/modals/Confirm.tsx:78
msgctxt "action"
msgid "Confirm"
msgstr ""
#: src/view/com/modals/AppealLabel.tsx:98 #: src/view/com/modals/AppealLabel.tsx:98
#: src/view/com/modals/SelfLabel.tsx:154 #: src/view/com/modals/SelfLabel.tsx:154
#: src/view/com/modals/VerifyEmail.tsx:231 #: src/view/com/modals/VerifyEmail.tsx:231
@@ -756,6 +751,12 @@ msgstr ""
msgid "Confirm" msgid "Confirm"
msgstr "Підтвердити" msgstr "Підтвердити"
#: src/view/com/modals/Confirm.tsx:75
#: src/view/com/modals/Confirm.tsx:78
msgctxt "action"
msgid "Confirm"
msgstr ""
#: src/view/com/modals/ChangeEmail.tsx:193 #: src/view/com/modals/ChangeEmail.tsx:193
#: src/view/com/modals/ChangeEmail.tsx:195 #: src/view/com/modals/ChangeEmail.tsx:195
msgid "Confirm Change" msgid "Confirm Change"
@@ -765,7 +766,7 @@ msgstr "Підтвердити"
msgid "Confirm content language settings" msgid "Confirm content language settings"
msgstr "Підтвердити перелік мов" msgstr "Підтвердити перелік мов"
#: src/view/com/modals/DeleteAccount.tsx:213 #: src/view/com/modals/DeleteAccount.tsx:216
msgid "Confirm delete account" msgid "Confirm delete account"
msgstr "Підтвердити видалення облікового запису" msgstr "Підтвердити видалення облікового запису"
@@ -957,7 +958,7 @@ msgstr "Видалити пароль для застосунку"
msgid "Delete List" msgid "Delete List"
msgstr "Видалити список" msgstr "Видалити список"
#: src/view/com/modals/DeleteAccount.tsx:216 #: src/view/com/modals/DeleteAccount.tsx:219
msgid "Delete my account" msgid "Delete my account"
msgstr "Видалити мій обліковий запис" msgstr "Видалити мій обліковий запис"
@@ -1678,7 +1679,7 @@ msgstr ""
msgid "Input new password" msgid "Input new password"
msgstr "" msgstr ""
#: src/view/com/modals/DeleteAccount.tsx:196 #: src/view/com/modals/DeleteAccount.tsx:199
msgid "Input password for account deletion" msgid "Input password for account deletion"
msgstr "" msgstr ""
@@ -2080,6 +2081,10 @@ msgstr ""
msgid "Mute thread" msgid "Mute thread"
msgstr "Ігнорувати пост" msgstr "Ігнорувати пост"
#: src/view/com/lists/ListCard.tsx:101
msgid "Muted"
msgstr ""
#: src/view/screens/Moderation.tsx:109 #: src/view/screens/Moderation.tsx:109
msgid "Muted accounts" msgid "Muted accounts"
msgstr "Ігноровані облікові записи" msgstr "Ігноровані облікові записи"
@@ -2194,11 +2199,6 @@ msgstr ""
msgid "Newest replies first" msgid "Newest replies first"
msgstr "Спочатку найновіші" msgstr "Спочатку найновіші"
#: src/view/com/auth/onboarding/WelcomeDesktop.tsx:103
msgctxt "action"
msgid "Next"
msgstr ""
#: src/view/com/auth/create/CreateAccount.tsx:154 #: src/view/com/auth/create/CreateAccount.tsx:154
#: src/view/com/auth/login/ForgotPasswordForm.tsx:178 #: src/view/com/auth/login/ForgotPasswordForm.tsx:178
#: src/view/com/auth/login/ForgotPasswordForm.tsx:188 #: src/view/com/auth/login/ForgotPasswordForm.tsx:188
@@ -2209,6 +2209,11 @@ msgstr ""
msgid "Next" msgid "Next"
msgstr "Далі" msgstr "Далі"
#: src/view/com/auth/onboarding/WelcomeDesktop.tsx:103
msgctxt "action"
msgid "Next"
msgstr ""
#: src/view/com/lightbox/Lightbox.web.tsx:142 #: src/view/com/lightbox/Lightbox.web.tsx:142
msgid "Next image" msgid "Next image"
msgstr "Наступне зображення" msgstr "Наступне зображення"
@@ -2482,7 +2487,7 @@ msgstr "Сторінку не знайдено"
#: src/view/com/auth/create/Step2.tsx:132 #: src/view/com/auth/create/Step2.tsx:132
#: src/view/com/auth/login/LoginForm.tsx:223 #: src/view/com/auth/login/LoginForm.tsx:223
#: src/view/com/auth/login/SetNewPasswordForm.tsx:132 #: src/view/com/auth/login/SetNewPasswordForm.tsx:132
#: src/view/com/modals/DeleteAccount.tsx:195 #: src/view/com/modals/DeleteAccount.tsx:198
msgid "Password" msgid "Password"
msgstr "Пароль" msgstr "Пароль"
@@ -2560,7 +2565,7 @@ msgstr "Будь ласка, введіть унікальну назву для
msgid "Please enter your email." msgid "Please enter your email."
msgstr "Будь ласка, введіть адресу ел. пошти." msgstr "Будь ласка, введіть адресу ел. пошти."
#: src/view/com/modals/DeleteAccount.tsx:184 #: src/view/com/modals/DeleteAccount.tsx:187
msgid "Please enter your password as well:" msgid "Please enter your password as well:"
msgstr "Будь ласка, також введіть ваш пароль:" msgstr "Будь ласка, також введіть ваш пароль:"
@@ -3427,8 +3432,8 @@ msgid "Subscribe to this list"
msgstr "Підписатися на цей список" msgstr "Підписатися на цей список"
#: src/view/com/lists/ListCard.tsx:101 #: src/view/com/lists/ListCard.tsx:101
msgid "Subscribed" #~ msgid "Subscribed"
msgstr "" #~ msgstr ""
#: src/view/screens/Search/Search.tsx:362 #: src/view/screens/Search/Search.tsx:362
msgid "Suggested Follows" msgid "Suggested Follows"
@@ -3562,7 +3567,7 @@ msgstr ""
msgid "There was an issue fetching notifications. Tap here to try again." msgid "There was an issue fetching notifications. Tap here to try again."
msgstr "" msgstr ""
#: src/view/com/posts/Feed.tsx:261 #: src/view/com/posts/Feed.tsx:263
msgid "There was an issue fetching posts. Tap here to try again." msgid "There was an issue fetching posts. Tap here to try again."
msgstr "" msgstr ""
@@ -3740,16 +3745,16 @@ msgstr "Перестати ігнорувати"
msgid "Unable to contact your service. Please check your Internet connection." msgid "Unable to contact your service. Please check your Internet connection."
msgstr "Не вдалося зв'язатися з вашим хостинг-провайдером. Перевірте ваше підключення до Інтернету." msgstr "Не вдалося зв'язатися з вашим хостинг-провайдером. Перевірте ваше підключення до Інтернету."
#: src/view/com/profile/ProfileHeader.tsx:475
msgctxt "action"
msgid "Unblock"
msgstr ""
#: src/view/com/profile/ProfileHeader.tsx:472 #: src/view/com/profile/ProfileHeader.tsx:472
#: src/view/screens/ProfileList.tsx:568 #: src/view/screens/ProfileList.tsx:568
msgid "Unblock" msgid "Unblock"
msgstr "Розблокувати" msgstr "Розблокувати"
#: src/view/com/profile/ProfileHeader.tsx:475
msgctxt "action"
msgid "Unblock"
msgstr ""
#: src/view/com/profile/ProfileHeader.tsx:308 #: src/view/com/profile/ProfileHeader.tsx:308
#: src/view/com/profile/ProfileHeader.tsx:392 #: src/view/com/profile/ProfileHeader.tsx:392
msgid "Unblock Account" msgid "Unblock Account"
@@ -3963,6 +3968,10 @@ msgstr "Відвідати сайт"
msgid "Warn" msgid "Warn"
msgstr "" msgstr ""
#: src/view/com/posts/DiscoverFallbackHeader.tsx:29
msgid "We ran out of posts from your follows. Here's the latest from"
msgstr ""
#: src/view/com/modals/AppealLabel.tsx:48 #: src/view/com/modals/AppealLabel.tsx:48
msgid "We'll look into your appeal promptly." msgid "We'll look into your appeal promptly."
msgstr "" msgstr ""
+44
View File
@@ -0,0 +1,44 @@
import React from 'react'
import {DialogControlProps} from '#/components/Dialog'
const DialogContext = React.createContext<{
activeDialogs: React.MutableRefObject<
Map<string, React.MutableRefObject<DialogControlProps>>
>
}>({
activeDialogs: {
current: new Map(),
},
})
const DialogControlContext = React.createContext<{
closeAllDialogs(): void
}>({
closeAllDialogs: () => {},
})
export function useDialogStateContext() {
return React.useContext(DialogContext)
}
export function useDialogStateControlContext() {
return React.useContext(DialogControlContext)
}
export function Provider({children}: React.PropsWithChildren<{}>) {
const activeDialogs = React.useRef<
Map<string, React.MutableRefObject<DialogControlProps>>
>(new Map())
const closeAllDialogs = React.useCallback(() => {
activeDialogs.current.forEach(dialog => dialog.current.close())
}, [])
const context = React.useMemo(() => ({activeDialogs}), [])
const controls = React.useMemo(() => ({closeAllDialogs}), [closeAllDialogs])
return (
<DialogContext.Provider value={context}>
<DialogControlContext.Provider value={controls}>
{children}
</DialogControlContext.Provider>
</DialogContext.Provider>
)
}
@@ -26,7 +26,7 @@ test('migrate: fresh install', async () => {
expect(AsyncStorage.getItem).toHaveBeenCalledWith('root') expect(AsyncStorage.getItem).toHaveBeenCalledWith('root')
expect(read).toHaveBeenCalledTimes(1) expect(read).toHaveBeenCalledTimes(1)
expect(logger.log).toHaveBeenCalledWith( expect(logger.info).toHaveBeenCalledWith(
'persisted state: no migration needed', 'persisted state: no migration needed',
) )
}) })
@@ -38,7 +38,7 @@ test('migrate: fresh install, existing new storage', async () => {
expect(AsyncStorage.getItem).toHaveBeenCalledWith('root') expect(AsyncStorage.getItem).toHaveBeenCalledWith('root')
expect(read).toHaveBeenCalledTimes(1) expect(read).toHaveBeenCalledTimes(1)
expect(logger.log).toHaveBeenCalledWith( expect(logger.info).toHaveBeenCalledWith(
'persisted state: no migration needed', 'persisted state: no migration needed',
) )
}) })
@@ -68,7 +68,7 @@ test('migrate: has legacy data', async () => {
await migrate() await migrate()
expect(write).toHaveBeenCalledWith(transform(fixtures.LEGACY_DATA_DUMP)) expect(write).toHaveBeenCalledWith(transform(fixtures.LEGACY_DATA_DUMP))
expect(logger.log).toHaveBeenCalledWith( expect(logger.info).toHaveBeenCalledWith(
'persisted state: migrated legacy storage', 'persisted state: migrated legacy storage',
) )
}) })
+2 -2
View File
@@ -164,14 +164,14 @@ export async function migrate() {
if (validate.success) { if (validate.success) {
await write(newData) await write(newData)
logger.log('persisted state: migrated legacy storage') logger.info('persisted state: migrated legacy storage')
} else { } else {
logger.error('persisted state: legacy data failed validation', { logger.error('persisted state: legacy data failed validation', {
error: validate.error, error: validate.error,
}) })
} }
} else { } else {
logger.log('persisted state: no migration needed') logger.info('persisted state: no migration needed')
} }
} catch (e: any) { } catch (e: any) {
logger.error(e, { logger.error(e, {
+2 -1
View File
@@ -272,7 +272,8 @@ export function usePinnedFeedsInfos(): {
}, },
}) })
} catch (e) { } catch (e) {
logger.warn(`usePinnedFeedsInfos: failed to fetch ${uri}`, { // expected failure
logger.info(`usePinnedFeedsInfos: failed to fetch ${uri}`, {
error: e, error: e,
}) })
} }
+1 -1
View File
@@ -167,7 +167,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
} }
broadcast.postMessage({event: unreadCountStr}) broadcast.postMessage({event: unreadCountStr})
} catch (e) { } catch (e) {
logger.error('Failed to check unread notifications', {error: e}) logger.warn('Failed to check unread notifications', {error: e})
} }
}, },
+6 -1
View File
@@ -18,6 +18,7 @@ import {LikesFeedAPI} from 'lib/api/feed/likes'
import {CustomFeedAPI} from 'lib/api/feed/custom' import {CustomFeedAPI} from 'lib/api/feed/custom'
import {ListFeedAPI} from 'lib/api/feed/list' import {ListFeedAPI} from 'lib/api/feed/list'
import {MergeFeedAPI} from 'lib/api/feed/merge' import {MergeFeedAPI} from 'lib/api/feed/merge'
import {HomeFeedAPI} from '#/lib/api/feed/home'
import {logger} from '#/logger' import {logger} from '#/logger'
import {STALE} from '#/state/queries' import {STALE} from '#/state/queries'
import {precacheFeedPosts as precacheResolvedUris} from './resolve-uri' import {precacheFeedPosts as precacheResolvedUris} from './resolve-uri'
@@ -338,7 +339,11 @@ function createApi(
feedTuners: FeedTunerFn[], feedTuners: FeedTunerFn[],
) { ) {
if (feedDesc === 'home') { if (feedDesc === 'home') {
return new MergeFeedAPI(params, feedTuners) if (params.mergeFeedEnabled) {
return new MergeFeedAPI(params, feedTuners)
} else {
return new HomeFeedAPI()
}
} else if (feedDesc === 'following') { } else if (feedDesc === 'following') {
return new FollowingFeedAPI() return new FollowingFeedAPI()
} else if (feedDesc.startsWith('author')) { } else if (feedDesc.startsWith('author')) {
+13 -1
View File
@@ -44,6 +44,8 @@ export type ApiContext = {
password: string password: string
handle: string handle: string
inviteCode?: string inviteCode?: string
verificationPhone?: string
verificationCode?: string
}) => Promise<void> }) => Promise<void>
login: (props: { login: (props: {
service: string service: string
@@ -203,7 +205,15 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
}, [setStateAndPersist, queryClient]) }, [setStateAndPersist, queryClient])
const createAccount = React.useCallback<ApiContext['createAccount']>( const createAccount = React.useCallback<ApiContext['createAccount']>(
async ({service, email, password, handle, inviteCode}: any) => { async ({
service,
email,
password,
handle,
inviteCode,
verificationPhone,
verificationCode,
}: any) => {
logger.info(`session: creating account`, { logger.info(`session: creating account`, {
service, service,
handle, handle,
@@ -217,6 +227,8 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
password, password,
email, email,
inviteCode, inviteCode,
verificationPhone,
verificationCode,
}) })
if (!agent.session) { if (!agent.session) {
-204
View File
@@ -1,204 +0,0 @@
import React from 'react'
import {Pressable, Text, PressableProps, TextProps} from 'react-native'
import * as tokens from '#/alf/tokens'
import {atoms} from '#/alf'
export type ButtonType =
| 'primary'
| 'secondary'
| 'tertiary'
| 'positive'
| 'negative'
export type ButtonSize = 'small' | 'large'
export type VariantProps = {
type?: ButtonType
size?: ButtonSize
}
type ButtonState = {
pressed: boolean
hovered: boolean
focused: boolean
}
export type ButtonProps = Omit<PressableProps, 'children'> &
VariantProps & {
children:
| ((props: {
state: ButtonState
type?: ButtonType
size?: ButtonSize
}) => React.ReactNode)
| React.ReactNode
| string
}
export type ButtonTextProps = TextProps & VariantProps
export function Button({children, style, type, size, ...rest}: ButtonProps) {
const {baseStyles, hoverStyles} = React.useMemo(() => {
const baseStyles = []
const hoverStyles = []
switch (type) {
case 'primary':
baseStyles.push({
backgroundColor: tokens.color.blue_500,
})
break
case 'secondary':
baseStyles.push({
backgroundColor: tokens.color.gray_200,
})
hoverStyles.push({
backgroundColor: tokens.color.gray_100,
})
break
default:
}
switch (size) {
case 'large':
baseStyles.push(
atoms.py_md,
atoms.px_xl,
atoms.rounded_md,
atoms.gap_sm,
)
break
case 'small':
baseStyles.push(
atoms.py_sm,
atoms.px_md,
atoms.rounded_sm,
atoms.gap_xs,
)
break
default:
}
return {
baseStyles,
hoverStyles,
}
}, [type, size])
const [state, setState] = React.useState({
pressed: false,
hovered: false,
focused: false,
})
const onPressIn = React.useCallback(() => {
setState(s => ({
...s,
pressed: true,
}))
}, [setState])
const onPressOut = React.useCallback(() => {
setState(s => ({
...s,
pressed: false,
}))
}, [setState])
const onHoverIn = React.useCallback(() => {
setState(s => ({
...s,
hovered: true,
}))
}, [setState])
const onHoverOut = React.useCallback(() => {
setState(s => ({
...s,
hovered: false,
}))
}, [setState])
const onFocus = React.useCallback(() => {
setState(s => ({
...s,
focused: true,
}))
}, [setState])
const onBlur = React.useCallback(() => {
setState(s => ({
...s,
focused: false,
}))
}, [setState])
return (
<Pressable
{...rest}
style={state => [
atoms.flex_row,
atoms.align_center,
...baseStyles,
...(state.hovered ? hoverStyles : []),
typeof style === 'function' ? style(state) : style,
]}
onPressIn={onPressIn}
onPressOut={onPressOut}
onHoverIn={onHoverIn}
onHoverOut={onHoverOut}
onFocus={onFocus}
onBlur={onBlur}>
{typeof children === 'string' ? (
<ButtonText type={type} size={size}>
{children}
</ButtonText>
) : typeof children === 'function' ? (
children({state, type, size})
) : (
children
)}
</Pressable>
)
}
export function ButtonText({
children,
style,
type,
size,
...rest
}: ButtonTextProps) {
const textStyles = React.useMemo(() => {
const base = []
switch (type) {
case 'primary':
base.push({color: tokens.color.white})
break
case 'secondary':
base.push({
color: tokens.color.gray_700,
})
break
default:
}
switch (size) {
case 'small':
base.push(atoms.text_sm, {paddingBottom: 1})
break
case 'large':
base.push(atoms.text_md, {paddingBottom: 1})
break
default:
}
return base
}, [type, size])
return (
<Text
{...rest}
style={[
atoms.flex_1,
atoms.font_semibold,
atoms.text_center,
...textStyles,
style,
]}>
{children}
</Text>
)
}
+24 -2
View File
@@ -22,12 +22,13 @@ import {
useSetSaveFeedsMutation, useSetSaveFeedsMutation,
DEFAULT_PROD_FEEDS, DEFAULT_PROD_FEEDS,
} from '#/state/queries/preferences' } from '#/state/queries/preferences'
import {IS_PROD} from '#/lib/constants' import {FEEDBACK_FORM_URL, IS_PROD} from '#/lib/constants'
import {Step1} from './Step1' import {Step1} from './Step1'
import {Step2} from './Step2' import {Step2} from './Step2'
import {Step3} from './Step3' import {Step3} from './Step3'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {TextLink} from '../../util/Link'
export function CreateAccount({onPressBack}: {onPressBack: () => void}) { export function CreateAccount({onPressBack}: {onPressBack: () => void}) {
const {screen} = useAnalytics() const {screen} = useAnalytics()
@@ -117,7 +118,7 @@ export function CreateAccount({onPressBack}: {onPressBack: () => void}) {
return ( return (
<LoggedOutLayout <LoggedOutLayout
leadin={`Step ${uiState.step}`} leadin=""
title={_(msg`Create Account`)} title={_(msg`Create Account`)}
description={_(msg`We're so excited to have you join us!`)}> description={_(msg`We're so excited to have you join us!`)}>
<ScrollView testID="createAccount" style={pal.view}> <ScrollView testID="createAccount" style={pal.view}>
@@ -176,6 +177,27 @@ export function CreateAccount({onPressBack}: {onPressBack: () => void}) {
</> </>
) : undefined} ) : undefined}
</View> </View>
<View style={styles.stepContainer}>
<View
style={[
s.flexRow,
s.alignCenter,
pal.viewLight,
{borderRadius: 8, paddingHorizontal: 14, paddingVertical: 12},
]}>
<Text type="md" style={pal.textLight}>
<Trans>Having trouble?</Trans>{' '}
</Text>
<TextLink
type="md"
style={pal.link}
text={_(msg`Contact support`)}
href={FEEDBACK_FORM_URL({email: uiState.email})}
/>
</View>
</View>
<View style={{height: isTabletOrDesktop ? 50 : 400}} /> <View style={{height: isTabletOrDesktop ? 50 : 400}} />
</ScrollView> </ScrollView>
</LoggedOutLayout> </LoggedOutLayout>
+189 -156
View File
@@ -1,25 +1,38 @@
import React from 'react' import React from 'react'
import {StyleSheet, TouchableWithoutFeedback, View} from 'react-native' import {
ActivityIndicator,
Keyboard,
StyleSheet,
TouchableWithoutFeedback,
View,
} from 'react-native'
import {CreateAccountState, CreateAccountDispatch, is18} from './state'
import {Text} from 'view/com/util/text/Text' import {Text} from 'view/com/util/text/Text'
import {DateInput} from 'view/com/util/forms/DateInput'
import {StepHeader} from './StepHeader' import {StepHeader} from './StepHeader'
import {CreateAccountState, CreateAccountDispatch} from './state'
import {useTheme} from 'lib/ThemeContext'
import {usePalette} from 'lib/hooks/usePalette'
import {s} from 'lib/styles' import {s} from 'lib/styles'
import {HelpTip} from '../util/HelpTip' import {usePalette} from 'lib/hooks/usePalette'
import {TextInput} from '../util/TextInput' import {TextInput} from '../util/TextInput'
import {Button} from 'view/com/util/forms/Button' import {Button} from '../../util/forms/Button'
import {Policies} from './Policies'
import {ErrorMessage} from 'view/com/util/error/ErrorMessage' import {ErrorMessage} from 'view/com/util/error/ErrorMessage'
import {msg, Trans} from '@lingui/macro' import {isWeb} from 'platform/detection'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
import {logger} from '#/logger'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {LOCAL_DEV_SERVICE, STAGING_SERVICE, PROD_SERVICE} from 'lib/constants' function sanitizeDate(date: Date): Date {
import {LOGIN_INCLUDE_DEV_SERVERS} from 'lib/build-flags' if (!date || date.toString() === 'Invalid Date') {
logger.error(`Create account: handled invalid date for birthDate`, {
hasDate: !!date,
})
return new Date()
}
return date
}
/** STEP 1: Your hosting provider
* @field Bluesky (default)
* @field Other (staging, local dev, your own PDS, etc.)
*/
export function Step1({ export function Step1({
uiState, uiState,
uiDispatch, uiDispatch,
@@ -28,136 +41,175 @@ export function Step1({
uiDispatch: CreateAccountDispatch uiDispatch: CreateAccountDispatch
}) { }) {
const pal = usePalette('default') const pal = usePalette('default')
const [isDefaultSelected, setIsDefaultSelected] = React.useState(true)
const {_} = useLingui() const {_} = useLingui()
const {openModal} = useModalControls()
const onPressDefault = React.useCallback(() => { const onPressSelectService = React.useCallback(() => {
setIsDefaultSelected(true) openModal({
uiDispatch({type: 'set-service-url', value: PROD_SERVICE}) name: 'server-input',
}, [setIsDefaultSelected, uiDispatch]) initialService: uiState.serviceUrl,
onSelect: (url: string) =>
uiDispatch({type: 'set-service-url', value: url}),
})
Keyboard.dismiss()
}, [uiDispatch, uiState.serviceUrl, openModal])
const onPressOther = React.useCallback(() => { const onPressWaitlist = React.useCallback(() => {
setIsDefaultSelected(false) openModal({name: 'waitlist'})
uiDispatch({type: 'set-service-url', value: 'https://'}) }, [openModal])
}, [setIsDefaultSelected, uiDispatch])
const onChangeServiceUrl = React.useCallback( const birthDate = React.useMemo(() => {
(v: string) => { return sanitizeDate(uiState.birthDate)
uiDispatch({type: 'set-service-url', value: v}) }, [uiState.birthDate])
},
[uiDispatch],
)
return ( return (
<View> <View>
<StepHeader step="1" title={_(msg`Your hosting provider`)} /> <StepHeader uiState={uiState} title={_(msg`Your account`)}>
<Text style={[pal.text, s.mb10]}> <View>
<Trans>This is the service that keeps you online.</Trans> <Button
</Text> testID="selectServiceButton"
<Option type="default"
testID="blueskyServerBtn" style={{
isSelected={isDefaultSelected} aspectRatio: 1,
label="Bluesky" justifyContent: 'center',
help="&nbsp;(default)" alignItems: 'center',
onPress={onPressDefault} }}
/> accessibilityLabel={_(msg`Select service`)}
<Option accessibilityHint={_(msg`Sets server for the Bluesky client`)}
testID="otherServerBtn" onPress={onPressSelectService}>
isSelected={!isDefaultSelected} <FontAwesomeIcon icon="server" size={21} />
label="Other" </Button>
onPress={onPressOther}> </View>
<View style={styles.otherForm}> </StepHeader>
<Text nativeID="addressProvider" style={[pal.text, s.mb5]}>
<Trans>Enter the address of your provider:</Trans> {!uiState.serviceDescription ? (
</Text> <ActivityIndicator />
<TextInput ) : (
testID="customServerInput" <>
icon="globe" {uiState.isInviteCodeRequired && (
placeholder={_(msg`Hosting provider address`)} <View style={s.pb20}>
value={uiState.serviceUrl} <Text type="md-medium" style={[pal.text, s.mb2]}>
editable <Trans>Invite code</Trans>
onChange={onChangeServiceUrl} </Text>
accessibilityHint={_(msg`Input hosting provider address`)} <TextInput
accessibilityLabel={_(msg`Hosting provider address`)} testID="inviteCodeInput"
accessibilityLabelledBy="addressProvider" icon="ticket"
/> placeholder={_(msg`Required for this provider`)}
{LOGIN_INCLUDE_DEV_SERVERS && ( value={uiState.inviteCode}
<View style={[s.flexRow, s.mt10]}> editable
<Button onChange={value => uiDispatch({type: 'set-invite-code', value})}
testID="stagingServerBtn" accessibilityLabel={_(msg`Invite code`)}
type="default" accessibilityHint={_(msg`Input invite code to proceed`)}
style={s.mr5} autoCapitalize="none"
label={_(msg`Staging`)} autoComplete="off"
onPress={() => onChangeServiceUrl(STAGING_SERVICE)} autoCorrect={false}
/> autoFocus={true}
<Button
testID="localDevServerBtn"
type="default"
label={_(msg`Dev Server`)}
onPress={() => onChangeServiceUrl(LOCAL_DEV_SERVICE)}
/> />
</View> </View>
)} )}
</View>
</Option> {!uiState.inviteCode && uiState.isInviteCodeRequired ? (
<View style={[s.flexRow, s.alignCenter]}>
<Text style={pal.text}>
<Trans>Don't have an invite code?</Trans>{' '}
</Text>
<TouchableWithoutFeedback
onPress={onPressWaitlist}
accessibilityLabel={_(msg`Join the waitlist.`)}
accessibilityHint="">
<View style={styles.touchable}>
<Text style={pal.link}>
<Trans>Join the waitlist.</Trans>
</Text>
</View>
</TouchableWithoutFeedback>
</View>
) : (
<>
<View style={s.pb20}>
<Text
type="md-medium"
style={[pal.text, s.mb2]}
nativeID="email">
<Trans>Email address</Trans>
</Text>
<TextInput
testID="emailInput"
icon="envelope"
placeholder={_(msg`Enter your email address`)}
value={uiState.email}
editable
onChange={value => uiDispatch({type: 'set-email', value})}
accessibilityLabel={_(msg`Email`)}
accessibilityHint={_(msg`Input email for Bluesky account`)}
accessibilityLabelledBy="email"
autoCapitalize="none"
autoComplete="off"
autoCorrect={false}
autoFocus={!uiState.isInviteCodeRequired}
/>
</View>
<View style={s.pb20}>
<Text
type="md-medium"
style={[pal.text, s.mb2]}
nativeID="password">
<Trans>Password</Trans>
</Text>
<TextInput
testID="passwordInput"
icon="lock"
placeholder={_(msg`Choose your password`)}
value={uiState.password}
editable
secureTextEntry
onChange={value => uiDispatch({type: 'set-password', value})}
accessibilityLabel={_(msg`Password`)}
accessibilityHint={_(msg`Set password`)}
accessibilityLabelledBy="password"
autoCapitalize="none"
autoComplete="off"
autoCorrect={false}
/>
</View>
<View style={s.pb20}>
<Text
type="md-medium"
style={[pal.text, s.mb2]}
nativeID="birthDate">
<Trans>Your birth date</Trans>
</Text>
<DateInput
handleAsUTC
testID="birthdayInput"
value={birthDate}
onChange={value =>
uiDispatch({type: 'set-birth-date', value})
}
buttonType="default-light"
buttonStyle={[pal.border, styles.dateInputButton]}
buttonLabelType="lg"
accessibilityLabel={_(msg`Birthday`)}
accessibilityHint={_(msg`Enter your birth date`)}
accessibilityLabelledBy="birthDate"
/>
</View>
{uiState.serviceDescription && (
<Policies
serviceDescription={uiState.serviceDescription}
needsGuardian={!is18(uiState)}
/>
)}
</>
)}
</>
)}
{uiState.error ? ( {uiState.error ? (
<ErrorMessage message={uiState.error} style={styles.error} /> <ErrorMessage message={uiState.error} style={styles.error} />
) : ( ) : undefined}
<HelpTip text={_(msg`You can change hosting providers at any time.`)} />
)}
</View>
)
}
function Option({
children,
isSelected,
label,
help,
onPress,
testID,
}: React.PropsWithChildren<{
isSelected: boolean
label: string
help?: string
onPress: () => void
testID?: string
}>) {
const theme = useTheme()
const pal = usePalette('default')
const {_} = useLingui()
const circleFillStyle = React.useMemo(
() => ({
backgroundColor: theme.palette.primary.background,
}),
[theme],
)
return (
<View style={[styles.option, pal.border]}>
<TouchableWithoutFeedback
onPress={onPress}
testID={testID}
accessibilityRole="button"
accessibilityLabel={label}
accessibilityHint={_(msg`Sets hosting provider to ${label}`)}>
<View style={styles.optionHeading}>
<View style={[styles.circle, pal.border]}>
{isSelected ? (
<View style={[circleFillStyle, styles.circleFill]} />
) : undefined}
</View>
<Text type="xl" style={pal.text}>
{label}
{help ? (
<Text type="xl" style={pal.textLight}>
{help}
</Text>
) : undefined}
</Text>
</View>
</TouchableWithoutFeedback>
{isSelected && children}
</View> </View>
) )
} }
@@ -165,34 +217,15 @@ function Option({
const styles = StyleSheet.create({ const styles = StyleSheet.create({
error: { error: {
borderRadius: 6, borderRadius: 6,
marginTop: 10,
}, },
dateInputButton: {
option: {
borderWidth: 1, borderWidth: 1,
borderRadius: 6, borderRadius: 6,
marginBottom: 10, paddingVertical: 14,
}, },
optionHeading: { // @ts-expect-error: Suppressing error due to incomplete `ViewStyle` type definition in react-native-web, missing `cursor` prop as discussed in https://github.com/necolas/react-native-web/issues/832.
flexDirection: 'row', touchable: {
alignItems: 'center', ...(isWeb && {cursor: 'pointer'}),
padding: 10,
},
circle: {
width: 26,
height: 26,
borderRadius: 15,
padding: 4,
borderWidth: 1,
marginRight: 10,
},
circleFill: {
width: 16,
height: 16,
borderRadius: 10,
},
otherForm: {
paddingBottom: 10,
paddingHorizontal: 12,
}, },
}) })
+143 -134
View File
@@ -1,39 +1,28 @@
import React from 'react' import React from 'react'
import {StyleSheet, TouchableWithoutFeedback, View} from 'react-native' import {
import {CreateAccountState, CreateAccountDispatch, is18} from './state' ActivityIndicator,
StyleSheet,
TouchableWithoutFeedback,
View,
} from 'react-native'
import {
CreateAccountState,
CreateAccountDispatch,
requestVerificationCode,
} from './state'
import {Text} from 'view/com/util/text/Text' import {Text} from 'view/com/util/text/Text'
import {DateInput} from 'view/com/util/forms/DateInput'
import {StepHeader} from './StepHeader' import {StepHeader} from './StepHeader'
import {s} from 'lib/styles' import {s} from 'lib/styles'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {TextInput} from '../util/TextInput' import {TextInput} from '../util/TextInput'
import {Policies} from './Policies' import {Button} from '../../util/forms/Button'
import {ErrorMessage} from 'view/com/util/error/ErrorMessage' import {ErrorMessage} from 'view/com/util/error/ErrorMessage'
import {isWeb} from 'platform/detection' import {isWeb} from 'platform/detection'
import {Trans, msg} from '@lingui/macro' import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals' import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {logger} from '#/logger' import parsePhoneNumber from 'libphonenumber-js'
function sanitizeDate(date: Date): Date {
if (!date || date.toString() === 'Invalid Date') {
logger.error(`Create account: handled invalid date for birthDate`, {
hasDate: !!date,
})
return new Date()
}
return date
}
/** STEP 2: Your account
* @field Invite code or waitlist
* @field Email address
* @field Email address
* @field Email address
* @field Password
* @field Birth date
* @readonly Terms of service & privacy policy
*/
export function Step2({ export function Step2({
uiState, uiState,
uiDispatch, uiDispatch,
@@ -43,130 +32,155 @@ export function Step2({
}) { }) {
const pal = usePalette('default') const pal = usePalette('default')
const {_} = useLingui() const {_} = useLingui()
const {openModal} = useModalControls() const {isMobile} = useWebMediaQueries()
const onPressWaitlist = React.useCallback(() => { const onPressRequest = React.useCallback(() => {
openModal({name: 'waitlist'}) if (
}, [openModal]) uiState.verificationPhone.length >= 9 &&
parsePhoneNumber(uiState.verificationPhone, 'US')
) {
requestVerificationCode({uiState, uiDispatch, _})
} else {
uiDispatch({
type: 'set-error',
value: _(
msg`There's something wrong with this number. Please include your country and/or area code!`,
),
})
}
}, [uiState, uiDispatch, _])
const birthDate = React.useMemo(() => { const onPressRetry = React.useCallback(() => {
return sanitizeDate(uiState.birthDate) uiDispatch({type: 'set-has-requested-verification-code', value: false})
}, [uiState.birthDate]) }, [uiDispatch])
const phoneNumberFormatted = React.useMemo(
() =>
uiState.hasRequestedVerificationCode
? parsePhoneNumber(
uiState.verificationPhone,
'US',
)?.formatInternational()
: '',
[uiState.hasRequestedVerificationCode, uiState.verificationPhone],
)
return ( return (
<View> <View>
<StepHeader step="2" title={_(msg`Your account`)} /> <StepHeader uiState={uiState} title={_(msg`SMS verification`)} />
{uiState.isInviteCodeRequired && ( {!uiState.hasRequestedVerificationCode ? (
<View style={s.pb20}> <>
<Text type="md-medium" style={[pal.text, s.mb2]}> <View style={s.pb20}>
<Trans>Invite code</Trans> <Text
</Text> type="md-medium"
<TextInput style={[pal.text, s.mb2]}
testID="inviteCodeInput" nativeID="phoneNumber">
icon="ticket" <Trans>Phone number</Trans>
placeholder={_(msg`Required for this provider`)} </Text>
value={uiState.inviteCode} <TextInput
editable testID="phoneInput"
onChange={value => uiDispatch({type: 'set-invite-code', value})} icon="phone"
accessibilityLabel={_(msg`Invite code`)} placeholder={_(msg`Enter your phone number`)}
accessibilityHint={_(msg`Input invite code to proceed`)} value={uiState.verificationPhone}
autoCapitalize="none" editable
autoComplete="off" onChange={value =>
autoCorrect={false} uiDispatch({type: 'set-verification-phone', value})
/> }
</View> accessibilityLabel={_(msg`Email`)}
)} accessibilityHint={_(
msg`Input phone number for SMS verification`,
)}
accessibilityLabelledBy="phoneNumber"
keyboardType="phone-pad"
autoCapitalize="none"
autoComplete="tel"
autoCorrect={false}
autoFocus={true}
/>
<Text type="sm" style={[pal.textLight, s.mt5]}>
<Trans>
Please enter a phone number that can receive SMS text messages.
</Trans>
</Text>
</View>
{!uiState.inviteCode && uiState.isInviteCodeRequired ? ( <View style={isMobile ? {} : {flexDirection: 'row'}}>
<Text style={[s.alignBaseline, pal.text]}> {uiState.isProcessing ? (
<Trans>Don't have an invite code?</Trans>{' '} <ActivityIndicator />
<TouchableWithoutFeedback ) : (
onPress={onPressWaitlist} <Button
accessibilityLabel={_(msg`Join the waitlist.`)} testID="requestCodeBtn"
accessibilityHint=""> type="primary"
<View style={styles.touchable}> label={_(msg`Request code`)}
<Text style={pal.link}> labelStyle={isMobile ? [s.flex1, s.textCenter, s.f17] : []}
<Trans>Join the waitlist.</Trans> style={
</Text> isMobile ? {paddingVertical: 12, paddingHorizontal: 20} : {}
</View> }
</TouchableWithoutFeedback> onPress={onPressRequest}
</Text> />
)}
</View>
</>
) : ( ) : (
<> <>
<View style={s.pb20}> <View style={s.pb20}>
<Text type="md-medium" style={[pal.text, s.mb2]} nativeID="email"> <View
<Trans>Email address</Trans> style={[
</Text> s.flexRow,
s.mb5,
s.alignCenter,
{justifyContent: 'space-between'},
]}>
<Text
type="md-medium"
style={pal.text}
nativeID="verificationCode">
<Trans>Verification code</Trans>{' '}
</Text>
<TouchableWithoutFeedback
onPress={onPressRetry}
accessibilityLabel={_(msg`Retry.`)}
accessibilityHint="">
<View style={styles.touchable}>
<Text
type="md-medium"
style={pal.link}
nativeID="verificationCode">
<Trans>Retry</Trans>
</Text>
</View>
</TouchableWithoutFeedback>
</View>
<TextInput <TextInput
testID="emailInput" testID="codeInput"
icon="envelope" icon="hashtag"
placeholder={_(msg`Enter your email address`)} placeholder={_(msg`XXXXXX`)}
value={uiState.email} value={uiState.verificationCode}
editable editable
onChange={value => uiDispatch({type: 'set-email', value})} onChange={value =>
uiDispatch({type: 'set-verification-code', value})
}
accessibilityLabel={_(msg`Email`)} accessibilityLabel={_(msg`Email`)}
accessibilityHint={_(msg`Input email for Bluesky waitlist`)} accessibilityHint={_(
accessibilityLabelledBy="email" msg`Input the verification code we have texted to you`,
)}
accessibilityLabelledBy="verificationCode"
keyboardType="phone-pad"
autoCapitalize="none" autoCapitalize="none"
autoComplete="off" autoComplete="one-time-code"
textContentType="oneTimeCode"
autoCorrect={false} autoCorrect={false}
autoFocus={true}
/> />
</View> <Text type="sm" style={[pal.textLight, s.mt5]}>
<Trans>Please enter the verification code sent to</Trans>{' '}
<View style={s.pb20}> {phoneNumberFormatted}.
<Text
type="md-medium"
style={[pal.text, s.mb2]}
nativeID="password">
<Trans>Password</Trans>
</Text> </Text>
<TextInput
testID="passwordInput"
icon="lock"
placeholder={_(msg`Choose your password`)}
value={uiState.password}
editable
secureTextEntry
onChange={value => uiDispatch({type: 'set-password', value})}
accessibilityLabel={_(msg`Password`)}
accessibilityHint={_(msg`Set password`)}
accessibilityLabelledBy="password"
autoCapitalize="none"
autoComplete="off"
autoCorrect={false}
/>
</View> </View>
<View style={s.pb20}>
<Text
type="md-medium"
style={[pal.text, s.mb2]}
nativeID="birthDate">
<Trans>Your birth date</Trans>
</Text>
<DateInput
handleAsUTC
testID="birthdayInput"
value={birthDate}
onChange={value => uiDispatch({type: 'set-birth-date', value})}
buttonType="default-light"
buttonStyle={[pal.border, styles.dateInputButton]}
buttonLabelType="lg"
accessibilityLabel={_(msg`Birthday`)}
accessibilityHint={_(msg`Enter your birth date`)}
accessibilityLabelledBy="birthDate"
/>
</View>
{uiState.serviceDescription && (
<Policies
serviceDescription={uiState.serviceDescription}
needsGuardian={!is18(uiState)}
/>
)}
</> </>
)} )}
{uiState.error ? ( {uiState.error ? (
<ErrorMessage message={uiState.error} style={styles.error} /> <ErrorMessage message={uiState.error} style={styles.error} />
) : undefined} ) : undefined}
@@ -179,11 +193,6 @@ const styles = StyleSheet.create({
borderRadius: 6, borderRadius: 6,
marginTop: 10, marginTop: 10,
}, },
dateInputButton: {
borderWidth: 1,
borderRadius: 6,
paddingVertical: 14,
},
// @ts-expect-error: Suppressing error due to incomplete `ViewStyle` type definition in react-native-web, missing `cursor` prop as discussed in https://github.com/necolas/react-native-web/issues/832. // @ts-expect-error: Suppressing error due to incomplete `ViewStyle` type definition in react-native-web, missing `cursor` prop as discussed in https://github.com/necolas/react-native-web/issues/832.
touchable: { touchable: {
...(isWeb && {cursor: 'pointer'}), ...(isWeb && {cursor: 'pointer'}),
+1 -1
View File
@@ -25,7 +25,7 @@ export function Step3({
const {_} = useLingui() const {_} = useLingui()
return ( return (
<View> <View>
<StepHeader step="3" title={_(msg`Your user handle`)} /> <StepHeader uiState={uiState} title={_(msg`Your user handle`)} />
<View style={s.pb10}> <View style={s.pb10}>
<TextInput <TextInput
testID="handleInput" testID="handleInput"
+26 -11
View File
@@ -3,27 +3,42 @@ import {StyleSheet, View} from 'react-native'
import {Text} from 'view/com/util/text/Text' import {Text} from 'view/com/util/text/Text'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {Trans} from '@lingui/macro' import {Trans} from '@lingui/macro'
import {CreateAccountState} from './state'
export function StepHeader({step, title}: {step: string; title: string}) { export function StepHeader({
uiState,
title,
children,
}: React.PropsWithChildren<{uiState: CreateAccountState; title: string}>) {
const pal = usePalette('default') const pal = usePalette('default')
const numSteps = uiState.isPhoneVerificationRequired ? 3 : 2
return ( return (
<View style={styles.container}> <View style={styles.container}>
<Text type="lg" style={[pal.textLight]}> <View>
{step === '3' ? ( <Text type="lg" style={[pal.textLight]}>
<Trans>Last step!</Trans> {uiState.step === 3 ? (
) : ( <Trans>Last step!</Trans>
<Trans>Step {step} of 3</Trans> ) : (
)} <Trans>
</Text> Step {uiState.step} of {numSteps}
<Text style={[pal.text]} type="title-xl"> </Trans>
{title} )}
</Text> </Text>
<Text style={[pal.text]} type="title-xl">
{title}
</Text>
</View>
{children}
</View> </View>
) )
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 20, marginBottom: 20,
}, },
}) })
+99 -8
View File
@@ -2,6 +2,7 @@ import {useReducer} from 'react'
import { import {
ComAtprotoServerDescribeServer, ComAtprotoServerDescribeServer,
ComAtprotoServerCreateAccount, ComAtprotoServerCreateAccount,
BskyAgent,
} from '@atproto/api' } from '@atproto/api'
import {I18nContext, useLingui} from '@lingui/react' import {I18nContext, useLingui} from '@lingui/react'
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
@@ -13,6 +14,7 @@ import {cleanError} from '#/lib/strings/errors'
import {DispatchContext as OnboardingDispatchContext} from '#/state/shell/onboarding' import {DispatchContext as OnboardingDispatchContext} from '#/state/shell/onboarding'
import {ApiContext as SessionApiContext} from '#/state/session' import {ApiContext as SessionApiContext} from '#/state/session'
import {DEFAULT_SERVICE} from '#/lib/constants' import {DEFAULT_SERVICE} from '#/lib/constants'
import parsePhoneNumber from 'libphonenumber-js'
export type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema export type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema
const DEFAULT_DATE = new Date(Date.now() - 60e3 * 60 * 24 * 365 * 20) // default to 20 years ago const DEFAULT_DATE = new Date(Date.now() - 60e3 * 60 * 24 * 365 * 20) // default to 20 years ago
@@ -27,6 +29,9 @@ export type CreateAccountAction =
| {type: 'set-invite-code'; value: string} | {type: 'set-invite-code'; value: string}
| {type: 'set-email'; value: string} | {type: 'set-email'; value: string}
| {type: 'set-password'; value: string} | {type: 'set-password'; value: string}
| {type: 'set-verification-phone'; value: string}
| {type: 'set-verification-code'; value: string}
| {type: 'set-has-requested-verification-code'; value: boolean}
| {type: 'set-handle'; value: string} | {type: 'set-handle'; value: string}
| {type: 'set-birth-date'; value: Date} | {type: 'set-birth-date'; value: Date}
| {type: 'next'} | {type: 'next'}
@@ -43,6 +48,9 @@ export interface CreateAccountState {
inviteCode: string inviteCode: string
email: string email: string
password: string password: string
verificationPhone: string
verificationCode: string
hasRequestedVerificationCode: boolean
handle: string handle: string
birthDate: Date birthDate: Date
@@ -50,6 +58,7 @@ export interface CreateAccountState {
canBack: boolean canBack: boolean
canNext: boolean canNext: boolean
isInviteCodeRequired: boolean isInviteCodeRequired: boolean
isPhoneVerificationRequired: boolean
} }
export type CreateAccountDispatch = (action: CreateAccountAction) => void export type CreateAccountDispatch = (action: CreateAccountAction) => void
@@ -66,15 +75,51 @@ export function useCreateAccount() {
inviteCode: '', inviteCode: '',
email: '', email: '',
password: '', password: '',
verificationPhone: '',
verificationCode: '',
hasRequestedVerificationCode: false,
handle: '', handle: '',
birthDate: DEFAULT_DATE, birthDate: DEFAULT_DATE,
canBack: false, canBack: false,
canNext: false, canNext: false,
isInviteCodeRequired: false, isInviteCodeRequired: false,
isPhoneVerificationRequired: false,
}) })
} }
export async function requestVerificationCode({
uiState,
uiDispatch,
_,
}: {
uiState: CreateAccountState
uiDispatch: CreateAccountDispatch
_: I18nContext['_']
}) {
const phoneNumber = parsePhoneNumber(uiState.verificationPhone, 'US')?.number
if (!phoneNumber) {
return
}
uiDispatch({type: 'set-error', value: ''})
uiDispatch({type: 'set-processing', value: true})
uiDispatch({type: 'set-verification-phone', value: phoneNumber})
try {
const agent = new BskyAgent({service: uiState.serviceUrl})
await agent.com.atproto.temp.requestPhoneVerification({
phoneNumber,
})
uiDispatch({type: 'set-has-requested-verification-code', value: true})
} catch (e: any) {
logger.error(
`Failed to request sms verification code (${e.status} status)`,
{error: e},
)
uiDispatch({type: 'set-error', value: cleanError(e.toString())})
}
uiDispatch({type: 'set-processing', value: false})
}
export async function submit({ export async function submit({
createAccount, createAccount,
onboardingDispatch, onboardingDispatch,
@@ -89,26 +134,36 @@ export async function submit({
_: I18nContext['_'] _: I18nContext['_']
}) { }) {
if (!uiState.email) { if (!uiState.email) {
uiDispatch({type: 'set-step', value: 2}) uiDispatch({type: 'set-step', value: 1})
return uiDispatch({ return uiDispatch({
type: 'set-error', type: 'set-error',
value: _(msg`Please enter your email.`), value: _(msg`Please enter your email.`),
}) })
} }
if (!EmailValidator.validate(uiState.email)) { if (!EmailValidator.validate(uiState.email)) {
uiDispatch({type: 'set-step', value: 2}) uiDispatch({type: 'set-step', value: 1})
return uiDispatch({ return uiDispatch({
type: 'set-error', type: 'set-error',
value: _(msg`Your email appears to be invalid.`), value: _(msg`Your email appears to be invalid.`),
}) })
} }
if (!uiState.password) { if (!uiState.password) {
uiDispatch({type: 'set-step', value: 2}) uiDispatch({type: 'set-step', value: 1})
return uiDispatch({ return uiDispatch({
type: 'set-error', type: 'set-error',
value: _(msg`Please choose your password.`), value: _(msg`Please choose your password.`),
}) })
} }
if (
uiState.isPhoneVerificationRequired &&
(!uiState.verificationPhone || !uiState.verificationCode)
) {
uiDispatch({type: 'set-step', value: 2})
return uiDispatch({
type: 'set-error',
value: _(msg`Please enter the code you received by SMS.`),
})
}
if (!uiState.handle) { if (!uiState.handle) {
uiDispatch({type: 'set-step', value: 3}) uiDispatch({type: 'set-step', value: 3})
return uiDispatch({ return uiDispatch({
@@ -127,6 +182,8 @@ export async function submit({
handle: createFullHandle(uiState.handle, uiState.userDomain), handle: createFullHandle(uiState.handle, uiState.userDomain),
password: uiState.password, password: uiState.password,
inviteCode: uiState.inviteCode.trim(), inviteCode: uiState.inviteCode.trim(),
verificationPhone: uiState.verificationPhone.trim(),
verificationCode: uiState.verificationCode.trim(),
}) })
} catch (e: any) { } catch (e: any) {
onboardingDispatch({type: 'skip'}) // undo starting the onboard onboardingDispatch({type: 'skip'}) // undo starting the onboard
@@ -135,6 +192,9 @@ export async function submit({
errMsg = _( errMsg = _(
msg`Invite code not accepted. Check that you input it correctly and try again.`, msg`Invite code not accepted. Check that you input it correctly and try again.`,
) )
uiDispatch({type: 'set-step', value: 1})
} else if (e.error === 'InvalidPhoneVerification') {
uiDispatch({type: 'set-step', value: 2})
} }
if ([400, 429].includes(e.status)) { if ([400, 429].includes(e.status)) {
@@ -201,6 +261,19 @@ function createReducer({_}: {_: I18nContext['_']}) {
case 'set-password': { case 'set-password': {
return compute({...state, password: action.value}) return compute({...state, password: action.value})
} }
case 'set-verification-phone': {
return compute({
...state,
verificationPhone: action.value,
hasRequestedVerificationCode: false,
})
}
case 'set-verification-code': {
return compute({...state, verificationCode: action.value.trim()})
}
case 'set-has-requested-verification-code': {
return compute({...state, hasRequestedVerificationCode: action.value})
}
case 'set-handle': { case 'set-handle': {
return compute({...state, handle: action.value}) return compute({...state, handle: action.value})
} }
@@ -208,7 +281,7 @@ function createReducer({_}: {_: I18nContext['_']}) {
return compute({...state, birthDate: action.value}) return compute({...state, birthDate: action.value})
} }
case 'next': { case 'next': {
if (state.step === 2) { if (state.step === 1) {
if (!is13(state)) { if (!is13(state)) {
return compute({ return compute({
...state, ...state,
@@ -218,10 +291,18 @@ function createReducer({_}: {_: I18nContext['_']}) {
}) })
} }
} }
return compute({...state, error: '', step: state.step + 1}) let increment = 1
if (state.step === 1 && !state.isPhoneVerificationRequired) {
increment = 2
}
return compute({...state, error: '', step: state.step + increment})
} }
case 'back': { case 'back': {
return compute({...state, error: '', step: state.step - 1}) let decrement = 1
if (state.step === 3 && !state.isPhoneVerificationRequired) {
decrement = 2
}
return compute({...state, error: '', step: state.step - decrement})
} }
} }
} }
@@ -230,12 +311,16 @@ function createReducer({_}: {_: I18nContext['_']}) {
function compute(state: CreateAccountState): CreateAccountState { function compute(state: CreateAccountState): CreateAccountState {
let canNext = true let canNext = true
if (state.step === 1) { if (state.step === 1) {
canNext = !!state.serviceDescription
} else if (state.step === 2) {
canNext = canNext =
!!state.serviceDescription &&
(!state.isInviteCodeRequired || !!state.inviteCode) && (!state.isInviteCodeRequired || !!state.inviteCode) &&
!!state.email && !!state.email &&
!!state.password !!state.password
} else if (state.step === 2) {
canNext =
!state.isPhoneVerificationRequired ||
(!!state.verificationPhone &&
isValidVerificationCode(state.verificationCode))
} else if (state.step === 3) { } else if (state.step === 3) {
canNext = !!state.handle canNext = !!state.handle
} }
@@ -244,5 +329,11 @@ function compute(state: CreateAccountState): CreateAccountState {
canBack: state.step > 1, canBack: state.step > 1,
canNext, canNext,
isInviteCodeRequired: !!state.serviceDescription?.inviteCodeRequired, isInviteCodeRequired: !!state.serviceDescription?.inviteCodeRequired,
isPhoneVerificationRequired:
!!state.serviceDescription?.phoneVerificationRequired,
} }
} }
function isValidVerificationCode(str: string): boolean {
return /[0-9]{6}/.test(str)
}
+3 -3
View File
@@ -74,7 +74,7 @@ export const ComposePost = observer(function ComposePost({
}: Props) { }: Props) {
const {currentAccount} = useSession() const {currentAccount} = useSession()
const {data: currentProfile} = useProfileQuery({did: currentAccount!.did}) const {data: currentProfile} = useProfileQuery({did: currentAccount!.did})
const {activeModals} = useModals() const {isModalActive, activeModals} = useModals()
const {openModal, closeModal} = useModalControls() const {openModal, closeModal} = useModalControls()
const {closeComposer} = useComposerControls() const {closeComposer} = useComposerControls()
const {track} = useAnalytics() const {track} = useAnalytics()
@@ -176,11 +176,11 @@ export const ComposePost = observer(function ComposePost({
[onPressCancel], [onPressCancel],
) )
useEffect(() => { useEffect(() => {
if (isWeb) { if (isWeb && !isModalActive) {
window.addEventListener('keydown', onEscape) window.addEventListener('keydown', onEscape)
return () => window.removeEventListener('keydown', onEscape) return () => window.removeEventListener('keydown', onEscape)
} }
}, [onEscape]) }, [onEscape, isModalActive])
const onPressAddLinkCard = useCallback( const onPressAddLinkCard = useCallback(
(uri: string) => { (uri: string) => {
+13 -5
View File
@@ -94,15 +94,23 @@ export const ListCard = ({
</Trans> </Trans>
))} ))}
</Text> </Text>
{!!list.viewer?.muted && ( <View style={s.flexRow}>
<View style={s.flexRow}> {list.viewer?.muted ? (
<View style={[s.mt5, pal.btn, styles.pill]}> <View style={[s.mt5, pal.btn, styles.pill]}>
<Text type="xs" style={pal.text}> <Text type="xs" style={pal.text}>
<Trans>Subscribed</Trans> <Trans>Muted</Trans>
</Text> </Text>
</View> </View>
</View> ) : null}
)}
{list.viewer?.blocked ? (
<View style={[s.mt5, pal.btn, styles.pill]}>
<Text type="xs" style={pal.text}>
<Trans>Blocked</Trans>
</Text>
</View>
) : null}
</View>
</View> </View>
{renderButton ? ( {renderButton ? (
<View style={styles.layoutButton}>{renderButton()}</View> <View style={styles.layoutButton}>{renderButton()}</View>
+10 -12
View File
@@ -182,19 +182,17 @@ export function Component({
]} ]}
testID="createOrEditListModal"> testID="createOrEditListModal">
<Text style={[styles.title, pal.text]}> <Text style={[styles.title, pal.text]}>
<Trans> {isCurateList ? (
{isCurateList ? ( list ? (
list ? ( <Trans>Edit User List</Trans>
<Trans>Edit User List</Trans>
) : (
<Trans>New User List</Trans>
)
) : list ? (
<Trans>Edit Moderation List</Trans>
) : ( ) : (
<Trans>New Moderation List</Trans> <Trans>New User List</Trans>
)} )
</Trans> ) : list ? (
<Trans>Edit Moderation List</Trans>
) : (
<Trans>New Moderation List</Trans>
)}
</Text> </Text>
{error !== '' && ( {error !== '' && (
<View style={styles.errorContainer}> <View style={styles.errorContainer}>
+5 -2
View File
@@ -160,7 +160,7 @@ export function Component({}: {}) {
{/* TODO: Update this label to be more concise */} {/* TODO: Update this label to be more concise */}
<Text <Text
type="lg" type="lg"
style={styles.description} style={[pal.text, styles.description]}
nativeID="confirmationCode"> nativeID="confirmationCode">
<Trans> <Trans>
Check your inbox for an email with the confirmation code to Check your inbox for an email with the confirmation code to
@@ -180,7 +180,10 @@ export function Component({}: {}) {
msg`Input confirmation code for account deletion`, msg`Input confirmation code for account deletion`,
)} )}
/> />
<Text type="lg" style={styles.description} nativeID="password"> <Text
type="lg"
style={[pal.text, styles.description]}
nativeID="password">
<Trans>Please enter your password as well:</Trans> <Trans>Please enter your password as well:</Trans>
</Text> </Text>
<TextInput <TextInput
+5 -1
View File
@@ -63,7 +63,11 @@ function Modal({modal}: {modal: ModalIface}) {
} }
const onPressMask = () => { const onPressMask = () => {
if (modal.name === 'crop-image' || modal.name === 'edit-image') { if (
modal.name === 'crop-image' ||
modal.name === 'edit-image' ||
modal.name === 'alt-text-image'
) {
return // dont close on mask presses during crop return // dont close on mask presses during crop
} }
closeModal() closeModal()
+21 -2
View File
@@ -20,6 +20,11 @@ import {useNavigation} from '@react-navigation/native'
import {NavigationProp} from 'lib/routes/types' import {NavigationProp} from 'lib/routes/types'
import {Logo} from '#/view/icons/Logo' import {Logo} from '#/view/icons/Logo'
import {IS_DEV} from '#/env'
import {atoms} from '#/alf'
import {Link as Link2} from '#/components/Link'
import {ColorPalette_Stroke2_Corner0_Rounded as ColorPalette} from '#/components/icons/ColorPalette'
export function FeedsTabBar( export function FeedsTabBar(
props: RenderTabBarFnProps & {testID?: string; onPressSelected: () => void}, props: RenderTabBarFnProps & {testID?: string; onPressSelected: () => void},
) { ) {
@@ -68,7 +73,7 @@ export function FeedsTabBar(
headerHeight.value = e.nativeEvent.layout.height headerHeight.value = e.nativeEvent.layout.height
}}> }}>
<View style={[pal.view, styles.topBar]}> <View style={[pal.view, styles.topBar]}>
<View style={[pal.view]}> <View style={[pal.view, {width: 100}]}>
<TouchableOpacity <TouchableOpacity
testID="viewHeaderDrawerBtn" testID="viewHeaderDrawerBtn"
onPress={onPressAvi} onPress={onPressAvi}
@@ -88,7 +93,21 @@ export function FeedsTabBar(
<View> <View>
<Logo width={30} /> <Logo width={30} />
</View> </View>
<View style={[pal.view, {width: 18}]}> <View
style={[
atoms.flex_row,
atoms.justify_end,
atoms.align_center,
atoms.gap_md,
pal.view,
{width: 100},
]}>
{IS_DEV && (
<Link2 to="/sys/debug">
<ColorPalette size="md" />
</Link2>
)}
{hasSession && ( {hasSession && (
<Link <Link
testID="viewHeaderHomeFeedPrefsBtn" testID="viewHeaderHomeFeedPrefsBtn"
@@ -0,0 +1,43 @@
import React from 'react'
import {View} from 'react-native'
import {Trans} from '@lingui/macro'
import {Text} from '../util/text/Text'
import {usePalette} from '#/lib/hooks/usePalette'
import {TextLink} from '../util/Link'
import {InfoCircleIcon} from '#/lib/icons'
export function DiscoverFallbackHeader() {
const pal = usePalette('default')
return (
<View
style={[
{
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 12,
paddingHorizontal: 12,
borderTopWidth: 1,
},
pal.border,
pal.viewLight,
]}>
<View style={{width: 68, paddingLeft: 12}}>
<InfoCircleIcon size={36} style={pal.textLight} strokeWidth={1.5} />
</View>
<View style={{flex: 1}}>
<Text type="md" style={pal.text}>
<Trans>
We ran out of posts from your follows. Here's the latest from{' '}
<TextLink
type="md-medium"
href="/profile/bsky.app/feed/whats-hot"
text="Discover"
style={pal.link}
/>
.
</Trans>
</Text>
</View>
</View>
)
}
+8
View File
@@ -30,6 +30,8 @@ import {useSession} from '#/state/session'
import {STALE} from '#/state/queries' import {STALE} from '#/state/queries'
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {DiscoverFallbackHeader} from './DiscoverFallbackHeader'
import {FALLBACK_MARKER_POST} from '#/lib/api/feed/home'
const LOADING_ITEM = {_reactKey: '__loading__'} const LOADING_ITEM = {_reactKey: '__loading__'}
const EMPTY_FEED_ITEM = {_reactKey: '__empty__'} const EMPTY_FEED_ITEM = {_reactKey: '__empty__'}
@@ -265,6 +267,12 @@ let Feed = ({
) )
} else if (item === LOADING_ITEM) { } else if (item === LOADING_ITEM) {
return <PostFeedLoadingPlaceholder /> return <PostFeedLoadingPlaceholder />
} else if (item.rootUri === FALLBACK_MARKER_POST.post.uri) {
// HACK
// tell the user we fell back to discover
// see home.ts (feed api) for more info
// -prf
return <DiscoverFallbackHeader />
} }
return <FeedSlice slice={item} /> return <FeedSlice slice={item} />
}, },
+8 -1
View File
@@ -306,6 +306,8 @@ export const TextLinkOnWebOnly = memo(function DesktopWebTextLink({
) )
}) })
const EXEMPT_PATHS = ['/robots.txt', '/security.txt', '/.well-known/']
// NOTE // NOTE
// we can't use the onPress given by useLinkProps because it will // we can't use the onPress given by useLinkProps because it will
// match most paths to the HomeTab routes while we actually want to // match most paths to the HomeTab routes while we actually want to
@@ -350,7 +352,12 @@ function onPressInner(
if (shouldHandle) { if (shouldHandle) {
href = convertBskyAppUrlIfNeeded(href) href = convertBskyAppUrlIfNeeded(href)
if (newTab || href.startsWith('http') || href.startsWith('mailto')) { if (
newTab ||
href.startsWith('http') ||
href.startsWith('mailto') ||
EXEMPT_PATHS.some(path => href.startsWith(path))
) {
openLink(href) openLink(href)
} else { } else {
closeModal() // close any active modals closeModal() // close any active modals
+2 -15
View File
@@ -22,7 +22,6 @@ import {Link} from '../Link'
import {ImageLayoutGrid} from '../images/ImageLayoutGrid' import {ImageLayoutGrid} from '../images/ImageLayoutGrid'
import {useLightboxControls, ImagesLightbox} from '#/state/lightbox' import {useLightboxControls, ImagesLightbox} from '#/state/lightbox'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {ExternalLinkEmbed} from './ExternalLinkEmbed' import {ExternalLinkEmbed} from './ExternalLinkEmbed'
import {MaybeQuoteEmbed} from './QuoteEmbed' import {MaybeQuoteEmbed} from './QuoteEmbed'
import {AutoSizedImage} from '../images/AutoSizedImage' import {AutoSizedImage} from '../images/AutoSizedImage'
@@ -51,7 +50,6 @@ export function PostEmbeds({
}) { }) {
const pal = usePalette('default') const pal = usePalette('default')
const {openLightbox} = useLightboxControls() const {openLightbox} = useLightboxControls()
const {isMobile} = useWebMediaQueries()
// quote post with media // quote post with media
// = // =
@@ -129,10 +127,7 @@ export function PostEmbeds({
dimensionsHint={aspectRatio} dimensionsHint={aspectRatio}
onPress={() => _openLightbox(0)} onPress={() => _openLightbox(0)}
onPressIn={() => onPressIn(0)} onPressIn={() => onPressIn(0)}
style={[ style={[styles.singleImage]}>
styles.singleImage,
isMobile && styles.singleImageMobile,
]}>
{alt === '' ? null : ( {alt === '' ? null : (
<View style={styles.altContainer}> <View style={styles.altContainer}>
<Text style={styles.alt} accessible={false}> <Text style={styles.alt} accessible={false}>
@@ -151,11 +146,7 @@ export function PostEmbeds({
images={embed.images} images={embed.images}
onPress={_openLightbox} onPress={_openLightbox}
onPressIn={onPressIn} onPressIn={onPressIn}
style={ style={embed.images.length === 1 ? [styles.singleImage] : undefined}
embed.images.length === 1
? [styles.singleImage, isMobile && styles.singleImageMobile]
: undefined
}
/> />
</View> </View>
) )
@@ -188,10 +179,6 @@ const styles = StyleSheet.create({
}, },
singleImage: { singleImage: {
borderRadius: 8, borderRadius: 8,
maxHeight: 1000,
},
singleImageMobile: {
maxHeight: 500,
}, },
extOuter: { extOuter: {
borderWidth: 1, borderWidth: 1,
+6 -3
View File
@@ -1,4 +1,5 @@
import React from 'react' import React from 'react'
import {StyleSheet, TextProps} from 'react-native'
import Svg, { import Svg, {
Path, Path,
Defs, Defs,
@@ -14,12 +15,14 @@ const ratio = 57 / 64
type Props = { type Props = {
fill?: PathProps['fill'] fill?: PathProps['fill']
} & SvgProps style?: TextProps['style']
} & Omit<SvgProps, 'style'>
export const Logo = React.forwardRef(function LogoImpl(props: Props, ref) { export const Logo = React.forwardRef(function LogoImpl(props: Props, ref) {
const {fill, ...rest} = props const {fill, ...rest} = props
const gradient = fill === 'sky' const gradient = fill === 'sky'
const _fill = gradient ? 'url(#sky)' : fill || colors.blue3 const styles = StyleSheet.flatten(props.style)
const _fill = gradient ? 'url(#sky)' : fill || styles?.color || colors.blue3
// @ts-ignore it's fiiiiine // @ts-ignore it's fiiiiine
const size = parseInt(rest.width || 32) const size = parseInt(rest.width || 32)
return ( return (
@@ -29,7 +32,7 @@ export const Logo = React.forwardRef(function LogoImpl(props: Props, ref) {
ref={ref} ref={ref}
viewBox="0 0 64 57" viewBox="0 0 64 57"
{...rest} {...rest}
style={{width: size, height: size * ratio}}> style={[{width: size, height: size * ratio}, styles]}>
{gradient && ( {gradient && (
<Defs> <Defs>
<LinearGradient id="sky" x1="0" y1="0" x2="0" y2="1"> <LinearGradient id="sky" x1="0" y1="0" x2="0" y2="1">
+6
View File
@@ -52,6 +52,7 @@ import {faGear} from '@fortawesome/free-solid-svg-icons/faGear'
import {faGlobe} from '@fortawesome/free-solid-svg-icons/faGlobe' import {faGlobe} from '@fortawesome/free-solid-svg-icons/faGlobe'
import {faHand} from '@fortawesome/free-solid-svg-icons/faHand' import {faHand} from '@fortawesome/free-solid-svg-icons/faHand'
import {faHand as farHand} from '@fortawesome/free-regular-svg-icons/faHand' import {faHand as farHand} from '@fortawesome/free-regular-svg-icons/faHand'
import {faHashtag} from '@fortawesome/free-solid-svg-icons/faHashtag'
import {faHeart} from '@fortawesome/free-regular-svg-icons/faHeart' import {faHeart} from '@fortawesome/free-regular-svg-icons/faHeart'
import {faHeart as fasHeart} from '@fortawesome/free-solid-svg-icons/faHeart' import {faHeart as fasHeart} from '@fortawesome/free-solid-svg-icons/faHeart'
import {faHouse} from '@fortawesome/free-solid-svg-icons/faHouse' import {faHouse} from '@fortawesome/free-solid-svg-icons/faHouse'
@@ -71,6 +72,7 @@ import {faPaste} from '@fortawesome/free-regular-svg-icons/faPaste'
import {faPen} from '@fortawesome/free-solid-svg-icons/faPen' import {faPen} from '@fortawesome/free-solid-svg-icons/faPen'
import {faPenNib} from '@fortawesome/free-solid-svg-icons/faPenNib' import {faPenNib} from '@fortawesome/free-solid-svg-icons/faPenNib'
import {faPenToSquare} from '@fortawesome/free-solid-svg-icons/faPenToSquare' import {faPenToSquare} from '@fortawesome/free-solid-svg-icons/faPenToSquare'
import {faPhone} from '@fortawesome/free-solid-svg-icons/faPhone'
import {faPlay} from '@fortawesome/free-solid-svg-icons/faPlay' import {faPlay} from '@fortawesome/free-solid-svg-icons/faPlay'
import {faPlus} from '@fortawesome/free-solid-svg-icons/faPlus' import {faPlus} from '@fortawesome/free-solid-svg-icons/faPlus'
import {faQuoteLeft} from '@fortawesome/free-solid-svg-icons/faQuoteLeft' import {faQuoteLeft} from '@fortawesome/free-solid-svg-icons/faQuoteLeft'
@@ -78,6 +80,7 @@ import {faReply} from '@fortawesome/free-solid-svg-icons/faReply'
import {faRetweet} from '@fortawesome/free-solid-svg-icons/faRetweet' import {faRetweet} from '@fortawesome/free-solid-svg-icons/faRetweet'
import {faRss} from '@fortawesome/free-solid-svg-icons/faRss' import {faRss} from '@fortawesome/free-solid-svg-icons/faRss'
import {faSatelliteDish} from '@fortawesome/free-solid-svg-icons/faSatelliteDish' import {faSatelliteDish} from '@fortawesome/free-solid-svg-icons/faSatelliteDish'
import {faServer} from '@fortawesome/free-solid-svg-icons/faServer'
import {faShare} from '@fortawesome/free-solid-svg-icons/faShare' import {faShare} from '@fortawesome/free-solid-svg-icons/faShare'
import {faShareFromSquare} from '@fortawesome/free-solid-svg-icons/faShareFromSquare' import {faShareFromSquare} from '@fortawesome/free-solid-svg-icons/faShareFromSquare'
import {faShield} from '@fortawesome/free-solid-svg-icons/faShield' import {faShield} from '@fortawesome/free-solid-svg-icons/faShield'
@@ -153,6 +156,7 @@ library.add(
faGlobe, faGlobe,
faHand, faHand,
farHand, farHand,
faHashtag,
faHeart, faHeart,
fasHeart, fasHeart,
faHouse, faHouse,
@@ -172,6 +176,7 @@ library.add(
faPen, faPen,
faPenNib, faPenNib,
faPenToSquare, faPenToSquare,
faPhone,
faPlay, faPlay,
faPlus, faPlus,
faQuoteLeft, faQuoteLeft,
@@ -179,6 +184,7 @@ library.add(
faRetweet, faRetweet,
faRss, faRss,
faSatelliteDish, faSatelliteDish,
faServer,
faShare, faShare,
faShareFromSquare, faShareFromSquare,
faShield, faShield,
-541
View File
@@ -1,541 +0,0 @@
import React from 'react'
import {View} from 'react-native'
import {CenteredView, ScrollView} from '#/view/com/util/Views'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {useSetColorMode} from '#/state/shell'
import * as tokens from '#/alf/tokens'
import {atoms as a, useTheme, useBreakpoints, ThemeProvider as Alf} from '#/alf'
import {Button, ButtonText} from '#/view/com/Button'
import {Text, H1, H2, H3, H4, H5, H6} from '#/view/com/Typography'
function ThemeSelector() {
const setColorMode = useSetColorMode()
return (
<View style={[a.flex_row, a.gap_md]}>
<Button
type="secondary"
size="small"
onPress={() => setColorMode('system')}>
System
</Button>
<Button
type="secondary"
size="small"
onPress={() => setColorMode('light')}>
Light
</Button>
<Button
type="secondary"
size="small"
onPress={() => setColorMode('dark')}>
Dark
</Button>
</View>
)
}
function BreakpointDebugger() {
const t = useTheme()
const breakpoints = useBreakpoints()
return (
<View>
<H3 style={[a.pb_md]}>Breakpoint Debugger</H3>
<Text style={[a.pb_md]}>
Current breakpoint: {!breakpoints.gtMobile && <Text>mobile</Text>}
{breakpoints.gtMobile && !breakpoints.gtTablet && <Text>tablet</Text>}
{breakpoints.gtTablet && <Text>desktop</Text>}
</Text>
<Text
style={[a.p_md, t.atoms.bg_contrast_100, {fontFamily: 'monospace'}]}>
{JSON.stringify(breakpoints, null, 2)}
</Text>
</View>
)
}
function ThemedSection() {
const t = useTheme()
return (
<View style={[t.atoms.bg, a.gap_md, a.p_xl]}>
<H3 style={[a.font_bold]}>theme.atoms.text</H3>
<View style={[a.flex_1, t.atoms.border, a.border_t]} />
<H3 style={[a.font_bold, t.atoms.text_contrast_700]}>
theme.atoms.text_contrast_700
</H3>
<View style={[a.flex_1, t.atoms.border, a.border_t]} />
<H3 style={[a.font_bold, t.atoms.text_contrast_500]}>
theme.atoms.text_contrast_500
</H3>
<View style={[a.flex_1, t.atoms.border_contrast_500, a.border_t]} />
<View style={[a.flex_row, a.gap_md]}>
<View
style={[
a.flex_1,
t.atoms.bg,
a.align_center,
a.justify_center,
{height: 60},
]}>
<Text>theme.bg</Text>
</View>
<View
style={[
a.flex_1,
t.atoms.bg_contrast_100,
a.align_center,
a.justify_center,
{height: 60},
]}>
<Text>theme.bg_contrast_100</Text>
</View>
</View>
<View style={[a.flex_row, a.gap_md]}>
<View
style={[
a.flex_1,
t.atoms.bg_contrast_200,
a.align_center,
a.justify_center,
{height: 60},
]}>
<Text>theme.bg_contrast_200</Text>
</View>
<View
style={[
a.flex_1,
t.atoms.bg_contrast_300,
a.align_center,
a.justify_center,
{height: 60},
]}>
<Text>theme.bg_contrast_300</Text>
</View>
</View>
<View style={[a.flex_row, a.gap_md]}>
<View
style={[
a.flex_1,
t.atoms.bg_positive,
a.align_center,
a.justify_center,
{height: 60},
]}>
<Text>theme.bg_positive</Text>
</View>
<View
style={[
a.flex_1,
t.atoms.bg_negative,
a.align_center,
a.justify_center,
{height: 60},
]}>
<Text>theme.bg_negative</Text>
</View>
</View>
</View>
)
}
export function DebugScreen() {
const t = useTheme()
return (
<ScrollView>
<CenteredView style={[t.atoms.bg]}>
<View style={[a.p_xl, a.gap_xxl, {paddingBottom: 200}]}>
<ThemeSelector />
<Alf theme="light">
<ThemedSection />
</Alf>
<Alf theme="dark">
<ThemedSection />
</Alf>
<H1>Heading 1</H1>
<H2>Heading 2</H2>
<H3>Heading 3</H3>
<H4>Heading 4</H4>
<H5>Heading 5</H5>
<H6>Heading 6</H6>
<Text style={[a.text_xxl]}>atoms.text_xxl</Text>
<Text style={[a.text_xl]}>atoms.text_xl</Text>
<Text style={[a.text_lg]}>atoms.text_lg</Text>
<Text style={[a.text_md]}>atoms.text_md</Text>
<Text style={[a.text_sm]}>atoms.text_sm</Text>
<Text style={[a.text_xs]}>atoms.text_xs</Text>
<Text style={[a.text_xxs]}>atoms.text_xxs</Text>
<View style={[a.gap_md, a.align_start]}>
<Button>
{({state}) => (
<View style={[a.p_md, a.rounded_full, t.atoms.bg_contrast_300]}>
<Text>Unstyled button, state: {JSON.stringify(state)}</Text>
</View>
)}
</Button>
<Button type="primary" size="small">
Button
</Button>
<Button type="secondary" size="small">
Button
</Button>
<Button type="primary" size="large">
Button
</Button>
<Button type="secondary" size="large">
Button
</Button>
<Button type="secondary" size="small">
{({type, size}) => (
<>
<FontAwesomeIcon icon={['fas', 'plus']} size={12} />
<ButtonText type={type} size={size}>
With an icon
</ButtonText>
</>
)}
</Button>
<Button type="primary" size="large">
{({state: _state, ...rest}) => (
<>
<FontAwesomeIcon icon={['fas', 'plus']} />
<ButtonText {...rest}>With an icon</ButtonText>
</>
)}
</Button>
</View>
<View style={[a.gap_md]}>
<View style={[a.flex_row, a.gap_md]}>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.gray_0},
]}
/>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.gray_100},
]}
/>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.gray_200},
]}
/>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.gray_300},
]}
/>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.gray_400},
]}
/>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.gray_500},
]}
/>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.gray_600},
]}
/>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.gray_700},
]}
/>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.gray_800},
]}
/>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.gray_900},
]}
/>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.gray_1000},
]}
/>
</View>
<View style={[a.flex_row, a.gap_md]}>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.blue_0},
]}
/>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.blue_100},
]}
/>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.blue_200},
]}
/>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.blue_300},
]}
/>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.blue_400},
]}
/>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.blue_500},
]}
/>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.blue_600},
]}
/>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.blue_700},
]}
/>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.blue_800},
]}
/>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.blue_900},
]}
/>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.blue_1000},
]}
/>
</View>
<View style={[a.flex_row, a.gap_md]}>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.green_0},
]}
/>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.green_100},
]}
/>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.green_200},
]}
/>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.green_300},
]}
/>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.green_400},
]}
/>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.green_500},
]}
/>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.green_600},
]}
/>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.green_700},
]}
/>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.green_800},
]}
/>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.green_900},
]}
/>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.green_1000},
]}
/>
</View>
<View style={[a.flex_row, a.gap_md]}>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.red_0},
]}
/>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.red_100},
]}
/>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.red_200},
]}
/>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.red_300},
]}
/>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.red_400},
]}
/>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.red_500},
]}
/>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.red_600},
]}
/>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.red_700},
]}
/>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.red_800},
]}
/>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.red_900},
]}
/>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.red_1000},
]}
/>
</View>
</View>
<View>
<H3 style={[a.pb_md, a.font_bold]}>Spacing</H3>
<View style={[a.gap_md]}>
<View style={[a.flex_row, a.align_center]}>
<Text style={{width: 80}}>xxs (2px)</Text>
<View style={[a.flex_1, a.pt_xxs, t.atoms.bg_contrast_300]} />
</View>
<View style={[a.flex_row, a.align_center]}>
<Text style={{width: 80}}>xs (4px)</Text>
<View style={[a.flex_1, a.pt_xs, t.atoms.bg_contrast_300]} />
</View>
<View style={[a.flex_row, a.align_center]}>
<Text style={{width: 80}}>sm (8px)</Text>
<View style={[a.flex_1, a.pt_sm, t.atoms.bg_contrast_300]} />
</View>
<View style={[a.flex_row, a.align_center]}>
<Text style={{width: 80}}>md (12px)</Text>
<View style={[a.flex_1, a.pt_md, t.atoms.bg_contrast_300]} />
</View>
<View style={[a.flex_row, a.align_center]}>
<Text style={{width: 80}}>lg (18px)</Text>
<View style={[a.flex_1, a.pt_lg, t.atoms.bg_contrast_300]} />
</View>
<View style={[a.flex_row, a.align_center]}>
<Text style={{width: 80}}>xl (24px)</Text>
<View style={[a.flex_1, a.pt_xl, t.atoms.bg_contrast_300]} />
</View>
<View style={[a.flex_row, a.align_center]}>
<Text style={{width: 80}}>xxl (32px)</Text>
<View style={[a.flex_1, a.pt_xxl, t.atoms.bg_contrast_300]} />
</View>
</View>
</View>
<BreakpointDebugger />
</View>
</CenteredView>
</ScrollView>
)
}
+1 -2
View File
@@ -19,7 +19,6 @@ import {useSession} from '#/state/session'
import {loadString, saveString} from '#/lib/storage' import {loadString, saveString} from '#/lib/storage'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {clamp} from '#/lib/numbers' import {clamp} from '#/lib/numbers'
import {PROD_DEFAULT_FEED} from '#/lib/constants'
type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Home'> type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Home'>
export function HomeScreen(props: Props) { export function HomeScreen(props: Props) {
@@ -112,7 +111,7 @@ function HomeScreenReady({
mergeFeedEnabled: Boolean(preferences.feedViewPrefs.lab_mergeFeedEnabled), mergeFeedEnabled: Boolean(preferences.feedViewPrefs.lab_mergeFeedEnabled),
mergeFeedSources: preferences.feedViewPrefs.lab_mergeFeedEnabled mergeFeedSources: preferences.feedViewPrefs.lab_mergeFeedEnabled
? preferences.feeds.saved ? preferences.feeds.saved
: [PROD_DEFAULT_FEED('whats-hot')], : [],
} }
}, [preferences]) }, [preferences])
+37 -17
View File
@@ -42,9 +42,13 @@ import {useSetDrawerOpen} from '#/state/shell'
import {useAnalytics} from '#/lib/analytics/analytics' import {useAnalytics} from '#/lib/analytics/analytics'
import {MagnifyingGlassIcon} from '#/lib/icons' import {MagnifyingGlassIcon} from '#/lib/icons'
import {useModerationOpts} from '#/state/queries/preferences' import {useModerationOpts} from '#/state/queries/preferences'
import {SearchResultCard} from '#/view/shell/desktop/Search' import {
MATCH_HANDLE,
SearchLinkCard,
SearchProfileCard,
} from '#/view/shell/desktop/Search'
import {useSetMinimalShellMode, useSetDrawerSwipeDisabled} from '#/state/shell' import {useSetMinimalShellMode, useSetDrawerSwipeDisabled} from '#/state/shell'
import {isWeb} from '#/platform/detection' import {isNative, isWeb} from '#/platform/detection'
import {listenSoftReset} from '#/state/events' import {listenSoftReset} from '#/state/events'
import {s} from '#/lib/styles' import {s} from '#/lib/styles'
@@ -83,9 +87,7 @@ function EmptyState({message, error}: {message: string; error?: string}) {
}, },
]}> ]}>
<View style={[pal.viewLight, {padding: 18, borderRadius: 8}]}> <View style={[pal.viewLight, {padding: 18, borderRadius: 8}]}>
<Text style={[pal.text]}> <Text style={[pal.text]}>{message}</Text>
<Trans>{message}</Trans>
</Text>
{error && ( {error && (
<> <>
@@ -511,6 +513,11 @@ export function SearchScreen(
onPressCancelSearch() onPressCancelSearch()
}, [onPressCancelSearch]) }, [onPressCancelSearch])
const queryMaybeHandle = React.useMemo(() => {
const match = MATCH_HANDLE.exec(query)
return match && match[1]
}, [query])
useFocusEffect( useFocusEffect(
React.useCallback(() => { React.useCallback(() => {
setMinimalShellMode(false) setMinimalShellMode(false)
@@ -617,18 +624,31 @@ export function SearchScreen(
dataSet={{stableGutters: '1'}} dataSet={{stableGutters: '1'}}
keyboardShouldPersistTaps="handled" keyboardShouldPersistTaps="handled"
keyboardDismissMode="on-drag"> keyboardDismissMode="on-drag">
{searchResults.length ? ( <SearchLinkCard
searchResults.map((item, i) => ( label={_(msg`Search for "${query}"`)}
<SearchResultCard onPress={isNative ? onSubmit : undefined}
key={item.did} to={
profile={item} isNative
moderation={moderateProfile(item, moderationOpts)} ? undefined
style={i === 0 ? {borderTopWidth: 0} : {}} : `/search?q=${encodeURIComponent(query)}`
/> }
)) style={{borderBottomWidth: 1}}
) : ( />
<EmptyState message={_(msg`No results found for ${query}`)} />
)} {queryMaybeHandle ? (
<SearchLinkCard
label={_(msg`Go to @${queryMaybeHandle}`)}
to={`/profile/${queryMaybeHandle}`}
/>
) : null}
{searchResults.map(item => (
<SearchProfileCard
key={item.did}
profile={item}
moderation={moderateProfile(item, moderationOpts)}
/>
))}
<View style={{height: 200}} /> <View style={{height: 200}} />
</ScrollView> </ScrollView>

Some files were not shown because too many files have changed in this diff Show More