### 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",
"version": "1.0.2",
"version": "1.0.3",
"description": "Peercord, A P2P Discord clone powered by Pear Runtime",
"author": "Mastercodeon",
"main": "index.js",

View File

@ -1,2 +1,2 @@
window.APP_VERSION = '1.0.2';
window.APP_VERSION_COLOR = 'hsl(119, 80%, 60%)';
window.APP_VERSION = '1.0.3';
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">
{/* 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' ? (
<FriendsView dms={dms} />
) : (
@ -741,7 +741,7 @@ export default function MainApp({ profile, setProfile, onLogout, updateState, si
{/* 1-on-1 Call View */}
{activeCall && (
<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}
targetProfile={activeCall.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) */}
{(activeGroupCall || activeVc) && (
<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}`}
serverTopicHex={activeVc?.serverId}
vcChannelId={activeVc?.channelId}

View File

@ -66,7 +66,34 @@ async function handleIdentity(network, peerKey, parsed) {
if (network.onPeerUpdate) network.onPeerUpdate(network.getPeerList());
network._emitMessages();
await network.trackPeerCore(parsed.coreKey);
// 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);
}
}
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 = []) {
this.displayName = displayName;
this.username = (username || 'unknown').toLowerCase();
@ -398,7 +423,23 @@ class P2PNetwork {
this.store.replicate(conn);
const peerKey = b4a.toString(info.publicKey, 'hex');
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));
if (this.onPeerUpdate) this.onPeerUpdate(this.getPeerList());
conn.on('data', async (data) => handleData(this, peerKey, data, conn));
@ -477,6 +518,9 @@ class P2PNetwork {
this.joinedTopics.add(topicHex);
const topic = b4a.from(topicHex, 'hex');
this.swarm.join(topic, { client: true, server: true });
this._broadcastIdentity();
if (!skipFlush) {
try { await this.swarm.flush(); } catch(e) {}
}

View File

@ -59,9 +59,5 @@ export function updateProfile(network, displayName, avatar, username, bio = '',
network._emitKnownProfiles();
if (!network.swarm) return;
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();
network._broadcastIdentity();
}

View File

@ -7,32 +7,37 @@ export function getAllMessages(network) {
return Array.from(network.messages.values()).filter(m => {
const ch = m.payload.channel;
if (ch && ch.length > 64 && ch[64] === '-') {
const topicHex = ch.substring(0, 64);
const chName = ch.substring(65);
if (!joinedTopics.has(topicHex)) return false;
if (!m.recipient && ch) {
if (ch.length > 64 && ch[64] === '-') {
const topicHex = ch.substring(0, 64);
const chName = ch.substring(65);
if (!joinedTopics.has(topicHex)) return false;
const server = network.servers.find(s => s.topicHex === topicHex);
if (server && network.myKey !== server.owner && network.myKey !== ADMIN_PUBLIC_KEY) {
const userRoles = server.memberRoles?.[network.myKey] || [];
const isServerAdmin = userRoles.some(rId => {
const r = server.roles?.find(role => role.id === rId);
return r && r.permissions.includes('admin');
});
if (!isServerAdmin) {
const channelPerms = server.channels?.permissions?.[chName];
if (channelPerms && channelPerms.length > 0) {
const hasChannelAccess = userRoles.some(rId => channelPerms.includes(rId));
if (!hasChannelAccess) return false;
}
const hasReadPerm = userRoles.some(rId => {
const server = network.servers.find(s => s.topicHex === topicHex);
if (server && network.myKey !== server.owner && network.myKey !== ADMIN_PUBLIC_KEY) {
const userRoles = server.memberRoles?.[network.myKey] || [];
const isServerAdmin = userRoles.some(rId => {
const r = server.roles?.find(role => role.id === rId);
return r && r.permissions.includes('read_messages');
return r && r.permissions.includes('admin');
});
if (!hasReadPerm && server.roles && server.roles.length > 0) return false;
if (!isServerAdmin) {
const channelPerms = server.channels?.permissions?.[chName];
if (channelPerms && channelPerms.length > 0) {
const hasChannelAccess = userRoles.some(rId => channelPerms.includes(rId));
if (!hasChannelAccess) return false;
}
const hasReadPerm = userRoles.some(rId => {
const r = server.roles?.find(role => role.id === rId);
return r && r.permissions.includes('read_messages');
});
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;
@ -352,50 +357,61 @@ export async function processMessage(network, msg) {
}
if (type === 'chat' || type === 'file') {
let canAccept = true;
let canAccept = false;
if (channel && channel.length > 64 && channel[64] === '-') {
const topicHex = channel.substring(0, 64);
const chName = channel.substring(65);
const server = network.servers.find(s => s.topicHex === topicHex);
if (server && msg.sender !== server.owner && msg.sender !== ADMIN_PUBLIC_KEY) {
const userRoles = server.memberRoles?.[msg.sender] || [];
const isServerAdmin = userRoles.some(rId => {
const r = server.roles?.find(role => role.id === rId);
return r && r.permissions.includes('admin');
});
if (!isServerAdmin) {
const channelPerms = server.channels?.permissions?.[chName];
if (channelPerms && channelPerms.length > 0) {
const hasChannelAccess = userRoles.some(rId => channelPerms.includes(rId));
if (!hasChannelAccess) canAccept = false;
}
if (server) {
canAccept = true;
if (msg.sender !== server.owner && msg.sender !== ADMIN_PUBLIC_KEY) {
const userRoles = server.memberRoles?.[msg.sender] || [];
const isServerAdmin = userRoles.some(rId => {
const r = server.roles?.find(role => role.id === rId);
return r && r.permissions.includes('admin');
});
if (canAccept) {
const channelSendPerms = server.channels?.send_permissions?.[chName];
if (channelSendPerms && channelSendPerms.length > 0) {
const hasChannelSendAccess = userRoles.some(rId => channelSendPerms.includes(rId));
if (!hasChannelSendAccess) canAccept = false;
if (!isServerAdmin) {
const channelPerms = server.channels?.permissions?.[chName];
if (channelPerms && channelPerms.length > 0) {
const hasChannelAccess = userRoles.some(rId => channelPerms.includes(rId));
if (!hasChannelAccess) canAccept = false;
}
}
if (canAccept) {
const hasSendPerm = userRoles.some(rId => {
const r = server.roles?.find(role => role.id === rId);
return r && r.permissions.includes('send_messages');
});
if (!hasSendPerm && server.roles && server.roles.length > 0) canAccept = false;
if (canAccept) {
const channelSendPerms = server.channels?.send_permissions?.[chName];
if (channelSendPerms && channelSendPerms.length > 0) {
const hasChannelSendAccess = userRoles.some(rId => channelSendPerms.includes(rId));
if (!hasChannelSendAccess) canAccept = false;
}
}
if (type === 'file') {
const hasFilePerm = userRoles.some(rId => {
if (canAccept) {
const hasSendPerm = userRoles.some(rId => {
const r = server.roles?.find(role => role.id === rId);
return r && r.permissions.includes('send_files');
return r && r.permissions.includes('send_messages');
});
if (!hasFilePerm && server.roles && server.roles.length > 0) canAccept = false;
if (!hasSendPerm && server.roles && server.roles.length > 0) canAccept = false;
if (type === 'file') {
const hasFilePerm = userRoles.some(rId => {
const r = server.roles?.find(role => role.id === rId);
return r && r.permissions.includes('send_files');
});
if (!hasFilePerm && server.roles && server.roles.length > 0) canAccept = false;
}
}
}
}
}
} 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)) {
@ -459,6 +475,7 @@ export async function sendDMRequest(network, targetKey, profile) {
await network.db.put('dm:' + targetKey, network.dms[targetKey]);
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 } });
network._broadcastIdentity();
}
export async function acceptDMRequest(network, targetKey) {
@ -468,6 +485,7 @@ export async function acceptDMRequest(network, targetKey) {
if (network.onDMsUpdate) network.onDMsUpdate({ ...network.dms });
}
await _appendEncryptedMessage(network, targetKey, { type: 'dm_accept' });
network._broadcastIdentity();
}
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) {
if (!network.swarm) return;
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' }); }