### v1.0.3 Changelog

**Privacy & Network Optimization**
* **Strict P2P Replication Filtering:** Nodes now only download and track hypercores from direct contacts or peers who share a Hub or Group Whisper with you. This prevents private activity from leaking to unrelated peers and significantly reduces background bandwidth.
* **Strict Message Validation:** Fixed an issue causing phantom unread badges and leaked notifications. The local database now strictly verifies your membership in a Hub or Group Whisper before accepting and storing incoming messages.
* **Transitive Group Chat Support:** You can now seamlessly receive and view messages from non-contacts inside a shared Group Whisper without needing to add them as a friend first.

**UI/UX & Bug Fixes**
* **Sidebar Hover Glitch Fixed:** Resolved the severe flickering on sidebar icons. Optimized the React component lifecycle to prevent the DOM from destroying and recreating the icons during background state updates.
* **Members Drawer Overlap Fix:** The chat area and input bar now smoothly resize and shift out of the way when the Online Users list is opened, ensuring the chat bar is never obscured on smaller screens.
This commit is contained in:
0% [█ █ █ █ █ █ █ █ █ █] 100% 2026-06-16 13:19:54 -05:00
parent 064a9b4cf4
commit 2689c5336c
7 changed files with 155 additions and 68 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "peercord", "name": "peercord",
"version": "1.0.2", "version": "1.0.3",
"description": "Peercord, A P2P Discord clone powered by Pear Runtime", "description": "Peercord, A P2P Discord clone powered by Pear Runtime",
"author": "Mastercodeon", "author": "Mastercodeon",
"main": "index.js", "main": "index.js",

View File

@ -1,2 +1,2 @@
window.APP_VERSION = '1.0.2'; window.APP_VERSION = '1.0.3';
window.APP_VERSION_COLOR = 'hsl(119, 80%, 60%)'; window.APP_VERSION_COLOR = 'hsl(120, 80%, 60%)';

View File

@ -704,7 +704,7 @@ export default function MainApp({ profile, setProfile, onLogout, updateState, si
<div className="flex-1 relative overflow-hidden flex"> <div className="flex-1 relative overflow-hidden flex">
{/* Chat Area (Hidden if CallView is active) */} {/* Chat Area (Hidden if CallView is active) */}
<div className={`flex-1 flex flex-col min-w-0 ${showCallView ? 'hidden' : ''}`}> <div className={`flex-1 flex flex-col min-w-0 transition-[margin] duration-300 ${showCallView ? 'hidden' : ''} ${isDrawerOpen && !isPinned ? 'mr-64' : ''}`}>
{activeView === 'dms' && activeDm === 'friends' ? ( {activeView === 'dms' && activeDm === 'friends' ? (
<FriendsView dms={dms} /> <FriendsView dms={dms} />
) : ( ) : (
@ -741,7 +741,7 @@ export default function MainApp({ profile, setProfile, onLogout, updateState, si
{/* 1-on-1 Call View */} {/* 1-on-1 Call View */}
{activeCall && ( {activeCall && (
<CallView <CallView
className={showCallView && isViewingCallDM ? 'flex-1 flex flex-col min-w-0' : 'hidden'} className={showCallView && isViewingCallDM ? `flex-1 flex flex-col min-w-0 transition-[margin] duration-300 ${isDrawerOpen && !isPinned ? 'mr-64' : ''}` : 'hidden'}
targetKey={activeCall.targetKey} targetKey={activeCall.targetKey}
targetProfile={activeCall.profile} targetProfile={activeCall.profile}
myProfile={profile} myProfile={profile}
@ -757,7 +757,7 @@ export default function MainApp({ profile, setProfile, onLogout, updateState, si
{/* Group Call View (Used for both DMs and Server VCs) */} {/* Group Call View (Used for both DMs and Server VCs) */}
{(activeGroupCall || activeVc) && ( {(activeGroupCall || activeVc) && (
<GroupCallView <GroupCallView
className={showCallView && (isViewingGroupCall || isViewingVC) ? 'flex-1 flex flex-col min-w-0' : 'hidden'} className={showCallView && (isViewingGroupCall || isViewingVC) ? `flex-1 flex flex-col min-w-0 transition-[margin] duration-300 ${isDrawerOpen && !isPinned ? 'mr-64' : ''}` : 'hidden'}
channel={activeGroupCall?.channel || `${activeVc.serverId}-${activeVc.channelId}`} channel={activeGroupCall?.channel || `${activeVc.serverId}-${activeVc.channelId}`}
serverTopicHex={activeVc?.serverId} serverTopicHex={activeVc?.serverId}
vcChannelId={activeVc?.channelId} vcChannelId={activeVc?.channelId}

View File

@ -66,7 +66,34 @@ async function handleIdentity(network, peerKey, parsed) {
if (network.onPeerUpdate) network.onPeerUpdate(network.getPeerList()); if (network.onPeerUpdate) network.onPeerUpdate(network.getPeerList());
network._emitMessages(); network._emitMessages();
// PRIVACY FILTER: Only track cores of peers we actually interact with
let shouldTrack = false;
// 1. Are they a direct friend? (Accepted or Pending)
if (network.dms[peerKey]) {
shouldTrack = true;
}
// 2. Do we share a Hub or Group Chat?
if (!shouldTrack && parsed.topics && Array.isArray(parsed.topics)) {
for (const t of parsed.topics) {
if (network.joinedTopics.has(t)) {
shouldTrack = true;
break;
}
}
}
// 3. Are they trying to send us a friend request?
if (!shouldTrack && parsed.pendingTargets && Array.isArray(parsed.pendingTargets)) {
if (parsed.pendingTargets.includes(network.myKey)) {
shouldTrack = true;
}
}
if (shouldTrack) {
await network.trackPeerCore(parsed.coreKey); await network.trackPeerCore(parsed.coreKey);
}
} }
function handleWhois(network, parsed, conn) { function handleWhois(network, parsed, conn) {

View File

@ -316,6 +316,31 @@ class P2PNetwork {
} }
} }
_broadcastIdentity() {
if (!this.swarm) return;
const pendingTargets = Object.entries(this.dms)
.filter(([_, data]) => data.status === 'pending_outgoing')
.map(([key]) => key);
const identityMsg = JSON.stringify({
type: 'identity',
displayName: this.displayName,
username: this.username,
avatar: this.avatar,
bio: this.bio,
connections: this.connections,
coreKey: this.coreKey,
topics: Array.from(this.joinedTopics),
pendingTargets
});
const payload = b4a.from(identityMsg);
for (const { conn } of this.peers.values()) {
try { conn.write(payload); } catch(e) {}
}
}
async initialize(seedHex, displayName, username, avatar = null, bio = '', connections = []) { async initialize(seedHex, displayName, username, avatar = null, bio = '', connections = []) {
this.displayName = displayName; this.displayName = displayName;
this.username = (username || 'unknown').toLowerCase(); this.username = (username || 'unknown').toLowerCase();
@ -398,7 +423,23 @@ class P2PNetwork {
this.store.replicate(conn); this.store.replicate(conn);
const peerKey = b4a.toString(info.publicKey, 'hex'); const peerKey = b4a.toString(info.publicKey, 'hex');
this.peers.set(peerKey, { conn, displayName: 'Unknown', username: 'unknown', avatar: null, bio: '', connections: [], coreKey: null }); this.peers.set(peerKey, { conn, displayName: 'Unknown', username: 'unknown', avatar: null, bio: '', connections: [], coreKey: null });
const identityMsg = JSON.stringify({ type: 'identity', displayName: this.displayName, username: this.username, avatar: this.avatar, bio: this.bio, connections: this.connections, coreKey: this.coreKey });
const pendingTargets = Object.entries(this.dms)
.filter(([_, data]) => data.status === 'pending_outgoing')
.map(([key]) => key);
const identityMsg = JSON.stringify({
type: 'identity',
displayName: this.displayName,
username: this.username,
avatar: this.avatar,
bio: this.bio,
connections: this.connections,
coreKey: this.coreKey,
topics: Array.from(this.joinedTopics),
pendingTargets
});
conn.write(b4a.from(identityMsg)); conn.write(b4a.from(identityMsg));
if (this.onPeerUpdate) this.onPeerUpdate(this.getPeerList()); if (this.onPeerUpdate) this.onPeerUpdate(this.getPeerList());
conn.on('data', async (data) => handleData(this, peerKey, data, conn)); conn.on('data', async (data) => handleData(this, peerKey, data, conn));
@ -477,6 +518,9 @@ class P2PNetwork {
this.joinedTopics.add(topicHex); this.joinedTopics.add(topicHex);
const topic = b4a.from(topicHex, 'hex'); const topic = b4a.from(topicHex, 'hex');
this.swarm.join(topic, { client: true, server: true }); this.swarm.join(topic, { client: true, server: true });
this._broadcastIdentity();
if (!skipFlush) { if (!skipFlush) {
try { await this.swarm.flush(); } catch(e) {} try { await this.swarm.flush(); } catch(e) {}
} }

View File

@ -59,9 +59,5 @@ export function updateProfile(network, displayName, avatar, username, bio = '',
network._emitKnownProfiles(); network._emitKnownProfiles();
if (!network.swarm) return; if (!network.swarm) return;
network._broadcastIdentity();
const identityMsg = JSON.stringify({ type: 'identity', displayName: network.displayName, username: network.username, avatar: network.avatar, bio: network.bio, connections: network.connections, coreKey: network.coreKey });
const payload = b4a.from(identityMsg);
for (const { conn } of network.peers.values()) conn.write(payload);
network._emitMessages();
} }

View File

@ -7,7 +7,8 @@ export function getAllMessages(network) {
return Array.from(network.messages.values()).filter(m => { return Array.from(network.messages.values()).filter(m => {
const ch = m.payload.channel; const ch = m.payload.channel;
if (ch && ch.length > 64 && ch[64] === '-') { if (!m.recipient && ch) {
if (ch.length > 64 && ch[64] === '-') {
const topicHex = ch.substring(0, 64); const topicHex = ch.substring(0, 64);
const chName = ch.substring(65); const chName = ch.substring(65);
if (!joinedTopics.has(topicHex)) return false; if (!joinedTopics.has(topicHex)) return false;
@ -34,6 +35,10 @@ export function getAllMessages(network) {
if (!hasReadPerm && server.roles && server.roles.length > 0) return false; if (!hasReadPerm && server.roles && server.roles.length > 0) return false;
} }
} }
} else if (ch.length === 64) {
// Group chat: Filter out messages for group chats we are not in
if (!joinedTopics.has(ch)) return false;
}
} }
return true; return true;
}).map(m => { }).map(m => {
@ -352,12 +357,16 @@ export async function processMessage(network, msg) {
} }
if (type === 'chat' || type === 'file') { if (type === 'chat' || type === 'file') {
let canAccept = true; let canAccept = false;
if (channel && channel.length > 64 && channel[64] === '-') { if (channel && channel.length > 64 && channel[64] === '-') {
const topicHex = channel.substring(0, 64); const topicHex = channel.substring(0, 64);
const chName = channel.substring(65); const chName = channel.substring(65);
const server = network.servers.find(s => s.topicHex === topicHex); const server = network.servers.find(s => s.topicHex === topicHex);
if (server && msg.sender !== server.owner && msg.sender !== ADMIN_PUBLIC_KEY) {
if (server) {
canAccept = true;
if (msg.sender !== server.owner && msg.sender !== ADMIN_PUBLIC_KEY) {
const userRoles = server.memberRoles?.[msg.sender] || []; const userRoles = server.memberRoles?.[msg.sender] || [];
const isServerAdmin = userRoles.some(rId => { const isServerAdmin = userRoles.some(rId => {
const r = server.roles?.find(role => role.id === rId); const r = server.roles?.find(role => role.id === rId);
@ -397,6 +406,13 @@ export async function processMessage(network, msg) {
} }
} }
} }
} else if (channel && channel.length === 64) {
// Group chat: Only accept if we are actually in this group chat
const gc = network.servers.find(s => s.topicHex === channel && s.isGroupChat);
if (gc) {
canAccept = true;
}
}
if (canAccept && !network.deletedMessages.has(id) && !network.messages.has(id)) { if (canAccept && !network.deletedMessages.has(id) && !network.messages.has(id)) {
network.messages.set(id, msg); network.messages.set(id, msg);
@ -459,6 +475,7 @@ export async function sendDMRequest(network, targetKey, profile) {
await network.db.put('dm:' + targetKey, network.dms[targetKey]); await network.db.put('dm:' + targetKey, network.dms[targetKey]);
if (network.onDMsUpdate) network.onDMsUpdate({ ...network.dms }); if (network.onDMsUpdate) network.onDMsUpdate({ ...network.dms });
await _appendEncryptedMessage(network, targetKey, { type: 'dm_request', profile: { displayName: network.displayName, username: network.username, avatar: network.avatar, bio: network.bio, connections: network.connections } }); await _appendEncryptedMessage(network, targetKey, { type: 'dm_request', profile: { displayName: network.displayName, username: network.username, avatar: network.avatar, bio: network.bio, connections: network.connections } });
network._broadcastIdentity();
} }
export async function acceptDMRequest(network, targetKey) { export async function acceptDMRequest(network, targetKey) {
@ -468,6 +485,7 @@ export async function acceptDMRequest(network, targetKey) {
if (network.onDMsUpdate) network.onDMsUpdate({ ...network.dms }); if (network.onDMsUpdate) network.onDMsUpdate({ ...network.dms });
} }
await _appendEncryptedMessage(network, targetKey, { type: 'dm_accept' }); await _appendEncryptedMessage(network, targetKey, { type: 'dm_accept' });
network._broadcastIdentity();
} }
export async function sendMessage(network, channel, text, replyTo = null) { export async function sendMessage(network, channel, text, replyTo = null) {
@ -493,7 +511,9 @@ export async function sendReaction(network, targetId, emoji, isDM = false, targe
export function sendEphemeral(network, payload) { export function sendEphemeral(network, payload) {
if (!network.swarm) return; if (!network.swarm) return;
const msg = b4a.from(JSON.stringify({ type: 'ephemeral', payload })); const msg = b4a.from(JSON.stringify({ type: 'ephemeral', payload }));
for (const { conn } of network.peers.values()) conn.write(msg); for (const { conn } of network.peers.values()) {
try { conn.write(msg); } catch(e) {}
}
} }
export function sendOffline(network) { sendEphemeral(network, { type: 'offline' }); } export function sendOffline(network) { sendEphemeral(network, { type: 'offline' }); }