fix(client): contact-request endpoint path + search screen polish
1. Contact requests silently 404'd
fetchContactRequests hit /api/relay/contacts, but the server mounts
the whole /relay/* group at root (no /api prefix). Result: every
poll returned 404, the catch swallowed it, and the notifications
tab stayed empty even after the user sent themselves a CONTACT_
REQUEST on-chain. Fixed the client path to /relay/contacts — same
pattern as sendEnvelope / fetchInbox in the v1.0.x relay cleanup.
2. Search screen was half-finished
SearchBar used a dual-state hack (idle-centered Text overlaid with
an invisible TextInput) that broke focus + alignment on Android and
sometimes ate taps. Rewrote as a plain single-row pill: icon +
TextInput + optional clear button. Fewer moving parts, predictable
focus, proper placeholder styling.
new-contact.tsx cleaned up:
- Title "New chat" → "Поиск" (matches the NavBar tab label and the
rest of the Russian UI).
- All labels localised: "Accept/Decline", "Intro", "Anti-spam fee",
fee-tier names, error messages, final CTA.
- Proper empty-state hint when query is empty (icon + headline +
explanation of @username / hex / DC prefix) instead of just a
floating helper text.
- Search button hidden until user types something — the empty-
state stands alone, no dead grey button under it.
- onClear handler on SearchBar resets the resolved profile too.
requests.tsx localised: title, empty-state, Accept/Decline button
copy, confirmation Alert text.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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 (
|
||||
<View style={{ flex: 1, backgroundColor: '#000000', paddingTop: insets.top }}>
|
||||
<Header
|
||||
title="New chat"
|
||||
title="Поиск"
|
||||
divider={false}
|
||||
left={<IconButton icon="chevron-back" size={36} onPress={() => router.back()} />}
|
||||
/>
|
||||
@@ -121,33 +121,56 @@ export default function NewContactScreen() {
|
||||
keyboardShouldPersistTaps="handled"
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<Text style={{ color: '#8b8b8b', fontSize: 13, lineHeight: 19, marginBottom: 14 }}>
|
||||
Enter a <Text style={{ color: '#ffffff', fontWeight: '600' }}>@username</Text>, a
|
||||
hex pubkey or a <Text style={{ color: '#ffffff', fontWeight: '600' }}>DC…</Text> address.
|
||||
</Text>
|
||||
|
||||
<SearchBar
|
||||
value={query}
|
||||
onChangeText={setQuery}
|
||||
placeholder="@alice or hex / DC address"
|
||||
placeholder="@alice, hex pubkey или DC-адрес"
|
||||
onSubmitEditing={search}
|
||||
autoFocus
|
||||
onClear={() => { setResolved(null); setError(null); }}
|
||||
/>
|
||||
|
||||
<Pressable
|
||||
onPress={search}
|
||||
disabled={searching || !query.trim()}
|
||||
style={({ pressed }) => ({
|
||||
flexDirection: 'row', alignItems: 'center', justifyContent: 'center',
|
||||
paddingVertical: 11, borderRadius: 999, marginTop: 12,
|
||||
backgroundColor: !query.trim() || searching ? '#1a1a1a' : pressed ? '#1a8cd8' : '#1d9bf0',
|
||||
})}
|
||||
>
|
||||
{searching ? (
|
||||
<ActivityIndicator color="#ffffff" size="small" />
|
||||
) : (
|
||||
<Text style={{ color: '#ffffff', fontWeight: '700', fontSize: 14 }}>Search</Text>
|
||||
)}
|
||||
</Pressable>
|
||||
{query.trim().length > 0 && (
|
||||
<Pressable
|
||||
onPress={search}
|
||||
disabled={searching}
|
||||
style={({ pressed }) => ({
|
||||
flexDirection: 'row', alignItems: 'center', justifyContent: 'center',
|
||||
paddingVertical: 11, borderRadius: 999, marginTop: 12,
|
||||
backgroundColor: searching ? '#1a1a1a' : pressed ? '#1a8cd8' : '#1d9bf0',
|
||||
})}
|
||||
>
|
||||
{searching ? (
|
||||
<ActivityIndicator color="#ffffff" size="small" />
|
||||
) : (
|
||||
<Text style={{ color: '#ffffff', fontWeight: '700', fontSize: 14 }}>Найти</Text>
|
||||
)}
|
||||
</Pressable>
|
||||
)}
|
||||
|
||||
{/* Empty-state hint — показываем когда ничего не введено и нет результата */}
|
||||
{query.trim().length === 0 && !resolved && (
|
||||
<View style={{ marginTop: 28, alignItems: 'center', paddingHorizontal: 16 }}>
|
||||
<View
|
||||
style={{
|
||||
width: 56, height: 56, borderRadius: 16,
|
||||
backgroundColor: '#0a0a0a',
|
||||
borderWidth: 1, borderColor: '#1f1f1f',
|
||||
alignItems: 'center', justifyContent: 'center',
|
||||
marginBottom: 12,
|
||||
}}
|
||||
>
|
||||
<Ionicons name="person-add-outline" size={24} color="#6a6a6a" />
|
||||
</View>
|
||||
<Text style={{ color: '#ffffff', fontSize: 15, fontWeight: '700', marginBottom: 6 }}>
|
||||
Найдите собеседника
|
||||
</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> адрес.
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<View style={{
|
||||
@@ -195,7 +218,7 @@ export default function NewContactScreen() {
|
||||
color: resolved.x25519 ? '#3ba55d' : '#f0b35a',
|
||||
fontSize: 11, fontWeight: '500',
|
||||
}}>
|
||||
{resolved.x25519 ? 'E2E-ready' : 'Key not published yet'}
|
||||
{resolved.x25519 ? 'E2E готов' : 'Ключ ещё не опубликован'}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
@@ -204,12 +227,12 @@ export default function NewContactScreen() {
|
||||
|
||||
{/* Intro */}
|
||||
<Text style={{ color: '#8b8b8b', fontSize: 12, marginTop: 18, marginBottom: 6 }}>
|
||||
Intro (optional, plaintext on-chain)
|
||||
Сообщение (опционально, видно в открытом виде на chain)
|
||||
</Text>
|
||||
<TextInput
|
||||
value={intro}
|
||||
onChangeText={setIntro}
|
||||
placeholder="Hey, it's Jordan from the conference"
|
||||
placeholder="Привет! Это Влад со встречи в среду"
|
||||
placeholderTextColor="#5a5a5a"
|
||||
multiline
|
||||
maxLength={140}
|
||||
@@ -227,7 +250,7 @@ export default function NewContactScreen() {
|
||||
|
||||
{/* Fee tier */}
|
||||
<Text style={{ color: '#8b8b8b', fontSize: 12, marginTop: 14, marginBottom: 6 }}>
|
||||
Anti-spam fee (goes to recipient)
|
||||
Плата за запрос (уходит получателю, anti-spam)
|
||||
</Text>
|
||||
<View style={{ flexDirection: 'row', gap: 8 }}>
|
||||
{FEE_TIERS.map(t => {
|
||||
@@ -276,7 +299,7 @@ export default function NewContactScreen() {
|
||||
<ActivityIndicator color="#ffffff" size="small" />
|
||||
) : (
|
||||
<Text style={{ color: '#ffffff', fontWeight: '700', fontSize: 14 }}>
|
||||
Send request · {formatAmount(fee + 1000)}
|
||||
Отправить запрос · {formatAmount(fee + 1000)}
|
||||
</Text>
|
||||
)}
|
||||
</Pressable>
|
||||
|
||||
Reference in New Issue
Block a user