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.
This commit is contained in:
parent
064a9b4cf4
commit
2689c5336c
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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%)';
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
||||||
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) {
|
function handleWhois(network, parsed, conn) {
|
||||||
|
|
|
||||||
|
|
@ -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) {}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
|
||||||
}
|
}
|
||||||
|
|
@ -7,32 +7,37 @@ 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) {
|
||||||
const topicHex = ch.substring(0, 64);
|
if (ch.length > 64 && ch[64] === '-') {
|
||||||
const chName = ch.substring(65);
|
const topicHex = ch.substring(0, 64);
|
||||||
if (!joinedTopics.has(topicHex)) return false;
|
const chName = ch.substring(65);
|
||||||
|
if (!joinedTopics.has(topicHex)) return false;
|
||||||
|
|
||||||
const server = network.servers.find(s => s.topicHex === topicHex);
|
const server = network.servers.find(s => s.topicHex === topicHex);
|
||||||
if (server && network.myKey !== server.owner && network.myKey !== ADMIN_PUBLIC_KEY) {
|
if (server && network.myKey !== server.owner && network.myKey !== ADMIN_PUBLIC_KEY) {
|
||||||
const userRoles = server.memberRoles?.[network.myKey] || [];
|
const userRoles = server.memberRoles?.[network.myKey] || [];
|
||||||
const isServerAdmin = userRoles.some(rId => {
|
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 r = server.roles?.find(role => role.id === 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;
|
return true;
|
||||||
|
|
@ -352,50 +357,61 @@ 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) {
|
|
||||||
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) {
|
if (server) {
|
||||||
const channelPerms = server.channels?.permissions?.[chName];
|
canAccept = true;
|
||||||
if (channelPerms && channelPerms.length > 0) {
|
if (msg.sender !== server.owner && msg.sender !== ADMIN_PUBLIC_KEY) {
|
||||||
const hasChannelAccess = userRoles.some(rId => channelPerms.includes(rId));
|
const userRoles = server.memberRoles?.[msg.sender] || [];
|
||||||
if (!hasChannelAccess) canAccept = false;
|
const isServerAdmin = userRoles.some(rId => {
|
||||||
}
|
const r = server.roles?.find(role => role.id === rId);
|
||||||
|
return r && r.permissions.includes('admin');
|
||||||
|
});
|
||||||
|
|
||||||
if (canAccept) {
|
if (!isServerAdmin) {
|
||||||
const channelSendPerms = server.channels?.send_permissions?.[chName];
|
const channelPerms = server.channels?.permissions?.[chName];
|
||||||
if (channelSendPerms && channelSendPerms.length > 0) {
|
if (channelPerms && channelPerms.length > 0) {
|
||||||
const hasChannelSendAccess = userRoles.some(rId => channelSendPerms.includes(rId));
|
const hasChannelAccess = userRoles.some(rId => channelPerms.includes(rId));
|
||||||
if (!hasChannelSendAccess) canAccept = false;
|
if (!hasChannelAccess) canAccept = false;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (canAccept) {
|
if (canAccept) {
|
||||||
const hasSendPerm = userRoles.some(rId => {
|
const channelSendPerms = server.channels?.send_permissions?.[chName];
|
||||||
const r = server.roles?.find(role => role.id === rId);
|
if (channelSendPerms && channelSendPerms.length > 0) {
|
||||||
return r && r.permissions.includes('send_messages');
|
const hasChannelSendAccess = userRoles.some(rId => channelSendPerms.includes(rId));
|
||||||
});
|
if (!hasChannelSendAccess) canAccept = false;
|
||||||
if (!hasSendPerm && server.roles && server.roles.length > 0) canAccept = false;
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (type === 'file') {
|
if (canAccept) {
|
||||||
const hasFilePerm = userRoles.some(rId => {
|
const hasSendPerm = userRoles.some(rId => {
|
||||||
const r = server.roles?.find(role => role.id === 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)) {
|
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]);
|
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' }); }
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user