From 2689c5336c2ef58b1c08b5f7a06acc99f92b06ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?0=25=20=5B=E2=96=88=20=E2=96=88=20=E2=96=88=20=E2=96=88=20?= =?UTF-8?q?=E2=96=88=20=E2=96=88=20=E2=96=88=20=E2=96=88=20=E2=96=88=20?= =?UTF-8?q?=E2=96=88=5D=20100=25?= Date: Tue, 16 Jun 2026 13:19:54 -0500 Subject: [PATCH] v1.0.3 ### 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. --- Peercord Source/package.json | 2 +- Peercord Source/public/version.js | 4 +- Peercord Source/src/components/MainApp.jsx | 6 +- Peercord Source/src/p2p/handlers.js | 29 ++++- Peercord Source/src/p2p/index.js | 46 ++++++- Peercord Source/src/p2p/modules/identity.js | 6 +- Peercord Source/src/p2p/modules/messaging.js | 130 +++++++++++-------- 7 files changed, 155 insertions(+), 68 deletions(-) diff --git a/Peercord Source/package.json b/Peercord Source/package.json index 2a0fb39..38da9f5 100644 --- a/Peercord Source/package.json +++ b/Peercord Source/package.json @@ -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", diff --git a/Peercord Source/public/version.js b/Peercord Source/public/version.js index 52ec0c0..0d9403e 100644 --- a/Peercord Source/public/version.js +++ b/Peercord Source/public/version.js @@ -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%)'; diff --git a/Peercord Source/src/components/MainApp.jsx b/Peercord Source/src/components/MainApp.jsx index 58220e6..f248955 100644 --- a/Peercord Source/src/components/MainApp.jsx +++ b/Peercord Source/src/components/MainApp.jsx @@ -704,7 +704,7 @@ export default function MainApp({ profile, setProfile, onLogout, updateState, si
{/* Chat Area (Hidden if CallView is active) */} -
+
{activeView === 'dms' && activeDm === 'friends' ? ( ) : ( @@ -741,7 +741,7 @@ export default function MainApp({ profile, setProfile, onLogout, updateState, si {/* 1-on-1 Call View */} {activeCall && ( 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) {} } diff --git a/Peercord Source/src/p2p/modules/identity.js b/Peercord Source/src/p2p/modules/identity.js index a640ce7..7474936 100644 --- a/Peercord Source/src/p2p/modules/identity.js +++ b/Peercord Source/src/p2p/modules/identity.js @@ -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(); } \ No newline at end of file diff --git a/Peercord Source/src/p2p/modules/messaging.js b/Peercord Source/src/p2p/modules/messaging.js index 72c79b0..d2bcbc2 100644 --- a/Peercord Source/src/p2p/modules/messaging.js +++ b/Peercord Source/src/p2p/modules/messaging.js @@ -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; - - 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 (!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; - 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 (type === 'file') { - const hasFilePerm = userRoles.some(rId => { + + 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 (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' }); }