feat: offline peers visible + push wake + iOS share sheet + no self-notification
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 44s
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 44s
- Offline push-subscribed devices appear dimmed in peer list - Tap offline peer to send wake push notification - Skip self-notification (own deviceId excluded) - iOS/Android share sheet via Web Share Target API - Online/offline indicator dot on peer avatars
This commit is contained in:
parent
0034e91672
commit
d1c4b3d196
@ -15,6 +15,8 @@ import {
|
|||||||
getVapidPublicKey,
|
getVapidPublicKey,
|
||||||
storeSubscription,
|
storeSubscription,
|
||||||
notifyOfflineDevices,
|
notifyOfflineDevices,
|
||||||
|
getOfflineSubscribers,
|
||||||
|
wakeDevice,
|
||||||
} from "./push.js";
|
} from "./push.js";
|
||||||
|
|
||||||
const PORT = parseInt(process.env.PORT || "3001", 10);
|
const PORT = parseInt(process.env.PORT || "3001", 10);
|
||||||
@ -207,6 +209,9 @@ wss.on("connection", (ws, req) => {
|
|||||||
case "subscribe-push":
|
case "subscribe-push":
|
||||||
handleSubscribePush(client, msg);
|
handleSubscribePush(client, msg);
|
||||||
break;
|
break;
|
||||||
|
case "wake-peer":
|
||||||
|
handleWakePeer(client, msg);
|
||||||
|
break;
|
||||||
case "leave":
|
case "leave":
|
||||||
handleLeave(client);
|
handleLeave(client);
|
||||||
break;
|
break;
|
||||||
@ -279,6 +284,24 @@ function handleHello(client: Client, msg: ClientMessage & { type: "hello" }): vo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Collect online device IDs
|
||||||
|
const onlineDeviceIds = new Set<string>();
|
||||||
|
for (const peer of lanRoom.clients.values()) {
|
||||||
|
if (peer.deviceId) onlineDeviceIds.add(peer.deviceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add offline push-subscribed peers to the list (excluding self)
|
||||||
|
const offlineSubs = getOfflineSubscribers(lanGroupKey, onlineDeviceIds, client.deviceId ?? undefined);
|
||||||
|
for (const sub of offlineSubs) {
|
||||||
|
peers.push({
|
||||||
|
peerId: `offline:${sub.deviceId}`,
|
||||||
|
displayName: sub.displayName,
|
||||||
|
deviceType: sub.deviceType,
|
||||||
|
online: false,
|
||||||
|
deviceId: sub.deviceId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
send(client.ws, {
|
send(client.ws, {
|
||||||
type: "welcome",
|
type: "welcome",
|
||||||
peerId: client.peerId,
|
peerId: client.peerId,
|
||||||
@ -287,18 +310,20 @@ function handleHello(client: Client, msg: ClientMessage & { type: "hello" }): vo
|
|||||||
vapidPublicKey: getVapidPublicKey(),
|
vapidPublicKey: getVapidPublicKey(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Notify offline devices that have push subscriptions for this LAN group
|
// Notify offline devices via push (skip self)
|
||||||
const onlineDeviceIds = new Set<string>();
|
notifyOfflineDevices(lanGroupKey, onlineDeviceIds, client.deviceId, client.displayName, client.deviceType);
|
||||||
for (const peer of lanRoom.clients.values()) {
|
|
||||||
if (peer.deviceId) onlineDeviceIds.add(peer.deviceId);
|
|
||||||
}
|
|
||||||
notifyOfflineDevices(lanGroupKey, onlineDeviceIds, client.displayName, client.deviceType);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSubscribePush(client: Client, msg: ClientMessage & { type: "subscribe-push" }): void {
|
function handleSubscribePush(client: Client, msg: ClientMessage & { type: "subscribe-push" }): void {
|
||||||
if (!msg.deviceId || !msg.subscription) return;
|
if (!msg.deviceId || !msg.subscription) return;
|
||||||
const lanGroupKey = getLanGroupKey(client.ip);
|
const lanGroupKey = getLanGroupKey(client.ip);
|
||||||
storeSubscription(msg.deviceId, client.displayName, msg.subscription as any, lanGroupKey);
|
storeSubscription(msg.deviceId, client.displayName, client.deviceType, msg.subscription as any, lanGroupKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleWakePeer(client: Client, msg: ClientMessage & { type: "wake-peer" }): void {
|
||||||
|
if (!msg.deviceId) return;
|
||||||
|
const lanGroupKey = getLanGroupKey(client.ip);
|
||||||
|
wakeDevice(lanGroupKey, msg.deviceId, client.displayName, client.deviceType);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCreatePublicRoom(client: Client): void {
|
function handleCreatePublicRoom(client: Client): void {
|
||||||
|
|||||||
@ -1,16 +1,16 @@
|
|||||||
import webpush from "web-push";
|
import webpush from "web-push";
|
||||||
import type { PushPayload } from "@anydrop/shared";
|
import type { PushPayload, DeviceType } from "@anydrop/shared";
|
||||||
|
|
||||||
const VAPID_PUBLIC_KEY = process.env.VAPID_PUBLIC_KEY || "";
|
const VAPID_PUBLIC_KEY = process.env.VAPID_PUBLIC_KEY || "";
|
||||||
const VAPID_PRIVATE_KEY = process.env.VAPID_PRIVATE_KEY || "";
|
const VAPID_PRIVATE_KEY = process.env.VAPID_PRIVATE_KEY || "";
|
||||||
const VAPID_SUBJECT = process.env.VAPID_SUBJECT || "mailto:contact@arthurbarre.fr";
|
const VAPID_SUBJECT = process.env.VAPID_SUBJECT || "mailto:contact@arthurbarre.fr";
|
||||||
|
|
||||||
// Subscription TTL: 24h (auto-clean stale entries)
|
|
||||||
const SUBSCRIPTION_TTL_MS = 24 * 60 * 60 * 1000;
|
const SUBSCRIPTION_TTL_MS = 24 * 60 * 60 * 1000;
|
||||||
|
|
||||||
export interface StoredSubscription {
|
export interface StoredSubscription {
|
||||||
deviceId: string;
|
deviceId: string;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
|
deviceType: DeviceType;
|
||||||
subscription: webpush.PushSubscription;
|
subscription: webpush.PushSubscription;
|
||||||
lanGroupKey: string;
|
lanGroupKey: string;
|
||||||
storedAt: number;
|
storedAt: number;
|
||||||
@ -29,10 +29,7 @@ export function initPush(): boolean {
|
|||||||
webpush.setVapidDetails(VAPID_SUBJECT, VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY);
|
webpush.setVapidDetails(VAPID_SUBJECT, VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY);
|
||||||
configured = true;
|
configured = true;
|
||||||
console.log("[push] Web Push configured");
|
console.log("[push] Web Push configured");
|
||||||
|
|
||||||
// Clean stale subscriptions every 10 min
|
|
||||||
setInterval(cleanStale, 10 * 60 * 1000);
|
setInterval(cleanStale, 10 * 60 * 1000);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -43,6 +40,7 @@ export function getVapidPublicKey(): string | undefined {
|
|||||||
export function storeSubscription(
|
export function storeSubscription(
|
||||||
deviceId: string,
|
deviceId: string,
|
||||||
displayName: string,
|
displayName: string,
|
||||||
|
deviceType: DeviceType,
|
||||||
subscription: webpush.PushSubscription,
|
subscription: webpush.PushSubscription,
|
||||||
lanGroupKey: string,
|
lanGroupKey: string,
|
||||||
): void {
|
): void {
|
||||||
@ -56,6 +54,7 @@ export function storeSubscription(
|
|||||||
group.set(deviceId, {
|
group.set(deviceId, {
|
||||||
deviceId,
|
deviceId,
|
||||||
displayName,
|
displayName,
|
||||||
|
deviceType,
|
||||||
subscription,
|
subscription,
|
||||||
lanGroupKey,
|
lanGroupKey,
|
||||||
storedAt: Date.now(),
|
storedAt: Date.now(),
|
||||||
@ -67,13 +66,44 @@ export function removeSubscription(deviceId: string, lanGroupKey: string): void
|
|||||||
subscriptions.get(lanGroupKey)?.delete(deviceId);
|
subscriptions.get(lanGroupKey)?.delete(deviceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all offline push-subscribed devices for a LAN group.
|
||||||
|
* Excludes devices that are currently online and the requesting device itself.
|
||||||
|
*/
|
||||||
|
export function getOfflineSubscribers(
|
||||||
|
lanGroupKey: string,
|
||||||
|
onlineDeviceIds: Set<string>,
|
||||||
|
excludeDeviceId?: string,
|
||||||
|
): StoredSubscription[] {
|
||||||
|
if (!configured) return [];
|
||||||
|
|
||||||
|
const group = subscriptions.get(lanGroupKey);
|
||||||
|
if (!group) return [];
|
||||||
|
|
||||||
|
const result: StoredSubscription[] = [];
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
for (const [deviceId, stored] of group) {
|
||||||
|
if (onlineDeviceIds.has(deviceId)) continue;
|
||||||
|
if (excludeDeviceId && deviceId === excludeDeviceId) continue;
|
||||||
|
if (now - stored.storedAt > SUBSCRIPTION_TTL_MS) {
|
||||||
|
group.delete(deviceId);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
result.push(stored);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Notify offline devices in a LAN group that a peer is nearby.
|
* Notify offline devices in a LAN group that a peer is nearby.
|
||||||
* `onlineDeviceIds` = set of deviceIds currently connected in this room.
|
* Skips the connecting device's own deviceId.
|
||||||
*/
|
*/
|
||||||
export async function notifyOfflineDevices(
|
export async function notifyOfflineDevices(
|
||||||
lanGroupKey: string,
|
lanGroupKey: string,
|
||||||
onlineDeviceIds: Set<string>,
|
onlineDeviceIds: Set<string>,
|
||||||
|
senderDeviceId: string | null,
|
||||||
peerDisplayName: string,
|
peerDisplayName: string,
|
||||||
peerDeviceType: string,
|
peerDeviceType: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
@ -90,10 +120,9 @@ export async function notifyOfflineDevices(
|
|||||||
const body = JSON.stringify(payload);
|
const body = JSON.stringify(payload);
|
||||||
|
|
||||||
for (const [deviceId, stored] of group) {
|
for (const [deviceId, stored] of group) {
|
||||||
// Skip devices that are currently online
|
|
||||||
if (onlineDeviceIds.has(deviceId)) continue;
|
if (onlineDeviceIds.has(deviceId)) continue;
|
||||||
|
// Never notify the sender's own device
|
||||||
// Skip stale subscriptions
|
if (senderDeviceId && deviceId === senderDeviceId) continue;
|
||||||
if (Date.now() - stored.storedAt > SUBSCRIPTION_TTL_MS) {
|
if (Date.now() - stored.storedAt > SUBSCRIPTION_TTL_MS) {
|
||||||
group.delete(deviceId);
|
group.delete(deviceId);
|
||||||
continue;
|
continue;
|
||||||
@ -103,7 +132,6 @@ export async function notifyOfflineDevices(
|
|||||||
await webpush.sendNotification(stored.subscription, body, { TTL: 60 });
|
await webpush.sendNotification(stored.subscription, body, { TTL: 60 });
|
||||||
console.log(`[push] notified ${stored.displayName} (${deviceId})`);
|
console.log(`[push] notified ${stored.displayName} (${deviceId})`);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
// 404/410 = subscription expired/invalid
|
|
||||||
if (err.statusCode === 404 || err.statusCode === 410) {
|
if (err.statusCode === 404 || err.statusCode === 410) {
|
||||||
group.delete(deviceId);
|
group.delete(deviceId);
|
||||||
console.log(`[push] removed expired subscription ${deviceId}`);
|
console.log(`[push] removed expired subscription ${deviceId}`);
|
||||||
@ -114,6 +142,45 @@ export async function notifyOfflineDevices(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a push notification to a specific device to wake it up.
|
||||||
|
*/
|
||||||
|
export async function wakeDevice(
|
||||||
|
lanGroupKey: string,
|
||||||
|
targetDeviceId: string,
|
||||||
|
senderDisplayName: string,
|
||||||
|
senderDeviceType: DeviceType,
|
||||||
|
): Promise<boolean> {
|
||||||
|
if (!configured) return false;
|
||||||
|
|
||||||
|
const group = subscriptions.get(lanGroupKey);
|
||||||
|
if (!group) return false;
|
||||||
|
|
||||||
|
const stored = group.get(targetDeviceId);
|
||||||
|
if (!stored) return false;
|
||||||
|
if (Date.now() - stored.storedAt > SUBSCRIPTION_TTL_MS) {
|
||||||
|
group.delete(targetDeviceId);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload: PushPayload = {
|
||||||
|
type: "peer-nearby",
|
||||||
|
displayName: senderDisplayName,
|
||||||
|
deviceType: senderDeviceType,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await webpush.sendNotification(stored.subscription, JSON.stringify(payload), { TTL: 60 });
|
||||||
|
console.log(`[push] woke ${stored.displayName} (${targetDeviceId})`);
|
||||||
|
return true;
|
||||||
|
} catch (err: any) {
|
||||||
|
if (err.statusCode === 404 || err.statusCode === 410) {
|
||||||
|
group.delete(targetDeviceId);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function cleanStale(): void {
|
function cleanStale(): void {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
for (const [key, group] of subscriptions) {
|
for (const [key, group] of subscriptions) {
|
||||||
|
|||||||
@ -32,12 +32,19 @@ export interface SubscribePushMessage {
|
|||||||
subscription: PushSubscriptionJSON;
|
subscription: PushSubscriptionJSON;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Client asks the server to send a push notification to wake a specific offline device */
|
||||||
|
export interface WakePeerMessage {
|
||||||
|
type: "wake-peer";
|
||||||
|
deviceId: string;
|
||||||
|
}
|
||||||
|
|
||||||
export type ClientMessage =
|
export type ClientMessage =
|
||||||
| HelloMessage
|
| HelloMessage
|
||||||
| CreatePublicRoomMessage
|
| CreatePublicRoomMessage
|
||||||
| SignalMessage
|
| SignalMessage
|
||||||
| LeaveMessage
|
| LeaveMessage
|
||||||
| SubscribePushMessage;
|
| SubscribePushMessage
|
||||||
|
| WakePeerMessage;
|
||||||
|
|
||||||
// ── Server → Client messages ──
|
// ── Server → Client messages ──
|
||||||
|
|
||||||
@ -46,6 +53,8 @@ export interface PeerInfo {
|
|||||||
displayName: string;
|
displayName: string;
|
||||||
deviceType: DeviceType;
|
deviceType: DeviceType;
|
||||||
avatar?: string;
|
avatar?: string;
|
||||||
|
online?: boolean; // default true; false = offline but reachable via push
|
||||||
|
deviceId?: string; // for offline peers (used to wake them)
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WelcomeMessage {
|
export interface WelcomeMessage {
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@ -4,6 +4,7 @@ interface PeerAvatarProps {
|
|||||||
displayName: string;
|
displayName: string;
|
||||||
deviceType: DeviceType;
|
deviceType: DeviceType;
|
||||||
avatar?: string;
|
avatar?: string;
|
||||||
|
online?: boolean;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
isSelected?: boolean;
|
isSelected?: boolean;
|
||||||
size?: "sm" | "md" | "lg";
|
size?: "sm" | "md" | "lg";
|
||||||
@ -22,8 +23,9 @@ const sizeClasses = {
|
|||||||
lg: { container: "w-20 h-20", icon: "text-3xl", img: "w-20 h-20" },
|
lg: { container: "w-20 h-20", icon: "text-3xl", img: "w-20 h-20" },
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function PeerAvatar({ displayName, deviceType, avatar, onClick, isSelected, size = "md" }: PeerAvatarProps) {
|
export default function PeerAvatar({ displayName, deviceType, avatar, online = true, onClick, isSelected, size = "md" }: PeerAvatarProps) {
|
||||||
const s = sizeClasses[size];
|
const s = sizeClasses[size];
|
||||||
|
const isOffline = !online;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
@ -31,31 +33,41 @@ export default function PeerAvatar({ displayName, deviceType, avatar, onClick, i
|
|||||||
className={`
|
className={`
|
||||||
flex flex-col items-center gap-2 group cursor-pointer
|
flex flex-col items-center gap-2 group cursor-pointer
|
||||||
transition-transform duration-200 hover:scale-105
|
transition-transform duration-200 hover:scale-105
|
||||||
|
${isOffline ? "opacity-50" : ""}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
<div
|
<div className="relative">
|
||||||
className={`
|
<div
|
||||||
${s.container}
|
className={`
|
||||||
rounded-full flex items-center justify-center overflow-hidden
|
${s.container}
|
||||||
transition-all duration-200
|
rounded-full flex items-center justify-center overflow-hidden
|
||||||
${isSelected
|
transition-all duration-200
|
||||||
? "ring-2 ring-brand-400 ring-offset-2 ring-offset-slate-950"
|
${isSelected
|
||||||
: ""
|
? "ring-2 ring-brand-400 ring-offset-2 ring-offset-slate-950"
|
||||||
}
|
: ""
|
||||||
${avatar ? "" : isSelected ? "bg-brand-500" : "bg-slate-800 hover:bg-slate-700"}
|
}
|
||||||
`}
|
${avatar ? "" : isSelected ? "bg-brand-500" : "bg-slate-800 hover:bg-slate-700"}
|
||||||
>
|
`}
|
||||||
{avatar ? (
|
>
|
||||||
<img
|
{avatar ? (
|
||||||
src={avatar}
|
<img
|
||||||
alt={displayName}
|
src={avatar}
|
||||||
className={`${s.img} rounded-full object-cover`}
|
alt={displayName}
|
||||||
/>
|
className={`${s.img} rounded-full object-cover ${isOffline ? "grayscale" : ""}`}
|
||||||
) : (
|
/>
|
||||||
<span className={s.icon}>{DEVICE_ICONS[deviceType]}</span>
|
) : (
|
||||||
)}
|
<span className={s.icon}>{DEVICE_ICONS[deviceType]}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{/* Online/offline indicator */}
|
||||||
|
<div
|
||||||
|
className={`
|
||||||
|
absolute -bottom-0.5 -right-0.5 w-3.5 h-3.5 rounded-full border-2 border-slate-950
|
||||||
|
${online ? "bg-green-500" : "bg-slate-500"}
|
||||||
|
`}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs text-slate-300 group-hover:text-white transition-colors max-w-[80px] truncate">
|
<span className={`text-xs transition-colors max-w-[80px] truncate ${isOffline ? "text-slate-500" : "text-slate-300 group-hover:text-white"}`}>
|
||||||
{displayName}
|
{displayName}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -22,14 +22,22 @@ export default function PeerList({ onPeerSelect }: PeerListProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sort: online first, then offline
|
||||||
|
const sorted = [...peers].sort((a, b) => {
|
||||||
|
const aOnline = a.online !== false ? 1 : 0;
|
||||||
|
const bOnline = b.online !== false ? 1 : 0;
|
||||||
|
return bOnline - aOnline;
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-wrap justify-center gap-6 py-8">
|
<div className="flex flex-wrap justify-center gap-6 py-8">
|
||||||
{peers.map((peer) => (
|
{sorted.map((peer) => (
|
||||||
<PeerAvatar
|
<PeerAvatar
|
||||||
key={peer.peerId}
|
key={peer.peerId}
|
||||||
displayName={peer.displayName}
|
displayName={peer.displayName}
|
||||||
deviceType={peer.deviceType}
|
deviceType={peer.deviceType}
|
||||||
avatar={peer.avatar}
|
avatar={peer.avatar}
|
||||||
|
online={peer.online !== false}
|
||||||
isSelected={selectedPeerId === peer.peerId}
|
isSelected={selectedPeerId === peer.peerId}
|
||||||
onClick={() => onPeerSelect(peer.peerId)}
|
onClick={() => onPeerSelect(peer.peerId)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -340,11 +340,16 @@ export function useSignaling(joinCode?: string) {
|
|||||||
signalingRef.current?.send({ type: "create-public-room" });
|
signalingRef.current?.send({ type: "create-public-room" });
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const wakePeer = useCallback((deviceId: string) => {
|
||||||
|
signalingRef.current?.send({ type: "wake-peer", deviceId });
|
||||||
|
}, []);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
sendFiles,
|
sendFiles,
|
||||||
sendText,
|
sendText,
|
||||||
acceptTransfer,
|
acceptTransfer,
|
||||||
rejectTransfer,
|
rejectTransfer,
|
||||||
createPublicRoom,
|
createPublicRoom,
|
||||||
|
wakePeer,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,9 +21,10 @@ export default function Home() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function HomeConnected() {
|
function HomeConnected() {
|
||||||
const { sendFiles, sendText, acceptTransfer, rejectTransfer, createPublicRoom } =
|
const { sendFiles, sendText, acceptTransfer, rejectTransfer, createPublicRoom, wakePeer } =
|
||||||
useSignaling();
|
useSignaling();
|
||||||
|
|
||||||
|
const peers = useStore((s) => s.peers);
|
||||||
const selectedPeerId = useStore((s) => s.selectedPeerId);
|
const selectedPeerId = useStore((s) => s.selectedPeerId);
|
||||||
const showTextModal = useStore((s) => s.showTextModal);
|
const showTextModal = useStore((s) => s.showTextModal);
|
||||||
const incomingRequest = useStore((s) => s.incomingRequest);
|
const incomingRequest = useStore((s) => s.incomingRequest);
|
||||||
@ -34,12 +35,20 @@ function HomeConnected() {
|
|||||||
|
|
||||||
const { deviceName, avatar } = useProfileStore();
|
const { deviceName, avatar } = useProfileStore();
|
||||||
const [showProfileEdit, setShowProfileEdit] = useState(false);
|
const [showProfileEdit, setShowProfileEdit] = useState(false);
|
||||||
|
const [wakingDeviceId, setWakingDeviceId] = useState<string | null>(null);
|
||||||
|
|
||||||
const handlePeerSelect = useCallback(
|
const handlePeerSelect = useCallback(
|
||||||
(peerId: string) => {
|
(peerId: string) => {
|
||||||
|
// Check if this is an offline peer
|
||||||
|
const peer = peers.find((p) => p.peerId === peerId);
|
||||||
|
if (peer && peer.online === false && peer.deviceId) {
|
||||||
|
wakePeer(peer.deviceId);
|
||||||
|
setWakingDeviceId(peer.deviceId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
setSelectedPeerId(selectedPeerId === peerId ? null : peerId);
|
setSelectedPeerId(selectedPeerId === peerId ? null : peerId);
|
||||||
},
|
},
|
||||||
[selectedPeerId, setSelectedPeerId],
|
[selectedPeerId, setSelectedPeerId, peers, wakePeer],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleFilesSelected = useCallback(
|
const handleFilesSelected = useCallback(
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user