diff --git a/README.md b/README.md
index 3be4b45..f54e471 100644
--- a/README.md
+++ b/README.md
@@ -35,39 +35,19 @@ LocalChat is a modern local chat app that allows users to create accounts, chat
* users are able to see and message groups
* users are able to recieve and send messages in real time
+# Design
+
+## [UI Design](https://www.figma.com/design/97l0hfe6ogeRn0mjbfe4Gy/Untitled?node-id=0-1&m=dev&t=j02J3MH5C7mYCmF1-1)
# running application
## FRONTEND
-npm install
+*I nstall Live Server VS Code extension
+* Run index.html with Live Server
## Development
-npm run dev
+*I nstall Live Server VS Code extension
+* Run index.html with Live Server
## Production
-* npm run build
-* npm start
-
-## Docker frontend (if you have environement setup)
-
-* npm run docker
-* npm run docker-start
-
-docker currently running in detach mode so you will need to add the following under environment variables
-
-* ENV NEXT_PUBLIC_API_BASE_URI ACTUAL_BASE_URL
-
-# BACKEND
-
-Visual Studio
-* select web.host as startup project
-* build application
-* run application under IIS Express
-
-# FRONTEND-CI
-
-* npm run ci
+* visit galane-dev.github.io/LocalChat/
-# Setup for husky
-In the client directory run the following command
-* npm run prepare
-
diff --git a/assets/images/icons/add.png b/assets/images/icons/add.png
new file mode 100644
index 0000000..dc1d6d0
Binary files /dev/null and b/assets/images/icons/add.png differ
diff --git a/assets/images/icons/image.png b/assets/images/icons/image.png
new file mode 100644
index 0000000..bd07199
Binary files /dev/null and b/assets/images/icons/image.png differ
diff --git a/assets/images/icons/image1.png b/assets/images/icons/image1.png
new file mode 100644
index 0000000..a698876
Binary files /dev/null and b/assets/images/icons/image1.png differ
diff --git a/assets/images/icons/profile.png b/assets/images/icons/profile.png
new file mode 100644
index 0000000..44f169b
Binary files /dev/null and b/assets/images/icons/profile.png differ
diff --git a/assets/images/icons/search.png b/assets/images/icons/search.png
new file mode 100644
index 0000000..1e7d0dd
Binary files /dev/null and b/assets/images/icons/search.png differ
diff --git a/assets/images/icons/send.png b/assets/images/icons/send.png
new file mode 100644
index 0000000..1ca4cb0
Binary files /dev/null and b/assets/images/icons/send.png differ
diff --git a/assets/images/logo.png b/assets/images/logo.png
new file mode 100644
index 0000000..e084b2a
Binary files /dev/null and b/assets/images/logo.png differ
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..c8e2e6b
--- /dev/null
+++ b/index.html
@@ -0,0 +1,34 @@
+
+
+
+ Authenticate | LocalChat
+
+
+
+
+
+
+
+
Welcome Back...
+
LocalChat is a local chat app
+ Your data is stored securely in your own computer, no one but you will be able to access the data
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/js/helpers/create-message-tile.js b/js/helpers/create-message-tile.js
new file mode 100644
index 0000000..250f349
--- /dev/null
+++ b/js/helpers/create-message-tile.js
@@ -0,0 +1,20 @@
+const createMessageTile = (message, isCurrentUser) => {
+ const tile=document.createElement('li');
+ tile.id='message-tile';
+
+ const messageText=document.createElement('p');
+ messageText.id='message-tile-text';
+ messageText.innerText=message.content;
+
+ const messageDate=document.createElement('p');
+ messageDate.id='message-date';
+ messageDate.textContent=new Date(message.timestamp).toDateString();
+
+ tile.style.backgroundColor=isCurrentUser?'rgb(244, 107, 107)'
+ :'rgb(169, 169, 169)';
+ tile.append(messageText, messageDate);
+
+ return tile;
+};
+
+export default createMessageTile;
\ No newline at end of file
diff --git a/js/helpers/create-user-tile.js b/js/helpers/create-user-tile.js
new file mode 100644
index 0000000..75acdb3
--- /dev/null
+++ b/js/helpers/create-user-tile.js
@@ -0,0 +1,29 @@
+import getLastText from "./get-last-text.js";
+import openChat from "../main-page.js"
+
+const createUserTile=(currentUser, user)=>{
+ const isGroup=user.participants!==undefined;
+ const tile=document.createElement('li');
+ const profilePicture=document.createElement('img');
+ const username=document.createElement('h3');
+ const lastMessage=document.createElement('p');
+ const onlineBadge=document.createElement('h4');
+
+ profilePicture.src='../assets/images/icons/image.png';
+ username.textContent=isGroup?user.name:user.username;
+ lastMessage.textContent=getLastText(isGroup,user.id);
+ onlineBadge.textContent = '•';
+ onlineBadge.style.color = 'green';
+ onlineBadge.style.display = (user.isOnline && !isGroup) ? 'inline' : 'none';
+
+ tile.addEventListener('click',()=>{
+ isGroup?openChat(user.id,null,'group')
+ :openChat(currentUser.id,user.id,'private');
+ });
+ tile.append(profilePicture, username, lastMessage, onlineBadge);
+
+ return tile;
+
+}
+
+export default createUserTile;
\ No newline at end of file
diff --git a/js/helpers/display-messages.js b/js/helpers/display-messages.js
new file mode 100644
index 0000000..28bc3f1
--- /dev/null
+++ b/js/helpers/display-messages.js
@@ -0,0 +1,24 @@
+import createMessageTile from "./create-message-tile.js";
+
+const displayMessages=(chat,messagesCount,currentUser)=>{
+ const chatsList=document.getElementById('chats-list');
+ const noMessages=document.getElementById('no-messages');
+
+ if(messagesCount>0){
+ chatsList.style.display='inline';
+ noMessages.style.display='none';
+ }
+ else{
+ chatsList.style.display='none';
+ noMessages.style.display='inline';
+ return;
+ }
+
+ chat.messages.forEach(currentMessage => {
+ const message= currentMessage;
+ const messageTile = createMessageTile(message, message.senderId === currentUser);
+ chatsList.appendChild(messageTile);
+ });
+}
+
+export default displayMessages;
\ No newline at end of file
diff --git a/js/helpers/get-chat-data.js b/js/helpers/get-chat-data.js
new file mode 100644
index 0000000..73dc2e8
--- /dev/null
+++ b/js/helpers/get-chat-data.js
@@ -0,0 +1,25 @@
+import Chat from "../models/chat.js";
+import User from "../models/user.js";
+import LocalStorageService from "../services/local-storage.js";
+
+const getChatData=(user1,user2,type)=>{
+ const chatsList=document.getElementById('chats-list');
+ chatsList.innerHTML='';
+ let chatId,currentChat,messagesCount;
+ if(type==='private'){
+ chatId=Chat.generateChatId(user1, user2);
+ const allChats=LocalStorageService.getChats();
+ currentChat=allChats.find(chat=>chat.id===chatId);
+ }
+ else{
+ const groups=LocalStorageService.getGroups();
+ currentChat=groups.find(group=>group.id===user1);
+ chatId=user1;
+ }
+
+ messagesCount=currentChat?.messages?.length||0;
+
+ return {chatId,currentChat,messagesCount};
+}
+
+export default getChatData;
\ No newline at end of file
diff --git a/js/helpers/get-last-text.js b/js/helpers/get-last-text.js
new file mode 100644
index 0000000..e90b37c
--- /dev/null
+++ b/js/helpers/get-last-text.js
@@ -0,0 +1,32 @@
+import LocalStorageService from "../services/local-storage.js";
+import SessionManager from "../services/session-manager.js";
+import Chat from "../models/chat.js";
+
+const getLastText=(isGroup,userId)=>{
+ let allChats = LocalStorageService.getChats();
+ let allGroups = LocalStorageService.getGroups();
+ let currentUserId = SessionManager.getUser().id;
+ let lastMsgText='No message yet'
+
+ if (isGroup) {
+ let group = allGroups.find(g=>g.id===userId);
+
+ if (group && group.messages && group.messages.length > 0) {
+ let lastMsg = group.messages[group.messages.length - 1];
+ lastMsgText = lastMsg.content;
+
+ }
+ }
+ else {
+ let chatId = Chat.generateChatId(currentUserId, userId);
+ let chat = allChats.find(c => c.id === chatId);
+
+ if (chat && chat.messages && chat.messages.length > 0) {
+ let lastMsg = chat.messages[chat.messages.length - 1];
+ lastMsgText = lastMsg.content;
+ }
+ }
+ return lastMsgText;
+}
+
+export default getLastText;
\ No newline at end of file
diff --git a/js/helpers/set-up-send-message-handler.js b/js/helpers/set-up-send-message-handler.js
new file mode 100644
index 0000000..afb53f8
--- /dev/null
+++ b/js/helpers/set-up-send-message-handler.js
@@ -0,0 +1,21 @@
+import LocalStorageService from "../services/local-storage.js";
+import openChat from "../main-page.js";
+
+const setupSendMessageHandler = (userId1, userId2, type, chatId) => {
+ const sendIcon=document.getElementById('message-send');
+ sendIcon.onclick = null;
+ sendIcon.onclick=()=>{
+ const content= document.getElementById('message-text').value.trim();
+ if (content==='') return;
+
+ if (type==='group') {
+ LocalStorageService.messageGroup(chatId, content, 'none');
+ } else {
+ LocalStorageService.sendMessage(chatId, content, 'none');
+ }
+ document.getElementById('message-text').value = '';
+ openChat(userId1, userId2, type);
+ };
+};
+
+export default setupSendMessageHandler;
\ No newline at end of file
diff --git a/js/helpers/switch-views.js b/js/helpers/switch-views.js
new file mode 100644
index 0000000..9f1a18f
--- /dev/null
+++ b/js/helpers/switch-views.js
@@ -0,0 +1,27 @@
+const switchMobileView=(view)=>{
+ const nav = document.querySelector('nav');
+ const main = document.querySelector('main');
+ const aside = document.querySelector('aside');
+
+ const isMobile = window.matchMedia("(max-width:600px)").matches;
+
+ if(!isMobile){
+ return;
+ }
+
+ nav.style.display = 'none';
+ main.style.display = 'none';
+ aside.style.display = 'none';
+
+ if(view === 'nav'){
+ nav.style.display = 'block';
+ }
+ else if(view === 'chat'){
+ main.style.display = 'flex';
+ }
+ else if(view === 'profile'){
+ aside.style.display = 'block';
+ }
+}
+
+export default switchMobileView;
diff --git a/js/helpers/update-header.js b/js/helpers/update-header.js
new file mode 100644
index 0000000..ac83e3a
--- /dev/null
+++ b/js/helpers/update-header.js
@@ -0,0 +1,25 @@
+import LocalStorageService from "../services/local-storage.js";
+
+const updateChatHeader=(chateeId,chatData,type)=>{
+ const chateeName=document.getElementById('chatee-name');
+ const chateeSideName=document.getElementById('chatee-side-name');
+ const chateeStatus=document.getElementById('chatee-status');
+ const chateeSideStatus=document.getElementById('chatee-side-status');
+
+ if(type==='private'){
+ const chatee=LocalStorageService.getUser(chateeId);
+ chateeName.textContent=chatee.username;
+ chateeSideName.textContent=chatee.username;
+ chateeStatus.textContent=chatee.isOnline?'Online':'Offline';
+ chateeSideStatus.textContent=chateeStatus.textContent;
+ }
+ else{
+ chateeName.textContent=chatData.currentChat.name;
+ chateeSideName.textContent=chatData.currentChat.name;
+ chateeStatus.textContent='Group';
+ chateeSideStatus.textContent='Group';
+ }
+
+}
+
+export default updateChatHeader;
\ No newline at end of file
diff --git a/js/landing-page.js b/js/landing-page.js
new file mode 100644
index 0000000..fc82e84
--- /dev/null
+++ b/js/landing-page.js
@@ -0,0 +1,63 @@
+import LocalStorageService from "./services/local-storage.js";
+import SessionManager from "./services/session-manager.js";
+import User from "./models/user.js";
+
+let isSignUp=true;
+const defaultView=document.getElementById('toggle-action');
+let authButton=document.getElementById('submit-button');
+
+//Toggle view for toggling between login and signup
+const toggleView=()=>{
+ let heading=document.getElementById('welcome-heading');
+ let toggleQuestion=document.getElementById('toggle-question');
+ let toggleView=document.getElementById('toggle-action');
+ let toggleMessage=document.getElementById('welcome-message');
+ let submitButton=document.getElementById('submit-button');
+ if(isSignUp){
+ heading.textContent='Hey newbie...';
+ toggleQuestion.textContent='Already have an account?';
+ toggleMessage.innerHTML='LocalChat is a local chat app. Your data is stored securely in your own computer, no one but you will be able to access the data.';
+ toggleView.textContent="Sign In";
+ submitButton.textContent='Sign Up';
+ }
+ else{
+ heading.textContent='Welcome Back...';
+ toggleQuestion.textContent='Don’t have an account?';
+ toggleMessage.innerHTML='LocalChat is a local chat app. Your data is stored securely in your own computer, no one but you will be able to access the data.';
+ toggleView.textContent='Sign Up';
+
+ }
+ isSignUp=!isSignUp;
+}
+
+
+const authenticate=()=>{
+ let username=document.getElementById('username-input').value;
+ let password=document.getElementById('password-input').value;
+ if(!isSignUp){
+ if(User.isUserNameUnique(username)){
+ LocalStorageService.createUser(username,password);
+ }
+ else{
+ alert('Please enter a new username, make it unique');
+ return;
+ }
+ }
+ //Login
+ let user=SessionManager.login(username,password);
+ if(user){
+ window.location.replace('./pages/main.html');
+ }
+ else{
+ alert('Login failed, ensure you have entered correct credentials');
+ return;
+ }
+}
+
+
+const main=()=>{
+ defaultView.addEventListener('click',()=>toggleView());
+ authButton.addEventListener('click', ()=>authenticate());
+}
+
+main();
\ No newline at end of file
diff --git a/js/main-page.js b/js/main-page.js
new file mode 100644
index 0000000..25ccfcb
--- /dev/null
+++ b/js/main-page.js
@@ -0,0 +1,160 @@
+import LocalStorageService from "./services/local-storage.js";
+import SessionManager from "./services/session-manager.js";
+import Chat from "./models/chat.js";
+import switchMobileView from "./helpers/switch-views.js";
+import createUserTile from "./helpers/create-user-tile.js";
+import User from "./models/user.js";
+import getChatData from "./helpers/get-chat-data.js";
+import updateChatHeader from "./helpers/update-header.js";
+import displayMessages from "./helpers/display-messages.js";
+import setupSendMessageHandler from "./helpers/set-up-send-message-handler.js";
+
+
+//State
+let currentChatId=null;
+let currentChatUser1=null;
+let currentChatUser2=null;
+let currentChatType='private';
+
+
+//Populate the users list with users from local storage
+const populateUsersList=(users)=>{
+ const usersList=document.getElementById('users-list');
+ const noUsersText=document.getElementById('no-users');
+ const currentUser=SessionManager.getUser();
+ usersList.innerHTML='';
+
+ if(users.length===0){
+ usersList.style.display='none';
+ noUsersText.style.display='block';
+ return;
+ }
+
+ usersList.style.display='block';
+ noUsersText.style.display='none';
+
+ users.forEach(user => {
+ const userTile=createUserTile(currentUser,user);
+ usersList.append(userTile);
+ });
+}
+
+
+//What happens when we click on a tile from users list
+const openChat=(userId1,userId2,type='private')=>{
+ switchMobileView('chat');
+ const chatData=getChatData(userId1,userId2,type);
+ updateChatHeader(userId2,chatData,type);
+ displayMessages(chatData.currentChat, chatData.messagesCount, SessionManager.getUser().id);
+ setupSendMessageHandler(userId1, userId2, type, chatData.chatId);
+
+ currentChatId = chatData.chatId;
+ currentChatUser1 = userId1;
+ currentChatUser2 = userId2;
+ currentChatType = type;
+}
+
+
+
+
+const searchUsers = () => {
+ const users = LocalStorageService.getUsers();
+ const searchText = document.getElementById('search-text').value.toLowerCase();
+ const filteredUsers = users.filter(user =>
+ user.username.toLowerCase().includes(searchText)
+ );
+ populateUsersList(filteredUsers);
+};
+
+const updateProfile = () => {
+ const username = document.getElementById('username-input').value;
+ const password = document.getElementById('password-input').value;
+ if(username.length===0 || password.length===0){
+ alert('Update failed. Please enter a valid username and password');
+ return;
+ }
+
+ if(!User.isUserNameUnique(username)){
+ alert('Update failed. Username taken, please enter a different username');
+ return;
+ }
+ LocalStorageService.updateProfile(username, password);
+};
+
+const createGroup = () => {
+ const input = prompt('Enter group name and member Ids(e.g "Group Name":id1,id2,id3...)');
+ const [groupName, membersString] = input.split(':');
+ const participants = membersString.split(',');
+
+ if(groupName.trim().length===0 || participants.length<2){
+ alert('Failed to create group. Ensure you set group name and add 1 or more participant');
+ return;
+ }
+
+ participants.push(SessionManager.getUser().username);
+ LocalStorageService.createGroup(groupName, participants);
+ location.reload();
+};
+
+const showUserProfile = () => {
+ document.getElementById('current-user').style.display = 'block';
+ document.getElementById('chatee').style.display = 'none';
+ switchMobileView('profile');
+};
+
+const showChateeProfile = () => {
+ document.getElementById('current-user').style.display = 'none';
+ document.getElementById('chatee').style.display = 'block';
+ switchMobileView('profile');
+};
+
+const cancelEdits = () => {
+ switchMobileView('nav');
+};
+
+const setupEventListeners = () => {
+ document.getElementById('search-icon').addEventListener('click', searchUsers);
+ document.getElementById('save-edits').addEventListener('click', updateProfile);
+ document.getElementById('logout').addEventListener('click', () => SessionManager.logout());
+ document.getElementById('add-group-icon').addEventListener('click', createGroup);
+ document.getElementById('profile-icon').addEventListener('click', showUserProfile);
+ document.getElementById('chatee-info').addEventListener('click', showChateeProfile);
+ document.getElementById('cancel-edits').addEventListener('click', cancelEdits);
+ document.getElementById('profile-back').addEventListener('click', () => switchMobileView('nav'));
+};
+
+const loadInitialData = () => {
+ let users=LocalStorageService.getUsers();
+ const groups=LocalStorageService.getGroups();
+ const currentUser=SessionManager.getUser();
+
+ users=users.filter(user=>user.id !== currentUser.id);
+ const myGroups=groups.filter(group=>group.participants.includes(currentUser.username));
+
+ let onlineUsers=users.filter(user=>user.isOnline===true);
+ let offlineUsers=users.filter(user=>user.isOnline===false);
+
+ users=[...onlineUsers,...myGroups,...offlineUsers];
+
+ populateUsersList(users);
+};
+
+const handleStorageChange = (event) => {
+ if (['chats', 'groups', 'users'].includes(event.key)) {
+ if (currentChatId !== null) {
+ openChat(currentChatUser1, currentChatUser2, currentChatType);
+ }
+ loadInitialData();
+ }
+};
+
+const main = () => {
+ setupEventListeners();
+ loadInitialData();
+ window.addEventListener('storage', handleStorageChange);
+};
+
+main();
+
+
+export default openChat;
\ No newline at end of file
diff --git a/js/models/chat.js b/js/models/chat.js
new file mode 100644
index 0000000..fa72f55
--- /dev/null
+++ b/js/models/chat.js
@@ -0,0 +1,19 @@
+class Chat{
+ constructor(chatId){
+ this.id=chatId;
+ this.user1Typing=false;
+ this.user2Typing=false;
+ this.messages=[];
+ }
+
+ static generateChatId=(userId1,userId2)=>{
+ if(userId1>userId2){
+ return userId1+'-'+userId2;
+ }
+ else{
+ return userId2+'-'+userId1;
+ }
+ }
+}
+
+export default Chat;
\ No newline at end of file
diff --git a/js/models/group.js b/js/models/group.js
new file mode 100644
index 0000000..651c9e4
--- /dev/null
+++ b/js/models/group.js
@@ -0,0 +1,10 @@
+class Group{
+ constructor(name,participants){
+ this.id=Math.floor(Math.random()*99999);
+ this.name=name;
+ this.messages=[];
+ this.participants=participants;
+ }
+}
+
+export default Group;
\ No newline at end of file
diff --git a/js/models/message.js b/js/models/message.js
new file mode 100644
index 0000000..6d0b28e
--- /dev/null
+++ b/js/models/message.js
@@ -0,0 +1,12 @@
+import SessionManager from "../services/session-manager.js";
+class Message{
+ constructor(content,replyTo){
+ this.id=Math.floor(Math.random()*99999);
+ this.content=content;
+ this.senderId=SessionManager.getUser().id;
+ this.replyTo=replyTo;
+ this.timestamp=Date.now();
+ }
+}
+
+export default Message;
diff --git a/js/models/user.js b/js/models/user.js
new file mode 100644
index 0000000..90f1b8e
--- /dev/null
+++ b/js/models/user.js
@@ -0,0 +1,24 @@
+import LocalStorageService from "../services/local-storage.js";
+
+class User{
+ constructor(username,password){
+
+ this.id=User.generateUserId();
+ this.username=username;
+ this.password=password;
+ this.isLoggedIn=false;
+ this.isOnline=false;
+ this.lastSeen=Date.now();
+ }
+
+ static isUserNameUnique=(username)=>{
+ let users=LocalStorageService.getUsers();
+ return !(users.find(user=>user.username===username));
+ }
+
+ static generateUserId=()=>{
+ return LocalStorageService.getUsers().length+1;
+ }
+}
+
+export default User;
\ No newline at end of file
diff --git a/js/services/local-storage.js b/js/services/local-storage.js
new file mode 100644
index 0000000..1089c0f
--- /dev/null
+++ b/js/services/local-storage.js
@@ -0,0 +1,103 @@
+import Message from "../models/message.js";
+import User from "../models/user.js";
+import Chat from "../models/chat.js";
+import SessionManager from "./session-manager.js";
+import Group from "../models/group.js";
+class LocalStorageService{
+ constructor(){
+
+ }
+
+ static createUser=(username,password)=>{
+ let users=JSON.parse(localStorage.getItem('users'))||[];
+ const user=new User(username,password);
+ users.push(user);
+ localStorage.setItem('users',JSON.stringify(users));
+ }
+
+ static createChat=(userId1,userId2)=>{
+ let chats=JSON.parse(localStorage.getItem('chats'));
+ let chatId=Chat.generateChatId(userId1,userId2)
+ let chat=new Chat(chatId);
+ chats.push(chat);
+ localStorage.setItem('chats',JSON.stringify(chats));
+ }
+
+ static sendMessage=(chatId,content,replyTo)=>{
+ let message=new Message(content,replyTo);
+ let chats=LocalStorageService.getChats();
+ let currentChat=chats.find(chat=>chat.id===chatId);
+ if(!currentChat){
+ currentChat=new Chat(chatId);
+ chats.push(currentChat);
+ }
+
+ currentChat.messages.push(message);
+
+ for(let i=0;i{
+ let message=new Message(content,replyTo);
+ let groups=LocalStorageService.getGroups();
+ for(let i=0;i{
+ let groups=JSON.parse(localStorage.getItem('groups'))||[];
+ let group=new Group(name,particpants);
+ groups.push(group);
+ localStorage.setItem('groups',JSON.stringify(groups));
+ }
+
+ static getUsers=()=>{
+ return JSON.parse(localStorage.getItem('users'))||[];
+ }
+
+ static getGroups=()=>{
+ return JSON.parse(localStorage.getItem('groups'))||[];
+ }
+
+ static getChats=()=>{
+ return JSON.parse(localStorage.getItem('chats'))||[];
+ }
+
+ static getUser=(userId)=>{
+ let users=LocalStorageService.getUsers();
+ return users.find(user=>user.id===userId);
+ }
+
+
+ static updateProfile=(username,password)=>{
+ let users=LocalStorageService.getUsers();
+ let user=SessionManager.getUser();
+
+ if(user){
+ for(let i=0;i{
+ let users=LocalStorageService.getUsers();
+ let user=users.find(user=>(user.username===username && user.password===password));
+ if(user){
+ for(let i=0;i{
+ let users=LocalStorageService.getUsers();
+ let user=SessionManager.getUser();
+
+ if(user){
+ for(let i=0;i{
+ return JSON.parse(sessionStorage.getItem('user'));
+ }
+
+}
+
+
+export default SessionManager;
\ No newline at end of file
diff --git a/pages/main.html b/pages/main.html
new file mode 100644
index 0000000..da556ac
--- /dev/null
+++ b/pages/main.html
@@ -0,0 +1,102 @@
+
+
+
+ Main | LocalChat
+
+
+
+
+
+
+
+
+
+
+