Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 10 additions & 40 deletions mobile_app/components/home/NearbyPeersCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,14 @@ import { useRouter } from "expo-router";
import React, { useMemo } from "react";
import { StyleSheet, Text, View } from "react-native";

import { Icon, Pill, PressSurface } from "@/components/primitives";
import { Icon, PeerIdenticon, Pill, PressSurface } from "@/components/primitives";
import type { PillTone } from "@/components/primitives";
import { useLxmfContext } from "@/context/LxmfContext";
import { useTheme } from "@/theme";

const AVATAR_PREVIEW_COUNT = 3;
const FRESH_WINDOW_SEC = 120; // peers announce-heard within 2 min count as "nearby"

function initialOf(alias: string | undefined): string {
if (!alias) return "?";
return alias.trim().charAt(0).toUpperCase() || "?";
}

// Peer presence strip above Recent.
//
// LxmfContext.peers is keyed by destHash and pruned to recent announces.
Expand Down Expand Up @@ -79,19 +74,6 @@ export function NearbyPeersCard() {
? "green"
: "amber";

const avatarPaletteBg = [
colors.primarySubtle,
colors.successSubtle,
colors.accentSubtle,
colors.warningSubtle,
];
const avatarPaletteFg = [
colors.primary,
colors.success,
colors.accent,
colors.warning,
];

return (
<PressSurface
accessibilityLabel="Open mesh map"
Expand Down Expand Up @@ -127,28 +109,16 @@ export function NearbyPeersCard() {
{previewPeers.length > 0 ? (
<View style={styles.avatarStack}>
{previewPeers.map((peer, index) => (
<View
// Identity-keyed constellation mark — the 2px background ring
// keeps the overlapped-stack separation the initials had.
<PeerIdenticon
borderColor={colors.background}
borderWidth={2}
key={peer.destHash}
style={[
styles.avatar,
{
backgroundColor: avatarPaletteBg[index % avatarPaletteBg.length],
borderColor: colors.background,
borderRadius: radii.full,
marginLeft: index === 0 ? 0 : -10,
},
]}
>
<Text
style={{
color: avatarPaletteFg[index % avatarPaletteFg.length],
fontFamily: fontFamily.sansBold,
fontSize: fontSize.sm,
}}
>
{initialOf(peer.displayName)}
</Text>
</View>
seed={peer.destHash}
size={32}
style={{ marginLeft: index === 0 ? 0 : -10 }}
/>
))}
{extraCount > 0 ? (
<View
Expand Down
11 changes: 3 additions & 8 deletions mobile_app/components/messages/GroupMembersSheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { View, Text, ScrollView, StyleSheet, Pressable } from 'react-native';
import { Feather } from '@expo/vector-icons';
import { fontFamily, fontSize, radii, useTheme } from '@/theme';
import { useGlass } from '@/hooks/useGlass';
import { AppBottomSheet } from '@/components/primitives';
import { AppBottomSheet, PeerIdenticon } from '@/components/primitives';
import { EmptyState } from '@/components/ui';
import type { LxmfGroup } from '@/context/LxmfContext';

Expand All @@ -21,13 +21,10 @@ function MemberRow({ hash, getDisplayName, colors, baseGlass }: {
colors: ReturnType<typeof useTheme>['colors'];
baseGlass: object;
}) {
const name = getDisplayName(hash);
const initials = name.slice(0, 2).toUpperCase();
const name = getDisplayName(hash);
return (
<View style={[S.memberRow, baseGlass]}>
<View style={[S.avatar, { backgroundColor: '#0d2f2a', borderColor: '#1a5c4f' }]}>
<Text style={[S.avatarText, { color: '#4ecdc4' }]}>{initials}</Text>
</View>
<PeerIdenticon seed={hash} size={34} />
<View style={S.memberInfo}>
<Text style={[S.memberName, { color: colors.textPrimary }]} numberOfLines={1}>{name}</Text>
<Text style={[S.memberHash, { color: colors.textTertiary }]}>{hash}</Text>
Expand Down Expand Up @@ -103,8 +100,6 @@ const S = StyleSheet.create({
countText: { fontFamily: fontFamily.sansMd, fontSize: 10, letterSpacing: 1.5, textTransform: 'uppercase' },
list: { flexGrow: 0 },
memberRow: { flexDirection: 'row', alignItems: 'center', gap: 10, padding: 10, borderRadius: radii.md },
avatar: { width: 34, height: 34, borderRadius: radii.md, alignItems: 'center', justifyContent: 'center', borderWidth: 0.5 },
avatarText: { fontFamily: fontFamily.sansMd, fontSize: fontSize.sm, fontWeight: '600' },
memberInfo: { flex: 1, minWidth: 0 },
memberName: { fontFamily: fontFamily.sansMd, fontSize: fontSize.sm, letterSpacing: 0.2 },
memberHash: { fontFamily: fontFamily.sansMd, fontSize: 9, letterSpacing: 0.5, marginTop: 2, opacity: 0.6 },
Expand Down
30 changes: 16 additions & 14 deletions mobile_app/components/messages/PeersDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import Reanimated, {
useSharedValue, useAnimatedStyle, withSpring, interpolate, Extrapolation,
} from 'react-native-reanimated';
import { fontFamily, fontSize, radii, spacing, useTheme } from '@/theme';
import { PeerIdenticon } from '@/components/primitives';
import { Pill } from '@/components/ui/Pill';
import { Skeleton } from '@/components/ui/Skeleton';
import { confirm } from '@/components/ui/ConfirmSheet';
Expand Down Expand Up @@ -140,20 +141,23 @@ function GroupAvatar() {
);
}

function PeerAvatar({ p, online, borderColor, bg, textColor }: {
readonly p: Peer; readonly online: boolean; readonly borderColor: string; readonly bg: string; readonly textColor: string;
function PeerAvatar({ p, online, borderColor, bg }: {
readonly p: Peer; readonly online: boolean; readonly borderColor: string; readonly bg: string;
}) {
return (
<View style={S.avatarWrap}>
<View style={[S.avatar, { backgroundColor: bg, borderColor }]}>
{p.beacon
? <Feather name="radio" size={17} color="#00e5ff" />
: <Text style={[S.avatarText, { color: textColor }]}>{p.handle.slice(5, 9)}</Text>
}
// Beacons are infrastructure, not pseudonymous people — keep the radio mark.
if (p.beacon) {
return (
<View style={S.avatarWrap}>
<View style={[S.avatar, { backgroundColor: bg, borderColor }]}>
<Feather name="radio" size={17} color="#00e5ff" />
</View>
<View style={[S.statusDot, { backgroundColor: online ? '#00e5ff' : '#3a4a54' }]} />
</View>
<View style={[S.statusDot, { backgroundColor: online ? '#00e5ff' : '#3a4a54' }]} />
</View>
);
);
}
// The dest hash is the identity — render its deterministic constellation
// mark so "same peer as before" is recognizable at a glance.
return <PeerIdenticon seed={p.destHash ?? p.handle} size={44} online={online} />;
}

// ── Tab bar ───────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -266,7 +270,6 @@ function PeerRow({
online={p.online}
bg={colors.surface2}
borderColor={colors.border}
textColor={colors.textSecondary}
/>
<View style={S.info}>
<View style={S.infoTop}>
Expand Down Expand Up @@ -514,7 +517,6 @@ const S = StyleSheet.create({
// ── Avatar ───────────────────────────────────────────────────────────────────
avatarWrap: { position: 'relative', width: 44, height: 44 },
avatar: { width: 44, height: 44, borderRadius: radii.full, alignItems: 'center', justifyContent: 'center', borderWidth: 0.5 },
avatarText: { fontFamily: fontFamily.sansMd, fontSize: fontSize.sm, fontWeight: '600' },
groupHash: { fontFamily: fontFamily.sansMd, fontSize: fontSize.lg, fontWeight: '700', color: '#4ecdc4' },
statusDot: { position: 'absolute', bottom: 0, right: 0, width: 11, height: 11, borderRadius: radii.full, borderWidth: 2, borderColor: '#060f16' },

Expand Down
11 changes: 10 additions & 1 deletion mobile_app/components/messages/ThreadHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { View, Text, Pressable, TextInput, StyleSheet } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { Feather } from '@expo/vector-icons';
import { fontFamily, fontSize, radii, useTheme } from '@/theme';
import { PeerIdenticon } from '@/components/primitives';
import { Pill } from '@/components/ui/Pill';
import { useGlass } from '../../hooks/useGlass';
import { useLxmfContext } from '@/context/LxmfContext';
Expand All @@ -19,6 +20,9 @@ interface Props {
nameKnown?: boolean;
/** 8-char hash prefix for anonymous-peer display. */
hashShort?: string;
/** Full dest hash — seeds the identicon so the header mark matches the
* drawer row for the same peer. */
destHash?: string;
onOpen: () => void;
onShareQR?: () => void;
onShowMembers?: () => void;
Expand Down Expand Up @@ -113,7 +117,7 @@ function EditIcon({ onPress }: { readonly onPress: () => void }) {

// ── ThreadHeader ──────────────────────────────────────────────────────────────

export const ThreadHeader = memo(function ThreadHeader({ peer, selfName, hops, iface, online, isGroup, memberCount, nameKnown, hashShort, onOpen, onShareQR, onShowMembers }: Props) {
export const ThreadHeader = memo(function ThreadHeader({ peer, selfName, hops, iface, online, isGroup, memberCount, nameKnown, hashShort, destHash, onOpen, onShareQR, onShowMembers }: Props) {
const { colors } = useTheme();
const baseGlass = useGlass();
const { updateDisplayName } = useLxmfContext();
Expand Down Expand Up @@ -191,6 +195,11 @@ export const ThreadHeader = memo(function ThreadHeader({ peer, selfName, hops, i
<Feather name={hasPeer ? 'arrow-left' : 'menu'} size={16} color={colors.textSecondary} />
</Pressable>

{/* Same constellation mark as the drawer row — confirms "same peer" even
when the thread shows Anonymous · prefix. Presence already has its own
dot in the status row, so the identicon renders without one. */}
{hasPeer && !isGroup && !!destHash && <PeerIdenticon seed={destHash} size={30} />}

<View style={{ flex: 1 }}>
{hasPeer && isGroup && <GroupInfo peer={peer} memberCount={memberCount} />}
{hasPeer && !isGroup && <PeerInfo peer={peer} hops={hops} iface={iface} online={online} nameKnown={nameKnown} hashShort={hashShort} />}
Expand Down
43 changes: 10 additions & 33 deletions mobile_app/components/nodes/MeshMap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import Reanimated, {
useSharedValue, useAnimatedStyle, runOnJS,
} from 'react-native-reanimated';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { PeerIdenticon } from '@/components/primitives';
import { fontFamily, fontSize, radii, useTheme } from '@/theme';
import type { NodeData } from './types';

Expand Down Expand Up @@ -117,27 +118,6 @@ function edges(placed: Placed[]): Edge[] {

const IFACE_COLOR_KEY = { TCP: 'primary', BLE: 'accent', RNode: 'textSecondary' } as const;

// ── Avatar ────────────────────────────────────────────────────────────────────
const AVATAR_PALETTE = [
'#FF6B6B', '#FF9F43', '#FECA57', '#48DBFB', '#FF9FF3',
'#54A0FF', '#5F27CD', '#00D2D3', '#10AC84', '#EE5A24',
'#C8D6E5', '#01ABC6',
];

function hashCode(s: string): number {
let h = 0;
for (let i = 0; i < s.length; i++) h = (Math.imul(31, h) + s.charCodeAt(i)) | 0;
return Math.abs(h);
}

function nodeAvatar(handle: string | null | undefined): { initial: string; color: string } {
const clean = (handle ?? '').replace('@', '');
return {
initial: clean.slice(0, 1).toUpperCase() || '?',
color: AVATAR_PALETTE[hashCode(clean) % AVATAR_PALETTE.length]!,
};
}

// ── PeerNode — memo so only the 2 selection-changing nodes re-render on tap ──
interface PeerNodeProps {
node: NodeData;
Expand All @@ -157,25 +137,22 @@ const IFACE_ICON: Record<string, React.ComponentProps<typeof MaterialCommunityIc

const PeerNode = memo(function PeerNode({ node: n, x, y, r, isSelected, ifcClr, textTertiary }: PeerNodeProps) {
const online = n.online !== false;
const avatar = nodeAvatar(n.handle);
const D = r * 2;
let borderW = 0.5;
if (isSelected) borderW = 1.5;
else if (online) borderW = 1;
const borderClr = isSelected || online ? ifcClr : 'rgba(255,255,255,0.10)';
return (
<View style={{ position: 'absolute', left: x - r - 6, top: y - r - 6, padding: 6, opacity: online ? 1 : 0.32 }}>
<View style={{
width: D, height: D, borderRadius: r,
backgroundColor: avatar.color,
borderWidth: borderW,
borderColor: borderClr,
alignItems: 'center', justifyContent: 'center',
}}>
<Text style={{ fontSize: r * 0.9, color: '#fff', fontWeight: '700', includeFontPadding: false }}>
{avatar.initial}
</Text>
</View>
{/* Same destHash-seeded mark as the drawer/thread surfaces — the map node
for a peer matches their row. Online reads via opacity + iface border
(no status dot at 24px). Seed falls back to handle like nodeId(). */}
<PeerIdenticon
seed={n.destHash ?? n.handle}
size={D}
borderColor={borderClr}
borderWidth={borderW}
/>
{/* Interface type badge — top-right corner */}
<View style={{
position: 'absolute', right: 2, top: 2,
Expand Down
Loading
Loading