Added libs

This commit is contained in:
Lucas
2026-01-25 13:55:46 +10:00
parent 575c682afc
commit f70af3c4ea
229 changed files with 26983 additions and 0 deletions

1
p2private/.mtimes.json Normal file
View File

@@ -0,0 +1 @@
{"config.json": 1756118682.0519147, "dataManager.py": 1757749170.2625794, "networks.py": 1757800810.4118917, "networksApiBlueprint.py": 1757750515.750099, "p2private.py": 1757803028.377009, "p2privateApiBlueprint.py": 1757825501.4272363, "vue\\router.js": 1755335641.4535544, "vue\\api\\p2privateApi.js": 1757756279.5687172, "vue\\api\\socketEvents.js": 1757748445.9564493, "vue\\views\\HomeView.vue": 1757756292.8398924}

1
p2private/config.json Normal file
View File

@@ -0,0 +1 @@
{}

47
p2private/dataManager.py Normal file
View File

@@ -0,0 +1,47 @@
from typing import Dict, List, Optional, Any
from libs.app.common.store import DataStore
from libs.fspn.utils.sha256_util import hash_string
class DataManager():
def __init__(self):
self.store = DataStore(path="p2private/data.json", default_data={"friends":[], "messages":[]})
# ------------------- FRIEND CRUD -------------------
def add_friend(self, pubkey: str, relays):
if self.get_friend(pubkey):
raise ValueError(f"Friend {pubkey} already exists")
friend = {"pubkey": pubkey, "relays": relays}
self.store.add_item("friends", friend, unique=True, id_field="pubkey", id=pubkey)
return friend
def get_friend(self, pubkey: str):
return self.store.get_item("friends", "pubkey", pubkey)
def delete_friend(self, pubkey: str) -> bool:
return self.store.remove_item("friends", "pubkey", pubkey)
def list_friends(self):
return self.store.list_items("friends")
def update_friend(self, pubkey: str, updates: Dict[str, Any]) -> bool:
return self.store.update_item("friends", "pubkey", pubkey, updates)
# ------------------- MESSAGE CRUD -------------------
def add_message(self, message):
if self.get_message(message['hash']):
raise ValueError(f"Message {message['hash']} already exists")
self.store.add_item("messages", message, unique=True, id_field="hash", id=message["hash"])
return message
def get_message(self, hash: str):
return self.store.get_item("messages", "hash", hash)
def delete_message(self, hash: str) -> bool:
return self.store.remove_item("messages", "hash", hash)
def list_messages(self):
return self.store.list_items("messages")
def update_message(self, hash: str, updates: Dict[str, Any]) -> bool:
return self.store.update_item("messages", "hash", hash, updates)

15
p2private/info.json Normal file
View File

@@ -0,0 +1,15 @@
{
"id": "p2private",
"version": 0.043,
"modules": [
{
"id": "p2private",
"version": 0
},
{
"id": "networks",
"version": 0
}
],
"frontend": "vue"
}

158
p2private/networks.py Normal file
View File

@@ -0,0 +1,158 @@
from typing import Dict, List, Optional, Any
from libs.fspn.utils.sha256_util import hash_string
from libs.app.common.store import DataStore
from libs.app.common.logging import get_logger
from libs.noSys.noSysModule import NoSysModule
from libs.noSys.peers import Peer
from .networksApiBlueprint import Blueprint
logger = get_logger("networks")
class Networks(NoSysModule):
def __init__(self, nosys_core):
super().__init__(nosys_core)
self.store = DataStore(path="p2private/networkData.json", default_data={"networks":[], "messages":[]})
self.network_states = {}
def setup(self):
self.nosys_core.modules.api.register_blueprint(Blueprint(self).blueprint)
from .p2private import P2private
self.p2private_module:P2private = self.nosys_core.modules.get("p2private", "p2private")
from libs.p2post.p2post import P2post
self.p2post_module:P2post = self.nosys_core.modules.get("p2post", "p2post")
def on_nosys_ready(self, event):
for network in self.networks:
if not self.get_network(network["id"]):
self.add_network(network["id"])
def on_module_message(self, event):
handler_action = getattr(self, 'on_'+event.data.get("action"))
handler_action(event)
def post_network_message(self, message, networks=[]):
for network_id in networks:
if not self.get_message(message["hash"]):
self.add_message(message["hash"], message["from"], message["to"], message["content"], message["timestamp"], message["signature"])
self.add_message_to_network(network_id, message["hash"])
state = self.network_states.get(network_id, [])
for peer in state.get("peers", []):
self.send_network_message(network_id, message["hash"], peer)
def on_network_connection(self, event):
network_id = event.network_id
peer_id = event.peer.id
if network_id not in self.network_states:
self.network_states[network_id] = {"peers":[], "status":0} # TODO ENUM status
self.network_states[network_id]["peers"].append(peer_id)
self.send_network_hash(network_id, peer_id)
def send_network_hash(self, network_id:str, peer_id:str):
hash = self.get_network_hash(network_id)
self.nosys_core.dispatcher.send_message({'action':'network_hash','network':network_id, "hash":hash}, peer_id, self.id)
def on_network_hash(self, event):
peer: Peer = event.peer
data = event.data
network_id = data["network"]
if data["hash"] != self.get_network_hash(network_id):
self.send_network_messages(network_id, peer.id)
def send_network_messages(self, network_id, peer_id):
messages = self.get_network(network_id)["messages"]
self.nosys_core.dispatcher.send_message({'action':'network_messages','network':network_id, "messages":messages}, peer_id, self.id)
def on_network_messages(self, event):
peer: Peer = event.peer
data = event.data
network_id = data["network"]
messages = data["messages"]
for hash in messages:
if not self.get_message(hash):
self.send_get_network_message(network_id, hash, peer.id)
def send_get_network_message(self, network_id, message_id, peer_id):
self.nosys_core.dispatcher.send_message({'action':'get_network_message', "network":network_id, "message":message_id}, peer_id, self.id)
def on_get_network_message(self, event):
peer: Peer = event.peer
data = event.data
self.send_network_message(data["network"], data["message"], peer.id)
def send_network_message(self, network_id, message_id, peer_id):
message = self.get_message(message_id)
self.nosys_core.dispatcher.send_message({'action':'network_message', "network":network_id, "message":message}, peer_id, self.id)
def on_network_message(self, event):
data = event.data
network_id = data["network"]
message = data["message"]
if not self.get_message(message["hash"]):
self.add_message(message["hash"], message["from"], message["to"], message["content"], message["timestamp"], message["signature"])
# TODO Check if not in network messages
self.add_message_to_network(network_id, message["hash"])
self.post_network_message(message, [network_id])
self.check_message_to_me(message)
def check_message_to_me(self, message):
if self.p2post_module.data.get_user(message["to"]):
self.p2private_module.data.add_message(message)
# TODO SEND RECEIVED SIGNATURE TO THE NETWORKS IF USER LOGGED
def get_network_hash(self, network_id: str) -> str:
network = self.get_network(network_id)
if not network:
return ""
combined = ''.join(sorted(network["messages"]))
return hash_string(combined)
# ------------------- MESSAGE CRUD -------------------
def add_message(self, hash: str, from_id:str, to_id:str, content:str, timestamp, signature:str):
if self.get_message(hash):
raise ValueError(f"Message {hash} already exists")
message = {"hash": hash, "from": from_id, "to":to_id, "content":content, "timestamp":timestamp, "signature":signature}
self.store.add_item("messages", message, unique=True, id_field="hash", id=hash)
return message
def get_message(self, hash: str):
return self.store.get_item("messages", "hash", hash)
def delete_message(self, hash: str) -> bool:
return self.store.remove_item("messages", "hash", hash)
def list_messages(self):
return self.store.list_items("messages")
def update_message(self, hash: str, updates: Dict[str, Any]) -> bool:
return self.store.update_item("messages", "hash", hash, updates)
# ------------------- NETWORK CRUD -------------------
def add_network(self, network_id: str):
if self.get_network(network_id):
raise ValueError(f"Network {network_id} already exists")
network = {"id": network_id, "messages": []}
self.store.add_item("networks", network, unique=True, id_field="id", id=network_id)
return network
def get_network(self, network_id: str):
return self.store.get_item("networks", "id", network_id)
def delete_network(self, network_id: str) -> bool:
return self.store.remove_item("networks", "id", network_id)
def list_networks(self):
return self.store.list_items("networks")
def update_network(self, network_id: str, updates: Dict[str, Any]) -> bool:
return self.store.update_item("networks", "id", network_id, updates)
def add_message_to_network(self, network_id:str, message_id:str):
network = self.get_network(network_id)
if network and message_id not in network["messages"]:
network["messages"].append(message_id)
self.update_network(network_id, network)
return True
return False

View File

@@ -0,0 +1,23 @@
from libs.api.apiBlueprint import ApiBlueprint
from libs.app.common.paths import ROOT_DIR
from flask import jsonify, request, send_from_directory
import os
import copy
import random
class Blueprint(ApiBlueprint):
def routes(self):
from .networks import Networks
self.module:Networks = self.module
@self.blueprint.route('/')
def show():
return self.module.name
@self.blueprint.route('/networks', methods=["GET"])
def networks():
if request.method == "GET":
return jsonify(self.module.networks)

161
p2private/p2private.py Normal file
View File

@@ -0,0 +1,161 @@
import os
from datetime import datetime, timezone
import json
import time
from libs.fspn.utils.wrapper_util import threaded
from libs.app.common.logging import get_logger
from libs.fspn.utils.sha256_util import hash_file, hash_bytes, hash_string
from libs.noSys.noSysModule import NoSysModule
from libs.noSys.events import Events as nosys_events, DynamicEvents as nosys_dynamic_events
from libs.noSys.peers import Peer
from libs.p2post.p2post import P2post
from .networks import Networks
from .dataManager import DataManager
from .p2privateApiBlueprint import Blueprint
logger = get_logger()
class P2private(NoSysModule):
def __init__(self, noSys):
super().__init__(noSys)
self.nosys_core.subscribe_event(nosys_events.USER_ADDED, self.on_user_added)
self.data = DataManager()
self.p2post:P2post = None
self.networks_module:Networks = None
self.requested_signatures = {}
self.friends_state = {}
def setup(self):
self.p2post = self.nosys_core.modules.get("p2post", "p2post")
self.networks_module = self.nosys_core.modules.get("p2private", "networks")
self.nosys_core.modules.api.register_blueprint(Blueprint(self).blueprint)
def on_nosys_ready(self, event):
pass
def add_friend(self, pubkey, relays):
self.data.add_friend(pubkey, relays)
self.set_friend_state(pubkey)
def on_user_added(self, event):
user_id:str = event.user_id
self.manage_friends(user_id)
@threaded
def manage_friends(self, my_user):
for friend in self.data.list_friends():
self.set_friend_state(friend["pubkey"])
while True:
for friend in self.data.list_friends():
self.manage_friend(friend)
time.sleep(10)
def set_friend_state(self, friend_pubkey):
# TODO Add friends inside users object data
self.friends_state[friend_pubkey] = {
"id": friend_pubkey,
"status": "dis", # TODO Enum
}
def manage_friend(self, user):
friend_id = user["pubkey"]
state = self.friends_state[friend_id]
if state["status"] == "dis":
print("FRIEND IS DISCONNECTED")
for relay in user["relays"]:
if self.networks_module.network_states.get(relay):
print(f"Connected to relay {relay}")
# TODO Check if friend is on in the network and send message to connect to a rendezvous
elif state["status"] == "con":
print("FRIEND IS CONNECTED")
def on_module_message(self, event):
handler_action = getattr(self, 'on_'+event.data.get("action"))
handler_action(event)
def create_message(self, from_user, to_user, content, medias):
medias_data = []
for media in medias:
if media["type"] == "local":
file_hash = os.path.splitext(os.path.basename(media["file_path"]))[0]
medias_data.append({"type":"local", "hash":file_hash})
self.p2post.data.add_media(file_hash, media["file_path"])
else:
medias_data.append({"type":media["type"], "url":media["url"]})
current_utc_datetime = datetime.now(timezone.utc)
utc_timestamp = current_utc_datetime.timestamp()
message = {
"timestamp": utc_timestamp,
"from": from_user,
"to": to_user,
"content": content, #TODO Hash it
"medias": medias_data
}
message_serialized = json.dumps(message, sort_keys=True, separators=(",", ":"))
message["hash"] = hash_bytes(message_serialized.encode('utf-8'))
request_id = self.nosys_core.modules.pmc.sign(message["hash"], from_user, self.signature_callback, f"New Message {message}")
self.requested_signatures[request_id] = message
logger.debug(f"Message waiting signature {request_id}: {message}")
return message["hash"]
def signature_callback(self, request_id, signature):
message = self.requested_signatures.get(request_id)
if signature and message:
message["signature"] = signature
self.send_message(message)
def send_message(self, message):
self.data.add_message(message)
direct_connections = self.nosys_core.peers.get_by_peer_user_id(message["to"])
if direct_connections:
logger.debug(f"Friend {message['to']} direct connected")
for peer in direct_connections:
# TODO Maybe check 'from user', peers can set to just receive message from the user in connection and networks
self.send_private_message(message, peer.id)
else:
logger.debug(f"Friend {message['to']} not direct connected. Posting message to user relays.")
to_user = message["to"]
relay_networks = self.get_friend_relay_networks(to_user) # TODO get from store
if not relay_networks:
logger.error(f"Not found a network relay to friend {to_user}")
# TODO Update message status
else:
self.networks_module.post_network_message(message, relay_networks)
def send_private_message(self, message, peer_id):
payload = {'action':'private_message','message':message}
self.nosys_core.dispatcher.send_message(payload, peer_id, self.id)
def on_private_message(self, event):
data = event.data
message = data["message"]
if not self.data.get_message(message["hash"]):
self.data.add_message(message)
logger.debug(f"New message received {message}")
else:
logger.debug("Message already exists")
# Read receipts ???? Talk to friends about it
def get_friend_relay_networks(self, user_id):
user = self.data.get_friend(user_id)
if user:
return user["relays"]
return []
def on_module_connection(self, event):
peer:Peer = event.peer
peer_user = peer.connection.security.peer_user
state = self.friends_state.get(peer_user)
if state:
state["status"] = "con"

BIN
p2private/p2private.zip Normal file

Binary file not shown.

View File

@@ -0,0 +1,85 @@
from libs.api.apiBlueprint import ApiBlueprint
from libs.app.common.paths import ROOT_DIR
from flask import jsonify, request, send_from_directory
import os
import copy
import random
MEDIAS_FOLDER = os.path.join(ROOT_DIR, "files")
class Blueprint(ApiBlueprint):
def routes(self):
from .p2private import P2private
self.module:P2private = self.module
@self.blueprint.route('/')
def show():
return self.module.name
@self.blueprint.route('/friends', methods=["GET", "POST", "DELETE"])
def friends():
if request.method == "GET":
return jsonify(self.module.data.list_friends())
elif request.method == "POST":
content:dict = request.json
self.module.add_friend(content["pubkey"], content["relays"])
return jsonify()
elif request.method == "DELETE":
pass
@self.blueprint.route('/messages/<path:user_id>/<path:friend_id>', methods=["GET", "POST"])
def messages():
if request.method == "GET":
return jsonify()
elif request.method == "POST":
content:dict = request.json
message = self.module.create_message(content["from"], content["to"], content["content"], content["medias"])
return jsonify(message)
# @self.blueprint.route('/posts', methods=["GET", "POST"])
# def create_post():
# if request.method == "GET":
# raw_posts = self.module.data_store.data["posts"]
# posts = copy.deepcopy(raw_posts)
# for post in posts:
# for media in post.get("medias"):
# if media.get("type") == "local":
# media_info = self.module.data_store.get_media(media.get("hash"))
# media["file_path"] = media_info.get("file_path")
# return jsonify(posts)
# elif request.method == "POST":
# content:dict = request.json
# hash = self.module.create_post(content["user"], content["content"], content["medias"], content["networks"])
# return jsonify(hash)
# @self.blueprint.route('/medias', methods=['POST'])
# def upload_file():
# file = request.files.get("file")
# if not file:
# return jsonify({"error": "Empty file"}), 400
# file_bytes = file.read()
# file_hash = hash_bytes(file_bytes)
# file_ext = os.path.splitext(file.filename)[1]
# file_path = os.path.join(MEDIAS_FOLDER, f"{file_hash}{file_ext}")
# if not os.path.exists(file_path):
# with open(file_path, "wb") as f:
# f.write(file_bytes)
# return jsonify({
# "status": "ok",
# "hash": file_hash,
# "path": f"{file_hash}{file_ext}"
# })
# @self.blueprint.route('/medias/<filename>')
# def media_file(filename):
# return send_from_directory(MEDIAS_FOLDER, filename)

View File

@@ -0,0 +1,109 @@
const p2privateApiUrl = "/api/p2private"
export const p2privateApi = {
async getNetworks(){
try {
const response = await fetch(p2privateApiUrl+'/networks')
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Error fetching networks: ${response.status} - ${errorText}`);
}
const result = await response.json()
// p2postStore.users = result
return result;
} catch (error) {
console.error("Error fetching networks:", error);
throw error;
}
},
async getFriends(){
try {
const response = await fetch(p2privateApiUrl+'/friends')
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Error fetching networks: ${response.status} - ${errorText}`);
}
const result = await response.json()
// p2postStore.users = result
return result;
} catch (error) {
console.error("Error fetching networks:", error);
throw error;
}
},
async createMessage(from, to, content, medias){
const data = {"from":from, "to":to, "content":content, "medias":medias}
const requestOptions = {
method: 'POST',
headers: {'Content-Type': 'application/json',},
body: JSON.stringify(data),
};
try {
const response = await fetch(p2privateApiUrl+"/messages", requestOptions);
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Error fetching posts: ${response.status} - ${errorText}`);
}
const result = await response.json();
return result;
} catch (error) {
console.error("Error fetching posts:", error);
throw error;
}
},
// async createPosts(user, content, medias, networks){
// const data = {"user": user, "content": content, "medias": medias, "networks": networks};
// const requestOptions = {
// method: 'POST',
// headers: {'Content-Type': 'application/json',},
// body: JSON.stringify(data),
// };
// try {
// const response = await fetch(p2postApiUrl+"/posts", requestOptions);
// if (!response.ok) {
// const errorText = await response.text();
// throw new Error(`Error fetching posts: ${response.status} - ${errorText}`);
// }
// const result = await response.json();
// return result;
// } catch (error) {
// console.error("Error fetching posts:", error);
// throw error;
// }
// },
// async uploadFile(file) {
// const formData = new FormData();
// formData.append("file", file);
// try {
// const response = await fetch(p2postApiUrl + "/medias", {
// method: "POST",
// body: formData,
// });
// if (!response.ok) {
// const errorText = await response.text();
// throw new Error(`Error uploading file: ${response.status} - ${errorText}`);
// }
// const result = await response.json();
// return result;
// } catch (error) {
// console.error("Error uploading file:", error);
// throw error;
// }
// }
}

View File

@@ -0,0 +1,12 @@
export default function registerSocketEvents(socket) {
socket.on('connect', (data) => {
console.log('Connected p2Private')
})
// socket.on('newPost', (data) => {
// p2postStore.addPost(data)
// console.log('New Post', data)
// })
}

7
p2private/vue/router.js Normal file
View File

@@ -0,0 +1,7 @@
import HomeView from "./views/HomeView.vue";
const routes = [
{path: '/', name:'p2private', component: HomeView},
]
export {routes};

View File

@@ -0,0 +1,254 @@
<script setup>
import Button from '@/components/buttons/Button.vue';
import Card from '@/components/cards/Card.vue';
import InputComboBox from '@/components/inputs/InputComboBox.vue';
import InputText from '@/components/inputs/InputText.vue';
import Label from '@/components/labels/Label.vue';
import { CheckIcon, DocumentTextIcon, EllipsisHorizontalIcon, GlobeAltIcon, LockClosedIcon, PaperAirplaneIcon, PaperClipIcon, ShieldCheckIcon, ShieldExclamationIcon, UserPlusIcon, UsersIcon, WifiIcon } from '@heroicons/vue/24/solid';
import { onMounted, ref } from 'vue';
import { p2privateApi } from '../api/p2privateApi';
import { p2postApi } from '@/modules/p2post/api/p2postApi';
// const friends = ref([, {nickname:'Nickname2', unreadCount:2, publicKey:'1234aed233423c', isOnline:false, lastSeen:new Date()}])
const friends = ref([])
const activeFriend = ref(null)
const myUsers = ref([])
const relays = ref([])
async function getRelays(){
relays.value = await p2privateApi.getNetworks()
}
async function getFriends(){
const result = await p2privateApi.getFriends()
for (const f of result){
const m1 = {id:1, senderId:'me', content:"test Meeeeeeeeeeeeeeeeeeeeee", timestamp:new Date(), status:"SENT"}
const m2 = {id:2, senderId:'peer', content:"test Peer", status:"RECEIVED", timestamp:new Date(), files:[{name:'file1', type:'image/jpeg', size:123}]}
const mes = [m1,m2]
const fr = {nickname:'Nickname', unreadCount:5, publicKey:f.pubkey, isOnline:true, messages:mes}
friends.value.push(fr)
}
}
async function getMyUsers(){
myUsers.value = await p2postApi.getMyUsers()
}
const inputMessageText = ref("")
async function createMessage(){
const randomIndex = Math.floor(Math.random() * myUsers.value.length);
const randomUser = myUsers.value[randomIndex]
await p2privateApi.createMessage(randomUser.pubkey, activeFriend.value.publicKey, inputMessageText.value, [])
}
onMounted(()=>{
getRelays()
getFriends()
getMyUsers()
})
</script>
<template>
<div class="min-h-screen bg-black text-yellow-400">
<header class="border-b border-yellow-400/20 py-6">
<div class="container mx-auto px-6">
<div class="flex items-center space-x-4">
<div class="text-3xl font-black">
<span class="text-yellow-400">P2</span>
<span class="text-white">PRIVATE</span>
</div>
<div class="w-1 h-8 bg-yellow-400"></div>
<span class="text-gray-400">Encrypted Peer-to-Peer Messaging</span>
</div>
</div>
</header>
<div class="flex h-screen">
<!-- Sidebar -->
<div class="w-80 border-r border-yellow-400/20 flex flex-col">
<!-- Add Peer -->
<Card class="m-4 border-yellow-400/20 p-4">
<h3 class="text-lg font-bold text-yellow-400 mb-4 flex items-center">
<UserPlusIcon class="h-5 w-5 mr-2" />
Add Friend
</h3>
<div class="space-y-3">
<div>
<Label class="text-gray-300 mb-2 block text-sm">Public Key</Label>
<InputText class="bg-black border-yellow-400/30 text-white font-mono text-sm" placeholder="Public Key"/>
</div>
<div>
<Label class="text-gray-300 mb-2 block text-sm">Relay Networks</Label>
<div class="space-y-2">
<div v-for="relay of relays" class="flex items-center space-x-2">
<input type="checkbox" :id="relay.id" name="options" :value="relay.name">
<Label class="text-white text-sm">
{{relay.name}}
</Label>
</div>
</div>
</div>
<Button @click="addFriend" class="w-full bg-yellow-400 hover:bg-yellow-300">
<UserPlusIcon class="h-5 w-5 mr-2 text-black" />
<span class="text-black">Add Peer</span>
</Button>
</div>
</Card>
<!-- Friends list -->
<div class="flex-1 overflow-hidden flex flex-col">
<div class="px-4 py-2 border-b border-yellow-400/20">
<h3 class="text-lg font-bold text-yellow-400 flex items-center">
<UsersIcon class="w-5 h-5 mr-2" />
Friends ({{friends.length}})
</h3>
</div>
<div class="flex-1 overflow-y-auto p-4 space-y-2">
<div v-for="friend in friends">
<div class="flex items-center space-x-3" @click="activeFriend = friend" :class="friend === activeFriend ? 'border rounded-lg border-yellow-400/30 p-1' : 'p-3'">
<div class="relative">
<img src="" alt="" class="w-12 h-12 rounded-full" />
<div class="absolute -bottom-1 -right-1 w-4 h-4 rounded-full border-2 border-gray-500"
:class="{'bg-green-400' : true}"
></div>
</div>
<div class="flex-1 min-w-0">
<div class="flex items-center justify-between">
<span class="text-white font-semibold truncate">{{friend.nickname}}</span>
<span v-if="friend.unreadCount>0" class="bg-yellow-400 text-black text-xs font-bold px-2 py-1 rounded-full">
{{friend.unreadCount}}
</span>
</div>
<p class="text-xs text-gray-400 font-mono truncate">{{friend.publicKey}}</p>
<div class="flex items-center space-x-1 mt-1 text-xs text-gray-500">
<div v-if="friend.isOnline" class="flex flex-row">
<WifiIcon class="w-5 h-5 text-green-400" />
Online
</div>
<div v-if="!friend.isOnline" class="flex flex-row">
<WifiIcon class="h-5 w-5 text-gray-400" />
<!-- Last seen {{ friend.lastSeen.toLocaleTimeString() }} -->
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Chat -->
<div v-if="activeFriend" class="flex-1 flex flex-col">
<!-- Chat Header -->
<div class="border-b border-yellow-400/20 p-4">
<div class="flex items-center justify-between">
<div class="flex items-center space-x-3">
<div class="relative">
<img
src=""
alt=""
class="w-10 h-10 rounded-full"
/>
<div class="absolute -bottom-1 -right-1 w-3 h-3 rounded-full border-2 border-gray-500"
:class="{'bg-green-400':activeFriend.isOnline}">
</div>
</div>
<div>
<h3 class="text-white font-semibold">{{activeFriend.nickname}}</h3>
<p class="text-xs text-gray-400 font-mono">{{activeFriend.publicKey}}</p>
</div>
</div>
<div class="flex items-center space-x-2">
<div class="flex items-center space-x-1 text-green-400">
<LockClosedIcon class="h-5 w-5" />
<span class="text-sm">End-to-End Encrypted</span>
</div>
<div class="flex items-center space-x-1 text-yellow-400">
<GlobeAltIcon class="h-5 w-5"/>
<span class="text-sm">Via Network Relay</span>
</div>
</div>
</div>
</div>
<!-- Messages -->
<div class="flex-1 overflow-y-auto p-4 space-y-4">
<div v-for="message in activeFriend.messages"
:key="message.id"
class="flex justify-start"
:class="{'justify-end':message.senderId=='me'}"
>
<!-- Message Content -->
<div class="max-w-xs lg:max-w-md px-4 py-2 rounded-lg" :class="message.senderId == 'me' ? 'bg-yellow-400 text-black' : 'bg-gray-900 text-white'">
{{message.content}}
<!-- Files -->
<div class="space-y-2 mb-2">
<div v-for="file in message.files" class="flex items-center space-x-2 p-2 rounded">
<Button class="p-1 h-auto hover:bg-black/20 gap-2">
<DocumentTextIcon class="w-5 h-5" />
<p class=" text-s">{{file.name}}</p>
<p class="text-xs opacity-70">{{file.size}}</p>
</Button>
</div>
</div>
<!-- Message Footer -->
<div class="flex items-center justify-between text-xs opacity-80">
<span>{{new Date(message.timestamp).toLocaleTimeString()}}</span>
<div class="flex items-center space-x-1">
<ShieldCheckIcon v-if="true" class="w-4 h-4"/>
<ShieldExclamationIcon v-else class="w-4 h-4"/>
<CheckIcon v-if="true" class="w-4 h-4"/>
<EllipsisHorizontalIcon v-else class="w-4 h-4"/>
</div>
</div>
</div>
</div>
</div>
<!-- Message Input -->
<div class="sticky bottom-0 border-t border-yellow-400/20 p-4">
<!-- File Preview TODO from create post preview-->
<div v-if="false" class="mb-3 p-3 bg-black border border-yellow-400/20 rounded-lg">
</div>
<div class="flex items-end space-x-2">
<Button class="border border-yellow-400 text-yellow-400 hover:bg-yellow-400 hover:text-black bg-transparent">
Add Files
</Button>
<div class="flex-1">
<InputText class="bg-black border-yellow-400/30 text-white resize-none" v-model="inputMessageText"/>
</div>
<Button @click="createMessage()" class="bg-yellow-400 text-black hover:bg-yellow-300">
<!-- <Send size={16} /> -->
Send
</Button>
</div>
</div>
</div>
<!-- No Chat Selected -->
<div v-if="!activeFriend" class="flex-1 flex items-center justify-center">
<div class="text-center">
<LockClosedIcon class="w-10 h-10 text-gray-600 mx-auto mb-4" />
<h3 class="text-xl font-semibold text-gray-400 mb-2">Select a friend to start chatting</h3>
<p class="text-gray-500">Your messages are end-to-end encrypted and decentralized</p>
</div>
</div>
</div>
</div>
</template>