diff --git a/client-app/app/(app)/new-contact.tsx b/client-app/app/(app)/new-contact.tsx
index e2636eb..05a8b40 100644
--- a/client-app/app/(app)/new-contact.tsx
+++ b/client-app/app/(app)/new-contact.tsx
@@ -27,9 +27,9 @@ import { SearchBar } from '@/components/SearchBar';
const MIN_CONTACT_FEE = 5000;
const FEE_TIERS = [
- { value: 5_000, label: 'Min' },
- { value: 10_000, label: 'Standard' },
- { value: 50_000, label: 'Priority' },
+ { value: 5_000, label: 'Базовая' },
+ { value: 10_000, label: 'Стандарт' },
+ { value: 50_000, label: 'Приоритет' },
];
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} is not registered on this chain`); return; }
+ if (!addr) { setError(`@${name} не зарегистрирован в этой сети`); 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 ?? 'Lookup failed');
+ setError(e?.message ?? 'Не удалось найти пользователя');
} finally {
setSearching(false);
}
@@ -80,7 +80,7 @@ export default function NewContactScreen() {
async function sendRequest() {
if (!resolved || !keyFile) return;
if (balance < fee + 1000) {
- Alert.alert('Insufficient balance', `Need ${formatAmount(fee + 1000)} (fee + network).`);
+ Alert.alert('Недостаточно средств', `Нужно ${formatAmount(fee + 1000)} (плата + сетевая комиссия).`);
return;
}
setSending(true); setError(null);
@@ -94,8 +94,8 @@ export default function NewContactScreen() {
});
await submitTx(tx);
Alert.alert(
- 'Request sent',
- `A contact request has been sent to ${resolved.nickname ? '@' + resolved.nickname : shortAddr(resolved.address)}.`,
+ 'Запрос отправлен',
+ `Запрос на общение отправлен ${resolved.nickname ? '@' + resolved.nickname : shortAddr(resolved.address)}.`,
[{ text: 'OK', onPress: () => router.back() }],
);
} catch (e: any) {
@@ -112,7 +112,7 @@ export default function NewContactScreen() {
return (
router.back()} />}
/>
@@ -121,33 +121,56 @@ export default function NewContactScreen() {
keyboardShouldPersistTaps="handled"
showsVerticalScrollIndicator={false}
>
-
- Enter a @username, a
- hex pubkey or a DC… address.
-
-
{ setResolved(null); setError(null); }}
/>
- ({
- flexDirection: 'row', alignItems: 'center', justifyContent: 'center',
- paddingVertical: 11, borderRadius: 999, marginTop: 12,
- backgroundColor: !query.trim() || searching ? '#1a1a1a' : pressed ? '#1a8cd8' : '#1d9bf0',
- })}
- >
- {searching ? (
-
- ) : (
- Search
- )}
-
+ {query.trim().length > 0 && (
+ ({
+ flexDirection: 'row', alignItems: 'center', justifyContent: 'center',
+ paddingVertical: 11, borderRadius: 999, marginTop: 12,
+ backgroundColor: searching ? '#1a1a1a' : pressed ? '#1a8cd8' : '#1d9bf0',
+ })}
+ >
+ {searching ? (
+
+ ) : (
+ Найти
+ )}
+
+ )}
+
+ {/* Empty-state hint — показываем когда ничего не введено и нет результата */}
+ {query.trim().length === 0 && !resolved && (
+
+
+
+
+
+ Найдите собеседника
+
+
+ Введите @username,
+ если человек зарегистрировал ник, либо полный hex pubkey или DC… адрес.
+
+
+ )}
{error && (
- {resolved.x25519 ? 'E2E-ready' : 'Key not published yet'}
+ {resolved.x25519 ? 'E2E готов' : 'Ключ ещё не опубликован'}
@@ -204,12 +227,12 @@ export default function NewContactScreen() {
{/* Intro */}
- Intro (optional, plaintext on-chain)
+ Сообщение (опционально, видно в открытом виде на chain)
- Anti-spam fee (goes to recipient)
+ Плата за запрос (уходит получателю, anti-spam)
{FEE_TIERS.map(t => {
@@ -276,7 +299,7 @@ export default function NewContactScreen() {
) : (
- Send request · {formatAmount(fee + 1000)}
+ Отправить запрос · {formatAmount(fee + 1000)}
)}
diff --git a/client-app/app/(app)/requests.tsx b/client-app/app/(app)/requests.tsx
index b3f9f59..dad308f 100644
--- a/client-app/app/(app)/requests.tsx
+++ b/client-app/app/(app)/requests.tsx
@@ -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('Accept failed', humanizeTxError(e));
+ Alert.alert('Не удалось принять', humanizeTxError(e));
} finally {
setAccepting(null);
}
@@ -59,12 +59,12 @@ export default function RequestsScreen() {
function decline(req: ContactRequest) {
Alert.alert(
- 'Decline request',
- `Decline request from ${req.username ? '@' + req.username : shortAddr(req.from)}?`,
+ 'Отклонить запрос',
+ `Отклонить запрос от ${req.username ? '@' + req.username : shortAddr(req.from)}?`,
[
- { text: 'Cancel', style: 'cancel' },
+ { text: 'Отмена', style: 'cancel' },
{
- text: 'Decline',
+ text: 'Отклонить',
style: 'destructive',
onPress: () => setRequests(requests.filter(r => r.txHash !== req.txHash)),
},
@@ -91,7 +91,7 @@ export default function RequestsScreen() {
{name}
- wants to message you · {relativeTime(req.timestamp)}
+ хочет добавить вас в контакты · {relativeTime(req.timestamp)}
{req.intro ? (
) : (
- Accept
+ Принять
)}
- Decline
+ Отклонить
@@ -147,16 +147,16 @@ export default function RequestsScreen() {
return (
-
+
{requests.length === 0 ? (
- All caught up
+ Всё прочитано
- Contact requests and network events will appear here.
+ Запросы на общение и события сети появятся здесь.
) : (
diff --git a/client-app/components/SearchBar.tsx b/client-app/components/SearchBar.tsx
index 0c63e53..fe972c9 100644
--- a/client-app/components/SearchBar.tsx
+++ b/client-app/components/SearchBar.tsx
@@ -1,12 +1,11 @@
/**
- * SearchBar — серый блок, в состоянии idle текст с иконкой 🔍 отцентрированы.
- *
- * Когда пользователь тапает/фокусирует — поле становится input-friendly, но
- * визуально рестайл не нужен: при наличии текста placeholder скрыт и
- * пользовательский ввод выравнивается влево автоматически (multiline off).
+ * SearchBar — single-TextInput pill. Icon + input в одном ряду, без
+ * idle/focused двойного состояния (раньше был хак с невидимым
+ * TextInput поверх отцентрированного Text — ломал focus и выравнивание
+ * на Android).
*/
-import React, { useState } from 'react';
-import { View, TextInput, Text } from 'react-native';
+import React from 'react';
+import { View, TextInput, Pressable } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
export interface SearchBarProps {
@@ -15,73 +14,55 @@ export interface SearchBarProps {
placeholder?: string;
autoFocus?: boolean;
onSubmitEditing?: () => void;
+ onClear?: () => void;
}
export function SearchBar({
- value, onChangeText, placeholder = 'Search', autoFocus, onSubmitEditing,
+ value, onChangeText, placeholder = 'Поиск', autoFocus, onSubmitEditing, onClear,
}: SearchBarProps) {
- const [focused, setFocused] = useState(false);
-
- // Placeholder центрируется пока нет фокуса И нет значения.
- // Как только юзер фокусируется или начинает печатать — иконка+текст
- // прыгают к левому краю, чтобы не мешать вводу.
- const centered = !focused && !value;
-
return (
- {centered ? (
- // ── Idle state — только текст+icon, отцентрированы.
- // Невидимый TextInput поверх ловит tap, чтобы не дергать focus вручную.
-
-
- {placeholder}
- setFocused(true)}
- onSubmitEditing={onSubmitEditing}
- returnKeyType="search"
- style={{
- position: 'absolute', left: 0, right: 0, top: 0, bottom: 0,
- color: 'transparent',
- // Скрываем cursor в idle-режиме; при focus компонент перерисуется.
- }}
- />
-
- ) : (
-
-
- setFocused(true)}
- onBlur={() => setFocused(false)}
- onSubmitEditing={onSubmitEditing}
- returnKeyType="search"
- style={{
- flex: 1,
- color: '#ffffff',
- fontSize: 14,
- padding: 0,
- includeFontPadding: false,
- }}
- />
-
+
+
+ {value.length > 0 && (
+ {
+ onChangeText('');
+ onClear?.();
+ }}
+ hitSlop={8}
+ >
+
+
)}
);
diff --git a/client-app/lib/api.ts b/client-app/lib/api.ts
index d72b0aa..859af26 100644
--- a/client-app/lib/api.ts
+++ b/client-app/lib/api.ts
@@ -302,7 +302,7 @@ export async function fetchInbox(x25519PubHex: string): Promise {
// ─── Contact requests (on-chain) ─────────────────────────────────────────────
/**
- * Maps blockchain.ContactInfo returned by GET /api/relay/contacts?pub=...
+ * Maps blockchain.ContactInfo returned by GET /relay/contacts?pub=...
* The response shape is { pub, count, contacts: ContactInfo[] }.
*/
export interface ContactRequestRaw {
@@ -316,7 +316,7 @@ export interface ContactRequestRaw {
}
export async function fetchContactRequests(edPubHex: string): Promise {
- const data = await get<{ contacts: ContactRequestRaw[] }>(`/api/relay/contacts?pub=${edPubHex}`);
+ const data = await get<{ contacts: ContactRequestRaw[] }>(`/relay/contacts?pub=${edPubHex}`);
return data.contacts ?? [];
}