chore(release): clean up repo for v0.0.1 release
Excluded from release bundle:
- CONTEXT.md, CHANGELOG.md (agent/project working notes)
- client-app/ (React Native messenger — tracked separately)
- contracts/hello_go/ (unused standalone example)
Kept contracts/counter/ and contracts/name_registry/ as vm-test fixtures
(referenced by vm/vm_test.go; NOT production contracts).
Docs refactor:
- docs/README.md — new top-level index with cross-references
- docs/quickstart.md — rewrite around single-node as primary path
- docs/node/README.md — full rewrite, all CLI flags, schema table
- docs/api/README.md — add /api/well-known-version, /api/update-check
- docs/contracts/README.md — split native (Go) vs WASM (user-deployable)
- docs/update-system.md — new, full 5-layer update system design
- README.md — link into docs/, drop CHANGELOG/client-app references
Build-time version system (inherited from earlier commits this branch):
- node --version / client --version with ldflags-injected metadata
- /api/well-known-version with {build, protocol_version, features[]}
- Peer-version gossip on dchain/version/v1
- /api/update-check against Gitea release API
- deploy/single/update.sh with semver guard + 15-min systemd jitter
This commit is contained in:
@@ -1,236 +0,0 @@
|
||||
/**
|
||||
* Contact requests screen — DChain explorer design style.
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { View, Text, FlatList, Alert, TouchableOpacity } from 'react-native';
|
||||
import { router } from 'expo-router';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { useStore } from '@/lib/store';
|
||||
import { buildAcceptContactTx, submitTx, getIdentity } from '@/lib/api';
|
||||
import { saveContact } from '@/lib/storage';
|
||||
import { shortAddr } from '@/lib/crypto';
|
||||
import { relativeTime } from '@/lib/utils';
|
||||
import { Avatar } from '@/components/ui/Avatar';
|
||||
import type { ContactRequest } from '@/lib/types';
|
||||
|
||||
const C = {
|
||||
bg: '#0b1220',
|
||||
surface: '#111a2b',
|
||||
surface2:'#162035',
|
||||
line: '#1c2840',
|
||||
text: '#e6edf9',
|
||||
muted: '#98a7c2',
|
||||
accent: '#7db5ff',
|
||||
ok: '#41c98a',
|
||||
warn: '#f0b35a',
|
||||
err: '#ff7a87',
|
||||
} as const;
|
||||
|
||||
export default function RequestsScreen() {
|
||||
const keyFile = useStore(s => s.keyFile);
|
||||
const requests = useStore(s => s.requests);
|
||||
const setRequests = useStore(s => s.setRequests);
|
||||
const upsertContact = useStore(s => s.upsertContact);
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
const [accepting, setAccepting] = useState<string | null>(null);
|
||||
|
||||
async function accept(req: ContactRequest) {
|
||||
if (!keyFile) return;
|
||||
setAccepting(req.txHash);
|
||||
try {
|
||||
// Fetch requester's identity to get their x25519 key for messaging
|
||||
const identity = await getIdentity(req.from);
|
||||
const x25519Pub = identity?.x25519_pub ?? '';
|
||||
|
||||
const tx = buildAcceptContactTx({
|
||||
from: keyFile.pub_key,
|
||||
to: req.from,
|
||||
privKey: keyFile.priv_key,
|
||||
});
|
||||
await submitTx(tx);
|
||||
|
||||
// Save contact with x25519 key (empty if they haven't registered one)
|
||||
const contact = {
|
||||
address: req.from,
|
||||
x25519Pub,
|
||||
username: req.username,
|
||||
addedAt: Date.now(),
|
||||
};
|
||||
upsertContact(contact);
|
||||
await saveContact(contact);
|
||||
|
||||
setRequests(requests.filter(r => r.txHash !== req.txHash));
|
||||
Alert.alert('Принято', `${req.username ? '@' + req.username : shortAddr(req.from)} добавлен в контакты.`);
|
||||
} catch (e: any) {
|
||||
Alert.alert('Ошибка', e.message);
|
||||
} finally {
|
||||
setAccepting(null);
|
||||
}
|
||||
}
|
||||
|
||||
function decline(req: ContactRequest) {
|
||||
Alert.alert(
|
||||
'Отклонить запрос',
|
||||
`Отклонить запрос от ${req.username ? '@' + req.username : shortAddr(req.from)}?`,
|
||||
[
|
||||
{ text: 'Отмена', style: 'cancel' },
|
||||
{
|
||||
text: 'Отклонить',
|
||||
style: 'destructive',
|
||||
onPress: () => setRequests(requests.filter(r => r.txHash !== req.txHash)),
|
||||
},
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1, backgroundColor: C.bg }}>
|
||||
{/* Header */}
|
||||
<View style={{
|
||||
flexDirection: 'row', alignItems: 'center', gap: 10,
|
||||
paddingHorizontal: 12,
|
||||
paddingTop: insets.top + 8, paddingBottom: 12,
|
||||
borderBottomWidth: 1, borderBottomColor: C.line,
|
||||
}}>
|
||||
<TouchableOpacity
|
||||
onPress={() => router.back()}
|
||||
activeOpacity={0.6}
|
||||
style={{ padding: 8 }}
|
||||
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
||||
>
|
||||
<Ionicons name="chevron-back" size={24} color={C.accent} />
|
||||
</TouchableOpacity>
|
||||
<Text style={{ color: C.text, fontSize: 18, fontWeight: '700', flex: 1 }}>
|
||||
Запросы контактов
|
||||
</Text>
|
||||
{requests.length > 0 && (
|
||||
<View style={{
|
||||
backgroundColor: C.accent, borderRadius: 999,
|
||||
paddingHorizontal: 10, paddingVertical: 3,
|
||||
minWidth: 24, alignItems: 'center',
|
||||
}}>
|
||||
<Text style={{ color: C.bg, fontSize: 12, fontWeight: '700' }}>
|
||||
{requests.length}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{requests.length === 0 ? (
|
||||
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center', paddingHorizontal: 32 }}>
|
||||
<View style={{
|
||||
width: 80, height: 80, borderRadius: 40,
|
||||
backgroundColor: C.surface,
|
||||
alignItems: 'center', justifyContent: 'center',
|
||||
marginBottom: 16,
|
||||
}}>
|
||||
<Ionicons name="mail-outline" size={36} color={C.muted} />
|
||||
</View>
|
||||
<Text style={{ color: C.text, fontSize: 17, fontWeight: '600', marginBottom: 6 }}>
|
||||
Нет входящих запросов
|
||||
</Text>
|
||||
<Text style={{ color: C.muted, fontSize: 13, textAlign: 'center', lineHeight: 20 }}>
|
||||
Когда кто-то пришлёт вам запрос в контакты, он появится здесь.
|
||||
</Text>
|
||||
</View>
|
||||
) : (
|
||||
<FlatList
|
||||
data={requests}
|
||||
keyExtractor={r => r.txHash}
|
||||
contentContainerStyle={{ padding: 14, paddingBottom: 24, gap: 12 }}
|
||||
renderItem={({ item: req }) => (
|
||||
<RequestCard
|
||||
req={req}
|
||||
isAccepting={accepting === req.txHash}
|
||||
onAccept={() => accept(req)}
|
||||
onDecline={() => decline(req)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function RequestCard({
|
||||
req, isAccepting, onAccept, onDecline,
|
||||
}: {
|
||||
req: ContactRequest;
|
||||
isAccepting: boolean;
|
||||
onAccept: () => void;
|
||||
onDecline: () => void;
|
||||
}) {
|
||||
const displayName = req.username ? `@${req.username}` : shortAddr(req.from);
|
||||
|
||||
return (
|
||||
<View style={{ backgroundColor: C.surface, borderRadius: 12, padding: 14 }}>
|
||||
{/* Sender info */}
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 12, marginBottom: 12 }}>
|
||||
<Avatar name={displayName} size="md" />
|
||||
<View style={{ flex: 1, minWidth: 0 }}>
|
||||
<Text style={{ color: C.text, fontWeight: '600', fontSize: 15 }} numberOfLines={1}>
|
||||
{displayName}
|
||||
</Text>
|
||||
<Text style={{ color: C.muted, fontFamily: 'monospace', fontSize: 11 }} numberOfLines={1}>
|
||||
{req.from}
|
||||
</Text>
|
||||
</View>
|
||||
<Text style={{ color: C.muted, fontSize: 12 }}>{relativeTime(req.timestamp)}</Text>
|
||||
</View>
|
||||
|
||||
{/* Intro message */}
|
||||
{!!req.intro && (
|
||||
<View style={{
|
||||
backgroundColor: C.surface2, borderRadius: 8,
|
||||
paddingHorizontal: 12, paddingVertical: 10, marginBottom: 12,
|
||||
}}>
|
||||
<Text style={{ color: C.muted, fontSize: 11, marginBottom: 4 }}>Приветствие</Text>
|
||||
<Text style={{ color: C.text, fontSize: 13, lineHeight: 19 }}>{req.intro}</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Divider */}
|
||||
<View style={{ height: 1, backgroundColor: C.line, marginBottom: 12 }} />
|
||||
|
||||
{/* Actions */}
|
||||
<View style={{ flexDirection: 'row', gap: 8 }}>
|
||||
<TouchableOpacity
|
||||
onPress={onAccept}
|
||||
disabled={isAccepting}
|
||||
activeOpacity={0.7}
|
||||
style={{
|
||||
flex: 1, paddingVertical: 11, borderRadius: 9,
|
||||
flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 6,
|
||||
backgroundColor: isAccepting ? C.surface2 : C.ok,
|
||||
}}
|
||||
>
|
||||
<Ionicons
|
||||
name={isAccepting ? 'hourglass-outline' : 'checkmark-outline'}
|
||||
size={16}
|
||||
color={isAccepting ? C.muted : C.bg}
|
||||
/>
|
||||
<Text style={{ color: isAccepting ? C.muted : C.bg, fontWeight: '700', fontSize: 14 }}>
|
||||
{isAccepting ? 'Принятие…' : 'Принять'}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
onPress={onDecline}
|
||||
disabled={isAccepting}
|
||||
activeOpacity={0.7}
|
||||
style={{
|
||||
flex: 1, paddingVertical: 11, borderRadius: 9,
|
||||
flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 6,
|
||||
backgroundColor: 'rgba(255,122,135,0.1)',
|
||||
borderWidth: 1, borderColor: 'rgba(255,122,135,0.18)',
|
||||
}}
|
||||
>
|
||||
<Ionicons name="close-outline" size={16} color={C.err} />
|
||||
<Text style={{ color: C.err, fontWeight: '700', fontSize: 14 }}>Отклонить</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user