chore(client): translate all user-visible strings to English
Mixed-language UI was confusing — onboarding said "Why DChain / How it
works / Your keys" in English headings but feature descriptions and
CTAs were in Russian; compose's confirm dialog was Russian; feed tabs
were Russian; error messages in humanizeTxError were Russian.
Everything user-facing is now English.
Files touched (only string literals, not comments):
app/index.tsx onboarding slides + CTA buttons
app/(app)/compose.tsx composer alerts, header button, placeholder,
attachment-size hint
app/(app)/feed/index.tsx tab labels (Following/For you/Trending),
empty-state hints, retry button
app/(app)/feed/[id].tsx post detail header + stats rows (Views,
Likes, Size, Paid to publish, Hosted on,
Hashtags)
app/(app)/feed/tag/[tag].tsx empty-state copy
app/(app)/profile/[address].tsx Profile header, Follow/Following,
Edit, Open chat, Address, Copied, Encryption,
Added, Members, unknown-contact hint
app/(app)/new-contact.tsx Search title, placeholder, Search button,
empty-state hint, E2E-ready indicator,
Intro label + placeholder, fee-tier labels
(Min / Standard / Priority), Send request,
Insufficient-balance alert, Request-sent
alert
app/(app)/requests.tsx Notifications title, empty-state, Accept /
Decline buttons, decline-confirm alert,
"wants to add you" line
components/SearchBar.tsx default placeholder
components/feed/PostCard.tsx long-press menu (Delete post, confirm,
Actions / Cancel)
components/feed/ShareSheet.tsx sheet title, contact-search placeholder,
empty state, Select contacts / Send button,
plural helper rewritten for English
components/chat/PostRefCard.tsx "POST" ribbon, "photo" indicator
lib/api.ts humanizeTxError (rate-limit, clock skew,
bad signature, 400/5xx/network-error
messages)
lib/dates.ts dateBucket now returns Today/Yesterday/
"Jun 17, 2025"; month array switched to
English short forms
Code comments left in Russian intentionally — they're developer
context, not user-facing. This commit is purely display-string.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -98,11 +98,11 @@ export default function ComposeScreen() {
|
||||
const perm = await ImagePicker.requestMediaLibraryPermissionsAsync();
|
||||
if (!perm.granted) {
|
||||
Alert.alert(
|
||||
'Нужен доступ к фото',
|
||||
'Откройте настройки и разрешите доступ к галерее.',
|
||||
'Photo access required',
|
||||
'Please enable photo library access in Settings.',
|
||||
[
|
||||
{ text: 'Отмена' },
|
||||
{ text: 'Настройки', onPress: () => Linking.openSettings() },
|
||||
{ text: 'Cancel' },
|
||||
{ text: 'Settings', onPress: () => Linking.openSettings() },
|
||||
],
|
||||
);
|
||||
return;
|
||||
@@ -132,8 +132,8 @@ export default function ComposeScreen() {
|
||||
|
||||
if (bytes.length > MAX_POST_BYTES - 512) {
|
||||
Alert.alert(
|
||||
'Слишком большое',
|
||||
`Картинка ${Math.round(bytes.length / 1024)} KB — лимит ${MAX_POST_BYTES / 1024} KB. Попробуйте выбрать поменьше.`,
|
||||
'Image too large',
|
||||
`Image is ${Math.round(bytes.length / 1024)} KB but the limit is ${MAX_POST_BYTES / 1024} KB. Try picking a smaller one.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -147,7 +147,7 @@ export default function ComposeScreen() {
|
||||
height: manipulated.height,
|
||||
});
|
||||
} catch (e: any) {
|
||||
Alert.alert('Не удалось', String(e?.message ?? e));
|
||||
Alert.alert('Failed', String(e?.message ?? e));
|
||||
} finally {
|
||||
setPicking(false);
|
||||
}
|
||||
@@ -159,19 +159,19 @@ export default function ComposeScreen() {
|
||||
// Balance guard.
|
||||
if (balance !== null && balance < estimatedFee) {
|
||||
Alert.alert(
|
||||
'Недостаточно средств',
|
||||
`Нужно ${formatFee(estimatedFee)}, на балансе ${formatFee(balance)}.`,
|
||||
'Insufficient balance',
|
||||
`Need ${formatFee(estimatedFee)}, have ${formatFee(balance)}.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
Alert.alert(
|
||||
'Опубликовать пост?',
|
||||
`Цена: ${formatFee(estimatedFee)}\nРазмер: ${Math.round(totalBytes / 1024 * 10) / 10} KB`,
|
||||
'Publish post?',
|
||||
`Cost: ${formatFee(estimatedFee)}\nSize: ${Math.round(totalBytes / 1024 * 10) / 10} KB`,
|
||||
[
|
||||
{ text: 'Отмена', style: 'cancel' },
|
||||
{ text: 'Cancel', style: 'cancel' },
|
||||
{
|
||||
text: 'Опубликовать',
|
||||
text: 'Publish',
|
||||
onPress: async () => {
|
||||
setBusy(true);
|
||||
try {
|
||||
@@ -185,7 +185,7 @@ export default function ComposeScreen() {
|
||||
// Close composer and open the new post.
|
||||
router.replace(`/(app)/feed/${postID}` as never);
|
||||
} catch (e: any) {
|
||||
Alert.alert('Не удалось опубликовать', humanizeTxError(e));
|
||||
Alert.alert('Failed to publish', humanizeTxError(e));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
@@ -235,7 +235,7 @@ export default function ComposeScreen() {
|
||||
fontSize: 14,
|
||||
}}
|
||||
>
|
||||
Опубликовать
|
||||
Publish
|
||||
</Text>
|
||||
)}
|
||||
</Pressable>
|
||||
@@ -251,7 +251,7 @@ export default function ComposeScreen() {
|
||||
<TextInput
|
||||
value={content}
|
||||
onChangeText={setContent}
|
||||
placeholder="Что происходит?"
|
||||
placeholder="What's happening?"
|
||||
placeholderTextColor="#5a5a5a"
|
||||
multiline
|
||||
maxLength={MAX_CONTENT_LENGTH}
|
||||
@@ -328,7 +328,7 @@ export default function ComposeScreen() {
|
||||
</Pressable>
|
||||
</View>
|
||||
<Text style={{ color: '#6a6a6a', fontSize: 11, marginTop: 6 }}>
|
||||
{Math.round(attach.size / 1024)} KB · метаданные удалят на сервере
|
||||
{Math.round(attach.size / 1024)} KB · metadata stripped on server
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
@@ -79,7 +79,7 @@ export default function PostDetailScreen() {
|
||||
<Header
|
||||
divider
|
||||
left={<IconButton icon="chevron-back" size={36} onPress={() => router.back()} />}
|
||||
title="Пост"
|
||||
title="Post"
|
||||
/>
|
||||
|
||||
{loading ? (
|
||||
@@ -94,7 +94,7 @@ export default function PostDetailScreen() {
|
||||
<View style={{ padding: 24, alignItems: 'center' }}>
|
||||
<Ionicons name="trash-outline" size={32} color="#6a6a6a" />
|
||||
<Text style={{ color: '#8b8b8b', marginTop: 10 }}>
|
||||
Пост удалён или больше недоступен
|
||||
Post deleted or no longer available
|
||||
</Text>
|
||||
</View>
|
||||
) : (
|
||||
@@ -131,18 +131,18 @@ export default function PostDetailScreen() {
|
||||
textTransform: 'uppercase',
|
||||
marginBottom: 10,
|
||||
}}>
|
||||
Информация о посте
|
||||
Post details
|
||||
</Text>
|
||||
|
||||
<DetailRow label="Просмотров" value={formatCount(stats?.views ?? post.views)} />
|
||||
<DetailRow label="Лайков" value={formatCount(stats?.likes ?? post.likes)} />
|
||||
<DetailRow label="Размер" value={`${Math.round(post.size / 1024 * 10) / 10} KB`} />
|
||||
<DetailRow label="Views" value={formatCount(stats?.views ?? post.views)} />
|
||||
<DetailRow label="Likes" value={formatCount(stats?.likes ?? post.likes)} />
|
||||
<DetailRow label="Size" value={`${Math.round(post.size / 1024 * 10) / 10} KB`} />
|
||||
<DetailRow
|
||||
label="Стоимость публикации"
|
||||
label="Paid to publish"
|
||||
value={formatFee(1000 + post.size)}
|
||||
/>
|
||||
<DetailRow
|
||||
label="Хостинг"
|
||||
label="Hosted on"
|
||||
value={shortAddr(post.hosting_relay)}
|
||||
mono
|
||||
/>
|
||||
@@ -151,7 +151,7 @@ export default function PostDetailScreen() {
|
||||
<>
|
||||
<View style={{ height: 1, backgroundColor: '#1f1f1f', marginVertical: 10 }} />
|
||||
<Text style={{ color: '#5a5a5a', fontSize: 11, marginBottom: 6 }}>
|
||||
Хештеги
|
||||
Hashtags
|
||||
</Text>
|
||||
<View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 6 }}>
|
||||
{post.hashtags.map(tag => (
|
||||
|
||||
@@ -31,9 +31,9 @@ import {
|
||||
type TabKey = 'following' | 'foryou' | 'trending';
|
||||
|
||||
const TAB_LABELS: Record<TabKey, string> = {
|
||||
following: 'Подписки',
|
||||
foryou: 'Для вас',
|
||||
trending: 'В тренде',
|
||||
following: 'Following',
|
||||
foryou: 'For you',
|
||||
trending: 'Trending',
|
||||
};
|
||||
|
||||
export default function FeedScreen() {
|
||||
@@ -201,15 +201,15 @@ export default function FeedScreen() {
|
||||
|
||||
const emptyHint = useMemo(() => {
|
||||
switch (tab) {
|
||||
case 'following': return 'Подпишитесь на кого-нибудь, чтобы увидеть их посты здесь.';
|
||||
case 'foryou': return 'Пока нет рекомендаций — возвращайтесь позже.';
|
||||
case 'trending': return 'В этой ленте пока тихо.';
|
||||
case 'following': return 'Follow someone to see their posts here.';
|
||||
case 'foryou': return 'No recommendations yet — come back later.';
|
||||
case 'trending': return 'Nothing trending yet.';
|
||||
}
|
||||
}, [tab]);
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1, backgroundColor: '#000000', paddingTop: insets.top }}>
|
||||
<TabHeader title="Лента" />
|
||||
<TabHeader title="Feed" />
|
||||
|
||||
{/* Tab strip — три таба, равномерно распределены по ширине
|
||||
(justifyContent: space-between). Каждый Pressable hug'ает
|
||||
@@ -305,14 +305,14 @@ export default function FeedScreen() {
|
||||
) : error ? (
|
||||
<EmptyState
|
||||
icon="alert-circle-outline"
|
||||
title="Не удалось загрузить ленту"
|
||||
title="Couldn't load feed"
|
||||
subtitle={error}
|
||||
onRetry={() => loadPosts(false)}
|
||||
/>
|
||||
) : (
|
||||
<EmptyState
|
||||
icon="newspaper-outline"
|
||||
title="Здесь пока пусто"
|
||||
title="Nothing to show yet"
|
||||
subtitle={emptyHint}
|
||||
/>
|
||||
)
|
||||
@@ -409,7 +409,7 @@ function EmptyState({
|
||||
})}
|
||||
>
|
||||
<Text style={{ color: '#ffffff', fontWeight: '700', fontSize: 13 }}>
|
||||
Попробовать снова
|
||||
Try again
|
||||
</Text>
|
||||
</Pressable>
|
||||
)}
|
||||
|
||||
@@ -119,10 +119,10 @@ export default function HashtagScreen() {
|
||||
}}>
|
||||
<Ionicons name="pricetag-outline" size={32} color="#6a6a6a" />
|
||||
<Text style={{ color: '#ffffff', fontWeight: '700', marginTop: 10 }}>
|
||||
Пока нет постов с этим тегом
|
||||
No posts with this tag yet
|
||||
</Text>
|
||||
<Text style={{ color: '#8b8b8b', textAlign: 'center', fontSize: 13, marginTop: 6 }}>
|
||||
Будьте первым — напишите пост с #{tag}
|
||||
Be the first — write a post with #{tag}
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
|
||||
@@ -27,9 +27,9 @@ import { SearchBar } from '@/components/SearchBar';
|
||||
|
||||
const MIN_CONTACT_FEE = 5000;
|
||||
const FEE_TIERS = [
|
||||
{ value: 5_000, label: 'Базовая' },
|
||||
{ value: 10_000, label: 'Стандарт' },
|
||||
{ value: 50_000, label: 'Приоритет' },
|
||||
{ value: 5_000, label: 'Min' },
|
||||
{ value: 10_000, label: 'Standard' },
|
||||
{ value: 50_000, label: 'Priority' },
|
||||
];
|
||||
|
||||
interface Resolved {
|
||||
@@ -61,7 +61,7 @@ export default function NewContactScreen() {
|
||||
if (q.startsWith('@') || (!q.match(/^[0-9a-f]{64}$/i) && !q.startsWith('DC'))) {
|
||||
const name = q.replace('@', '');
|
||||
const addr = await resolveUsername(settings.contractId, name);
|
||||
if (!addr) { setError(`@${name} не зарегистрирован в этой сети`); return; }
|
||||
if (!addr) { setError(`@${name} is not registered on this chain`); return; }
|
||||
address = addr;
|
||||
}
|
||||
const identity = await getIdentity(address);
|
||||
@@ -71,7 +71,7 @@ export default function NewContactScreen() {
|
||||
x25519: identity?.x25519_pub || undefined,
|
||||
});
|
||||
} catch (e: any) {
|
||||
setError(e?.message ?? 'Не удалось найти пользователя');
|
||||
setError(e?.message ?? 'Lookup failed');
|
||||
} finally {
|
||||
setSearching(false);
|
||||
}
|
||||
@@ -80,7 +80,7 @@ export default function NewContactScreen() {
|
||||
async function sendRequest() {
|
||||
if (!resolved || !keyFile) return;
|
||||
if (balance < fee + 1000) {
|
||||
Alert.alert('Недостаточно средств', `Нужно ${formatAmount(fee + 1000)} (плата + сетевая комиссия).`);
|
||||
Alert.alert('Insufficient balance', `Need ${formatAmount(fee + 1000)} (request fee + network).`);
|
||||
return;
|
||||
}
|
||||
setSending(true); setError(null);
|
||||
@@ -94,8 +94,8 @@ export default function NewContactScreen() {
|
||||
});
|
||||
await submitTx(tx);
|
||||
Alert.alert(
|
||||
'Запрос отправлен',
|
||||
`Запрос на общение отправлен ${resolved.nickname ? '@' + resolved.nickname : shortAddr(resolved.address)}.`,
|
||||
'Request sent',
|
||||
`A contact request has been sent to ${resolved.nickname ? '@' + resolved.nickname : shortAddr(resolved.address)}.`,
|
||||
[{ text: 'OK', onPress: () => router.back() }],
|
||||
);
|
||||
} catch (e: any) {
|
||||
@@ -112,7 +112,7 @@ export default function NewContactScreen() {
|
||||
return (
|
||||
<View style={{ flex: 1, backgroundColor: '#000000', paddingTop: insets.top }}>
|
||||
<Header
|
||||
title="Поиск"
|
||||
title="Search"
|
||||
divider={false}
|
||||
left={<IconButton icon="chevron-back" size={36} onPress={() => router.back()} />}
|
||||
/>
|
||||
@@ -124,7 +124,7 @@ export default function NewContactScreen() {
|
||||
<SearchBar
|
||||
value={query}
|
||||
onChangeText={setQuery}
|
||||
placeholder="@alice, hex pubkey или DC-адрес"
|
||||
placeholder="@alice, hex pubkey or DC address"
|
||||
onSubmitEditing={search}
|
||||
autoFocus
|
||||
onClear={() => { setResolved(null); setError(null); }}
|
||||
@@ -143,7 +143,7 @@ export default function NewContactScreen() {
|
||||
{searching ? (
|
||||
<ActivityIndicator color="#ffffff" size="small" />
|
||||
) : (
|
||||
<Text style={{ color: '#ffffff', fontWeight: '700', fontSize: 14 }}>Найти</Text>
|
||||
<Text style={{ color: '#ffffff', fontWeight: '700', fontSize: 14 }}>Search</Text>
|
||||
)}
|
||||
</Pressable>
|
||||
)}
|
||||
@@ -163,11 +163,11 @@ export default function NewContactScreen() {
|
||||
<Ionicons name="person-add-outline" size={24} color="#6a6a6a" />
|
||||
</View>
|
||||
<Text style={{ color: '#ffffff', fontSize: 15, fontWeight: '700', marginBottom: 6 }}>
|
||||
Найдите собеседника
|
||||
Find someone to message
|
||||
</Text>
|
||||
<Text style={{ color: '#8b8b8b', fontSize: 13, textAlign: 'center', lineHeight: 19 }}>
|
||||
Введите <Text style={{ color: '#ffffff', fontWeight: '600' }}>@username</Text>,
|
||||
если человек зарегистрировал ник, либо полный hex pubkey или <Text style={{ color: '#ffffff', fontWeight: '600' }}>DC…</Text> адрес.
|
||||
Enter an <Text style={{ color: '#ffffff', fontWeight: '600' }}>@username</Text> if
|
||||
the person registered one, or paste a full hex pubkey or <Text style={{ color: '#ffffff', fontWeight: '600' }}>DC…</Text> address.
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
@@ -218,7 +218,7 @@ export default function NewContactScreen() {
|
||||
color: resolved.x25519 ? '#3ba55d' : '#f0b35a',
|
||||
fontSize: 11, fontWeight: '500',
|
||||
}}>
|
||||
{resolved.x25519 ? 'E2E готов' : 'Ключ ещё не опубликован'}
|
||||
{resolved.x25519 ? 'E2E ready' : 'Key not published yet'}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
@@ -227,12 +227,12 @@ export default function NewContactScreen() {
|
||||
|
||||
{/* Intro */}
|
||||
<Text style={{ color: '#8b8b8b', fontSize: 12, marginTop: 18, marginBottom: 6 }}>
|
||||
Сообщение (опционально, видно в открытом виде на chain)
|
||||
Intro (optional, plaintext on-chain)
|
||||
</Text>
|
||||
<TextInput
|
||||
value={intro}
|
||||
onChangeText={setIntro}
|
||||
placeholder="Привет! Это Влад со встречи в среду"
|
||||
placeholder="Hey, it's Jordan from the conference"
|
||||
placeholderTextColor="#5a5a5a"
|
||||
multiline
|
||||
maxLength={140}
|
||||
@@ -250,7 +250,7 @@ export default function NewContactScreen() {
|
||||
|
||||
{/* Fee tier */}
|
||||
<Text style={{ color: '#8b8b8b', fontSize: 12, marginTop: 14, marginBottom: 6 }}>
|
||||
Плата за запрос (уходит получателю, anti-spam)
|
||||
Anti-spam fee (goes to the recipient)
|
||||
</Text>
|
||||
{/* Fee-tier pills.
|
||||
Layout (background, border, padding) lives on a static
|
||||
@@ -314,7 +314,7 @@ export default function NewContactScreen() {
|
||||
<ActivityIndicator color="#ffffff" size="small" />
|
||||
) : (
|
||||
<Text style={{ color: '#ffffff', fontWeight: '700', fontSize: 14 }}>
|
||||
Отправить запрос · {formatAmount(fee + 1000)}
|
||||
Send request · {formatAmount(fee + 1000)}
|
||||
</Text>
|
||||
)}
|
||||
</Pressable>
|
||||
|
||||
@@ -48,7 +48,7 @@ export default function ProfileScreen() {
|
||||
const isMe = !!keyFile && keyFile.pub_key === address;
|
||||
const displayName = contact?.username
|
||||
? `@${contact.username}`
|
||||
: contact?.alias ?? (isMe ? 'Вы' : shortAddr(address ?? '', 6));
|
||||
: contact?.alias ?? (isMe ? 'You' : shortAddr(address ?? '', 6));
|
||||
|
||||
const copyAddress = async () => {
|
||||
if (!address) return;
|
||||
@@ -85,7 +85,7 @@ export default function ProfileScreen() {
|
||||
return (
|
||||
<View style={{ flex: 1, backgroundColor: '#000000', paddingTop: insets.top }}>
|
||||
<Header
|
||||
title="Профиль"
|
||||
title="Profile"
|
||||
divider
|
||||
left={<IconButton icon="chevron-back" size={36} onPress={() => router.back()} />}
|
||||
/>
|
||||
@@ -124,7 +124,7 @@ export default function ProfileScreen() {
|
||||
fontSize: 13,
|
||||
}}
|
||||
>
|
||||
{following ? 'Вы подписаны' : 'Подписаться'}
|
||||
{following ? 'Following' : 'Follow'}
|
||||
</Text>
|
||||
)}
|
||||
</Pressable>
|
||||
@@ -139,7 +139,7 @@ export default function ProfileScreen() {
|
||||
})}
|
||||
>
|
||||
<Text style={{ color: '#ffffff', fontWeight: '700', fontSize: 13 }}>
|
||||
Редактировать
|
||||
Edit
|
||||
</Text>
|
||||
</Pressable>
|
||||
)}
|
||||
@@ -177,7 +177,7 @@ export default function ProfileScreen() {
|
||||
>
|
||||
<Ionicons name="chatbubble-outline" size={15} color="#ffffff" />
|
||||
<Text style={{ color: '#ffffff', fontWeight: '600', fontSize: 14 }}>
|
||||
Открыть чат
|
||||
Open chat
|
||||
</Text>
|
||||
</Pressable>
|
||||
)}
|
||||
@@ -203,7 +203,7 @@ export default function ProfileScreen() {
|
||||
})}
|
||||
>
|
||||
<Text style={{ color: '#8b8b8b', fontSize: 13, flex: 1 }}>
|
||||
Адрес
|
||||
Address
|
||||
</Text>
|
||||
<Text
|
||||
style={{
|
||||
@@ -214,7 +214,7 @@ export default function ProfileScreen() {
|
||||
}}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{copied ? 'Скопировано' : shortAddr(address ?? '')}
|
||||
{copied ? 'Copied' : shortAddr(address ?? '')}
|
||||
</Text>
|
||||
<Ionicons
|
||||
name={copied ? 'checkmark' : 'copy-outline'}
|
||||
@@ -229,17 +229,17 @@ export default function ProfileScreen() {
|
||||
<>
|
||||
<Divider />
|
||||
<InfoRow
|
||||
label="Шифрование"
|
||||
label="Encryption"
|
||||
value={contact.x25519Pub
|
||||
? 'end-to-end (NaCl)'
|
||||
: 'ключ ещё не опубликован'}
|
||||
: 'key not published yet'}
|
||||
danger={!contact.x25519Pub}
|
||||
icon={contact.x25519Pub ? 'lock-closed' : 'lock-open'}
|
||||
/>
|
||||
|
||||
<Divider />
|
||||
<InfoRow
|
||||
label="Добавлен"
|
||||
label="Added"
|
||||
value={new Date(contact.addedAt).toLocaleDateString()}
|
||||
icon="calendar-outline"
|
||||
/>
|
||||
@@ -251,7 +251,7 @@ export default function ProfileScreen() {
|
||||
<>
|
||||
<Divider />
|
||||
<InfoRow
|
||||
label="Участников"
|
||||
label="Members"
|
||||
value="—"
|
||||
icon="people-outline"
|
||||
/>
|
||||
@@ -270,7 +270,7 @@ export default function ProfileScreen() {
|
||||
paddingHorizontal: 24,
|
||||
lineHeight: 17,
|
||||
}}>
|
||||
Этот пользователь пока не в ваших контактах. Нажмите «Подписаться», чтобы видеть его посты в ленте, или добавьте в чаты через @username.
|
||||
This user isn't in your contacts yet. Tap "Follow" to see their posts in your feed, or add them as a chat contact via @username.
|
||||
</Text>
|
||||
)}
|
||||
</ScrollView>
|
||||
|
||||
@@ -51,7 +51,7 @@ export default function RequestsScreen() {
|
||||
setRequests(requests.filter(r => r.txHash !== req.txHash));
|
||||
router.replace(`/(app)/chats/${req.from}` as never);
|
||||
} catch (e: any) {
|
||||
Alert.alert('Не удалось принять', humanizeTxError(e));
|
||||
Alert.alert('Accept failed', humanizeTxError(e));
|
||||
} finally {
|
||||
setAccepting(null);
|
||||
}
|
||||
@@ -59,12 +59,12 @@ export default function RequestsScreen() {
|
||||
|
||||
function decline(req: ContactRequest) {
|
||||
Alert.alert(
|
||||
'Отклонить запрос',
|
||||
`Отклонить запрос от ${req.username ? '@' + req.username : shortAddr(req.from)}?`,
|
||||
'Decline request',
|
||||
`Decline request from ${req.username ? '@' + req.username : shortAddr(req.from)}?`,
|
||||
[
|
||||
{ text: 'Отмена', style: 'cancel' },
|
||||
{ text: 'Cancel', style: 'cancel' },
|
||||
{
|
||||
text: 'Отклонить',
|
||||
text: 'Decline',
|
||||
style: 'destructive',
|
||||
onPress: () => setRequests(requests.filter(r => r.txHash !== req.txHash)),
|
||||
},
|
||||
@@ -91,7 +91,7 @@ export default function RequestsScreen() {
|
||||
{name}
|
||||
</Text>
|
||||
<Text style={{ color: '#8b8b8b', fontSize: 12, marginTop: 2 }}>
|
||||
хочет добавить вас в контакты · {relativeTime(req.timestamp)}
|
||||
wants to add you as a contact · {relativeTime(req.timestamp)}
|
||||
</Text>
|
||||
{req.intro ? (
|
||||
<Text
|
||||
@@ -123,7 +123,7 @@ export default function RequestsScreen() {
|
||||
{isAccepting ? (
|
||||
<ActivityIndicator size="small" color="#ffffff" />
|
||||
) : (
|
||||
<Text style={{ color: '#ffffff', fontWeight: '700', fontSize: 13 }}>Принять</Text>
|
||||
<Text style={{ color: '#ffffff', fontWeight: '700', fontSize: 13 }}>Accept</Text>
|
||||
)}
|
||||
</Pressable>
|
||||
<Pressable
|
||||
@@ -137,7 +137,7 @@ export default function RequestsScreen() {
|
||||
borderWidth: 1, borderColor: '#1f1f1f',
|
||||
})}
|
||||
>
|
||||
<Text style={{ color: '#ffffff', fontWeight: '600', fontSize: 13 }}>Отклонить</Text>
|
||||
<Text style={{ color: '#ffffff', fontWeight: '600', fontSize: 13 }}>Decline</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
</View>
|
||||
@@ -147,16 +147,16 @@ export default function RequestsScreen() {
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1, backgroundColor: '#000000', paddingTop: insets.top }}>
|
||||
<TabHeader title="Уведомления" />
|
||||
<TabHeader title="Notifications" />
|
||||
|
||||
{requests.length === 0 ? (
|
||||
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center', paddingHorizontal: 32 }}>
|
||||
<Ionicons name="notifications-outline" size={42} color="#3a3a3a" />
|
||||
<Text style={{ color: '#ffffff', fontSize: 16, fontWeight: '700', marginTop: 10 }}>
|
||||
Всё прочитано
|
||||
All caught up
|
||||
</Text>
|
||||
<Text style={{ color: '#8b8b8b', fontSize: 13, textAlign: 'center', marginTop: 6, lineHeight: 19 }}>
|
||||
Запросы на общение и события сети появятся здесь.
|
||||
Contact requests and network events will appear here.
|
||||
</Text>
|
||||
</View>
|
||||
) : (
|
||||
|
||||
Reference in New Issue
Block a user