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 MIN_CONTACT_FEE = 5000;
|
||||||
const FEE_TIERS = [
|
const FEE_TIERS = [
|
||||||
{ value: 5_000, label: 'Min' },
|
{ value: 5_000, label: 'Базовая' },
|
||||||
{ value: 10_000, label: 'Standard' },
|
{ value: 10_000, label: 'Стандарт' },
|
||||||
{ value: 50_000, label: 'Priority' },
|
{ value: 50_000, label: 'Приоритет' },
|
||||||
];
|
];
|
||||||
|
|
||||||
interface Resolved {
|
interface Resolved {
|
||||||
@@ -61,7 +61,7 @@ export default function NewContactScreen() {
|
|||||||
if (q.startsWith('@') || (!q.match(/^[0-9a-f]{64}$/i) && !q.startsWith('DC'))) {
|
if (q.startsWith('@') || (!q.match(/^[0-9a-f]{64}$/i) && !q.startsWith('DC'))) {
|
||||||
const name = q.replace('@', '');
|
const name = q.replace('@', '');
|
||||||
const addr = await resolveUsername(settings.contractId, name);
|
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;
|
address = addr;
|
||||||
}
|
}
|
||||||
const identity = await getIdentity(address);
|
const identity = await getIdentity(address);
|
||||||
@@ -71,7 +71,7 @@ export default function NewContactScreen() {
|
|||||||
x25519: identity?.x25519_pub || undefined,
|
x25519: identity?.x25519_pub || undefined,
|
||||||
});
|
});
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
setError(e?.message ?? 'Lookup failed');
|
setError(e?.message ?? 'Не удалось найти пользователя');
|
||||||
} finally {
|
} finally {
|
||||||
setSearching(false);
|
setSearching(false);
|
||||||
}
|
}
|
||||||
@@ -80,7 +80,7 @@ export default function NewContactScreen() {
|
|||||||
async function sendRequest() {
|
async function sendRequest() {
|
||||||
if (!resolved || !keyFile) return;
|
if (!resolved || !keyFile) return;
|
||||||
if (balance < fee + 1000) {
|
if (balance < fee + 1000) {
|
||||||
Alert.alert('Insufficient balance', `Need ${formatAmount(fee + 1000)} (fee + network).`);
|
Alert.alert('Недостаточно средств', `Нужно ${formatAmount(fee + 1000)} (плата + сетевая комиссия).`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setSending(true); setError(null);
|
setSending(true); setError(null);
|
||||||
@@ -94,8 +94,8 @@ export default function NewContactScreen() {
|
|||||||
});
|
});
|
||||||
await submitTx(tx);
|
await submitTx(tx);
|
||||||
Alert.alert(
|
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() }],
|
[{ text: 'OK', onPress: () => router.back() }],
|
||||||
);
|
);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
@@ -112,7 +112,7 @@ export default function NewContactScreen() {
|
|||||||
return (
|
return (
|
||||||
<View style={{ flex: 1, backgroundColor: '#000000', paddingTop: insets.top }}>
|
<View style={{ flex: 1, backgroundColor: '#000000', paddingTop: insets.top }}>
|
||||||
<Header
|
<Header
|
||||||
title="New chat"
|
title="Поиск"
|
||||||
divider={false}
|
divider={false}
|
||||||
left={<IconButton icon="chevron-back" size={36} onPress={() => router.back()} />}
|
left={<IconButton icon="chevron-back" size={36} onPress={() => router.back()} />}
|
||||||
/>
|
/>
|
||||||
@@ -121,33 +121,56 @@ export default function NewContactScreen() {
|
|||||||
keyboardShouldPersistTaps="handled"
|
keyboardShouldPersistTaps="handled"
|
||||||
showsVerticalScrollIndicator={false}
|
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
|
<SearchBar
|
||||||
value={query}
|
value={query}
|
||||||
onChangeText={setQuery}
|
onChangeText={setQuery}
|
||||||
placeholder="@alice or hex / DC address"
|
placeholder="@alice, hex pubkey или DC-адрес"
|
||||||
onSubmitEditing={search}
|
onSubmitEditing={search}
|
||||||
|
autoFocus
|
||||||
|
onClear={() => { setResolved(null); setError(null); }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Pressable
|
{query.trim().length > 0 && (
|
||||||
onPress={search}
|
<Pressable
|
||||||
disabled={searching || !query.trim()}
|
onPress={search}
|
||||||
style={({ pressed }) => ({
|
disabled={searching}
|
||||||
flexDirection: 'row', alignItems: 'center', justifyContent: 'center',
|
style={({ pressed }) => ({
|
||||||
paddingVertical: 11, borderRadius: 999, marginTop: 12,
|
flexDirection: 'row', alignItems: 'center', justifyContent: 'center',
|
||||||
backgroundColor: !query.trim() || searching ? '#1a1a1a' : pressed ? '#1a8cd8' : '#1d9bf0',
|
paddingVertical: 11, borderRadius: 999, marginTop: 12,
|
||||||
})}
|
backgroundColor: searching ? '#1a1a1a' : pressed ? '#1a8cd8' : '#1d9bf0',
|
||||||
>
|
})}
|
||||||
{searching ? (
|
>
|
||||||
<ActivityIndicator color="#ffffff" size="small" />
|
{searching ? (
|
||||||
) : (
|
<ActivityIndicator color="#ffffff" size="small" />
|
||||||
<Text style={{ color: '#ffffff', fontWeight: '700', fontSize: 14 }}>Search</Text>
|
) : (
|
||||||
)}
|
<Text style={{ color: '#ffffff', fontWeight: '700', fontSize: 14 }}>Найти</Text>
|
||||||
</Pressable>
|
)}
|
||||||
|
</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 && (
|
{error && (
|
||||||
<View style={{
|
<View style={{
|
||||||
@@ -195,7 +218,7 @@ export default function NewContactScreen() {
|
|||||||
color: resolved.x25519 ? '#3ba55d' : '#f0b35a',
|
color: resolved.x25519 ? '#3ba55d' : '#f0b35a',
|
||||||
fontSize: 11, fontWeight: '500',
|
fontSize: 11, fontWeight: '500',
|
||||||
}}>
|
}}>
|
||||||
{resolved.x25519 ? 'E2E-ready' : 'Key not published yet'}
|
{resolved.x25519 ? 'E2E готов' : 'Ключ ещё не опубликован'}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@@ -204,12 +227,12 @@ export default function NewContactScreen() {
|
|||||||
|
|
||||||
{/* Intro */}
|
{/* Intro */}
|
||||||
<Text style={{ color: '#8b8b8b', fontSize: 12, marginTop: 18, marginBottom: 6 }}>
|
<Text style={{ color: '#8b8b8b', fontSize: 12, marginTop: 18, marginBottom: 6 }}>
|
||||||
Intro (optional, plaintext on-chain)
|
Сообщение (опционально, видно в открытом виде на chain)
|
||||||
</Text>
|
</Text>
|
||||||
<TextInput
|
<TextInput
|
||||||
value={intro}
|
value={intro}
|
||||||
onChangeText={setIntro}
|
onChangeText={setIntro}
|
||||||
placeholder="Hey, it's Jordan from the conference"
|
placeholder="Привет! Это Влад со встречи в среду"
|
||||||
placeholderTextColor="#5a5a5a"
|
placeholderTextColor="#5a5a5a"
|
||||||
multiline
|
multiline
|
||||||
maxLength={140}
|
maxLength={140}
|
||||||
@@ -227,7 +250,7 @@ export default function NewContactScreen() {
|
|||||||
|
|
||||||
{/* Fee tier */}
|
{/* Fee tier */}
|
||||||
<Text style={{ color: '#8b8b8b', fontSize: 12, marginTop: 14, marginBottom: 6 }}>
|
<Text style={{ color: '#8b8b8b', fontSize: 12, marginTop: 14, marginBottom: 6 }}>
|
||||||
Anti-spam fee (goes to recipient)
|
Плата за запрос (уходит получателю, anti-spam)
|
||||||
</Text>
|
</Text>
|
||||||
<View style={{ flexDirection: 'row', gap: 8 }}>
|
<View style={{ flexDirection: 'row', gap: 8 }}>
|
||||||
{FEE_TIERS.map(t => {
|
{FEE_TIERS.map(t => {
|
||||||
@@ -276,7 +299,7 @@ export default function NewContactScreen() {
|
|||||||
<ActivityIndicator color="#ffffff" size="small" />
|
<ActivityIndicator color="#ffffff" size="small" />
|
||||||
) : (
|
) : (
|
||||||
<Text style={{ color: '#ffffff', fontWeight: '700', fontSize: 14 }}>
|
<Text style={{ color: '#ffffff', fontWeight: '700', fontSize: 14 }}>
|
||||||
Send request · {formatAmount(fee + 1000)}
|
Отправить запрос · {formatAmount(fee + 1000)}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</Pressable>
|
</Pressable>
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export default function RequestsScreen() {
|
|||||||
setRequests(requests.filter(r => r.txHash !== req.txHash));
|
setRequests(requests.filter(r => r.txHash !== req.txHash));
|
||||||
router.replace(`/(app)/chats/${req.from}` as never);
|
router.replace(`/(app)/chats/${req.from}` as never);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
Alert.alert('Accept failed', humanizeTxError(e));
|
Alert.alert('Не удалось принять', humanizeTxError(e));
|
||||||
} finally {
|
} finally {
|
||||||
setAccepting(null);
|
setAccepting(null);
|
||||||
}
|
}
|
||||||
@@ -59,12 +59,12 @@ export default function RequestsScreen() {
|
|||||||
|
|
||||||
function decline(req: ContactRequest) {
|
function decline(req: ContactRequest) {
|
||||||
Alert.alert(
|
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',
|
style: 'destructive',
|
||||||
onPress: () => setRequests(requests.filter(r => r.txHash !== req.txHash)),
|
onPress: () => setRequests(requests.filter(r => r.txHash !== req.txHash)),
|
||||||
},
|
},
|
||||||
@@ -91,7 +91,7 @@ export default function RequestsScreen() {
|
|||||||
{name}
|
{name}
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={{ color: '#8b8b8b', fontSize: 12, marginTop: 2 }}>
|
<Text style={{ color: '#8b8b8b', fontSize: 12, marginTop: 2 }}>
|
||||||
wants to message you · {relativeTime(req.timestamp)}
|
хочет добавить вас в контакты · {relativeTime(req.timestamp)}
|
||||||
</Text>
|
</Text>
|
||||||
{req.intro ? (
|
{req.intro ? (
|
||||||
<Text
|
<Text
|
||||||
@@ -123,7 +123,7 @@ export default function RequestsScreen() {
|
|||||||
{isAccepting ? (
|
{isAccepting ? (
|
||||||
<ActivityIndicator size="small" color="#ffffff" />
|
<ActivityIndicator size="small" color="#ffffff" />
|
||||||
) : (
|
) : (
|
||||||
<Text style={{ color: '#ffffff', fontWeight: '700', fontSize: 13 }}>Accept</Text>
|
<Text style={{ color: '#ffffff', fontWeight: '700', fontSize: 13 }}>Принять</Text>
|
||||||
)}
|
)}
|
||||||
</Pressable>
|
</Pressable>
|
||||||
<Pressable
|
<Pressable
|
||||||
@@ -137,7 +137,7 @@ export default function RequestsScreen() {
|
|||||||
borderWidth: 1, borderColor: '#1f1f1f',
|
borderWidth: 1, borderColor: '#1f1f1f',
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<Text style={{ color: '#ffffff', fontWeight: '600', fontSize: 13 }}>Decline</Text>
|
<Text style={{ color: '#ffffff', fontWeight: '600', fontSize: 13 }}>Отклонить</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@@ -147,16 +147,16 @@ export default function RequestsScreen() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={{ flex: 1, backgroundColor: '#000000', paddingTop: insets.top }}>
|
<View style={{ flex: 1, backgroundColor: '#000000', paddingTop: insets.top }}>
|
||||||
<TabHeader title="Notifications" />
|
<TabHeader title="Уведомления" />
|
||||||
|
|
||||||
{requests.length === 0 ? (
|
{requests.length === 0 ? (
|
||||||
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center', paddingHorizontal: 32 }}>
|
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center', paddingHorizontal: 32 }}>
|
||||||
<Ionicons name="notifications-outline" size={42} color="#3a3a3a" />
|
<Ionicons name="notifications-outline" size={42} color="#3a3a3a" />
|
||||||
<Text style={{ color: '#ffffff', fontSize: 16, fontWeight: '700', marginTop: 10 }}>
|
<Text style={{ color: '#ffffff', fontSize: 16, fontWeight: '700', marginTop: 10 }}>
|
||||||
All caught up
|
Всё прочитано
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={{ color: '#8b8b8b', fontSize: 13, textAlign: 'center', marginTop: 6, lineHeight: 19 }}>
|
<Text style={{ color: '#8b8b8b', fontSize: 13, textAlign: 'center', marginTop: 6, lineHeight: 19 }}>
|
||||||
Contact requests and network events will appear here.
|
Запросы на общение и события сети появятся здесь.
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
/**
|
/**
|
||||||
* SearchBar — серый блок, в состоянии idle текст с иконкой 🔍 отцентрированы.
|
* SearchBar — single-TextInput pill. Icon + input в одном ряду, без
|
||||||
*
|
* idle/focused двойного состояния (раньше был хак с невидимым
|
||||||
* Когда пользователь тапает/фокусирует — поле становится input-friendly, но
|
* TextInput поверх отцентрированного Text — ломал focus и выравнивание
|
||||||
* визуально рестайл не нужен: при наличии текста placeholder скрыт и
|
* на Android).
|
||||||
* пользовательский ввод выравнивается влево автоматически (multiline off).
|
|
||||||
*/
|
*/
|
||||||
import React, { useState } from 'react';
|
import React from 'react';
|
||||||
import { View, TextInput, Text } from 'react-native';
|
import { View, TextInput, Pressable } from 'react-native';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
|
|
||||||
export interface SearchBarProps {
|
export interface SearchBarProps {
|
||||||
@@ -15,73 +14,55 @@ export interface SearchBarProps {
|
|||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
autoFocus?: boolean;
|
autoFocus?: boolean;
|
||||||
onSubmitEditing?: () => void;
|
onSubmitEditing?: () => void;
|
||||||
|
onClear?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SearchBar({
|
export function SearchBar({
|
||||||
value, onChangeText, placeholder = 'Search', autoFocus, onSubmitEditing,
|
value, onChangeText, placeholder = 'Поиск', autoFocus, onSubmitEditing, onClear,
|
||||||
}: SearchBarProps) {
|
}: SearchBarProps) {
|
||||||
const [focused, setFocused] = useState(false);
|
|
||||||
|
|
||||||
// Placeholder центрируется пока нет фокуса И нет значения.
|
|
||||||
// Как только юзер фокусируется или начинает печатать — иконка+текст
|
|
||||||
// прыгают к левому краю, чтобы не мешать вводу.
|
|
||||||
const centered = !focused && !value;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: '#1a1a1a',
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
backgroundColor: '#111111',
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: '#1f1f1f',
|
||||||
borderRadius: 999,
|
borderRadius: 999,
|
||||||
paddingHorizontal: 14,
|
paddingHorizontal: 14,
|
||||||
paddingVertical: 9,
|
gap: 8,
|
||||||
minHeight: 36,
|
|
||||||
justifyContent: 'center',
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{centered ? (
|
<Ionicons name="search" size={16} color="#6a6a6a" />
|
||||||
// ── Idle state — только текст+icon, отцентрированы.
|
<TextInput
|
||||||
// Невидимый TextInput поверх ловит tap, чтобы не дергать focus вручную.
|
value={value}
|
||||||
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'center' }}>
|
onChangeText={onChangeText}
|
||||||
<Ionicons name="search" size={14} color="#8b8b8b" style={{ marginRight: 6 }} />
|
placeholder={placeholder}
|
||||||
<Text style={{ color: '#8b8b8b', fontSize: 14 }}>{placeholder}</Text>
|
placeholderTextColor="#5a5a5a"
|
||||||
<TextInput
|
autoCapitalize="none"
|
||||||
value={value}
|
autoCorrect={false}
|
||||||
onChangeText={onChangeText}
|
autoFocus={autoFocus}
|
||||||
autoFocus={autoFocus}
|
onSubmitEditing={onSubmitEditing}
|
||||||
onFocus={() => setFocused(true)}
|
returnKeyType="search"
|
||||||
onSubmitEditing={onSubmitEditing}
|
style={{
|
||||||
returnKeyType="search"
|
flex: 1,
|
||||||
style={{
|
color: '#ffffff',
|
||||||
position: 'absolute', left: 0, right: 0, top: 0, bottom: 0,
|
fontSize: 14,
|
||||||
color: 'transparent',
|
paddingVertical: 10,
|
||||||
// Скрываем cursor в idle-режиме; при focus компонент перерисуется.
|
padding: 0,
|
||||||
}}
|
includeFontPadding: false,
|
||||||
/>
|
}}
|
||||||
</View>
|
/>
|
||||||
) : (
|
{value.length > 0 && (
|
||||||
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
<Pressable
|
||||||
<Ionicons name="search" size={14} color="#8b8b8b" style={{ marginRight: 8 }} />
|
onPress={() => {
|
||||||
<TextInput
|
onChangeText('');
|
||||||
value={value}
|
onClear?.();
|
||||||
onChangeText={onChangeText}
|
}}
|
||||||
placeholder={placeholder}
|
hitSlop={8}
|
||||||
placeholderTextColor="#8b8b8b"
|
>
|
||||||
autoCapitalize="none"
|
<Ionicons name="close-circle" size={16} color="#6a6a6a" />
|
||||||
autoCorrect={false}
|
</Pressable>
|
||||||
autoFocus
|
|
||||||
onFocus={() => setFocused(true)}
|
|
||||||
onBlur={() => setFocused(false)}
|
|
||||||
onSubmitEditing={onSubmitEditing}
|
|
||||||
returnKeyType="search"
|
|
||||||
style={{
|
|
||||||
flex: 1,
|
|
||||||
color: '#ffffff',
|
|
||||||
fontSize: 14,
|
|
||||||
padding: 0,
|
|
||||||
includeFontPadding: false,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -302,7 +302,7 @@ export async function fetchInbox(x25519PubHex: string): Promise<Envelope[]> {
|
|||||||
// ─── Contact requests (on-chain) ─────────────────────────────────────────────
|
// ─── 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[] }.
|
* The response shape is { pub, count, contacts: ContactInfo[] }.
|
||||||
*/
|
*/
|
||||||
export interface ContactRequestRaw {
|
export interface ContactRequestRaw {
|
||||||
@@ -316,7 +316,7 @@ export interface ContactRequestRaw {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchContactRequests(edPubHex: string): Promise<ContactRequestRaw[]> {
|
export async function fetchContactRequests(edPubHex: string): Promise<ContactRequestRaw[]> {
|
||||||
const data = await get<{ contacts: ContactRequestRaw[] }>(`/api/relay/contacts?pub=${edPubHex}`);
|
const data = await get<{ contacts: ContactRequestRaw[] }>(`/relay/contacts?pub=${edPubHex}`);
|
||||||
return data.contacts ?? [];
|
return data.contacts ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user