From 631914c3a764417db37d087d8a0fa9c58cf714b9 Mon Sep 17 00:00:00 2001 From: marlon-erler Date: Tue, 9 Jul 2024 04:35:38 +0200 Subject: [PATCH 01/22] cleanup --- src/model.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/model.ts b/src/model.ts index 54a2f243..a77b0a76 100644 --- a/src/model.ts +++ b/src/model.ts @@ -2,12 +2,10 @@ import * as React from "bloatless-react"; import { arrayToUint8, - decode, decrypt, encrypt, generateIV, importKey, - splitMergedString, uInt8ToArray, } from "./cryptUtility"; From 4003ab701a3859f78332b522a11c8b27f8e3fe98 Mon Sep 17 00:00:00 2001 From: marlon-erler Date: Tue, 9 Jul 2024 05:26:54 +0200 Subject: [PATCH 02/22] cleanup, start rebuilding model --- bun.lockb | Bin 11423 -> 11435 bytes package.json | 2 +- src/Model/chatModel.ts | 160 ++++++++++++++++++++ src/Model/model.ts | 65 ++++++++ src/Model/viewModel.ts | 21 +++ src/cryptUtility.ts | 34 +++++ src/model.ts | 333 ----------------------------------------- src/utility.ts | 21 +++ 8 files changed, 302 insertions(+), 334 deletions(-) create mode 100644 src/Model/chatModel.ts create mode 100644 src/Model/model.ts create mode 100644 src/Model/viewModel.ts delete mode 100644 src/model.ts create mode 100644 src/utility.ts diff --git a/bun.lockb b/bun.lockb index 416051a8aabb66ae8dba6f1d7de15f98a7699244..51310240ecabd8c56df4778e3db27cc7454b92ef 100755 GIT binary patch delta 1068 zcmYLIZAepL6n^h@x;v(<>E^oLWSdb2wskc%G)oED#L6`b!^*_@Q4SYDnnErY(TCE9 z^5WN@OfmW}*pESye_~o7_(zi0PO1|N~)R6wDD4p12I4-TpaTm=*cd;%2opcMN9PRdSEskoocr&{nFoge)e&Q^%O z(1)XEf!V^0jY6m>%&LHi>a$304cLjWnxh~UZNNm|1Te8y9Zh^^fr+^RU?t(&wTPM% z6X^o12q#D7DS(LW)Jjr%8kjhC0GN2tWnjhOb_W>ZYX&CzCL%etO6sR|0JFgtfe(h@ zeNhZ{(y6$UW~iY+xvEB8@cpl)m08^Gd3$ozl^YYttTd+ed0)}(+@ryg;fg%F=GV5t z&+>QMkGrp*4>8Vnp50e^J-3!8cP33;crz1=w{`az4|G<;l)#K(i-PV7{J_94-7q^2 zvM`dzKD{C^Z8)Rk$Z{1jR~>r&Mm9}ppjucrKh8I@S>)eMDo^#=et5aK#Se`vN4DV!$B)gfRG*V|9S#~A)2xmLuytGq@x9{k|24tdHU@6H86u5pa>q z;7Z(5X^grJ*|m`GfTPg4S0HSxA_j0fVq^+*o)~_-g?O5s%@-F4>(eqR8%(pkT(AxJeUcj delta 1055 zcmYLIdq`7Z6#wq^&fVc-E}i3YlR2}8h-=P_UNk}7VnsfBh=s;{giZxTg+f|`(F+Qc zPYnf&k}_EYF+$Q{O$ssyw2)AVu|R2M_J^?`4Lf)De!KtN-|w8?IluGWd(Y>szF*yI zG)(z}?W-T!E)Sc#@;_JpsPV6U@6Yk9Jg2|@oaxO;f2{5+J9t%%5K2VI-Pl>z-Q3!M zI%tHpL0k)%3TOvR13Vz*v!ys0Fdgz6lvo!A6#C`?g?c}rpnIjbA5aIeOUkzZg1_Be zB^8tc3I_R7%mWGrETC|ZbvURNFbF6Nm;n^@7g9XxtDtNY#p5B`Lbc)r+M@8l8B6d; z{1FC-6@Q@*E0W?6Iu>PTfR#kqP%IY$b|RX~V@O9QFtKg|nAp1oOsqSuAUO=|WVAN~ zOmc~jxd0P;r+|rmTd|x?Njfkv@uUmD>`}iREa`0lCOS{Va$CTnewtzx^uiZ`4~F1{ zkpfrHyKyhgQiHx~RYOA2vQ?Sbm{;((*s?wlM?Xs12%W+&pS_LPo9ka!Mar)a4=~BU zSBpm9?{)eoQbrQIE3Q}G@v(V}rpMz9HT}Z>(r&O0a87_JzB%0{1?~*Ktz+3+Fgppd zFw%tGTE;i4JFVo&a+R>{Wm;`yKTWqfx*9vX5E{i6Jy%Hn-8*IZUF)~DX@)Wx%>_mi zLgepO9-YnIo}n3&l_0*O=XkOW_juFfr*Qo_S!Z!T&%qijK^57hX|!hB6{d^is#2`Z zt)tlIUAa+tdo^&oM+&8avcN9eNR$# lwm-6N#|{HW)#4s_cHjZSHA;#3tbM-oS=}nC5YOc<{s)x}4deg- diff --git a/package.json b/package.json index 96bd64f7..27806c66 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "dependencies": { - "bloatless-react": "^1.2.5", + "bloatless-react": "^1.2.6", "esbuild": "latest", "udn-frontend": "^1.0.7-beta1" }, diff --git a/src/Model/chatModel.ts b/src/Model/chatModel.ts new file mode 100644 index 00000000..2cf82cd7 --- /dev/null +++ b/src/Model/chatModel.ts @@ -0,0 +1,160 @@ +import * as React from "bloatless-react"; + +import { UDN, isConnected } from "./model"; +import { decryptString, encryptString } from "../cryptUtility"; + +import { senderName } from "./viewModel"; +import { storageKeys } from "../utility"; + +// TYPES +// message +export interface Message { + channel: string; + sender: string; + body: string; + isoDate: string; +} + +export class Message { + constructor( + public channel: string, + public sender: string, + public body: string, + public isoDate: string + ) {} +} + +// chat +export class Chat { + id: string; + + primaryChannel: React.State; + secondaryChannels: React.ListState; + + encryptionKey: React.State; + messages: React.ListState; + + composingMessage: React.State; + cannotSendMessage: React.State; + + newSecondaryChannelName: React.State; + cannotAddSecondaryChannel: React.State; + + // init + constructor(id: string = React.UUID()) { + this.primaryChannel = React.restoreState( + storageKeys.primaryChannel(id), + "" + ); + this.secondaryChannels = React.restoreListState( + storageKeys.secondaryChannels(id) + ); + + this.encryptionKey = React.restoreState(storageKeys.encyptionKey(id), ""); + this.messages = React.restoreListState(storageKeys.messages(id)); + + this.composingMessage = React.restoreState( + storageKeys.composingMessage(id), + "" + ); + this.newSecondaryChannelName = new React.State(""); + + this.cannotSendMessage = React.createProxyState( + [this.primaryChannel, this.composingMessage, isConnected], + () => + this.primaryChannel.value == "" || + this.composingMessage.value == "" || + isConnected.value == false + ); + this.cannotAddSecondaryChannel = React.createProxyState( + [this.newSecondaryChannelName], + () => this.newSecondaryChannelName.value == "" + ); + } + + // messages + async sendMessage(): Promise { + if (this.cannotSendMessage.value == true) return; + + // get channels + const secondaryChannelNames: string[] = [ + ...this.secondaryChannels.value.values(), + ].map((channel) => channel); + const allChannelNames: string[] = [ + this.primaryChannel.value, + ...secondaryChannelNames, + ]; + + // encrypt + const encrypted = + (await encryptString( + this.composingMessage.value, + this.encryptionKey.value + )) || this.composingMessage.value; + if (encrypted == undefined) return; + + // create object + const joinedChannelName = allChannelNames.join("/"); + const messageObject = new Message( + joinedChannelName, + senderName.value, + encrypted, + new Date().toISOString() + ); + const messageString = JSON.stringify(messageObject); + + // send + UDN.sendMessage(joinedChannelName, messageString); + + // clear + this.composingMessage.value = ""; + } + + clearMessages(): void { + this.messages.clear(); + } + + deleteMessage(message: Message): void { + this.messages.remove(message); + } + + async decryptReceivedMessage(message: Message): Promise { + message.body = await decryptString(message.body, this.encryptionKey.value); + this.messages.callSubscriptions(); + } + + // channel + setChannel(): void {} + + addSecondaryChannel(): void { + if (this.cannotAddSecondaryChannel.value == true) return; + + this.secondaryChannels.add(this.newSecondaryChannelName.value); + this.newSecondaryChannelName.value = ""; + } + + removeSecondaryChannel(channel: string): void { + this.secondaryChannels.remove(channel); + } +} + +// INIT +const chatIds = React.restoreListState("chat-ids"); +const chatArray = [...chatIds.value.values()].map((id) => new Chat(id)); +export const chats = new React.ListState(chatArray); + +// METHODS +export function createChatWithName(name: string): void { + const newChat = new Chat(); + newChat.primaryChannel.value = name; + chatIds.add(newChat.id); +} + +export function removeChat(chat: Chat): void { + Object.values(storageKeys).forEach((cb) => { + localStorage.removeItem(cb(chat.id)); + }); + + chats.remove(chat); + chatIds.remove(chat.id); +} diff --git a/src/Model/model.ts b/src/Model/model.ts new file mode 100644 index 00000000..34525a6f --- /dev/null +++ b/src/Model/model.ts @@ -0,0 +1,65 @@ +import * as React from "bloatless-react"; + +import { + arrayToUint8, + decrypt, + encrypt, + generateIV, + importKey, + uInt8ToArray, +} from "../cryptUtility"; + +import UDNFrontend from "udn-frontend"; +import { serverAddress } from "./viewModel"; +import { translation } from "../translations"; + +export const UDN = new UDNFrontend(); + +// CONNECTION +// state +export const isConnected = new React.State(false); +export const currentAddress = new React.State(""); + +export const cannotDisonnect = React.createProxyState( + [isConnected], + () => isConnected.value == false +); + +export const cannotConnect = React.createProxyState( + [serverAddress, currentAddress, isConnected], + () => + serverAddress.value == "" || + (currentAddress.value == serverAddress.value && isConnected.value == true) +); + +// methods +export function connect(): void { + if (cannotConnect.value == true) return; + currentAddress.value = serverAddress.value; + isConnected.value = false; + UDN.connect(serverAddress.value); +} + +export function disconnect(): void { + if (cannotDisonnect.value == true) return; + UDN.disconnect(); +} + +// LISTENERS +UDN.onconnect = () => { + isConnected.value = true; +}; + + +UDN.onmessage = (data) => { + +}; + +UDN.ondisconnect = () => { + isConnected.value = false; +}; + +// INIT +if (serverAddress.value != "") { + connect(); +} diff --git a/src/Model/viewModel.ts b/src/Model/viewModel.ts new file mode 100644 index 00000000..085771b9 --- /dev/null +++ b/src/Model/viewModel.ts @@ -0,0 +1,21 @@ +import * as React from "bloatless-react"; + +import { translation } from "../translations"; + +// connection +export const currentAddress = new React.State(""); + +export const serverAddress = React.restoreState("socket-address", ""); +export const isConnected = new React.State(false); +export const connectionMessage = React.createProxyState([serverAddress], () => + translation.connectedTo(serverAddress.value) +); + +// misc +export const isEncryptionUnavailable = window.crypto.subtle == undefined; +export const senderName = React.restoreState("sender-name", ""); + +// chat +export const newChatPrimaryChannelName = new React.State(""); +export const newChatSecondaryChannelName = new React.State(""); +export const newChatSecondaryChannelNames = new React.ListState(); \ No newline at end of file diff --git a/src/cryptUtility.ts b/src/cryptUtility.ts index 58a904c6..a0ae34f4 100644 --- a/src/cryptUtility.ts +++ b/src/cryptUtility.ts @@ -1,6 +1,40 @@ const IV_SIZE = 12; const ENCRYPTION_ALG = "AES-GCM"; +// minimal +export async function encryptString( + plaintext: string, + passphrase: string +): Promise { + if (!window.crypto.subtle) return false; + + const iv = generateIV(); + const key = await importKey(passphrase, "encrypt"); + const encryptedArray = await encrypt(iv, key, plaintext); + + const encryptionData = { + iv: uInt8ToArray(iv), + encryptedArray: uInt8ToArray(encryptedArray), + }; + return btoa(JSON.stringify(encryptionData)); +} + +export async function decryptString( + cyphertext: string, + passphrase: string +): Promise { + try { + const encrypionData = JSON.parse(atob(cyphertext)); + const iv = arrayToUint8(encrypionData.iv); + const encryptedArray = arrayToUint8(encrypionData.encryptedArray); + + const key = await importKey(passphrase, "decrypt"); + return await decrypt(iv, key, encryptedArray); + } catch { + return cyphertext; + } +} + // process export function encode(string: string): Uint8Array { return new TextEncoder().encode(string); diff --git a/src/model.ts b/src/model.ts deleted file mode 100644 index a77b0a76..00000000 --- a/src/model.ts +++ /dev/null @@ -1,333 +0,0 @@ -import * as React from "bloatless-react"; - -import { - arrayToUint8, - decrypt, - encrypt, - generateIV, - importKey, - uInt8ToArray, -} from "./cryptUtility"; - -import UDNFrontend from "udn-frontend"; -import { translation } from "./translations"; - -const UDN = new UDNFrontend(); - -// TYPE -export class Channel implements React.Identifiable { - id = React.UUID(); - constructor(public channelName: string) {} -} - -export interface Message extends React.Identifiable { - channel: string; - sender: string; - body: string; - isoDate: string; -} - -export class Message implements React.Identifiable { - id = React.UUID(); - - constructor( - public channel: string, - public sender: string, - public body: string, - public isoDate: string - ) {} -} - -// STATE -// connection -export const currentAddress = new React.State(""); -export const serverAddress = React.restoreState("socket-address", ""); -export const isConnected = new React.State(false); -export const connectionMessage = React.createProxyState([serverAddress], () => - translation.connectedTo(serverAddress.value) -); - -export const cannotDisonnect = React.createProxyState( - [isConnected], - () => isConnected.value == false -); - -export const cannotConnect = React.createProxyState( - [serverAddress, currentAddress, isConnected], - () => - serverAddress.value == "" || - (currentAddress.value == serverAddress.value && isConnected.value == true) -); - -// mailbox -export const mailboxId = React.restoreState("mailbox-id", ""); -export const isMailboxConnected = new React.State(false); - -export const cannotRequestMailbox = React.createProxyState( - [isConnected, isMailboxConnected], - () => isConnected.value == false || isMailboxConnected.value == true -); - -export const cannotDeleteMailbox = React.createProxyState( - [isConnected, mailboxId, isMailboxConnected], - () => - isConnected.value == false || - mailboxId.value == "" || - isMailboxConnected.value == false -); - -// encryption -export const encryptionKey = React.restoreState("encryption-key", ""); -export const isEncryptionUnavailable = window.crypto.subtle == undefined; - -// channel -export const currentPrimaryChannel = new React.State(""); -export const primaryChannel = React.restoreState("primary-channel", ""); - -export const newSecondaryChannelName = new React.State(""); -export const secondaryChannels = - React.restoreListState("secondary-channels"); - -export const cannotSetChannel = React.createProxyState( - [primaryChannel, currentPrimaryChannel, isConnected], - () => primaryChannel.value == "" || isConnected.value == false -); - -export const cannotLeaveChannel = React.createProxyState( - [currentPrimaryChannel, isConnected], - () => currentPrimaryChannel.value == "" || isConnected.value == false -); - -export const cannotAddSecondaryChannel = React.createProxyState( - [newSecondaryChannelName], - () => newSecondaryChannelName.value == "" -); - -// sending -export const senderName = React.restoreState("sender-name", ""); - -export const messageBody = React.restoreState("message", ""); - -export const cannotSendMessage = React.createProxyState( - [currentPrimaryChannel, messageBody, senderName, isConnected], - () => - isConnected.value == false || - currentPrimaryChannel.value == "" || - messageBody.value == "" || - senderName.value == "" -); - -// receiving -export const messages = React.restoreListState("messages"); - -// METHODS -// connection -export function connect(): void { - if (cannotConnect.value == true) return; - currentAddress.value = serverAddress.value; - isConnected.value = false; - UDN.connect(serverAddress.value); -} - -export function disconnect(): void { - if (cannotDisonnect.value == true) return; - UDN.disconnect(); -} - -// mailbox -export function requestMailbox(): void { - UDN.requestMailbox(); -} - -export function deleteMailbox(): void { - UDN.deleteMailbox(mailboxId.value); -} - -export function updateMailbox(): void { - if (!isMailboxConnected || mailboxId.value == "") return; - - deleteMailbox(); - requestMailbox(); -} - -// messages -export async function sendMessage(): Promise { - if (cannotSendMessage.value == true) return; - - // get channels - const secondaryChannelNames: string[] = [ - ...secondaryChannels.value.values(), - ].map((channel) => channel.channelName); - const allChannelNames: string[] = [ - primaryChannel.value, - ...secondaryChannelNames, - ]; - - // encrypt - const encrypted = await encryptMessage(); - if (encrypted == undefined) return; - - // create object - const joinedChannelName = allChannelNames.join("/"); - const messageObject = new Message( - joinedChannelName, - senderName.value, - encrypted, - new Date().toISOString() - ); - const messageString = JSON.stringify(messageObject); - - // send - UDN.sendMessage(joinedChannelName, messageString); - - // clear - messageBody.value = ""; -} - -export function clearMessageHistory(): void { - messages.clear(); -} - -export function deleteMessage(message: Message): void { - messages.remove(message); -} - -// channels -export function setChannel(shouldUpdateMailbox: boolean = true): void { - if (cannotSetChannel.value == true) return; - - if (currentPrimaryChannel != undefined) { - UDN.unsubscribe(currentPrimaryChannel.value); - } - UDN.subscribe(primaryChannel.value); - - if (shouldUpdateMailbox) { - updateMailbox(); - } -} - -export function leaveChannel(): void { - if (cannotLeaveChannel.value == true) return; - - UDN.unsubscribe(currentPrimaryChannel.value); -} - -export function addSecondaryChannel(): void { - if (cannotAddSecondaryChannel.value == true) return; - - const channelObject = new Channel(newSecondaryChannelName.value); - secondaryChannels.add(channelObject); - newSecondaryChannelName.value = ""; -} - -export function removeSecondaryChannel(channel: Channel): void { - secondaryChannels.remove(channel); -} - -// encryption -export async function decryptReceivedMessage(message: Message): Promise { - message.body = await decryptMessage(message.body); - messages.callSubscriptions(); -} - -// LISTENERS -UDN.onconnect = () => { - isConnected.value = true; - setChannel(false); - - if (mailboxId.value != "") { - UDN.connectMailbox(mailboxId.value); - } -}; - -UDN.onmailboxcreate = (id) => { - mailboxId.value = id; - UDN.connectMailbox(id); -}; - -UDN.onmailboxconnect = () => { - console.log("connected") - isMailboxConnected.value = true; -}; - -UDN.onmailboxdelete = () => { - isMailboxConnected.value = false; - mailboxId.value = ""; - setChannel(); -}; - -UDN.onmessage = (data) => { - if (data.subscribed != undefined && data.messageChannel != undefined) { - return handleSubscriptionConfirmation(data.messageChannel, data.subscribed); - } else if (data.messageBody) { - handleMessage(data.messageBody); - } -}; - -UDN.ondisconnect = () => { - isConnected.value = false; - isMailboxConnected.value = false; -}; - -// UTILITY -function handleSubscriptionConfirmation( - channel: string, - isSubscribed: boolean -): void { - if (isSubscribed == true) { - currentPrimaryChannel.value = channel; - } else { - currentPrimaryChannel.value = ""; - } -} - -async function handleMessage(body: string): Promise { - try { - const object: Message = JSON.parse(body); - if (!object.channel || !object.sender || !object.body || !object.isoDate) - return; - - const messageObject = new Message( - object.channel, - object.sender, - await decryptMessage(object.body), - object.isoDate - ); - messages.add(messageObject); - } catch {} -} - -async function encryptMessage(): Promise { - if (encryptionKey.value == "") return messageBody.value; - - if (!window.crypto.subtle) return messageBody.value; - - const iv = generateIV(); - const key = await importKey(encryptionKey.value, "encrypt"); - const encryptedArray = await encrypt(iv, key, messageBody.value); - - const encryptionData = { - iv: uInt8ToArray(iv), - encryptedArray: uInt8ToArray(encryptedArray), - }; - return btoa(JSON.stringify(encryptionData)); -} - -async function decryptMessage(encryptionData: string): Promise { - try { - const encrypionData = JSON.parse(atob(encryptionData)); - const iv = arrayToUint8(encrypionData.iv); - const encryptedArray = arrayToUint8(encrypionData.encryptedArray); - - const key = await importKey(encryptionKey.value, "decrypt"); - return await decrypt(iv, key, encryptedArray); - } catch (error) { - console.error(error); - return encryptionData; - } -} - -// INIT -if (serverAddress.value != "") { - connect(); -} diff --git a/src/utility.ts b/src/utility.ts new file mode 100644 index 00000000..95152fd0 --- /dev/null +++ b/src/utility.ts @@ -0,0 +1,21 @@ +export const storageKeys = { + primaryChannel(id: string): string { + return id + "primary-channel"; + }, + + secondaryChannels(id: string): string { + return id + "secondary-channels"; + }, + + encyptionKey(id: string): string { + return id + "encryption-key"; + }, + + messages(id: string): string { + return id + "messages"; + }, + + composingMessage(id: string): string { + return id + "composing-message"; + }, +}; From ff0f3d9499bf65e123ab2f39df0716439152b3f3 Mon Sep 17 00:00:00 2001 From: marlon-erler Date: Tue, 9 Jul 2024 11:39:23 +0200 Subject: [PATCH 03/22] remodel --- src/Model/chatModel.ts | 88 ++++++++++++++++++++++++++++-------------- src/Model/model.ts | 15 ++----- src/Model/viewModel.ts | 5 +-- 3 files changed, 66 insertions(+), 42 deletions(-) diff --git a/src/Model/chatModel.ts b/src/Model/chatModel.ts index 2cf82cd7..c11620eb 100644 --- a/src/Model/chatModel.ts +++ b/src/Model/chatModel.ts @@ -3,46 +3,42 @@ import * as React from "bloatless-react"; import { UDN, isConnected } from "./model"; import { decryptString, encryptString } from "../cryptUtility"; +import { Message } from "udn-frontend"; import { senderName } from "./viewModel"; import { storageKeys } from "../utility"; // TYPES // message -export interface Message { +export interface ChatMessage { channel: string; sender: string; body: string; isoDate: string; } -export class Message { - constructor( - public channel: string, - public sender: string, - public body: string, - public isoDate: string - ) {} -} - // chat export class Chat { id: string; - primaryChannel: React.State; + isSubscribed = new React.State(false); + currentChannel = new React.State(""); + + primaryChannelInput: React.State; secondaryChannels: React.ListState; encryptionKey: React.State; - messages: React.ListState; + messages: React.ListState; composingMessage: React.State; - cannotSendMessage: React.State; - newSecondaryChannelName: React.State; + + cannotSendMessage: React.State; cannotAddSecondaryChannel: React.State; + cannotSetChannel: React.State; // init constructor(id: string = React.UUID()) { - this.primaryChannel = React.restoreState( + this.primaryChannelInput = React.restoreState( storageKeys.primaryChannel(id), "" ); @@ -60,16 +56,48 @@ export class Chat { this.newSecondaryChannelName = new React.State(""); this.cannotSendMessage = React.createProxyState( - [this.primaryChannel, this.composingMessage, isConnected], + [ + this.primaryChannelInput, + this.composingMessage, + this.isSubscribed, + isConnected, + ], () => - this.primaryChannel.value == "" || + this.primaryChannelInput.value == "" || this.composingMessage.value == "" || + this.isSubscribed.value == false || isConnected.value == false ); this.cannotAddSecondaryChannel = React.createProxyState( [this.newSecondaryChannelName], () => this.newSecondaryChannelName.value == "" ); + this.cannotSetChannel = React.createProxyState( + [this.primaryChannelInput, this.currentChannel], + () => + this.primaryChannelInput.value == "" || + this.primaryChannelInput.value == this.currentChannel.value + ); + } + + // handlers + onmessage(data: Message): void { + if (data.messageChannel && data.messageChannel != this.currentChannel.value) + return; + + if (data.subscribed != undefined) this.handleSubscription(data.subscribed); + + if (!data.messageBody) return; + const { sender, body, channel, isoDate } = JSON.parse(data.messageBody); + this.handleMessage({ sender, body, channel, isoDate }); + } + + handleSubscription(isSubscribed: boolean): void { + this.isSubscribed.value = isSubscribed; + } + + handleMessage(chatMessage: ChatMessage): void { + this.messages.add(chatMessage); } // messages @@ -81,7 +109,7 @@ export class Chat { ...this.secondaryChannels.value.values(), ].map((channel) => channel); const allChannelNames: string[] = [ - this.primaryChannel.value, + this.primaryChannelInput.value, ...secondaryChannelNames, ]; @@ -95,12 +123,12 @@ export class Chat { // create object const joinedChannelName = allChannelNames.join("/"); - const messageObject = new Message( - joinedChannelName, - senderName.value, - encrypted, - new Date().toISOString() - ); + const messageObject: ChatMessage = { + channel: joinedChannelName, + sender: senderName.value, + body: encrypted, + isoDate: new Date().toISOString(), + }; const messageString = JSON.stringify(messageObject); // send @@ -114,17 +142,21 @@ export class Chat { this.messages.clear(); } - deleteMessage(message: Message): void { + deleteMessage(message: ChatMessage): void { this.messages.remove(message); } - async decryptReceivedMessage(message: Message): Promise { + async decryptReceivedMessage(message: ChatMessage): Promise { message.body = await decryptString(message.body, this.encryptionKey.value); this.messages.callSubscriptions(); } // channel - setChannel(): void {} + setChannel(): void { + if (this.cannotSetChannel.value == true) return; + this.currentChannel.value = this.primaryChannelInput.value; + UDN.subscribe(this.currentChannel.value); + } addSecondaryChannel(): void { if (this.cannotAddSecondaryChannel.value == true) return; @@ -146,7 +178,7 @@ export const chats = new React.ListState(chatArray); // METHODS export function createChatWithName(name: string): void { const newChat = new Chat(); - newChat.primaryChannel.value = name; + newChat.primaryChannelInput.value = name; chatIds.add(newChat.id); } diff --git a/src/Model/model.ts b/src/Model/model.ts index 34525a6f..e9a8a504 100644 --- a/src/Model/model.ts +++ b/src/Model/model.ts @@ -1,17 +1,8 @@ import * as React from "bloatless-react"; -import { - arrayToUint8, - decrypt, - encrypt, - generateIV, - importKey, - uInt8ToArray, -} from "../cryptUtility"; - import UDNFrontend from "udn-frontend"; +import { chats } from "./chatModel"; import { serverAddress } from "./viewModel"; -import { translation } from "../translations"; export const UDN = new UDNFrontend(); @@ -52,7 +43,9 @@ UDN.onconnect = () => { UDN.onmessage = (data) => { - + chats.value.forEach((chat) => { + chat.onmessage(data); + }) }; UDN.ondisconnect = () => { diff --git a/src/Model/viewModel.ts b/src/Model/viewModel.ts index 085771b9..66b5287c 100644 --- a/src/Model/viewModel.ts +++ b/src/Model/viewModel.ts @@ -1,5 +1,6 @@ import * as React from "bloatless-react"; +import { Chat } from "./chatModel"; import { translation } from "../translations"; // connection @@ -16,6 +17,4 @@ export const isEncryptionUnavailable = window.crypto.subtle == undefined; export const senderName = React.restoreState("sender-name", ""); // chat -export const newChatPrimaryChannelName = new React.State(""); -export const newChatSecondaryChannelName = new React.State(""); -export const newChatSecondaryChannelNames = new React.ListState(); \ No newline at end of file +export const selectedChat = new React.State(undefined); From 48633ce53491b6784cde8efa09facf1e1d350eec Mon Sep 17 00:00:00 2001 From: marlon-erler Date: Tue, 9 Jul 2024 14:11:20 +0200 Subject: [PATCH 04/22] prepare UI --- bun.lockb | Bin 11435 -> 11435 bytes dist/index.css | 24 - dist/index.js | 804 +++++++++++++-------------- package.json | 2 +- src/Model/chatModel.ts | 3 +- src/Model/model.ts | 73 ++- src/Model/viewModel.ts | 20 - src/Tabs/messageTab.tsx | 15 +- src/Tabs/settingsTab.tsx | 12 +- src/Views/communicationSection.tsx | 116 ---- src/Views/connectionSection.tsx | 84 ++- src/Views/connectionSettingsView.tsx | 77 --- src/Views/connectionStatusView.tsx | 19 - src/Views/encryptionSection.tsx | 45 -- src/Views/messageComposer.tsx | 48 +- src/Views/threadView.tsx | 128 +++-- src/index.tsx | 17 - src/styles.css | 30 - src/translations.ts | 24 +- 19 files changed, 640 insertions(+), 901 deletions(-) delete mode 100644 dist/index.css delete mode 100644 src/Model/viewModel.ts delete mode 100644 src/Views/communicationSection.tsx delete mode 100644 src/Views/connectionSettingsView.tsx delete mode 100644 src/Views/connectionStatusView.tsx delete mode 100644 src/Views/encryptionSection.tsx delete mode 100644 src/styles.css diff --git a/bun.lockb b/bun.lockb index 51310240ecabd8c56df4778e3db27cc7454b92ef..03cb61d1c372159e5f380edeeaeffa4d789978eb 100755 GIT binary patch delta 139 zcmV;60CfMWS*ux)E+AsjT+VSw96G^)8Ef(RzZ(X4Rz7M@kHIouHnEnqw`Z|V)(8Ou zlTiXIvrq|b3qVS5t|Mvn?x(QQ5NG|D5co$uoN8039BmG-qZm_Y${^k}JLdtAjcjXw t=bWD%J&wHzj}*Uz!-?|IK~Gkx5{0w<6bcFfHnRvH#tZ{9E-;hPE84V)JQe@| delta 143 zcmV;A0C4}SS*ux)E+F>r{O%3+bU=z*>|x{)(8ax z00006lTiXEvrq|b3qa=kEqn;z{g$~+>tZ?vvov2HE0}$%0*M=rp-!Y-JVy`o{THtp xFouasYIa-h2vNUnBW{;%f#t{;8p~X^(G0Ww6bcFfHM0mG#tZ{8E;f_VE80~PJ@o(p diff --git a/dist/index.css b/dist/index.css deleted file mode 100644 index fc07bd94..00000000 --- a/dist/index.css +++ /dev/null @@ -1,24 +0,0 @@ -/* src/styles.css */ -#settings-tab > div { - display: flex; - flex-direction: column; - gap: 2rem; -} -#settings-tab > div > div { - width: var(--max-input-width); - min-width: 100%; - max-width: 100%; - flex: 1; -} -article:not([connected]) .connected-only { - display: none; -} -article:not([connected]) .disconnected-only { - display: flex; -} -article[connected] .connected-only { - display: flex; -} -article[connected] .disconnected-only { - display: none; -} diff --git a/dist/index.js b/dist/index.js index 25c17b26..0fa19633 100644 --- a/dist/index.js +++ b/dist/index.js @@ -99,10 +99,9 @@ remove(...items) { items.forEach((item) => { this.value.delete(item); - const id = item.id; - if (!this.removalHandlers.has(id)) return; - this.removalHandlers.get(id)(item); - this.removalHandlers.delete(id); + if (!this.removalHandlers.has(item)) return; + this.removalHandlers.get(item)(item); + this.removalHandlers.delete(item); }); this.callSubscriptions(); } @@ -115,7 +114,7 @@ [...this.value.values()].forEach(handler); } handleRemoval(item, handler) { - this.removalHandlers.set(item.id, handler); + this.removalHandlers.set(item, handler); } // stringification toString() { @@ -180,28 +179,10 @@ break; } case "subscribe": { - if (directiveValue == "children") { - element.style.scrollBehavior = "smooth"; - try { - const [listState, toElement] = value; - listState.handleAddition((newItem) => { - const child = toElement(newItem); - listState.handleRemoval( - newItem, - () => child.remove() - ); - element.append(child); - element.scrollTop = element.scrollHeight; - }); - } catch { - throw `error: cannot process subscribe:children directive because ListItemConverter is not defined. Usage: "subscribe:children={[list, converter]}"; you can find a more detailed example in the documentation`; - } - } else { - const state = value; - state.subscribe( - (newValue) => element[directiveValue] = newValue - ); - } + const state = value; + state.subscribe( + (newValue) => element[directiveValue] = newValue + ); break; } case "bind": { @@ -233,6 +214,40 @@ ); break; } + case "children": { + switch (directiveValue) { + case "set": { + const state = value; + state.subscribe((newValue) => { + element.innerHTML = ""; + element.append(newValue); + }); + break; + } + case "append": + case "appendandscroll": + case "prepend": { + element.style.scrollBehavior = "smooth"; + try { + const [listState, toElement] = value; + listState.handleAddition((newItem) => { + const child = toElement(newItem); + listState.handleRemoval( + newItem, + () => child.remove() + ); + if (directiveValue == "append") { + element.append(child); + } else if (directiveValue == "prepend") { + element.prepend(child); + } + }); + } catch { + throw `error: cannot process subscribe:children directive because ListItemConverter is not defined. Usage: "subscribe:children={[list, converter]}"; you can find a more detailed example in the documentation`; + } + } + } + } default: element.setAttribute(attributename, value); } @@ -244,6 +259,28 @@ // src/cryptUtility.ts var IV_SIZE = 12; var ENCRYPTION_ALG = "AES-GCM"; + async function encryptString(plaintext, passphrase) { + if (!window.crypto.subtle) return false; + const iv = generateIV(); + const key = await importKey(passphrase, "encrypt"); + const encryptedArray = await encrypt(iv, key, plaintext); + const encryptionData = { + iv: uInt8ToArray(iv), + encryptedArray: uInt8ToArray(encryptedArray) + }; + return btoa(JSON.stringify(encryptionData)); + } + async function decryptString(cyphertext, passphrase) { + try { + const encrypionData = JSON.parse(atob(cyphertext)); + const iv = arrayToUint8(encrypionData.iv); + const encryptedArray = arrayToUint8(encrypionData.encryptedArray); + const key = await importKey(passphrase, "decrypt"); + return await decrypt(iv, key, encryptedArray); + } catch { + return cyphertext; + } + } function encode(string) { return new TextEncoder().encode(string); } @@ -292,6 +329,143 @@ return new Uint8Array(array); } + // src/utility.ts + var storageKeys = { + primaryChannel(id) { + return id + "primary-channel"; + }, + secondaryChannels(id) { + return id + "secondary-channels"; + }, + encyptionKey(id) { + return id + "encryption-key"; + }, + messages(id) { + return id + "messages"; + }, + composingMessage(id) { + return id + "composing-message"; + } + }; + + // src/Model/chatModel.ts + var Chat = class { + id; + isSubscribed = new State(false); + currentChannel = new State(""); + primaryChannelInput; + secondaryChannels; + encryptionKey; + messages; + composingMessage; + newSecondaryChannelName; + cannotSendMessage; + cannotAddSecondaryChannel; + cannotSetChannel; + // init + constructor(id = UUID()) { + this.primaryChannelInput = restoreState( + storageKeys.primaryChannel(id), + "" + ); + this.secondaryChannels = restoreListState( + storageKeys.secondaryChannels(id) + ); + this.encryptionKey = restoreState(storageKeys.encyptionKey(id), ""); + this.messages = restoreListState(storageKeys.messages(id)); + this.composingMessage = restoreState( + storageKeys.composingMessage(id), + "" + ); + this.newSecondaryChannelName = new State(""); + this.cannotSendMessage = createProxyState( + [ + this.primaryChannelInput, + this.composingMessage, + this.isSubscribed, + isConnected + ], + () => this.primaryChannelInput.value == "" || this.composingMessage.value == "" || this.isSubscribed.value == false || isConnected.value == false + ); + this.cannotAddSecondaryChannel = createProxyState( + [this.newSecondaryChannelName], + () => this.newSecondaryChannelName.value == "" + ); + this.cannotSetChannel = createProxyState( + [this.primaryChannelInput, this.currentChannel], + () => this.primaryChannelInput.value == "" || this.primaryChannelInput.value == this.currentChannel.value + ); + } + // handlers + onmessage(data) { + if (data.messageChannel && data.messageChannel != this.currentChannel.value) + return; + if (data.subscribed != void 0) this.handleSubscription(data.subscribed); + if (!data.messageBody) return; + const { sender, body, channel, isoDate } = JSON.parse(data.messageBody); + this.handleMessage({ sender, body, channel, isoDate }); + } + handleSubscription(isSubscribed) { + this.isSubscribed.value = isSubscribed; + } + handleMessage(chatMessage) { + this.messages.add(chatMessage); + } + // messages + async sendMessage() { + if (this.cannotSendMessage.value == true) return; + const secondaryChannelNames = [ + ...this.secondaryChannels.value.values() + ].map((channel) => channel); + const allChannelNames = [ + this.primaryChannelInput.value, + ...secondaryChannelNames + ]; + const encrypted = await encryptString( + this.composingMessage.value, + this.encryptionKey.value + ) || this.composingMessage.value; + if (encrypted == void 0) return; + const joinedChannelName = allChannelNames.join("/"); + const messageObject = { + channel: joinedChannelName, + sender: senderName.value, + body: encrypted, + isoDate: (/* @__PURE__ */ new Date()).toISOString() + }; + const messageString = JSON.stringify(messageObject); + UDN.sendMessage(joinedChannelName, messageString); + this.composingMessage.value = ""; + } + clearMessages() { + this.messages.clear(); + } + deleteMessage(message) { + this.messages.remove(message); + } + async decryptReceivedMessage(message) { + message.body = await decryptString(message.body, this.encryptionKey.value); + this.messages.callSubscriptions(); + } + // channel + setChannel() { + if (this.cannotSetChannel.value == true) return; + this.currentChannel.value = this.primaryChannelInput.value; + UDN.subscribe(this.currentChannel.value); + } + addSecondaryChannel() { + if (this.cannotAddSecondaryChannel.value == true) return; + this.secondaryChannels.add(this.newSecondaryChannelName.value); + this.newSecondaryChannelName.value = ""; + } + removeSecondaryChannel(channel) { + this.secondaryChannels.remove(channel); + } + }; + var chatIds = restoreListState("chat-ids"); + var chatArray = [...chatIds.value.values()].map((id) => new Chat(id)); + var chats = new ListState(chatArray); + // node_modules/udn-frontend/index.ts var UDNFrontend = class { ws; @@ -404,15 +578,82 @@ } }; + // src/Model/model.ts + var UDN = new UDNFrontend(); + var currentAddress = new State(""); + var isConnected = new State(false); + var serverAddressInput = restoreState("socket-address", ""); + var cannotDisonnect = createProxyState( + [isConnected], + () => isConnected.value == false + ); + var cannotConnect = createProxyState( + [serverAddressInput, currentAddress, isConnected], + () => serverAddressInput.value == "" || currentAddress.value == serverAddressInput.value && isConnected.value == true + ); + function connect() { + if (cannotConnect.value == true) return; + currentAddress.value = serverAddressInput.value; + isConnected.value = false; + UDN.connect(serverAddressInput.value); + } + function disconnect() { + if (cannotDisonnect.value == true) return; + UDN.disconnect(); + } + UDN.onconnect = () => { + isConnected.value = true; + }; + UDN.onmessage = (data) => { + chats.value.forEach((chat) => { + chat.onmessage(data); + }); + }; + UDN.ondisconnect = () => { + isConnected.value = false; + }; + var mailboxId = restoreState("mailbox-id", ""); + var isMailboxActive = new State(false); + var cannotDeleteMailbox = createProxyState( + [mailboxId, isMailboxActive], + () => mailboxId.value == "" || isMailboxActive.value == false + ); + var cannotRequestMailbox = createProxyState( + [isConnected, isMailboxActive], + () => isConnected.value == false || isMailboxActive.value == true + ); + function requestMailbox() { + if (cannotRequestMailbox.value == true) return; + UDN.requestMailbox(); + } + function deleteMailbox() { + if (cannotDeleteMailbox.value == true) return; + UDN.deleteMailbox(mailboxId.value); + } + UDN.onmailboxcreate = (id) => { + mailboxId.value = id; + UDN.connectMailbox(id); + }; + UDN.onmailboxconnect = () => { + isMailboxActive.value = true; + }; + UDN.onmailboxdelete = () => { + isMailboxActive.value = false; + mailboxId.value = ""; + }; + var isEncryptionUnavailable = window.crypto.subtle == void 0; + var senderName = restoreState("sender-name", ""); + var selectedChat = new State(void 0); + if (serverAddressInput.value != "") { + connect(); + } + // src/translations.ts var englishTranslations = { // general setInput: "Set", - connectionStatus: "Connection status", - connectedTo: (server) => `Connected to "${server}"`, - disconnected: "Disconnected", - // settings - settings: "Settings", + // overview + overview: "Overview", connection: "Connection", communication: "Communication", encryption: "Encryption", @@ -420,6 +661,8 @@ serverAddressPlaceholder: "wss://192.168.0.69:3000", connectToServer: "Connect", disconnect: "Disonnect", + resetAddress: "Reset address", + noChatSelected: "No chat selected", primaryChannel: "Primary channel", leaveChannel: "Leave", channelPlaceholder: "my-channel", @@ -451,11 +694,8 @@ es: { // general setInput: "OK", - connectionStatus: "Estado de Conexi\xF3n", - connectedTo: (server) => `Conectado a "${server}"`, - disconnected: "Desconectado", // settings - settings: "Configuraci\xF3n", + overview: "Configuraci\xF3n", connection: "Conexi\xF3n", communication: "Comunicaci\xF3n", encryption: "Cifrado", @@ -492,11 +732,8 @@ de: { // general setInput: "OK", - connectionStatus: "Verbindungsstatus", - connectedTo: (server) => `Verbunden mit "${server}"`, - disconnected: "Verbindung getrennt", // settings - settings: "Einstellungen", + overview: "Einstellungen", connection: "Verbindung", communication: "Kommunikation", encryption: "Verschl\xFCsselung", @@ -534,279 +771,93 @@ var language = navigator.language.substring(0, 2); var translation = allTranslations[language] ?? allTranslations.en; - // src/model.ts - var UDN = new UDNFrontend(); - var Channel = class { - constructor(channelName) { - this.channelName = channelName; - } - id = UUID(); - }; - var Message = class { - constructor(channel, sender, body, isoDate) { - this.channel = channel; - this.sender = sender; - this.body = body; - this.isoDate = isoDate; - } - id = UUID(); - }; - var currentAddress = new State(""); - var serverAddress = restoreState("socket-address", ""); - var isConnected = new State(false); - var connectionMessage = createProxyState( - [serverAddress], - () => translation.connectedTo(serverAddress.value) - ); - var cannotDisonnect = createProxyState( - [isConnected], - () => isConnected.value == false - ); - var cannotConnect = createProxyState( - [serverAddress, currentAddress, isConnected], - () => serverAddress.value == "" || currentAddress.value == serverAddress.value && isConnected.value == true - ); - var mailboxId = restoreState("mailbox-id", ""); - var isMailboxConnected = new State(false); - var cannotRequestMailbox = createProxyState( - [isConnected, isMailboxConnected], - () => isConnected.value == false || isMailboxConnected.value == true - ); - var cannotDeleteMailbox = createProxyState( - [isConnected, mailboxId, isMailboxConnected], - () => isConnected.value == false || mailboxId.value == "" || isMailboxConnected.value == false - ); - var encryptionKey = restoreState("encryption-key", ""); - var isEncryptionUnavailable = window.crypto.subtle == void 0; - var currentPrimaryChannel = new State(""); - var primaryChannel = restoreState("primary-channel", ""); - var newSecondaryChannelName = new State(""); - var secondaryChannels = restoreListState("secondary-channels"); - var cannotSetChannel = createProxyState( - [primaryChannel, currentPrimaryChannel, isConnected], - () => primaryChannel.value == "" || isConnected.value == false - ); - var cannotLeaveChannel = createProxyState( - [currentPrimaryChannel, isConnected], - () => currentPrimaryChannel.value == "" || isConnected.value == false - ); - var cannotAddSecondaryChannel = createProxyState( - [newSecondaryChannelName], - () => newSecondaryChannelName.value == "" - ); - var senderName = restoreState("sender-name", ""); - var messageBody = restoreState("message", ""); - var cannotSendMessage = createProxyState( - [currentPrimaryChannel, messageBody, senderName, isConnected], - () => isConnected.value == false || currentPrimaryChannel.value == "" || messageBody.value == "" || senderName.value == "" - ); - var messages = restoreListState("messages"); - function connect() { - if (cannotConnect.value == true) return; - currentAddress.value = serverAddress.value; - isConnected.value = false; - UDN.connect(serverAddress.value); - } - function disconnect() { - if (cannotDisonnect.value == true) return; - UDN.disconnect(); - } - function requestMailbox() { - UDN.requestMailbox(); - } - function deleteMailbox() { - UDN.deleteMailbox(mailboxId.value); - } - function updateMailbox() { - if (!isMailboxConnected || mailboxId.value == "") return; - deleteMailbox(); - requestMailbox(); - } - async function sendMessage() { - if (cannotSendMessage.value == true) return; - const secondaryChannelNames = [ - ...secondaryChannels.value.values() - ].map((channel) => channel.channelName); - const allChannelNames = [ - primaryChannel.value, - ...secondaryChannelNames - ]; - const encrypted = await encryptMessage(); - if (encrypted == void 0) return; - const joinedChannelName = allChannelNames.join("/"); - const messageObject = new Message( - joinedChannelName, - senderName.value, - encrypted, - (/* @__PURE__ */ new Date()).toISOString() - ); - const messageString = JSON.stringify(messageObject); - UDN.sendMessage(joinedChannelName, messageString); - messageBody.value = ""; - } - function clearMessageHistory() { - messages.clear(); - } - function deleteMessage(message) { - messages.remove(message); - } - function setChannel(shouldUpdateMailbox = true) { - if (cannotSetChannel.value == true) return; - if (currentPrimaryChannel != void 0) { - UDN.unsubscribe(currentPrimaryChannel.value); - } - UDN.subscribe(primaryChannel.value); - if (shouldUpdateMailbox) { - updateMailbox(); - } - } - function leaveChannel() { - if (cannotLeaveChannel.value == true) return; - UDN.unsubscribe(currentPrimaryChannel.value); - } - function addSecondaryChannel() { - if (cannotAddSecondaryChannel.value == true) return; - const channelObject = new Channel(newSecondaryChannelName.value); - secondaryChannels.add(channelObject); - newSecondaryChannelName.value = ""; - } - function removeSecondaryChannel(channel) { - secondaryChannels.remove(channel); - } - async function decryptReceivedMessage(message) { - message.body = await decryptMessage(message.body); - messages.callSubscriptions(); - } - UDN.onconnect = () => { - isConnected.value = true; - setChannel(false); - if (mailboxId.value != "") { - UDN.connectMailbox(mailboxId.value); - } - }; - UDN.onmailboxcreate = (id) => { - mailboxId.value = id; - UDN.connectMailbox(id); - }; - UDN.onmailboxconnect = () => { - console.log("connected"); - isMailboxConnected.value = true; - }; - UDN.onmailboxdelete = () => { - isMailboxConnected.value = false; - mailboxId.value = ""; - setChannel(); - }; - UDN.onmessage = (data) => { - if (data.subscribed != void 0 && data.messageChannel != void 0) { - return handleSubscriptionConfirmation(data.messageChannel, data.subscribed); - } else if (data.messageBody) { - handleMessage(data.messageBody); - } - }; - UDN.ondisconnect = () => { - isConnected.value = false; - isMailboxConnected.value = false; - }; - function handleSubscriptionConfirmation(channel, isSubscribed) { - if (isSubscribed == true) { - currentPrimaryChannel.value = channel; - } else { - currentPrimaryChannel.value = ""; - } - } - async function handleMessage(body) { - try { - const object = JSON.parse(body); - if (!object.channel || !object.sender || !object.body || !object.isoDate) - return; - const messageObject = new Message( - object.channel, - object.sender, - await decryptMessage(object.body), - object.isoDate - ); - messages.add(messageObject); - } catch { - } - } - async function encryptMessage() { - if (encryptionKey.value == "") return messageBody.value; - if (!window.crypto.subtle) return messageBody.value; - const iv = generateIV(); - const key = await importKey(encryptionKey.value, "encrypt"); - const encryptedArray = await encrypt(iv, key, messageBody.value); - const encryptionData = { - iv: uInt8ToArray(iv), - encryptedArray: uInt8ToArray(encryptedArray) - }; - return btoa(JSON.stringify(encryptionData)); - } - async function decryptMessage(encryptionData) { - try { - const encrypionData = JSON.parse(atob(encryptionData)); - const iv = arrayToUint8(encrypionData.iv); - const encryptedArray = arrayToUint8(encrypionData.encryptedArray); - const key = await importKey(encryptionKey.value, "decrypt"); - return await decrypt(iv, key, encryptedArray); - } catch (error) { - console.error(error); - return encryptionData; - } - } - if (serverAddress.value != "") { - connect(); - } - // src/Views/messageComposer.tsx function MessageComposer() { - return /* @__PURE__ */ createElement("div", { class: "flex-row width-100" }, /* @__PURE__ */ createElement( - "input", - { - class: "width-100 flex-1", - style: "max-width: unset", - placeholder: translation.composerPlaceholder, - "bind:value": messageBody, - "on:enter": sendMessage - } - ), /* @__PURE__ */ createElement( - "button", - { - class: "primary", - "on:click": sendMessage, - "toggle:disabled": cannotSendMessage - }, - /* @__PURE__ */ createElement("span", { class: "icon" }, "send") - )); + const composerContent = createProxyState([selectedChat], () => { + if (selectedChat.value == void 0) return /* @__PURE__ */ createElement("div", null); + const chat = selectedChat.value; + return /* @__PURE__ */ createElement("div", { class: "flex-row width-100" }, " ", /* @__PURE__ */ createElement( + "input", + { + class: "width-100 flex-1", + style: "max-width: unset", + placeholder: translation.composerPlaceholder, + "bind:value": chat.composingMessage, + "on:enter": chat.sendMessage + } + ), /* @__PURE__ */ createElement( + "button", + { + class: "primary", + "on:click": chat.sendMessage, + "toggle:disabled": chat.cannotSendMessage + }, + /* @__PURE__ */ createElement("span", { class: "icon" }, "send") + )); + }); + return /* @__PURE__ */ createElement("div", { "children:set": composerContent }); } // src/Views/threadView.tsx - var messageConverter = (message) => { - const messageBody2 = createProxyState([messages], () => message.body); - function copyMessage() { - navigator.clipboard.writeText(message.body); - } - function decrypt2() { - decryptReceivedMessage(message); - } - function remove() { - deleteMessage(message); - } - return /* @__PURE__ */ createElement("div", { class: "tile width-100 flex-no padding-0" }, /* @__PURE__ */ createElement("div", { class: "flex-column" }, /* @__PURE__ */ createElement("div", { class: "flex-row justify-apart align-center secondary" }, /* @__PURE__ */ createElement("span", { class: "padding-h ellipsis" }, /* @__PURE__ */ createElement("b", { class: "info" }, message.sender), " - ", message.channel), /* @__PURE__ */ createElement("span", { class: "flex-row" }, /* @__PURE__ */ createElement("button", { "aria-label": translation.copyMessage, "on:click": copyMessage }, /* @__PURE__ */ createElement("span", { class: "icon" }, "content_copy")), /* @__PURE__ */ createElement("button", { "aria-label": translation.decryptMessage, "on:click": decrypt2 }, /* @__PURE__ */ createElement("span", { class: "icon" }, "key")), /* @__PURE__ */ createElement("button", { "aria-label": translation.deleteMessage, "on:click": remove }, /* @__PURE__ */ createElement("span", { class: "icon" }, "delete")))), /* @__PURE__ */ createElement("div", { class: "flex-column padding-h padding-bottom" }, /* @__PURE__ */ createElement("b", { class: "break-word", "subscribe:innerText": messageBody2 }), /* @__PURE__ */ createElement("span", { class: "secondary" }, new Date(message.isoDate).toLocaleString())))); - }; function ThreadView() { - return /* @__PURE__ */ createElement( - "div", - { - class: "flex-column gap", - "subscribe:children": [messages, messageConverter] - } - ); + const threadViewContent = createProxyState([selectedChat], () => { + if (selectedChat.value == void 0) + return /* @__PURE__ */ createElement("div", null, translation.noChatSelected); + const chat = selectedChat.value; + const messageConverter = (message) => { + function copyMessage() { + navigator.clipboard.writeText(message.body); + } + function decrypt2() { + chat.decryptReceivedMessage(message); + } + function remove() { + chat.deleteMessage(message); + } + return /* @__PURE__ */ createElement("div", { class: "tile width-100 flex-no padding-0" }, /* @__PURE__ */ createElement("div", { class: "flex-column" }, /* @__PURE__ */ createElement("div", { class: "flex-row justify-apart align-center secondary" }, /* @__PURE__ */ createElement("span", { class: "padding-h ellipsis" }, /* @__PURE__ */ createElement("b", { class: "info" }, message.sender), " - ", message.channel), /* @__PURE__ */ createElement("span", { class: "flex-row" }, /* @__PURE__ */ createElement( + "button", + { + "aria-label": translation.copyMessage, + "on:click": copyMessage + }, + /* @__PURE__ */ createElement("span", { class: "icon" }, "content_copy") + ), /* @__PURE__ */ createElement( + "button", + { + "aria-label": translation.decryptMessage, + "on:click": decrypt2 + }, + /* @__PURE__ */ createElement("span", { class: "icon" }, "key") + ), /* @__PURE__ */ createElement( + "button", + { + "aria-label": translation.deleteMessage, + "on:click": remove + }, + /* @__PURE__ */ createElement("span", { class: "icon" }, "delete") + ))), /* @__PURE__ */ createElement("div", { class: "flex-column padding-h padding-bottom" }, /* @__PURE__ */ createElement("b", { class: "break-word", "subscribe:innerText": message.body }), /* @__PURE__ */ createElement("span", { class: "secondary" }, new Date(message.isoDate).toLocaleString())))); + }; + return /* @__PURE__ */ createElement( + "div", + { + class: "flex-column gap", + "children:append": [chat.messages, messageConverter] + } + ); + }); + return /* @__PURE__ */ createElement("div", { "children:set": threadViewContent }); } // src/Tabs/messageTab.tsx function MessageTab() { - return /* @__PURE__ */ createElement("article", { id: "message-tab" }, /* @__PURE__ */ createElement("header", null, translation.messages, /* @__PURE__ */ createElement("span", null, /* @__PURE__ */ createElement( + function clearMessageHistory() { + selectedChat.value?.clearMessages(); + } + const headerText = createProxyState( + [selectedChat], + () => selectedChat.value != void 0 ? selectedChat.value.currentChannel : translation.messages + ); + return /* @__PURE__ */ createElement("article", { id: "message-tab" }, /* @__PURE__ */ createElement("header", null, /* @__PURE__ */ createElement("span", { "subscribe:innerText": headerText }), /* @__PURE__ */ createElement("span", null, /* @__PURE__ */ createElement( "button", { "aria-label": translation.clearHistory, @@ -816,108 +867,46 @@ ))), ThreadView(), /* @__PURE__ */ createElement("footer", null, MessageComposer())); } - // src/Views/communicationSection.tsx - var secondaryChannelConverter = (channel) => { - function remove() { - removeSecondaryChannel(channel); - } - return /* @__PURE__ */ createElement("div", { class: "tile padding-0" }, /* @__PURE__ */ createElement("div", { class: "flex-row justify-apart align-center" }, /* @__PURE__ */ createElement("b", { class: "padding-h" }, channel.channelName), /* @__PURE__ */ createElement( - "button", - { - class: "danger", - "aria-label": translation.removeSecondaryChannel, - "on:click": remove - }, - /* @__PURE__ */ createElement("span", { class: "icon" }, "delete") - ))); - }; - function CommunicationSection() { - return /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("h2", null, translation.communication), /* @__PURE__ */ createElement("label", { class: "tile" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "account_circle"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translation.myName), /* @__PURE__ */ createElement( - "input", - { - "bind:value": senderName, - placeholder: translation.yourNamePlaceholder - } - ))), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement("label", { class: "tile" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "forum"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translation.primaryChannel), /* @__PURE__ */ createElement( - "input", - { - "bind:value": primaryChannel, - "on:enter": setChannel, - placeholder: translation.channelPlaceholder - } - ))), /* @__PURE__ */ createElement("div", { class: "flex-row width-input justify-end" }, /* @__PURE__ */ createElement( - "button", - { - class: "danger width-50", - "on:click": leaveChannel, - "toggle:disabled": cannotLeaveChannel - }, - translation.leaveChannel - ), /* @__PURE__ */ createElement( - "button", - { - class: "primary width-50", - "on:click": setChannel, - "toggle:disabled": cannotSetChannel - }, - translation.setInput, - /* @__PURE__ */ createElement("span", { class: "icon" }, "check") - )), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement("div", { class: "flex-row width-input margin-bottom" }, /* @__PURE__ */ createElement( - "input", - { - "bind:value": newSecondaryChannelName, - "on:enter": addSecondaryChannel, - placeholder: translation.newSecondaryChannelPlaceholder, - class: "width-100 flex" - } - ), /* @__PURE__ */ createElement( - "button", - { - class: "primary", - "on:click": addSecondaryChannel, - "toggle:disabled": cannotAddSecondaryChannel, - "aria-label": translation.addSecondaryChannel - }, - /* @__PURE__ */ createElement("span", { class: "icon" }, "add") - )), /* @__PURE__ */ createElement( - "div", - { - class: "flex-column gap", - "subscribe:children": [secondaryChannels, secondaryChannelConverter] - } - )); - } - - // src/Views/connectionSettingsView.tsx - function ConnectionInputView() { + // src/Views/connectionSection.tsx + function ConnectionSection() { return /* @__PURE__ */ createElement("div", { class: "flex-column" }, /* @__PURE__ */ createElement("label", { class: "tile" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "cell_tower"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translation.serverAddress), /* @__PURE__ */ createElement( "input", { - "bind:value": serverAddress, + "bind:value": serverAddressInput, "on:enter": connect, placeholder: translation.serverAddressPlaceholder } ))), /* @__PURE__ */ createElement("div", { class: "flex-row width-input justify-end" }, /* @__PURE__ */ createElement( "button", { - class: "danger width-50", + class: "danger width-100 flex-1", + "aria-label": translation.disconnect, "on:click": disconnect, "toggle:disabled": cannotDisonnect }, - translation.disconnect + /* @__PURE__ */ createElement("span", { class: "icon" }, "close") ), /* @__PURE__ */ createElement( "button", { - class: "primary width-50", + class: "width-100 flex-1", + "aria-label": translation.resetAddress, + "on:click": disconnect, + "toggle:disabled": cannotDisonnect + }, + /* @__PURE__ */ createElement("span", { class: "icon" }, "undo") + ), /* @__PURE__ */ createElement( + "button", + { + class: "primary width-100 flex-1", + "aria-label": translation.connectToServer, "on:click": connect, "toggle:disabled": cannotConnect }, - translation.connectToServer, /* @__PURE__ */ createElement("span", { class: "icon" }, "arrow_forward") )), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement("div", { class: "tile" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "inbox"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("b", null, translation.mailbox), /* @__PURE__ */ createElement("span", { class: "secondary" }, translation.mailboxExplanation))), /* @__PURE__ */ createElement("div", { class: "flex-row width-input justify-end" }, /* @__PURE__ */ createElement( "button", { - class: "danger width-50", + class: "danger width-100 flex-1", "on:click": deleteMailbox, "toggle:disabled": cannotDeleteMailbox }, @@ -925,7 +914,7 @@ ), /* @__PURE__ */ createElement( "button", { - class: "primary width-50", + class: "primary width-100 flex-1", "on:click": requestMailbox, "toggle:disabled": cannotRequestMailbox }, @@ -934,57 +923,12 @@ ))); } - // src/Views/connectionStatusView.tsx - function ConnectionStatusView() { - return /* @__PURE__ */ createElement("div", { class: "tile width-input" }, /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("b", null, translation.connectionStatus), /* @__PURE__ */ createElement( - "span", - { - class: "success connected-only", - "subscribe:innerText": connectionMessage - } - ), /* @__PURE__ */ createElement("span", { class: "error disconnected-only" }, translation.disconnected))); - } - - // src/Views/connectionSection.tsx - function ConnectionSection() { - return /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("h2", null, translation.connection), ConnectionStatusView(), /* @__PURE__ */ createElement("hr", null), ConnectionInputView()); - } - - // src/Views/encryptionSection.tsx - var shouldShowKey = new State(false); - var inputType = createProxyState( - [shouldShowKey], - () => shouldShowKey.value ? "text" : "password" - ); - function EncryptionSection() { - return /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("h2", null, translation.encryption), /* @__PURE__ */ createElement( - "div", - { - class: "error tile margin-bottom", - "toggle:hidden": !isEncryptionUnavailable - }, - /* @__PURE__ */ createElement("span", { class: "icon" }, "warning"), - /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("b", null, translation.encryptionUnavailableTitle), /* @__PURE__ */ createElement("p", null, translation.encryptionUnavailableMessage)) - ), /* @__PURE__ */ createElement("label", { class: "tile" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "key"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translation.encryptionKey), /* @__PURE__ */ createElement( - "input", - { - "toggle:disabled": isEncryptionUnavailable, - "bind:value": encryptionKey, - "set:type": inputType, - placeholder: translation.encryptionKeyPlaceholder - } - ))), /* @__PURE__ */ createElement("label", { class: "inline" }, /* @__PURE__ */ createElement("input", { type: "checkbox", "bind:checked": shouldShowKey }), translation.showEncryptionKey)); - } - // src/Tabs/settingsTab.tsx function SettingsTab() { - return /* @__PURE__ */ createElement("article", { id: "settings-tab", "toggle:connected": isConnected }, /* @__PURE__ */ createElement("header", null, translation.settings), /* @__PURE__ */ createElement("div", null, ConnectionSection(), CommunicationSection(), EncryptionSection())); + return /* @__PURE__ */ createElement("article", { id: "settings-tab", "toggle:connected": isConnected }, /* @__PURE__ */ createElement("header", null, translation.overview), /* @__PURE__ */ createElement("div", null, ConnectionSection())); } // src/index.tsx - document.body.prepend( - /* @__PURE__ */ createElement("menu", { class: "mobile-only" }, /* @__PURE__ */ createElement("a", { class: "tab-link", href: "#settings-tab", active: true }, /* @__PURE__ */ createElement("span", { class: "icon" }, "settings"), translation.settings), /* @__PURE__ */ createElement("a", { class: "tab-link", href: "#message-tab" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "forum"), translation.messages)) - ); document.querySelector("main").append(SettingsTab(), MessageTab()); document.querySelector("main").classList.add("split"); })(); diff --git a/package.json b/package.json index 27806c66..88a71235 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "dependencies": { - "bloatless-react": "^1.2.6", + "bloatless-react": "^1.3.0", "esbuild": "latest", "udn-frontend": "^1.0.7-beta1" }, diff --git a/src/Model/chatModel.ts b/src/Model/chatModel.ts index c11620eb..8a2e2f37 100644 --- a/src/Model/chatModel.ts +++ b/src/Model/chatModel.ts @@ -1,10 +1,9 @@ import * as React from "bloatless-react"; -import { UDN, isConnected } from "./model"; +import { UDN, isConnected, senderName } from "./model"; import { decryptString, encryptString } from "../cryptUtility"; import { Message } from "udn-frontend"; -import { senderName } from "./viewModel"; import { storageKeys } from "../utility"; // TYPES diff --git a/src/Model/model.ts b/src/Model/model.ts index e9a8a504..20c5230e 100644 --- a/src/Model/model.ts +++ b/src/Model/model.ts @@ -1,15 +1,17 @@ import * as React from "bloatless-react"; +import { Chat, chats } from "./chatModel"; + import UDNFrontend from "udn-frontend"; -import { chats } from "./chatModel"; -import { serverAddress } from "./viewModel"; export const UDN = new UDNFrontend(); // CONNECTION // state -export const isConnected = new React.State(false); export const currentAddress = new React.State(""); +export const isConnected = new React.State(false); + +export const serverAddressInput = React.restoreState("socket-address", ""); export const cannotDisonnect = React.createProxyState( [isConnected], @@ -17,18 +19,19 @@ export const cannotDisonnect = React.createProxyState( ); export const cannotConnect = React.createProxyState( - [serverAddress, currentAddress, isConnected], + [serverAddressInput, currentAddress, isConnected], () => - serverAddress.value == "" || - (currentAddress.value == serverAddress.value && isConnected.value == true) + serverAddressInput.value == "" || + (currentAddress.value == serverAddressInput.value && + isConnected.value == true) ); // methods export function connect(): void { if (cannotConnect.value == true) return; - currentAddress.value = serverAddress.value; + currentAddress.value = serverAddressInput.value; isConnected.value = false; - UDN.connect(serverAddress.value); + UDN.connect(serverAddressInput.value); } export function disconnect(): void { @@ -36,23 +39,69 @@ export function disconnect(): void { UDN.disconnect(); } -// LISTENERS +export function resetAddressInput(): void { + this.serverAddressInput.value = this.serverAddress.value; +} + +// listeners UDN.onconnect = () => { isConnected.value = true; }; - UDN.onmessage = (data) => { chats.value.forEach((chat) => { chat.onmessage(data); - }) + }); }; UDN.ondisconnect = () => { isConnected.value = false; }; +// MAILBOX +export const mailboxId = React.restoreState("mailbox-id", ""); +export const isMailboxActive = new React.State(false); + +export const cannotDeleteMailbox = React.createProxyState( + [mailboxId, isMailboxActive], + () => mailboxId.value == "" || isMailboxActive.value == false +); +export const cannotRequestMailbox = React.createProxyState( + [isConnected, isMailboxActive], + () => isConnected.value == false || isMailboxActive.value == true +); + +export function requestMailbox(): void { + if (cannotRequestMailbox.value == true) return; + UDN.requestMailbox(); +} + +export function deleteMailbox(): void { + if (cannotDeleteMailbox.value == true) return; + UDN.deleteMailbox(mailboxId.value); +} + +UDN.onmailboxcreate = (id) => { + mailboxId.value = id; + UDN.connectMailbox(id); +} + +UDN.onmailboxconnect = () => { + isMailboxActive.value = true; +} + +UDN.onmailboxdelete = () => { + isMailboxActive.value = false; + mailboxId.value = ""; +} + +// MISC +export const isEncryptionUnavailable = window.crypto.subtle == undefined; +export const senderName = React.restoreState("sender-name", ""); + +export const selectedChat = new React.State(undefined); + // INIT -if (serverAddress.value != "") { +if (serverAddressInput.value != "") { connect(); } diff --git a/src/Model/viewModel.ts b/src/Model/viewModel.ts deleted file mode 100644 index 66b5287c..00000000 --- a/src/Model/viewModel.ts +++ /dev/null @@ -1,20 +0,0 @@ -import * as React from "bloatless-react"; - -import { Chat } from "./chatModel"; -import { translation } from "../translations"; - -// connection -export const currentAddress = new React.State(""); - -export const serverAddress = React.restoreState("socket-address", ""); -export const isConnected = new React.State(false); -export const connectionMessage = React.createProxyState([serverAddress], () => - translation.connectedTo(serverAddress.value) -); - -// misc -export const isEncryptionUnavailable = window.crypto.subtle == undefined; -export const senderName = React.restoreState("sender-name", ""); - -// chat -export const selectedChat = new React.State(undefined); diff --git a/src/Tabs/messageTab.tsx b/src/Tabs/messageTab.tsx index d0960fa3..f07e58f7 100644 --- a/src/Tabs/messageTab.tsx +++ b/src/Tabs/messageTab.tsx @@ -2,14 +2,25 @@ import * as React from "bloatless-react"; import { MessageComposer } from "../Views/messageComposer"; import { ThreadView } from "../Views/threadView"; -import { clearMessageHistory } from "../model"; +import { selectedChat } from "../Model/model"; import { translation } from "../translations"; export function MessageTab() { + function clearMessageHistory() { + selectedChat.value?.clearMessages(); + } + + const headerText = React.createProxyState([selectedChat], () => + selectedChat.value != undefined + ? selectedChat.value.currentChannel + : translation.messages + ); + return (
- {translation.messages} + +
); } diff --git a/src/Views/communicationSection.tsx b/src/Views/communicationSection.tsx deleted file mode 100644 index fad3639a..00000000 --- a/src/Views/communicationSection.tsx +++ /dev/null @@ -1,116 +0,0 @@ -import * as React from "bloatless-react"; - -import { - Channel, - addSecondaryChannel, - cannotAddSecondaryChannel, - cannotLeaveChannel, - cannotSetChannel, - leaveChannel, - newSecondaryChannelName, - primaryChannel, - removeSecondaryChannel, - secondaryChannels, - senderName, - setChannel, -} from "../model"; - -import { translation } from "../translations"; - -const secondaryChannelConverter: React.ListItemConverter = ( - channel -) => { - function remove() { - removeSecondaryChannel(channel); - } - - return ( -
-
- {channel.channelName} - - -
-
- ); -}; - -export function CommunicationSection() { - return ( -
-

{translation.communication}

- - - -
- - -
- - -
- -
- -
- - -
- -
-
- ); -} diff --git a/src/Views/connectionSection.tsx b/src/Views/connectionSection.tsx index 78beff27..f541eb15 100644 --- a/src/Views/connectionSection.tsx +++ b/src/Views/connectionSection.tsx @@ -1,16 +1,86 @@ import * as React from "bloatless-react"; -import { ConnectionInputView } from "./connectionSettingsView"; -import { ConnectionStatusView } from "./connectionStatusView"; +import { + cannotConnect, + cannotDeleteMailbox, + cannotDisonnect, + cannotRequestMailbox, + connect, + deleteMailbox, + disconnect, + requestMailbox, + serverAddressInput, +} from "../Model/model"; + import { translation } from "../translations"; export function ConnectionSection() { return ( -
-

{translation.connection}

- {ConnectionStatusView()} -
- {ConnectionInputView()} +
+ +
+ + + +
+ +
+ +
+ inbox +
+ {translation.mailbox} + {translation.mailboxExplanation} +
+
+
+ + +
); } diff --git a/src/Views/connectionSettingsView.tsx b/src/Views/connectionSettingsView.tsx deleted file mode 100644 index 502b757d..00000000 --- a/src/Views/connectionSettingsView.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import * as React from "bloatless-react"; - -import { - cannotConnect, - cannotDeleteMailbox, - cannotDisonnect, - cannotRequestMailbox, - connect, - deleteMailbox, - disconnect, - requestMailbox, - serverAddress, -} from "../model"; - -import { translation } from "../translations"; - -export function ConnectionInputView() { - return ( -
- -
- - -
- -
- -
- inbox -
- {translation.mailbox} - {translation.mailboxExplanation} -
-
-
- - -
-
- ); -} diff --git a/src/Views/connectionStatusView.tsx b/src/Views/connectionStatusView.tsx deleted file mode 100644 index 9a5ec25a..00000000 --- a/src/Views/connectionStatusView.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import * as React from "bloatless-react"; - -import { connectionMessage } from "../model"; -import { translation } from "../translations"; - -export function ConnectionStatusView() { - return ( -
-
- {translation.connectionStatus} - - {translation.disconnected} -
-
- ); -} diff --git a/src/Views/encryptionSection.tsx b/src/Views/encryptionSection.tsx deleted file mode 100644 index 8533bd9c..00000000 --- a/src/Views/encryptionSection.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import * as React from "bloatless-react"; - -import { encryptionKey, isEncryptionUnavailable } from "../model"; - -import { translation } from "../translations"; - -const shouldShowKey = new React.State(false); -const inputType = React.createProxyState([shouldShowKey], () => - shouldShowKey.value ? "text" : "password" -); - -export function EncryptionSection() { - return ( -
-

{translation.encryption}

-
- warning -
- {translation.encryptionUnavailableTitle} -

{translation.encryptionUnavailableMessage}

-
-
- - - -
- ); -} diff --git a/src/Views/messageComposer.tsx b/src/Views/messageComposer.tsx index aec184b5..4fdc230f 100644 --- a/src/Views/messageComposer.tsx +++ b/src/Views/messageComposer.tsx @@ -1,26 +1,34 @@ import * as React from "bloatless-react"; -import { cannotSendMessage, messageBody, sendMessage } from "../model"; - +import { Chat } from "../Model/chatModel"; +import { selectedChat } from "../Model/model"; import { translation } from "../translations"; export function MessageComposer() { - return ( -
- - -
- ); + const composerContent = React.createProxyState([selectedChat], () => { + if (selectedChat.value == undefined) return
; + + const chat = selectedChat.value; + return ( +
+ {" "} + + +
+ ); + }); + + return
; } diff --git a/src/Views/threadView.tsx b/src/Views/threadView.tsx index d236ac27..25576140 100644 --- a/src/Views/threadView.tsx +++ b/src/Views/threadView.tsx @@ -1,64 +1,78 @@ import * as React from "bloatless-react"; -import { - Message, - decryptReceivedMessage, - deleteMessage, - messages, -} from "../model"; +import { Chat, ChatMessage } from "../Model/chatModel"; +import { selectedChat } from "../Model/model"; import { translation } from "../translations"; -const messageConverter: React.ListItemConverter = (message) => { - const messageBody = React.createProxyState([messages], () => message.body); - - function copyMessage() { - navigator.clipboard.writeText(message.body); - } - - function decrypt() { - decryptReceivedMessage(message); - } - - function remove() { - deleteMessage(message); - } - - return ( -
-
-
- - {message.sender} - {message.channel} - - - - - - -
-
- - - {new Date(message.isoDate).toLocaleString()} - -
-
-
- ); -}; - export function ThreadView() { - return ( -
- ); + const threadViewContent = React.createProxyState([selectedChat], () => { + if (selectedChat.value == undefined) + return
{translation.noChatSelected}
; + + const chat = selectedChat.value; + + const messageConverter: React.ListItemConverter = ( + message + ) => { + function copyMessage() { + navigator.clipboard.writeText(message.body); + } + + function decrypt() { + chat.decryptReceivedMessage(message); + } + + function remove() { + chat.deleteMessage(message); + } + + return ( +
+
+
+ + {message.sender} - {message.channel} + + + + + + +
+
+ + + {new Date(message.isoDate).toLocaleString()} + +
+
+
+ ); + }; + + return ( +
+ ); + }); + + return
; } diff --git a/src/index.tsx b/src/index.tsx index f46ab735..ac34d779 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,22 +1,5 @@ -import "./styles.css"; - -import * as React from "bloatless-react"; - import { MessageTab } from "./Tabs/messageTab"; import { SettingsTab } from "./Tabs/settingsTab"; -import { translation } from "./translations"; -document.body.prepend( - - - settings - {translation.settings} - - - forum - {translation.messages} - - -); document.querySelector("main")!.append(SettingsTab(), MessageTab()); document.querySelector("main")!.classList.add("split"); diff --git a/src/styles.css b/src/styles.css deleted file mode 100644 index 275a3da4..00000000 --- a/src/styles.css +++ /dev/null @@ -1,30 +0,0 @@ -#settings-tab > div { - display: flex; - flex-direction: column; - gap: 2rem; -} - -#settings-tab > div > div { - width: var(--max-input-width); - min-width: 100%; - max-width: 100%; - flex: 1; -} - -/* CONNECTION */ - -article:not([connected]) .connected-only { - display: none; -} - -article:not([connected]) .disconnected-only { - display: flex; -} - -article[connected] .connected-only { - display: flex; -} - -article[connected] .disconnected-only { - display: none; -} diff --git a/src/translations.ts b/src/translations.ts index ae9f4a98..7674f55c 100644 --- a/src/translations.ts +++ b/src/translations.ts @@ -1,19 +1,23 @@ const englishTranslations = { // general setInput: "Set", - connectionStatus: "Connection status", - connectedTo: (server: string) => `Connected to "${server}"`, - disconnected: "Disconnected", - // settings - settings: "Settings", + // overview + overview: "Overview", + connection: "Connection", communication: "Communication", encryption: "Encryption", + serverAddress: "Server Address", serverAddressPlaceholder: "wss://192.168.0.69:3000", + connectToServer: "Connect", disconnect: "Disonnect", + resetAddress: "Reset address", + + noChatSelected: "No chat selected", + primaryChannel: "Primary channel", leaveChannel: "Leave", channelPlaceholder: "my-channel", @@ -50,12 +54,9 @@ const allTranslations: { [key: string]: typeof englishTranslations } = { es: { // general setInput: "OK", - connectionStatus: "Estado de Conexión", - connectedTo: (server: string) => `Conectado a "${server}"`, - disconnected: "Desconectado", // settings - settings: "Configuración", + overview: "Configuración", connection: "Conexión", communication: "Comunicación", encryption: "Cifrado", @@ -96,12 +97,9 @@ const allTranslations: { [key: string]: typeof englishTranslations } = { de: { // general setInput: "OK", - connectionStatus: "Verbindungsstatus", - connectedTo: (server: string) => `Verbunden mit "${server}"`, - disconnected: "Verbindung getrennt", // settings - settings: "Einstellungen", + overview: "Einstellungen", connection: "Verbindung", communication: "Kommunikation", encryption: "Verschlüsselung", From 2d7b9e54da3b477941ad59a0ee6f2bf4fb42825e Mon Sep 17 00:00:00 2001 From: marlon-erler Date: Tue, 9 Jul 2024 14:18:38 +0200 Subject: [PATCH 05/22] improve connection section --- dist/index.js | 27 ++++++++++++++++++++------- src/Model/model.ts | 26 ++++++++++++++++++++------ src/Views/connectionSection.tsx | 12 +++++++----- 3 files changed, 47 insertions(+), 18 deletions(-) diff --git a/dist/index.js b/dist/index.js index 0fa19633..ef8606da 100644 --- a/dist/index.js +++ b/dist/index.js @@ -580,9 +580,13 @@ // src/Model/model.ts var UDN = new UDNFrontend(); - var currentAddress = new State(""); var isConnected = new State(false); var serverAddressInput = restoreState("socket-address", ""); + var didRequestConnection = restoreState( + "did-request-connection", + false + ); + var currentAddress = restoreState("current-address", ""); var cannotDisonnect = createProxyState( [isConnected], () => isConnected.value == false @@ -591,16 +595,25 @@ [serverAddressInput, currentAddress, isConnected], () => serverAddressInput.value == "" || currentAddress.value == serverAddressInput.value && isConnected.value == true ); + var cannotResetAddress = createProxyState( + [serverAddressInput, currentAddress], + () => serverAddressInput.value == currentAddress.value + ); function connect() { if (cannotConnect.value == true) return; currentAddress.value = serverAddressInput.value; isConnected.value = false; + didRequestConnection.value = true; UDN.connect(serverAddressInput.value); } function disconnect() { + didRequestConnection.value = false; if (cannotDisonnect.value == true) return; UDN.disconnect(); } + function resetAddressInput() { + serverAddressInput.value = currentAddress.value; + } UDN.onconnect = () => { isConnected.value = true; }; @@ -644,7 +657,7 @@ var isEncryptionUnavailable = window.crypto.subtle == void 0; var senderName = restoreState("sender-name", ""); var selectedChat = new State(void 0); - if (serverAddressInput.value != "") { + if (serverAddressInput.value != "" && didRequestConnection.value == true) { connect(); } @@ -879,7 +892,7 @@ ))), /* @__PURE__ */ createElement("div", { class: "flex-row width-input justify-end" }, /* @__PURE__ */ createElement( "button", { - class: "danger width-100 flex-1", + class: "danger width-100 flex-1 justify-center", "aria-label": translation.disconnect, "on:click": disconnect, "toggle:disabled": cannotDisonnect @@ -888,16 +901,16 @@ ), /* @__PURE__ */ createElement( "button", { - class: "width-100 flex-1", + class: "width-100 flex-1 justify-center", "aria-label": translation.resetAddress, - "on:click": disconnect, - "toggle:disabled": cannotDisonnect + "on:click": resetAddressInput, + "toggle:disabled": cannotResetAddress }, /* @__PURE__ */ createElement("span", { class: "icon" }, "undo") ), /* @__PURE__ */ createElement( "button", { - class: "primary width-100 flex-1", + class: "primary width-100 flex-1 justify-center", "aria-label": translation.connectToServer, "on:click": connect, "toggle:disabled": cannotConnect diff --git a/src/Model/model.ts b/src/Model/model.ts index 20c5230e..a27b6d5d 100644 --- a/src/Model/model.ts +++ b/src/Model/model.ts @@ -8,10 +8,14 @@ export const UDN = new UDNFrontend(); // CONNECTION // state -export const currentAddress = new React.State(""); export const isConnected = new React.State(false); export const serverAddressInput = React.restoreState("socket-address", ""); +export const didRequestConnection = React.restoreState( + "did-request-connection", + false +); +export const currentAddress = React.restoreState("current-address", ""); export const cannotDisonnect = React.createProxyState( [isConnected], @@ -26,21 +30,31 @@ export const cannotConnect = React.createProxyState( isConnected.value == true) ); +export const cannotResetAddress = React.createProxyState( + [serverAddressInput, currentAddress], + () => serverAddressInput.value == currentAddress.value +); + // methods export function connect(): void { if (cannotConnect.value == true) return; + currentAddress.value = serverAddressInput.value; + isConnected.value = false; + didRequestConnection.value = true; + UDN.connect(serverAddressInput.value); } export function disconnect(): void { + didRequestConnection.value = false; if (cannotDisonnect.value == true) return; UDN.disconnect(); } export function resetAddressInput(): void { - this.serverAddressInput.value = this.serverAddress.value; + serverAddressInput.value = currentAddress.value; } // listeners @@ -84,16 +98,16 @@ export function deleteMailbox(): void { UDN.onmailboxcreate = (id) => { mailboxId.value = id; UDN.connectMailbox(id); -} +}; UDN.onmailboxconnect = () => { isMailboxActive.value = true; -} +}; UDN.onmailboxdelete = () => { isMailboxActive.value = false; mailboxId.value = ""; -} +}; // MISC export const isEncryptionUnavailable = window.crypto.subtle == undefined; @@ -102,6 +116,6 @@ export const senderName = React.restoreState("sender-name", ""); export const selectedChat = new React.State(undefined); // INIT -if (serverAddressInput.value != "") { +if (serverAddressInput.value != "" && didRequestConnection.value == true) { connect(); } diff --git a/src/Views/connectionSection.tsx b/src/Views/connectionSection.tsx index f541eb15..f6861edf 100644 --- a/src/Views/connectionSection.tsx +++ b/src/Views/connectionSection.tsx @@ -5,10 +5,12 @@ import { cannotDeleteMailbox, cannotDisonnect, cannotRequestMailbox, + cannotResetAddress, connect, deleteMailbox, disconnect, requestMailbox, + resetAddressInput, serverAddressInput, } from "../Model/model"; @@ -30,7 +32,7 @@ export function ConnectionSection() {
- - - {ThreadView()} -
{MessageComposer()}
- - ); + function clearMessageHistory() { + chat.clearMessages(); + } + + return ( +
+
+ + + + + + + + +
+ {ThreadView(chat)} +
{MessageComposer(chat)}
+
+ ); + }); + + return
; } diff --git a/src/Views/messageComposer.tsx b/src/Views/messageComposer.tsx index 4fdc230f..8a6a4fe9 100644 --- a/src/Views/messageComposer.tsx +++ b/src/Views/messageComposer.tsx @@ -1,34 +1,26 @@ import * as React from "bloatless-react"; import { Chat } from "../Model/chatModel"; -import { selectedChat } from "../Model/model"; import { translation } from "../translations"; -export function MessageComposer() { - const composerContent = React.createProxyState([selectedChat], () => { - if (selectedChat.value == undefined) return
; - - const chat = selectedChat.value; - return ( -
- {" "} - - -
- ); - }); - - return
; +export function MessageComposer(chat: Chat) { + return ( +
+ {" "} + + +
+ ); } diff --git a/src/Views/threadView.tsx b/src/Views/threadView.tsx index 25576140..fc25a9b9 100644 --- a/src/Views/threadView.tsx +++ b/src/Views/threadView.tsx @@ -2,77 +2,62 @@ import * as React from "bloatless-react"; import { Chat, ChatMessage } from "../Model/chatModel"; -import { selectedChat } from "../Model/model"; import { translation } from "../translations"; -export function ThreadView() { - const threadViewContent = React.createProxyState([selectedChat], () => { - if (selectedChat.value == undefined) - return
{translation.noChatSelected}
; +export function ThreadView(chat: Chat) { + const messageConverter: React.ListItemConverter = (message) => { + function copyMessage() { + navigator.clipboard.writeText(message.body); + } - const chat = selectedChat.value; + function decrypt() { + chat.decryptReceivedMessage(message); + } - const messageConverter: React.ListItemConverter = ( - message - ) => { - function copyMessage() { - navigator.clipboard.writeText(message.body); - } - - function decrypt() { - chat.decryptReceivedMessage(message); - } - - function remove() { - chat.deleteMessage(message); - } - - return ( -
-
-
- - {message.sender} - {message.channel} - - - - - - -
-
- - - {new Date(message.isoDate).toLocaleString()} - -
-
-
- ); - }; + function remove() { + chat.deleteMessage(message); + } return ( -
+
+
+
+ + {message.sender} - {message.channel} + + + + + + +
+
+ + + {new Date(message.isoDate).toLocaleString()} + +
+
+
); - }); + }; - return
; + return ( +
+ ); } diff --git a/src/translations.ts b/src/translations.ts index 7674f55c..4238cfc7 100644 --- a/src/translations.ts +++ b/src/translations.ts @@ -1,6 +1,7 @@ const englishTranslations = { // general setInput: "Set", + back: "Back", // overview overview: "Overview", From dae32fa6e7e451b21ab835c6ad0844728b0a8743 Mon Sep 17 00:00:00 2001 From: marlon-erler Date: Tue, 9 Jul 2024 15:15:17 +0200 Subject: [PATCH 07/22] add chat list section --- dist/index.js | 477 ++++++++++++++++++-------------- dist/styles.css | 5 + src/Model/chatModel.ts | 21 +- src/Model/model.ts | 25 +- src/Tabs/messageTab.tsx | 4 +- src/Tabs/settingsTab.tsx | 6 +- src/Views/chatListSection.tsx | 62 +++++ src/Views/connectionSection.tsx | 2 + src/translations.ts | 9 +- 9 files changed, 389 insertions(+), 222 deletions(-) create mode 100644 src/Views/chatListSection.tsx diff --git a/dist/index.js b/dist/index.js index 6de3455c..4b0748e1 100644 --- a/dist/index.js +++ b/dist/index.js @@ -364,6 +364,7 @@ cannotSetChannel; // init constructor(id = UUID()) { + this.id = id; this.primaryChannelInput = restoreState( storageKeys.primaryChannel(id), "" @@ -380,12 +381,12 @@ this.newSecondaryChannelName = new State(""); this.cannotSendMessage = createProxyState( [ - this.primaryChannelInput, + this.currentChannel, this.composingMessage, this.isSubscribed, isConnected ], - () => this.primaryChannelInput.value == "" || this.composingMessage.value == "" || this.isSubscribed.value == false || isConnected.value == false + () => this.currentChannel.value == "" || this.composingMessage.value == "" || this.isSubscribed.value == false || isConnected.value == false ); this.cannotAddSecondaryChannel = createProxyState( [this.newSecondaryChannelName], @@ -418,7 +419,7 @@ ...this.secondaryChannels.value.values() ].map((channel) => channel); const allChannelNames = [ - this.primaryChannelInput.value, + this.currentChannel.value, ...secondaryChannelNames ]; const encrypted = await encryptString( @@ -463,206 +464,20 @@ } }; var chatIds = restoreListState("chat-ids"); - var chatArray = [...chatIds.value.values()].map((id) => new Chat(id)); - var chats = new ListState(chatArray); - - // node_modules/udn-frontend/index.ts - var UDNFrontend = class { - ws; - // HANDLERS - connectionHandler = () => { - }; - disconnectionHandler = () => { - }; - messageHandler = (data) => { - }; - mailboxHandler = (mailboxId2) => { - }; - mailboxConnectionHandler = (mailboxId2) => { - }; - mailboxDeleteHandler = (mailboxId2) => { - }; - // INIT - set onconnect(handler) { - this.connectionHandler = handler; - } - set ondisconnect(handler) { - this.disconnectionHandler = handler; - } - set onmessage(handler) { - this.messageHandler = handler; - } - set onmailboxcreate(handler) { - this.mailboxHandler = handler; - } - set onmailboxconnect(handler) { - this.mailboxConnectionHandler = handler; - } - set onmailboxdelete(handler) { - this.mailboxDeleteHandler = handler; - } - // UTILITY METHODS - send(messageObject) { - if (this.ws == void 0) return false; - const messageString = JSON.stringify(messageObject); - this.ws.send(messageString); - return true; - } - // PUBLIC METHODS - // connection - connect(address) { - try { - this.disconnect(); - this.ws = new WebSocket(address); - this.ws.addEventListener("open", this.connectionHandler); - this.ws.addEventListener("close", this.disconnectionHandler); - this.ws.addEventListener("message", (message) => { - const dataString = message.data.toString(); - const data = JSON.parse(dataString); - if (data.assignedMailboxId) { - return this.mailboxHandler(data.assignedMailboxId); - } else if (data.connectedMailboxId) { - return this.mailboxConnectionHandler(data.connectedMailboxId); - } else if (data.deletedMailbox) { - return this.mailboxDeleteHandler(data.deletedMailbox); - } else { - this.messageHandler(data); - } - }); - } catch (error) { - console.error(error); - } - } - disconnect() { - this.ws?.close(); - } - // message - sendMessage(channel, body) { - const messageObject = { - messageChannel: channel, - messageBody: body - }; - return this.send(messageObject); - } - // subscription - subscribe(channel) { - const messageObject = { - subscribeChannel: channel - }; - return this.send(messageObject); - } - unsubscribe(channel) { - const messageObject = { - unsubscribeChannel: channel - }; - return this.send(messageObject); - } - // mailbox - requestMailbox() { - const messageObject = { - requestingMailboxSetup: true - }; - return this.send(messageObject); - } - connectMailbox(mailboxId2) { - const messageObject = { - requestedMailbox: mailboxId2 - }; - return this.send(messageObject); - } - deleteMailbox(mailboxId2) { - const messageObject = { - deletingMailbox: mailboxId2 - }; - return this.send(messageObject); - } - }; - - // src/Model/model.ts - var UDN = new UDNFrontend(); - var isConnected = new State(false); - var serverAddressInput = restoreState("socket-address", ""); - var didRequestConnection = restoreState( - "did-request-connection", - false - ); - var currentAddress = restoreState("current-address", ""); - var cannotDisonnect = createProxyState( - [isConnected], - () => isConnected.value == false - ); - var cannotConnect = createProxyState( - [serverAddressInput, currentAddress, isConnected], - () => serverAddressInput.value == "" || currentAddress.value == serverAddressInput.value && isConnected.value == true - ); - var cannotResetAddress = createProxyState( - [serverAddressInput, currentAddress], - () => serverAddressInput.value == currentAddress.value - ); - function connect() { - if (cannotConnect.value == true) return; - currentAddress.value = serverAddressInput.value; - isConnected.value = false; - didRequestConnection.value = true; - UDN.connect(serverAddressInput.value); + var chats = new ListState(); + chatIds.value.forEach((id) => chats.add(new Chat(id))); + function createChatWithName(name) { + const newChat = new Chat(); + newChat.currentChannel.value = name; + chatIds.add(newChat.id); + chats.add(newChat); } - function disconnect() { - didRequestConnection.value = false; - if (cannotDisonnect.value == true) return; - UDN.disconnect(); - } - function resetAddressInput() { - serverAddressInput.value = currentAddress.value; - } - UDN.onconnect = () => { - isConnected.value = true; - }; - UDN.onmessage = (data) => { - chats.value.forEach((chat) => { - chat.onmessage(data); + function removeChat(chat) { + Object.values(storageKeys).forEach((cb) => { + localStorage.removeItem(cb(chat.id)); }); - }; - UDN.ondisconnect = () => { - isConnected.value = false; - }; - var mailboxId = restoreState("mailbox-id", ""); - var isMailboxActive = new State(false); - var cannotDeleteMailbox = createProxyState( - [mailboxId, isMailboxActive], - () => mailboxId.value == "" || isMailboxActive.value == false - ); - var cannotRequestMailbox = createProxyState( - [isConnected, isMailboxActive], - () => isConnected.value == false || isMailboxActive.value == true - ); - function requestMailbox() { - if (cannotRequestMailbox.value == true) return; - UDN.requestMailbox(); - } - function deleteMailbox() { - if (cannotDeleteMailbox.value == true) return; - UDN.deleteMailbox(mailboxId.value); - } - UDN.onmailboxcreate = (id) => { - mailboxId.value = id; - UDN.connectMailbox(id); - }; - UDN.onmailboxconnect = () => { - isMailboxActive.value = true; - }; - UDN.onmailboxdelete = () => { - isMailboxActive.value = false; - mailboxId.value = ""; - }; - var isEncryptionUnavailable = window.crypto.subtle == void 0; - var senderName = restoreState("sender-name", ""); - var selectedChat = new State(void 0); - function closeChat() { - selectedChat.value = void 0; - document.getElementById("settings-tab")?.scrollIntoView(); - } - if (serverAddressInput.value != "" && didRequestConnection.value == true) { - connect(); + chats.remove(chat); + chatIds.remove(chat.id); } // src/translations.ts @@ -673,13 +488,17 @@ // overview overview: "Overview", connection: "Connection", - communication: "Communication", - encryption: "Encryption", + chats: "Chats", serverAddress: "Server Address", serverAddressPlaceholder: "wss://192.168.0.69:3000", connectToServer: "Connect", disconnect: "Disonnect", resetAddress: "Reset address", + newChatPrimaryChannel: "Primary channel", + newChatNamePlaceholder: "my-channel", + addChat: "Add", + removeChat: "Remove chat", + // messages noChatSelected: "No chat selected", primaryChannel: "Primary channel", leaveChannel: "Leave", @@ -857,7 +676,7 @@ function clearMessageHistory() { chat.clearMessages(); } - return /* @__PURE__ */ createElement("article", { id: "message-tab" }, /* @__PURE__ */ createElement("header", { class: "padding-0" }, /* @__PURE__ */ createElement("span", { class: "flex-row align-center" }, /* @__PURE__ */ createElement("button", { "aria-label": translation.back, "on:click": closeChat }, /* @__PURE__ */ createElement("span", { class: "icon" }, "arrow_back")), /* @__PURE__ */ createElement("span", { "subscribe:innerText": chat.currentChannel })), /* @__PURE__ */ createElement("span", null, /* @__PURE__ */ createElement( + return /* @__PURE__ */ createElement("article", { id: "message-tab" }, /* @__PURE__ */ createElement("header", { class: "padding-0" }, /* @__PURE__ */ createElement("span", { class: "flex-row align-center" }, /* @__PURE__ */ createElement("button", { "aria-label": translation.back, "on:click": closeChatView }, /* @__PURE__ */ createElement("span", { class: "icon" }, "arrow_back")), /* @__PURE__ */ createElement("span", { "subscribe:innerText": chat.currentChannel })), /* @__PURE__ */ createElement("span", null, /* @__PURE__ */ createElement( "button", { "aria-label": translation.clearHistory, @@ -869,9 +688,44 @@ return /* @__PURE__ */ createElement("div", { "children:set": messageTabContent }); } + // src/Views/chatListSection.tsx + var chatConverter = (chat) => { + function remove() { + removeChat(chat); + } + return /* @__PURE__ */ createElement("div", { class: "tile padding-0" }, /* @__PURE__ */ createElement("div", { class: "flex-row width-100 justify-apart align-center" }, /* @__PURE__ */ createElement("span", { class: "padding-h ellipsis", "subscribe:innerText": chat.currentChannel }), /* @__PURE__ */ createElement( + "button", + { + class: "danger", + "aria-label": translation.removeChat, + "on:click": remove + }, + /* @__PURE__ */ createElement("span", { class: "icon" }, "delete") + ))); + }; + function ChatListSection() { + return /* @__PURE__ */ createElement("div", { class: "flex-column" }, /* @__PURE__ */ createElement("h2", null, translation.chats), /* @__PURE__ */ createElement("label", { class: "tile" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "forum"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translation.newChatPrimaryChannel), /* @__PURE__ */ createElement( + "input", + { + "bind:value": newChatName, + placeholder: translation.newChatNamePlaceholder, + "on:enter": createChat + } + ))), /* @__PURE__ */ createElement("div", { class: "flex-row justify-end" }, /* @__PURE__ */ createElement( + "button", + { + class: "primary width-50", + "toggle:disabled": cannotCreateChat, + "on:click": createChat + }, + translation.addChat, + /* @__PURE__ */ createElement("span", { class: "icon" }, "add") + )), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement("div", { class: "flex-column gap", "children:append": [chats, chatConverter] })); + } + // src/Views/connectionSection.tsx function ConnectionSection() { - return /* @__PURE__ */ createElement("div", { class: "flex-column" }, /* @__PURE__ */ createElement("label", { class: "tile" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "cell_tower"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translation.serverAddress), /* @__PURE__ */ createElement( + return /* @__PURE__ */ createElement("div", { class: "flex-column" }, /* @__PURE__ */ createElement("h2", null, translation.connection), /* @__PURE__ */ createElement("label", { class: "tile" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "cell_tower"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translation.serverAddress), /* @__PURE__ */ createElement( "input", { "bind:value": serverAddressInput, @@ -927,10 +781,221 @@ // src/Tabs/settingsTab.tsx function SettingsTab() { - return /* @__PURE__ */ createElement("article", { id: "settings-tab", "toggle:connected": isConnected }, /* @__PURE__ */ createElement("header", null, translation.overview), /* @__PURE__ */ createElement("div", null, ConnectionSection())); + return /* @__PURE__ */ createElement("article", { id: "settings-tab", "toggle:connected": isConnected }, /* @__PURE__ */ createElement("header", null, translation.overview), /* @__PURE__ */ createElement("div", { class: "flex-column large-gap" }, ConnectionSection(), ChatListSection())); } + // node_modules/udn-frontend/index.ts + var UDNFrontend = class { + ws; + // HANDLERS + connectionHandler = () => { + }; + disconnectionHandler = () => { + }; + messageHandler = (data) => { + }; + mailboxHandler = (mailboxId2) => { + }; + mailboxConnectionHandler = (mailboxId2) => { + }; + mailboxDeleteHandler = (mailboxId2) => { + }; + // INIT + set onconnect(handler) { + this.connectionHandler = handler; + } + set ondisconnect(handler) { + this.disconnectionHandler = handler; + } + set onmessage(handler) { + this.messageHandler = handler; + } + set onmailboxcreate(handler) { + this.mailboxHandler = handler; + } + set onmailboxconnect(handler) { + this.mailboxConnectionHandler = handler; + } + set onmailboxdelete(handler) { + this.mailboxDeleteHandler = handler; + } + // UTILITY METHODS + send(messageObject) { + if (this.ws == void 0) return false; + const messageString = JSON.stringify(messageObject); + this.ws.send(messageString); + return true; + } + // PUBLIC METHODS + // connection + connect(address) { + try { + this.disconnect(); + this.ws = new WebSocket(address); + this.ws.addEventListener("open", this.connectionHandler); + this.ws.addEventListener("close", this.disconnectionHandler); + this.ws.addEventListener("message", (message) => { + const dataString = message.data.toString(); + const data = JSON.parse(dataString); + if (data.assignedMailboxId) { + return this.mailboxHandler(data.assignedMailboxId); + } else if (data.connectedMailboxId) { + return this.mailboxConnectionHandler(data.connectedMailboxId); + } else if (data.deletedMailbox) { + return this.mailboxDeleteHandler(data.deletedMailbox); + } else { + this.messageHandler(data); + } + }); + } catch (error) { + console.error(error); + } + } + disconnect() { + this.ws?.close(); + } + // message + sendMessage(channel, body) { + const messageObject = { + messageChannel: channel, + messageBody: body + }; + return this.send(messageObject); + } + // subscription + subscribe(channel) { + const messageObject = { + subscribeChannel: channel + }; + return this.send(messageObject); + } + unsubscribe(channel) { + const messageObject = { + unsubscribeChannel: channel + }; + return this.send(messageObject); + } + // mailbox + requestMailbox() { + const messageObject = { + requestingMailboxSetup: true + }; + return this.send(messageObject); + } + connectMailbox(mailboxId2) { + const messageObject = { + requestedMailbox: mailboxId2 + }; + return this.send(messageObject); + } + deleteMailbox(mailboxId2) { + const messageObject = { + deletingMailbox: mailboxId2 + }; + return this.send(messageObject); + } + }; + // src/index.tsx + var UDN = new UDNFrontend(); + var isConnected = new State(false); + var serverAddressInput = restoreState("socket-address", ""); + var didRequestConnection = restoreState( + "did-request-connection", + false + ); + var currentAddress = restoreState("current-address", ""); + var cannotDisonnect = createProxyState( + [isConnected], + () => isConnected.value == false + ); + var cannotConnect = createProxyState( + [serverAddressInput, currentAddress, isConnected], + () => serverAddressInput.value == "" || currentAddress.value == serverAddressInput.value && isConnected.value == true + ); + var cannotResetAddress = createProxyState( + [serverAddressInput, currentAddress], + () => serverAddressInput.value == currentAddress.value + ); + function connect() { + if (cannotConnect.value == true) return; + currentAddress.value = serverAddressInput.value; + isConnected.value = false; + didRequestConnection.value = true; + UDN.connect(serverAddressInput.value); + } + function disconnect() { + didRequestConnection.value = false; + if (cannotDisonnect.value == true) return; + UDN.disconnect(); + } + function resetAddressInput() { + serverAddressInput.value = currentAddress.value; + } + UDN.onconnect = () => { + isConnected.value = true; + }; + UDN.onmessage = (data) => { + chats.value.forEach((chat) => { + chat.onmessage(data); + }); + }; + UDN.ondisconnect = () => { + isConnected.value = false; + }; + var mailboxId = restoreState("mailbox-id", ""); + var isMailboxActive = new State(false); + var cannotDeleteMailbox = createProxyState( + [mailboxId, isMailboxActive], + () => mailboxId.value == "" || isMailboxActive.value == false + ); + var cannotRequestMailbox = createProxyState( + [isConnected, isMailboxActive], + () => isConnected.value == false || isMailboxActive.value == true + ); + function requestMailbox() { + if (cannotRequestMailbox.value == true) return; + UDN.requestMailbox(); + } + function deleteMailbox() { + if (cannotDeleteMailbox.value == true) return; + UDN.deleteMailbox(mailboxId.value); + } + function updateMailbox() { + deleteMailbox(); + updateMailbox(); + } + UDN.onmailboxcreate = (id) => { + mailboxId.value = id; + UDN.connectMailbox(id); + }; + UDN.onmailboxconnect = () => { + isMailboxActive.value = true; + }; + UDN.onmailboxdelete = () => { + isMailboxActive.value = false; + mailboxId.value = ""; + }; + var isEncryptionUnavailable = window.crypto.subtle == void 0; + var senderName = restoreState("sender-name", ""); + var selectedChat = new State(void 0); + var newChatName = new State(""); + var cannotCreateChat = createProxyState( + [newChatName], + () => newChatName.value == "" + ); + function createChat() { + if (cannotCreateChat.value == true) return; + createChatWithName(newChatName.value); + newChatName.value = ""; + } + function closeChatView() { + selectedChat.value = void 0; + document.getElementById("settings-tab")?.scrollIntoView(); + } + if (serverAddressInput.value != "" && didRequestConnection.value == true) { + connect(); + } document.querySelector("main").append(SettingsTab(), MessageTab()); document.querySelector("main").classList.add("split"); })(); diff --git a/dist/styles.css b/dist/styles.css index ec3eafe2..c48e0d43 100644 --- a/dist/styles.css +++ b/dist/styles.css @@ -214,6 +214,7 @@ SIZES --gap-between-tiles: 4px; --gap-in-tab-links: 1rem; --gap-in-controls: 1.2rem; + --large-gap: 2rem; /* margin */ --label-top-padding: 0.25rem; @@ -448,6 +449,10 @@ label.inline, gap: var(--gap-between-tiles) !important; } +.large-gap { + gap: var(--large-gap) !important; +} + header, button { justify-content: space-between; diff --git a/src/Model/chatModel.ts b/src/Model/chatModel.ts index 8a2e2f37..eccde818 100644 --- a/src/Model/chatModel.ts +++ b/src/Model/chatModel.ts @@ -1,6 +1,6 @@ import * as React from "bloatless-react"; -import { UDN, isConnected, senderName } from "./model"; +import { UDN, isConnected, senderName } from "../Model/model"; import { decryptString, encryptString } from "../cryptUtility"; import { Message } from "udn-frontend"; @@ -37,6 +37,9 @@ export class Chat { // init constructor(id: string = React.UUID()) { + this.id = id; + + // channels this.primaryChannelInput = React.restoreState( storageKeys.primaryChannel(id), "" @@ -45,6 +48,7 @@ export class Chat { storageKeys.secondaryChannels(id) ); + // messaging this.encryptionKey = React.restoreState(storageKeys.encyptionKey(id), ""); this.messages = React.restoreListState(storageKeys.messages(id)); @@ -54,15 +58,16 @@ export class Chat { ); this.newSecondaryChannelName = new React.State(""); + // guards this.cannotSendMessage = React.createProxyState( [ - this.primaryChannelInput, + this.currentChannel, this.composingMessage, this.isSubscribed, isConnected, ], () => - this.primaryChannelInput.value == "" || + this.currentChannel.value == "" || this.composingMessage.value == "" || this.isSubscribed.value == false || isConnected.value == false @@ -108,7 +113,7 @@ export class Chat { ...this.secondaryChannels.value.values(), ].map((channel) => channel); const allChannelNames: string[] = [ - this.primaryChannelInput.value, + this.currentChannel.value, ...secondaryChannelNames, ]; @@ -171,14 +176,16 @@ export class Chat { // INIT const chatIds = React.restoreListState("chat-ids"); -const chatArray = [...chatIds.value.values()].map((id) => new Chat(id)); -export const chats = new React.ListState(chatArray); +export const chats = new React.ListState(); + +chatIds.value.forEach((id) => chats.add(new Chat(id))); // METHODS export function createChatWithName(name: string): void { const newChat = new Chat(); - newChat.primaryChannelInput.value = name; + newChat.currentChannel.value = name; chatIds.add(newChat.id); + chats.add(newChat); } export function removeChat(chat: Chat): void { diff --git a/src/Model/model.ts b/src/Model/model.ts index 322c813c..48c99d79 100644 --- a/src/Model/model.ts +++ b/src/Model/model.ts @@ -1,13 +1,12 @@ import * as React from "bloatless-react"; -import { Chat, chats } from "./chatModel"; +import { Chat, chats, createChatWithName } from "./chatModel"; import UDNFrontend from "udn-frontend"; export const UDN = new UDNFrontend(); // CONNECTION -// state export const isConnected = new React.State(false); export const serverAddressInput = React.restoreState("socket-address", ""); @@ -38,7 +37,7 @@ export const cannotResetAddress = React.createProxyState( // methods export function connect(): void { if (cannotConnect.value == true) return; - + currentAddress.value = serverAddressInput.value; isConnected.value = false; @@ -95,6 +94,11 @@ export function deleteMailbox(): void { UDN.deleteMailbox(mailboxId.value); } +export function updateMailbox(): void { + deleteMailbox(); + updateMailbox(); +} + UDN.onmailboxcreate = (id) => { mailboxId.value = id; UDN.connectMailbox(id); @@ -114,8 +118,21 @@ export const isEncryptionUnavailable = window.crypto.subtle == undefined; export const senderName = React.restoreState("sender-name", ""); export const selectedChat = new React.State(undefined); +export const newChatName = new React.State(""); -export function closeChat() { +export const cannotCreateChat = React.createProxyState( + [newChatName], + () => newChatName.value == "" +); + +export function createChat() { + if (cannotCreateChat.value == true) return; + + createChatWithName(newChatName.value); + newChatName.value = ""; +} + +export function closeChatView() { selectedChat.value = undefined; document.getElementById("settings-tab")?.scrollIntoView(); } diff --git a/src/Tabs/messageTab.tsx b/src/Tabs/messageTab.tsx index 6257da82..66aef052 100644 --- a/src/Tabs/messageTab.tsx +++ b/src/Tabs/messageTab.tsx @@ -1,6 +1,6 @@ import * as React from "bloatless-react"; -import { closeChat, selectedChat } from "../Model/model"; +import { closeChatView, selectedChat } from "../Model/model"; import { MessageComposer } from "../Views/messageComposer"; import { ThreadView } from "../Views/threadView"; @@ -25,7 +25,7 @@ export function MessageTab() {
- diff --git a/src/Tabs/settingsTab.tsx b/src/Tabs/settingsTab.tsx index e2624321..96d82831 100644 --- a/src/Tabs/settingsTab.tsx +++ b/src/Tabs/settingsTab.tsx @@ -1,5 +1,6 @@ import * as React from "bloatless-react"; +import { ChatListSection } from "../Views/chatListSection"; import { ConnectionSection } from "../Views/connectionSection"; import { isConnected } from "../Model/model"; import { translation } from "../translations"; @@ -8,7 +9,10 @@ export function SettingsTab() { return (
{translation.overview}
-
{ConnectionSection()}
+
+ {ConnectionSection()} + {ChatListSection()} +
); } diff --git a/src/Views/chatListSection.tsx b/src/Views/chatListSection.tsx new file mode 100644 index 00000000..ca804498 --- /dev/null +++ b/src/Views/chatListSection.tsx @@ -0,0 +1,62 @@ +import * as React from "bloatless-react"; + +import { Chat, chats, removeChat } from "../Model/chatModel"; +import { cannotCreateChat, createChat, newChatName } from "../Model/model"; + +import { translation } from "../translations"; + +const chatConverter: React.ListItemConverter = (chat) => { + function remove() { + removeChat(chat); + } + + return ( +
+
+ + + +
+
+ ); +}; + +export function ChatListSection() { + return ( +
+

{translation.chats}

+ + +
+ +
+ +
+ +
+
+ ); +} diff --git a/src/Views/connectionSection.tsx b/src/Views/connectionSection.tsx index f6861edf..e39da79e 100644 --- a/src/Views/connectionSection.tsx +++ b/src/Views/connectionSection.tsx @@ -19,6 +19,8 @@ import { translation } from "../translations"; export function ConnectionSection() { return (
+

{translation.connection}

+
{ThreadView(chat)}
{MessageComposer(chat)}
+ + {ChatOptionModal(chat, isShowingOptions)}
); }); diff --git a/src/Tabs/settingsTab.tsx b/src/Tabs/overviewTab.tsx similarity index 50% rename from src/Tabs/settingsTab.tsx rename to src/Tabs/overviewTab.tsx index 96d82831..ffd0938c 100644 --- a/src/Tabs/settingsTab.tsx +++ b/src/Tabs/overviewTab.tsx @@ -1,15 +1,28 @@ import * as React from "bloatless-react"; +import { isConnected, isEncryptionAvailable } from "../Model/model"; + import { ChatListSection } from "../Views/chatListSection"; import { ConnectionSection } from "../Views/connectionSection"; -import { isConnected } from "../Model/model"; import { translation } from "../translations"; -export function SettingsTab() { +export function OverviewTab() { return (
{translation.overview}
+
+ +
+ warning +
+ {translation.encryptionUnavailableTitle} + + {translation.encryptionUnavailableMessage} + +
+
+ {ConnectionSection()} {ChatListSection()}
diff --git a/src/Views/chatListSection.tsx b/src/Views/chatListSection.tsx index ca804498..77c95788 100644 --- a/src/Views/chatListSection.tsx +++ b/src/Views/chatListSection.tsx @@ -1,29 +1,34 @@ import * as React from "bloatless-react"; -import { Chat, chats, removeChat } from "../Model/chatModel"; -import { cannotCreateChat, createChat, newChatName } from "../Model/model"; +import { Chat, removeChat } from "../Model/chatModel"; +import { + cannotCreateChat, + chats, + createChat, + newChatName, + selectChat, + selectedChat, +} from "../Model/model"; import { translation } from "../translations"; const chatConverter: React.ListItemConverter = (chat) => { - function remove() { - removeChat(chat); + function select() { + selectChat(chat); } + const isSelected = React.createProxyState( + [selectedChat], + () => selectedChat.value == chat + ); + return ( -
-
- - - +
+ arrow_forward + ); }; @@ -56,7 +61,10 @@ export function ChatListSection() {
-
+
); } diff --git a/src/Views/chatOptionModal.tsx b/src/Views/chatOptionModal.tsx new file mode 100644 index 00000000..3ea958c4 --- /dev/null +++ b/src/Views/chatOptionModal.tsx @@ -0,0 +1,119 @@ +import * as React from "bloatless-react"; + +import { Chat } from "../Model/chatModel"; +import { translation } from "../translations"; + +export function ChatOptionModal(chat: Chat, isPresented: React.State) { + function closeModal() { + isPresented.value = false; + } + + const secondaryChannelConverter: React.ListItemConverter = ( + secondaryChannel + ) => { + function remove() { + chat.removeSecondaryChannel(secondaryChannel); + } + + return ( +
+
+ {secondaryChannel} + +
+
+ ); + }; + + return ( + + ); +} diff --git a/src/Views/connectionSection.tsx b/src/Views/connectionSection.tsx index e39da79e..f45b5d0f 100644 --- a/src/Views/connectionSection.tsx +++ b/src/Views/connectionSection.tsx @@ -43,7 +43,7 @@ export function ConnectionSection() {
- + {message.body} {new Date(message.isoDate).toLocaleString()} diff --git a/src/index.tsx b/src/index.tsx index ac34d779..aa7178fb 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,5 +1,5 @@ import { MessageTab } from "./Tabs/messageTab"; -import { SettingsTab } from "./Tabs/settingsTab"; +import { OverviewTab } from "./Tabs/overviewTab"; -document.querySelector("main")!.append(SettingsTab(), MessageTab()); +document.querySelector("main")!.append(OverviewTab(), MessageTab()); document.querySelector("main")!.classList.add("split"); diff --git a/src/translations.ts b/src/translations.ts index 22355947..4999895b 100644 --- a/src/translations.ts +++ b/src/translations.ts @@ -1,7 +1,9 @@ const englishTranslations = { // general - setInput: "Set", + set: "Set", back: "Back", + undoChanges: "Undo changes", + close: "Close", // overview overview: "Overview", @@ -9,46 +11,46 @@ const englishTranslations = { connection: "Connection", chats: "Chats", + encryptionUnavailableTitle: "Encryption is not available", + encryptionUnavailableMessage: + "Obtain this app via HTTPS or continue without encryption", + serverAddress: "Server Address", serverAddressPlaceholder: "wss://192.168.0.69:3000", connectToServer: "Connect", disconnect: "Disonnect", - resetAddress: "Reset address", - newChatPrimaryChannel: "Primary channel", - newChatNamePlaceholder: "my-channel", - addChat: "Add", - removeChat: "Remove chat", - - // messages - noChatSelected: "No chat selected", - - primaryChannel: "Primary channel", - leaveChannel: "Leave", - channelPlaceholder: "my-channel", - addSecondaryChannel: "Add secondary channel", - removeSecondaryChannel: "Remove secondary channel", - newSecondaryChannelPlaceholder: "Add secondary channel", - encryptionKey: "Encryption key", - encryptionKeyPlaceholder: "1jg028ej40d", - showEncryptionKey: "Show encryption key", - myName: "My Name", - yourNamePlaceholder: "Jane Doe", mailbox: "Mailbox", requestMailbox: "Enable", deleteMailbox: "Disable", mailboxExplanation: "When you're disconnected, messages will be kept on the server temporarily", + newChatPrimaryChannel: "Primary channel", + newChatNamePlaceholder: "my-channel", + addChat: "Add", + removeChat: "Remove chat", + // messages - messages: "Messages", + showChatOptions: "show chat options", + configureChatTitle: "Configure Chat", + + primaryChannel: "Primary channel", + primaryChannelPlaceholder: "my-channel", + + secondaryChannel: "Secondary channel", + secondaryChannelPlaceholder: "Add secondary channel", + addSecondaryChannel: "Add secondary channel", + removeSecondaryChannel: "Remove secondary channel", + + encryptionKey: "Encryption key", + encryptionKeyPlaceholder: "n10d2482dg283hg", + + noChatSelected: "No chat selected", + composerPlaceholder: "Type a message...", sendMessage: "Send", - clearHistory: "Clear history", - encryptionUnavailableTitle: "Encryption is not available", - encryptionUnavailableMessage: - "Obtain this app via HTTPS or continue without encryption", decryptMessage: "Decrypt message", copyMessage: "Copy message", @@ -57,92 +59,6 @@ const englishTranslations = { const allTranslations: { [key: string]: typeof englishTranslations } = { en: englishTranslations, - es: { - // general - setInput: "OK", - - // settings - overview: "Configuración", - connection: "Conexión", - communication: "Comunicación", - encryption: "Cifrado", - serverAddress: "Dirección del servidor", - serverAddressPlaceholder: "wss://192.168.0.69:3000", - connectToServer: "Conectar", - disconnect: "Desconectar", - primaryChannel: "Canal principal", - leaveChannel: "Salir", - channelPlaceholder: "mi-canal", - addSecondaryChannel: "Añadir canal segundario", - removeSecondaryChannel: "Eliminar canal segundario", - newSecondaryChannelPlaceholder: "Añadir canal segundario", - encryptionKey: "Clave de cifrada", - encryptionKeyPlaceholder: "1jg028ej40d", - showEncryptionKey: "Mostrar clave de cifrado", - myName: "Mi nombre", - yourNamePlaceholder: "Juan Pérez", - mailbox: "Buzón", - requestMailbox: "Activar", - deleteMailbox: "Desactivar", - mailboxExplanation: - "Si estás sin conexión, los mensajes se guardarán temporalmente en el servidor", - - // messages - messages: "Mensajes", - composerPlaceholder: "Escribe un mensaje...", - sendMessage: "Enviar", - clearHistory: "Borrar historial", - encryptionUnavailableTitle: "Cifrado no disponible", - encryptionUnavailableMessage: - "Obtén esta página a través de HTTPS para cifrar o continúa sin cifrado", - - decryptMessage: "Descifrar mensaje", - copyMessage: "Copiar mensaje", - deleteMessage: "Eliminar mensaje", - }, - de: { - // general - setInput: "OK", - - // settings - overview: "Einstellungen", - connection: "Verbindung", - communication: "Kommunikation", - encryption: "Verschlüsselung", - serverAddress: "Serveraddresse", - serverAddressPlaceholder: "wss://192.168.0.69:3000", - connectToServer: "Verbinden", - disconnect: "Trennen", - primaryChannel: "Hauptkanal", - leaveChannel: "Verlassen", - channelPlaceholder: "mein-kanal", - addSecondaryChannel: "Zweitkanal hinzufügen", - removeSecondaryChannel: "Zweitkanal entfernen", - newSecondaryChannelPlaceholder: "Zweitkanal hinzufügen", - encryptionKey: "Schlüssel", - encryptionKeyPlaceholder: "1jg028ej40d", - showEncryptionKey: "Schlüssel anzeigen", - myName: "Mein Name", - yourNamePlaceholder: "Max Mustermann", - mailbox: "Briefkasten", - requestMailbox: "Aktivieren", - deleteMailbox: "Deaktivieren", - mailboxExplanation: - "Nachrichten werden temporär auf dem Server gespeichert, wenn deine Verbindung getrennt ist", - - // messages - messages: "Nachrichten", - composerPlaceholder: "Neue Nachricht...", - sendMessage: "Senden", - clearHistory: "Nachrichtenverlauf leeren", - encryptionUnavailableTitle: "Verschlüsselung nicht möglich", - encryptionUnavailableMessage: - "Um Nachrichten zu verschlüsseln, lade diese Seite über HTTPS.", - - decryptMessage: "Nachricht entschlüsseln", - copyMessage: "Nachricht kopieren", - deleteMessage: "Nachricht löschen", - }, }; const language = navigator.language.substring(0, 2); From a2a135af0cf0f01be00f2dd4db34c5fa6d063adc Mon Sep 17 00:00:00 2001 From: marlon-erler Date: Tue, 9 Jul 2024 17:27:15 +0200 Subject: [PATCH 09/22] add sender name field --- dist/index.js | 16 +++++++++++++++- src/Tabs/overviewTab.tsx | 3 ++- src/Views/personalSection.tsx | 23 +++++++++++++++++++++++ src/translations.ts | 4 ++++ 4 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 src/Views/personalSection.tsx diff --git a/dist/index.js b/dist/index.js index 8b7b04cd..621e5d78 100644 --- a/dist/index.js +++ b/dist/index.js @@ -711,8 +711,11 @@ close: "Close", // overview overview: "Overview", + personal: "You", connection: "Connection", chats: "Chats", + yourName: "Your Name", + namePlaceholder: "Jane Doe", encryptionUnavailableTitle: "Encryption is not available", encryptionUnavailableMessage: "Obtain this app via HTTPS or continue without encryption", serverAddress: "Server Address", @@ -1007,9 +1010,20 @@ ))); } + // src/Views/personalSection.tsx + function PersonalSection() { + return /* @__PURE__ */ createElement("div", { class: "flex-column" }, /* @__PURE__ */ createElement("h2", null, translation.personal), /* @__PURE__ */ createElement("label", { class: "tile" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "account_circle"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translation.yourName), /* @__PURE__ */ createElement( + "input", + { + "bind:value": senderName, + placeholder: translation.namePlaceholder + } + )))); + } + // src/Tabs/overviewTab.tsx function OverviewTab() { - return /* @__PURE__ */ createElement("article", { id: "settings-tab", "toggle:connected": isConnected }, /* @__PURE__ */ createElement("header", null, translation.overview), /* @__PURE__ */ createElement("div", { class: "flex-column large-gap" }, /* @__PURE__ */ createElement("div", { class: "tile error flex-no", "toggle:hidden": isEncryptionAvailable }, /* @__PURE__ */ createElement("span", { class: "icon" }, "warning"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("b", null, translation.encryptionUnavailableTitle), /* @__PURE__ */ createElement("span", { class: "secondary" }, translation.encryptionUnavailableMessage))), ConnectionSection(), ChatListSection())); + return /* @__PURE__ */ createElement("article", { id: "settings-tab", "toggle:connected": isConnected }, /* @__PURE__ */ createElement("header", null, translation.overview), /* @__PURE__ */ createElement("div", { class: "flex-column large-gap" }, /* @__PURE__ */ createElement("div", { class: "tile error flex-no", "toggle:hidden": isEncryptionAvailable }, /* @__PURE__ */ createElement("span", { class: "icon" }, "warning"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("b", null, translation.encryptionUnavailableTitle), /* @__PURE__ */ createElement("span", { class: "secondary" }, translation.encryptionUnavailableMessage))), PersonalSection(), ConnectionSection(), ChatListSection())); } // src/index.tsx diff --git a/src/Tabs/overviewTab.tsx b/src/Tabs/overviewTab.tsx index ffd0938c..e94b48ee 100644 --- a/src/Tabs/overviewTab.tsx +++ b/src/Tabs/overviewTab.tsx @@ -4,6 +4,7 @@ import { isConnected, isEncryptionAvailable } from "../Model/model"; import { ChatListSection } from "../Views/chatListSection"; import { ConnectionSection } from "../Views/connectionSection"; +import { PersonalSection } from "../Views/personalSection"; import { translation } from "../translations"; export function OverviewTab() { @@ -12,7 +13,6 @@ export function OverviewTab() {
{translation.overview}
-
warning
@@ -23,6 +23,7 @@ export function OverviewTab() {
+ {PersonalSection()} {ConnectionSection()} {ChatListSection()}
diff --git a/src/Views/personalSection.tsx b/src/Views/personalSection.tsx new file mode 100644 index 00000000..331e2e03 --- /dev/null +++ b/src/Views/personalSection.tsx @@ -0,0 +1,23 @@ +import * as React from "bloatless-react"; + +import { senderName } from "../Model/model"; +import { translation } from "../translations"; + +export function PersonalSection() { + return ( +
+

{translation.personal}

+ + +
+ ); +} diff --git a/src/translations.ts b/src/translations.ts index 4999895b..0236e060 100644 --- a/src/translations.ts +++ b/src/translations.ts @@ -8,9 +8,13 @@ const englishTranslations = { // overview overview: "Overview", + personal: "You", connection: "Connection", chats: "Chats", + yourName: "Your Name", + namePlaceholder: "Jane Doe", + encryptionUnavailableTitle: "Encryption is not available", encryptionUnavailableMessage: "Obtain this app via HTTPS or continue without encryption", From 1202789a4380804d39df30324a34a2e57f3495f0 Mon Sep 17 00:00:00 2001 From: marlon-erler Date: Tue, 9 Jul 2024 17:32:51 +0200 Subject: [PATCH 10/22] allow deleting chat --- dist/index.js | 16 ++++++++++++++-- src/Model/chatModel.ts | 22 +++++++++++----------- src/Views/chatOptionModal.tsx | 14 +++++++++++++- src/translations.ts | 3 ++- 4 files changed, 40 insertions(+), 15 deletions(-) diff --git a/dist/index.js b/dist/index.js index 621e5d78..d2a24669 100644 --- a/dist/index.js +++ b/dist/index.js @@ -403,6 +403,14 @@ () => this.primaryChannelInput.value == this.primaryChannel.value ); } + // general + deleteSelf = () => { + Object.values(storageKeys).forEach((storageKey) => { + localStorage.removeItem(storageKey(this.id)); + }); + chats.remove(this); + chatIds.remove(this.id); + }; // handlers onmessage = async (data) => { if (data.messageChannel && data.messageChannel != this.primaryChannel.value) @@ -729,7 +737,6 @@ newChatPrimaryChannel: "Primary channel", newChatNamePlaceholder: "my-channel", addChat: "Add", - removeChat: "Remove chat", // messages showChatOptions: "show chat options", configureChatTitle: "Configure Chat", @@ -741,6 +748,7 @@ removeSecondaryChannel: "Remove secondary channel", encryptionKey: "Encryption key", encryptionKeyPlaceholder: "n10d2482dg283hg", + removeChat: "Remove chat", noChatSelected: "No chat selected", composerPlaceholder: "Type a message...", sendMessage: "Send", @@ -759,6 +767,10 @@ function closeModal() { isPresented.value = false; } + function deleteChat() { + chat.deleteSelf(); + closeModal(); + } const secondaryChannelConverter = (secondaryChannel) => { function remove() { chat.removeSecondaryChannel(secondaryChannel); @@ -830,7 +842,7 @@ placeholder: translation.encryptionKeyPlaceholder, "bind:value": chat.encryptionKey } - )))), /* @__PURE__ */ createElement("button", { class: "danger", "on:click": closeModal }, translation.close, /* @__PURE__ */ createElement("span", { class: "icon" }, "close")))); + ))), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement("button", { class: "danger", "on:click": deleteChat }, translation.removeChat, /* @__PURE__ */ createElement("span", { class: "icon" }, "delete"))), /* @__PURE__ */ createElement("button", { class: "danger", "on:click": closeModal }, translation.close, /* @__PURE__ */ createElement("span", { class: "icon" }, "close")))); } // src/Views/messageComposer.tsx diff --git a/src/Model/chatModel.ts b/src/Model/chatModel.ts index cdea662b..89890ff5 100644 --- a/src/Model/chatModel.ts +++ b/src/Model/chatModel.ts @@ -86,11 +86,20 @@ export class Chat { ); this.cannotUndoChannel = React.createProxyState( [this.primaryChannelInput, this.primaryChannel], - () => - this.primaryChannelInput.value == this.primaryChannel.value + () => this.primaryChannelInput.value == this.primaryChannel.value ); } + // general + deleteSelf = () => { + Object.values(storageKeys).forEach((storageKey) => { + localStorage.removeItem(storageKey(this.id)); + }); + + chats.remove(this); + chatIds.remove(this.id); + } + // handlers onmessage = async (data: Message): Promise => { if (data.messageChannel && data.messageChannel != this.primaryChannel.value) @@ -201,12 +210,3 @@ export function createChatWithName(name: string): void { UDN.subscribe(name); } - -export function removeChat(chat: Chat): void { - Object.values(storageKeys).forEach((cb) => { - localStorage.removeItem(cb(chat.id)); - }); - - chats.remove(chat); - chatIds.remove(chat.id); -} diff --git a/src/Views/chatOptionModal.tsx b/src/Views/chatOptionModal.tsx index 3ea958c4..a1eec98a 100644 --- a/src/Views/chatOptionModal.tsx +++ b/src/Views/chatOptionModal.tsx @@ -8,6 +8,11 @@ export function ChatOptionModal(chat: Chat, isPresented: React.State) { isPresented.value = false; } + function deleteChat() { + chat.deleteSelf() + closeModal(); + } + const secondaryChannelConverter: React.ListItemConverter = ( secondaryChannel ) => { @@ -108,8 +113,15 @@ export function ChatOptionModal(chat: Chat, isPresented: React.State) { >
+ +
+ + - diff --git a/src/translations.ts b/src/translations.ts index 0236e060..5c31229d 100644 --- a/src/translations.ts +++ b/src/translations.ts @@ -34,7 +34,6 @@ const englishTranslations = { newChatPrimaryChannel: "Primary channel", newChatNamePlaceholder: "my-channel", addChat: "Add", - removeChat: "Remove chat", // messages showChatOptions: "show chat options", @@ -51,6 +50,8 @@ const englishTranslations = { encryptionKey: "Encryption key", encryptionKeyPlaceholder: "n10d2482dg283hg", + removeChat: "Remove chat", + noChatSelected: "No chat selected", composerPlaceholder: "Type a message...", From cfe186f1d0adddc16b30e8c692654e464172b97f Mon Sep 17 00:00:00 2001 From: marlon-erler Date: Tue, 9 Jul 2024 17:35:32 +0200 Subject: [PATCH 11/22] fix layout --- dist/index.js | 7 ++++--- src/Model/model.ts | 1 + src/Tabs/messageTab.tsx | 8 +++++--- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/dist/index.js b/dist/index.js index d2a24669..861862ab 100644 --- a/dist/index.js +++ b/dist/index.js @@ -704,6 +704,7 @@ } function selectChat(chat) { selectedChat.value = chat; + document.getElementById("message-tab")?.scrollIntoView(); } chatIds.value.forEach((id) => chats.add(new Chat(id))); if (serverAddressInput.value != "" && didRequestConnection.value == true) { @@ -842,7 +843,7 @@ placeholder: translation.encryptionKeyPlaceholder, "bind:value": chat.encryptionKey } - ))), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement("button", { class: "danger", "on:click": deleteChat }, translation.removeChat, /* @__PURE__ */ createElement("span", { class: "icon" }, "delete"))), /* @__PURE__ */ createElement("button", { class: "danger", "on:click": closeModal }, translation.close, /* @__PURE__ */ createElement("span", { class: "icon" }, "close")))); + ))), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement("button", { class: "danger", "on:click": deleteChat }, translation.removeChat, /* @__PURE__ */ createElement("span", { class: "icon" }, "delete"))), /* @__PURE__ */ createElement("button", { "on:click": closeModal }, translation.close, /* @__PURE__ */ createElement("span", { class: "icon" }, "close")))); } // src/Views/messageComposer.tsx @@ -917,7 +918,7 @@ function hideOptions() { isShowingOptions.value = false; } - return /* @__PURE__ */ createElement("article", { id: "message-tab" }, /* @__PURE__ */ createElement("header", { class: "padding-0" }, /* @__PURE__ */ createElement("span", { class: "flex-row align-center" }, /* @__PURE__ */ createElement("button", { "aria-label": translation.back, "on:click": closeChatView }, /* @__PURE__ */ createElement("span", { class: "icon" }, "arrow_back")), /* @__PURE__ */ createElement("span", { "subscribe:innerText": chat.primaryChannel })), /* @__PURE__ */ createElement("span", null, /* @__PURE__ */ createElement( + return /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("header", { class: "padding-0" }, /* @__PURE__ */ createElement("span", { class: "flex-row align-center" }, /* @__PURE__ */ createElement("button", { "aria-label": translation.back, "on:click": closeChatView }, /* @__PURE__ */ createElement("span", { class: "icon" }, "arrow_back")), /* @__PURE__ */ createElement("span", { "subscribe:innerText": chat.primaryChannel })), /* @__PURE__ */ createElement("span", null, /* @__PURE__ */ createElement( "button", { "aria-label": translation.showChatOptions, @@ -926,7 +927,7 @@ /* @__PURE__ */ createElement("span", { class: "icon" }, "tune") ))), ThreadView(chat), /* @__PURE__ */ createElement("footer", null, MessageComposer(chat)), ChatOptionModal(chat, isShowingOptions)); }); - return /* @__PURE__ */ createElement("div", { "children:set": messageTabContent }); + return /* @__PURE__ */ createElement("article", { id: "message-tab", "children:set": messageTabContent }); } // src/Views/chatListSection.tsx diff --git a/src/Model/model.ts b/src/Model/model.ts index 21048169..45db37a6 100644 --- a/src/Model/model.ts +++ b/src/Model/model.ts @@ -147,6 +147,7 @@ export function closeChatView() { export function selectChat(chat: Chat) { selectedChat.value = chat; + document.getElementById("message-tab")?.scrollIntoView(); } chatIds.value.forEach((id) => chats.add(new Chat(id))); diff --git a/src/Tabs/messageTab.tsx b/src/Tabs/messageTab.tsx index af468d84..70203dc5 100644 --- a/src/Tabs/messageTab.tsx +++ b/src/Tabs/messageTab.tsx @@ -27,7 +27,7 @@ export function MessageTab() { } return ( -
+
+ {ThreadView(chat)} +
{MessageComposer(chat)}
{ChatOptionModal(chat, isShowingOptions)} -
+
); }); - return
; + return
; } From 5667a55b747e14ea1ea6e2e24ece2a490cf73469 Mon Sep 17 00:00:00 2001 From: marlon-erler Date: Tue, 9 Jul 2024 17:38:50 +0200 Subject: [PATCH 12/22] fix decryption --- dist/index.js | 6 +++++- src/Views/threadView.tsx | 7 ++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/dist/index.js b/dist/index.js index 861862ab..9887b88a 100644 --- a/dist/index.js +++ b/dist/index.js @@ -880,6 +880,10 @@ function remove() { chat.deleteMessage(message); } + const messageBody = createProxyState( + [chat.messages], + () => message.body + ); return /* @__PURE__ */ createElement("div", { class: "tile width-100 flex-no padding-0" }, /* @__PURE__ */ createElement("div", { class: "flex-column" }, /* @__PURE__ */ createElement("div", { class: "flex-row justify-apart align-center secondary" }, /* @__PURE__ */ createElement("span", { class: "padding-h ellipsis" }, /* @__PURE__ */ createElement("b", { class: "info" }, message.sender), " - ", message.channel), /* @__PURE__ */ createElement("span", { class: "flex-row" }, /* @__PURE__ */ createElement( "button", { @@ -894,7 +898,7 @@ "on:click": decrypt2 }, /* @__PURE__ */ createElement("span", { class: "icon" }, "key") - ), /* @__PURE__ */ createElement("button", { "aria-label": translation.deleteMessage, "on:click": remove }, /* @__PURE__ */ createElement("span", { class: "icon" }, "delete")))), /* @__PURE__ */ createElement("div", { class: "flex-column padding-h padding-bottom" }, /* @__PURE__ */ createElement("b", { class: "break-word" }, message.body), /* @__PURE__ */ createElement("span", { class: "secondary" }, new Date(message.isoDate).toLocaleString())))); + ), /* @__PURE__ */ createElement("button", { "aria-label": translation.deleteMessage, "on:click": remove }, /* @__PURE__ */ createElement("span", { class: "icon" }, "delete")))), /* @__PURE__ */ createElement("div", { class: "flex-column padding-h padding-bottom" }, /* @__PURE__ */ createElement("b", { class: "break-word", "subscribe:innerText": messageBody }), /* @__PURE__ */ createElement("span", { class: "secondary" }, new Date(message.isoDate).toLocaleString())))); }; return /* @__PURE__ */ createElement( "div", diff --git a/src/Views/threadView.tsx b/src/Views/threadView.tsx index c893fc78..38d6786f 100644 --- a/src/Views/threadView.tsx +++ b/src/Views/threadView.tsx @@ -18,6 +18,11 @@ export function ThreadView(chat: Chat) { chat.deleteMessage(message); } + const messageBody = React.createProxyState( + [chat.messages], + () => message.body + ); + return (
@@ -44,7 +49,7 @@ export function ThreadView(chat: Chat) {
- {message.body} + {new Date(message.isoDate).toLocaleString()} From 8a74d7c389be3c8b80f945a592fbfe1e4de18c0a Mon Sep 17 00:00:00 2001 From: marlon-erler Date: Tue, 9 Jul 2024 18:21:28 +0200 Subject: [PATCH 13/22] improve ui, scroll new messages --- bun.lockb | Bin 11435 -> 11441 bytes dist/index.js | 30 +++++++++++++----------- dist/styles.css | 2 +- package.json | 2 +- src/Tabs/messageTab.tsx | 49 ++++++++++++++++++--------------------- src/Views/threadView.tsx | 4 +++- 6 files changed, 44 insertions(+), 43 deletions(-) diff --git a/bun.lockb b/bun.lockb index 03cb61d1c372159e5f380edeeaeffa4d789978eb..bef7116e0ca9844295db3e95a1c990b4d32cfdb9 100755 GIT binary patch delta 193 zcmV;y06zb#S+QA=E+M!*bO*|n3i%SfrD^lx9)gxUoo*CHER-pC1clMB0+k`LPt6Do zOaK6Y0RR910{{R30h6f$F9E2tSqWqdKtu}KdReaAL4GADV7HJ;UG2H)oq?FVtd>tW z&tPZnNf<{M004lK vQ35cNfE5I@&=)ZW2LfzibY*jNvj`s&0|6Pc8zQv{2Qe*TWprUNlhG^MFs(S*ux)E+JykT+VSw96G^)8Ef(RzZ(X4Rz7M@kHIouHnEnqw`Uu%Pt6Dk zMgRbS0RR911Cvn#E0d4{2mzwASqWqdKuT|}BWd;Sr?AoxXZ@ED_(wgQYE!5jZ4R%a z7*lAmjVct2><{M004lK pQ35cNfE5I@&=)ZW2LWC&E;24Qvj`s&0|67W8zQv{0Wg!-E81jjMrQy3 diff --git a/dist/index.js b/dist/index.js index 9887b88a..e5695093 100644 --- a/dist/index.js +++ b/dist/index.js @@ -220,7 +220,7 @@ const state = value; state.subscribe((newValue) => { element.innerHTML = ""; - element.append(newValue); + element.append(...[newValue].flat()); }); break; } @@ -900,13 +900,15 @@ /* @__PURE__ */ createElement("span", { class: "icon" }, "key") ), /* @__PURE__ */ createElement("button", { "aria-label": translation.deleteMessage, "on:click": remove }, /* @__PURE__ */ createElement("span", { class: "icon" }, "delete")))), /* @__PURE__ */ createElement("div", { class: "flex-column padding-h padding-bottom" }, /* @__PURE__ */ createElement("b", { class: "break-word", "subscribe:innerText": messageBody }), /* @__PURE__ */ createElement("span", { class: "secondary" }, new Date(message.isoDate).toLocaleString())))); }; - return /* @__PURE__ */ createElement( + const listElement = /* @__PURE__ */ createElement( "div", { class: "flex-column gap", "children:append": [chat.messages, messageConverter] } ); + chat.messages.handleAddition(() => listElement.scrollTop = listElement.scrollHeight); + return listElement; } // src/Tabs/messageTab.tsx @@ -919,17 +921,19 @@ function showOptions() { isShowingOptions.value = true; } - function hideOptions() { - isShowingOptions.value = false; - } - return /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("header", { class: "padding-0" }, /* @__PURE__ */ createElement("span", { class: "flex-row align-center" }, /* @__PURE__ */ createElement("button", { "aria-label": translation.back, "on:click": closeChatView }, /* @__PURE__ */ createElement("span", { class: "icon" }, "arrow_back")), /* @__PURE__ */ createElement("span", { "subscribe:innerText": chat.primaryChannel })), /* @__PURE__ */ createElement("span", null, /* @__PURE__ */ createElement( - "button", - { - "aria-label": translation.showChatOptions, - "on:click": showOptions - }, - /* @__PURE__ */ createElement("span", { class: "icon" }, "tune") - ))), ThreadView(chat), /* @__PURE__ */ createElement("footer", null, MessageComposer(chat)), ChatOptionModal(chat, isShowingOptions)); + return [ + /* @__PURE__ */ createElement("header", { class: "padding-0" }, /* @__PURE__ */ createElement("span", { class: "flex-row align-center" }, /* @__PURE__ */ createElement("button", { "aria-label": translation.back, "on:click": closeChatView }, /* @__PURE__ */ createElement("span", { class: "icon" }, "arrow_back")), /* @__PURE__ */ createElement("span", { "subscribe:innerText": chat.primaryChannel })), /* @__PURE__ */ createElement("span", null, /* @__PURE__ */ createElement( + "button", + { + "aria-label": translation.showChatOptions, + "on:click": showOptions + }, + /* @__PURE__ */ createElement("span", { class: "icon" }, "tune") + ))), + ThreadView(chat), + /* @__PURE__ */ createElement("footer", null, MessageComposer(chat)), + ChatOptionModal(chat, isShowingOptions) + ]; }); return /* @__PURE__ */ createElement("article", { id: "message-tab", "children:set": messageTabContent }); } diff --git a/dist/styles.css b/dist/styles.css index 4fff6a30..dfe09f5c 100644 --- a/dist/styles.css +++ b/dist/styles.css @@ -246,7 +246,7 @@ SIZES --safe-area-top: env(safe-area-inset-top); --safe-area-left: env(safe-area-inset-left); --safe-area-right: env(safe-area-inset-right); - --safe-area-bottom: max(env(safe-area-inset-bottom), var(--control-padding)); + --safe-area-bottom: env(safe-area-inset-bottom); } /* diff --git a/package.json b/package.json index 88a71235..996bd862 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "dependencies": { - "bloatless-react": "^1.3.0", + "bloatless-react": "^1.3.1-beta1", "esbuild": "latest", "udn-frontend": "^1.0.7-beta1" }, diff --git a/src/Tabs/messageTab.tsx b/src/Tabs/messageTab.tsx index 70203dc5..3c531d08 100644 --- a/src/Tabs/messageTab.tsx +++ b/src/Tabs/messageTab.tsx @@ -22,37 +22,32 @@ export function MessageTab() { function showOptions() { isShowingOptions.value = true; } - function hideOptions() { - isShowingOptions.value = false; - } - return ( -
-
- - + return [ +
+ + - - - - - -
+ +
+ + + +
, - {ThreadView(chat)} - -
{MessageComposer(chat)}
+ ThreadView(chat), - {ChatOptionModal(chat, isShowingOptions)} -
- ); +
{MessageComposer(chat)}
, + + ChatOptionModal(chat, isShowingOptions), + ]; }); return
; diff --git a/src/Views/threadView.tsx b/src/Views/threadView.tsx index 38d6786f..59e4166d 100644 --- a/src/Views/threadView.tsx +++ b/src/Views/threadView.tsx @@ -59,10 +59,12 @@ export function ThreadView(chat: Chat) { ); }; - return ( + const listElement = (
); + chat.messages.handleAddition(() => listElement.scrollTop = listElement.scrollHeight); + return listElement; } From f523b621aac293aaeba06e087f95900211dd219e Mon Sep 17 00:00:00 2001 From: marlon-erler Date: Tue, 9 Jul 2024 18:23:35 +0200 Subject: [PATCH 14/22] add clear messages button --- dist/index.js | 3 ++- src/Views/chatOptionModal.tsx | 17 ++++++++++++----- src/translations.ts | 1 + 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/dist/index.js b/dist/index.js index e5695093..2c63a490 100644 --- a/dist/index.js +++ b/dist/index.js @@ -750,6 +750,7 @@ encryptionKey: "Encryption key", encryptionKeyPlaceholder: "n10d2482dg283hg", removeChat: "Remove chat", + clearChatMessages: "Clear chat messages", noChatSelected: "No chat selected", composerPlaceholder: "Type a message...", sendMessage: "Send", @@ -843,7 +844,7 @@ placeholder: translation.encryptionKeyPlaceholder, "bind:value": chat.encryptionKey } - ))), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement("button", { class: "danger", "on:click": deleteChat }, translation.removeChat, /* @__PURE__ */ createElement("span", { class: "icon" }, "delete"))), /* @__PURE__ */ createElement("button", { "on:click": closeModal }, translation.close, /* @__PURE__ */ createElement("span", { class: "icon" }, "close")))); + ))), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement("div", { class: "flex-column gap" }, /* @__PURE__ */ createElement("button", { class: "danger", "on:click": chat.clearMessages }, translation.clearChatMessages, /* @__PURE__ */ createElement("span", { class: "icon" }, "delete_sweep")), /* @__PURE__ */ createElement("button", { class: "danger", "on:click": deleteChat }, translation.removeChat, /* @__PURE__ */ createElement("span", { class: "icon" }, "delete")))), /* @__PURE__ */ createElement("button", { "on:click": closeModal }, translation.close, /* @__PURE__ */ createElement("span", { class: "icon" }, "close")))); } // src/Views/messageComposer.tsx diff --git a/src/Views/chatOptionModal.tsx b/src/Views/chatOptionModal.tsx index a1eec98a..1b7f2a55 100644 --- a/src/Views/chatOptionModal.tsx +++ b/src/Views/chatOptionModal.tsx @@ -9,7 +9,7 @@ export function ChatOptionModal(chat: Chat, isPresented: React.State) { } function deleteChat() { - chat.deleteSelf() + chat.deleteSelf(); closeModal(); } @@ -116,10 +116,17 @@ export function ChatOptionModal(chat: Chat, isPresented: React.State) {
- +
+ + + +
); - chat.messages.handleAddition(() => listElement.scrollTop = listElement.scrollHeight); + chat.messages.handleAddition(() => { + const scrollFromBottom = + listElement.scrollHeight - + (listElement.scrollTop + listElement.offsetHeight); + if (scrollFromBottom > 400) return; + + listElement.scrollTop = listElement.scrollHeight; + }); return listElement; } From c15ead428e424a26b7a528005e67cf90b43cf9c6 Mon Sep 17 00:00:00 2001 From: marlon-erler Date: Tue, 9 Jul 2024 18:44:13 +0200 Subject: [PATCH 16/22] fix secondary channels --- dist/index.js | 8 ++++---- src/Model/chatModel.ts | 9 +++++---- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/dist/index.js b/dist/index.js index 889cc0e9..245748fe 100644 --- a/dist/index.js +++ b/dist/index.js @@ -413,8 +413,9 @@ }; // handlers onmessage = async (data) => { - if (data.messageChannel && data.messageChannel != this.primaryChannel.value) - return; + if (!data.messageChannel) return; + const channels = data.messageChannel.split("/"); + if (!this.primaryChannel.value in channels) return; if (data.subscribed != void 0) this.handleSubscription(data.subscribed); if (!data.messageBody) return; const { sender, body, channel, isoDate } = JSON.parse(data.messageBody); @@ -436,7 +437,7 @@ if (this.cannotSendMessage.value == true) return; const secondaryChannelNames = [ ...this.secondaryChannels.value.values() - ].map((channel) => channel); + ]; const allChannelNames = [ this.primaryChannel.value, ...secondaryChannelNames @@ -453,7 +454,6 @@ isoDate: (/* @__PURE__ */ new Date()).toISOString() }; const messageString = JSON.stringify(messageObject); - console.log(messageString); UDN.sendMessage(joinedChannelName, messageString); this.composingMessage.value = ""; }; diff --git a/src/Model/chatModel.ts b/src/Model/chatModel.ts index 89890ff5..780dc2b8 100644 --- a/src/Model/chatModel.ts +++ b/src/Model/chatModel.ts @@ -98,12 +98,13 @@ export class Chat { chats.remove(this); chatIds.remove(this.id); - } + }; // handlers onmessage = async (data: Message): Promise => { - if (data.messageChannel && data.messageChannel != this.primaryChannel.value) - return; + if (!data.messageChannel) return; + const channels = data.messageChannel.split("/"); + if (!this.primaryChannel.value in channels) return; if (data.subscribed != undefined) this.handleSubscription(data.subscribed); @@ -132,7 +133,7 @@ export class Chat { // get channels const secondaryChannelNames: string[] = [ ...this.secondaryChannels.value.values(), - ].map((channel) => channel); + ]; const allChannelNames: string[] = [ this.primaryChannel.value, ...secondaryChannelNames, From 6e040105f175c5cbea87f78e223d66c966c33702 Mon Sep 17 00:00:00 2001 From: marlon-erler Date: Tue, 9 Jul 2024 18:44:32 +0200 Subject: [PATCH 17/22] cleanup --- src/Model/chatModel.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Model/chatModel.ts b/src/Model/chatModel.ts index 780dc2b8..d7040e00 100644 --- a/src/Model/chatModel.ts +++ b/src/Model/chatModel.ts @@ -104,7 +104,7 @@ export class Chat { onmessage = async (data: Message): Promise => { if (!data.messageChannel) return; const channels = data.messageChannel.split("/"); - if (!this.primaryChannel.value in channels) return; + if (this.primaryChannel.value in channels == false) return; if (data.subscribed != undefined) this.handleSubscription(data.subscribed); From bc45992ecbc3a2fb8b25c351ba5d567b419c3b15 Mon Sep 17 00:00:00 2001 From: marlon-erler Date: Tue, 9 Jul 2024 18:46:02 +0200 Subject: [PATCH 18/22] fix chat option layout --- dist/index.js | 6 +++--- src/Views/chatOptionModal.tsx | 22 ++++++++++++---------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/dist/index.js b/dist/index.js index 245748fe..a4d222d8 100644 --- a/dist/index.js +++ b/dist/index.js @@ -415,7 +415,7 @@ onmessage = async (data) => { if (!data.messageChannel) return; const channels = data.messageChannel.split("/"); - if (!this.primaryChannel.value in channels) return; + if (this.primaryChannel.value in channels == false) return; if (data.subscribed != void 0) this.handleSubscription(data.subscribed); if (!data.messageBody) return; const { sender, body, channel, isoDate } = JSON.parse(data.messageBody); @@ -839,13 +839,13 @@ secondaryChannelConverter ] } - ), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement("label", { class: "tile" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "key"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translation.encryptionKey), /* @__PURE__ */ createElement( + ), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("label", { class: "tile" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "key"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translation.encryptionKey), /* @__PURE__ */ createElement( "input", { placeholder: translation.encryptionKeyPlaceholder, "bind:value": chat.encryptionKey } - ))), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement("div", { class: "flex-column gap" }, /* @__PURE__ */ createElement("button", { class: "danger", "on:click": chat.clearMessages }, translation.clearChatMessages, /* @__PURE__ */ createElement("span", { class: "icon" }, "delete_sweep")), /* @__PURE__ */ createElement("button", { class: "danger", "on:click": deleteChat }, translation.removeChat, /* @__PURE__ */ createElement("span", { class: "icon" }, "delete")))), /* @__PURE__ */ createElement("button", { "on:click": closeModal }, translation.close, /* @__PURE__ */ createElement("span", { class: "icon" }, "close")))); + )))), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement("div", { class: "flex-column gap" }, /* @__PURE__ */ createElement("button", { class: "danger", "on:click": chat.clearMessages }, translation.clearChatMessages, /* @__PURE__ */ createElement("span", { class: "icon" }, "delete_sweep")), /* @__PURE__ */ createElement("button", { class: "danger", "on:click": deleteChat }, translation.removeChat, /* @__PURE__ */ createElement("span", { class: "icon" }, "delete")))), /* @__PURE__ */ createElement("button", { "on:click": closeModal }, translation.close, /* @__PURE__ */ createElement("span", { class: "icon" }, "close")))); } // src/Views/messageComposer.tsx diff --git a/src/Views/chatOptionModal.tsx b/src/Views/chatOptionModal.tsx index 1b7f2a55..b7e9a44a 100644 --- a/src/Views/chatOptionModal.tsx +++ b/src/Views/chatOptionModal.tsx @@ -103,16 +103,18 @@ export function ChatOptionModal(chat: Chat, isPresented: React.State) {
- +
+ +

From 7ce85688a2f109b81cb64bd19ffd621b249fa7bc Mon Sep 17 00:00:00 2001 From: marlon-erler Date: Tue, 9 Jul 2024 18:51:46 +0200 Subject: [PATCH 19/22] fix channel recognition --- dist/index.js | 2 +- src/Model/chatModel.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dist/index.js b/dist/index.js index a4d222d8..04d12c0c 100644 --- a/dist/index.js +++ b/dist/index.js @@ -415,7 +415,7 @@ onmessage = async (data) => { if (!data.messageChannel) return; const channels = data.messageChannel.split("/"); - if (this.primaryChannel.value in channels == false) return; + if (channels.indexOf(this.primaryChannel.value) == -1) return; if (data.subscribed != void 0) this.handleSubscription(data.subscribed); if (!data.messageBody) return; const { sender, body, channel, isoDate } = JSON.parse(data.messageBody); diff --git a/src/Model/chatModel.ts b/src/Model/chatModel.ts index d7040e00..db099017 100644 --- a/src/Model/chatModel.ts +++ b/src/Model/chatModel.ts @@ -104,7 +104,7 @@ export class Chat { onmessage = async (data: Message): Promise => { if (!data.messageChannel) return; const channels = data.messageChannel.split("/"); - if (this.primaryChannel.value in channels == false) return; + if (channels.indexOf(this.primaryChannel.value) == -1) return; if (data.subscribed != undefined) this.handleSubscription(data.subscribed); From f4cb1d858cef4d9d0b23ac406e09286aae97d7f1 Mon Sep 17 00:00:00 2001 From: marlon-erler Date: Tue, 9 Jul 2024 18:54:44 +0200 Subject: [PATCH 20/22] fix mailbox --- dist/index.js | 9 +++++++-- src/Model/model.ts | 13 +++++++++---- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/dist/index.js b/dist/index.js index 04d12c0c..29134a2b 100644 --- a/dist/index.js +++ b/dist/index.js @@ -641,11 +641,15 @@ function resetAddressInput() { serverAddressInput.value = currentAddress.value; } - UDN.onconnect = () => { - isConnected.value = true; + function subscribeChannels() { chats.value.forEach((chat) => { UDN.subscribe(chat.primaryChannel.value); }); + } + UDN.onconnect = () => { + isConnected.value = true; + subscribeChannels(); + if (mailboxId.value != "") UDN.connectMailbox(mailboxId.value); }; UDN.onmessage = (data) => { chats.value.forEach((chat) => { @@ -683,6 +687,7 @@ UDN.onmailboxdelete = () => { isMailboxActive.value = false; mailboxId.value = ""; + subscribeChannels(); }; var isEncryptionAvailable = window.crypto.subtle != void 0; var senderName = restoreState("sender-name", ""); diff --git a/src/Model/model.ts b/src/Model/model.ts index 45db37a6..56cba749 100644 --- a/src/Model/model.ts +++ b/src/Model/model.ts @@ -56,13 +56,17 @@ export function resetAddressInput(): void { serverAddressInput.value = currentAddress.value; } -// listeners -UDN.onconnect = () => { - isConnected.value = true; - +export function subscribeChannels() { chats.value.forEach((chat) => { UDN.subscribe(chat.primaryChannel.value); }); +} + +// listeners +UDN.onconnect = () => { + isConnected.value = true; + subscribeChannels(); + if (mailboxId.value != "") UDN.connectMailbox(mailboxId.value); }; UDN.onmessage = (data) => { @@ -115,6 +119,7 @@ UDN.onmailboxconnect = () => { UDN.onmailboxdelete = () => { isMailboxActive.value = false; mailboxId.value = ""; + subscribeChannels(); }; // MISC From b80c59e00e9f3823081525899e4b8bfca5d27865 Mon Sep 17 00:00:00 2001 From: marlon-erler Date: Tue, 9 Jul 2024 18:58:05 +0200 Subject: [PATCH 21/22] improve ui --- dist/index.js | 6 ++++-- src/Model/chatModel.ts | 2 ++ src/Views/threadView.tsx | 3 ++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/dist/index.js b/dist/index.js index 29134a2b..73a8f2c7 100644 --- a/dist/index.js +++ b/dist/index.js @@ -386,9 +386,10 @@ this.primaryChannel, this.composingMessage, this.isSubscribed, - isConnected + isConnected, + senderName ], - () => this.primaryChannel.value == "" || this.composingMessage.value == "" || this.isSubscribed.value == false || isConnected.value == false + () => this.primaryChannel.value == "" || this.composingMessage.value == "" || senderName.value == "" || this.isSubscribed.value == false || isConnected.value == false ); this.cannotAddSecondaryChannel = createProxyState( [this.newSecondaryChannelName], @@ -919,6 +920,7 @@ if (scrollFromBottom > 400) return; listElement.scrollTop = listElement.scrollHeight; }); + setTimeout(() => listElement.scrollTop = listElement.scrollHeight, 50); return listElement; } diff --git a/src/Model/chatModel.ts b/src/Model/chatModel.ts index db099017..df950993 100644 --- a/src/Model/chatModel.ts +++ b/src/Model/chatModel.ts @@ -67,10 +67,12 @@ export class Chat { this.composingMessage, this.isSubscribed, isConnected, + senderName, ], () => this.primaryChannel.value == "" || this.composingMessage.value == "" || + senderName.value == "" || this.isSubscribed.value == false || isConnected.value == false ); diff --git a/src/Views/threadView.tsx b/src/Views/threadView.tsx index 8bde7c32..81395623 100644 --- a/src/Views/threadView.tsx +++ b/src/Views/threadView.tsx @@ -70,8 +70,9 @@ export function ThreadView(chat: Chat) { listElement.scrollHeight - (listElement.scrollTop + listElement.offsetHeight); if (scrollFromBottom > 400) return; - + listElement.scrollTop = listElement.scrollHeight; }); + setTimeout(() => (listElement.scrollTop = listElement.scrollHeight), 50); return listElement; } From 6b3ed0121ef53b07249913dc564f5e822dbae716 Mon Sep 17 00:00:00 2001 From: marlon-erler Date: Tue, 9 Jul 2024 19:17:11 +0200 Subject: [PATCH 22/22] add translations --- dist/index.js | 101 ++++++++++++++++++++++++--- src/Views/chatListSection.tsx | 6 +- src/Views/personalSection.tsx | 2 - src/translations.ts | 128 ++++++++++++++++++++++++++++++++-- 4 files changed, 217 insertions(+), 20 deletions(-) diff --git a/dist/index.js b/dist/index.js index 73a8f2c7..56fcc999 100644 --- a/dist/index.js +++ b/dist/index.js @@ -727,7 +727,6 @@ close: "Close", // overview overview: "Overview", - personal: "You", connection: "Connection", chats: "Chats", yourName: "Your Name", @@ -742,14 +741,12 @@ requestMailbox: "Enable", deleteMailbox: "Disable", mailboxExplanation: "When you're disconnected, messages will be kept on the server temporarily", - newChatPrimaryChannel: "Primary channel", - newChatNamePlaceholder: "my-channel", + primaryChannel: "Primary channel", + primaryChannelPlaceholder: "my-channel", addChat: "Add", // messages showChatOptions: "show chat options", configureChatTitle: "Configure Chat", - primaryChannel: "Primary channel", - primaryChannelPlaceholder: "my-channel", secondaryChannel: "Secondary channel", secondaryChannelPlaceholder: "Add secondary channel", addSecondaryChannel: "Add secondary channel", @@ -766,7 +763,93 @@ deleteMessage: "Delete message" }; var allTranslations = { - en: englishTranslations + en: englishTranslations, + es: { + // general + set: "Guardar", + back: "Atr\xE1s", + undoChanges: "Deshacer", + close: "Cerrar", + // overview + overview: "Inicio", + connection: "Conexion", + chats: "Chats", + yourName: "Tu nombre", + namePlaceholder: "Juan P\xE9rez", + encryptionUnavailableTitle: "Cifrado no disponible", + encryptionUnavailableMessage: "Obt\xE9n esta aplicaci\xF3n a traves de HTTPS o contin\xFAa sin cifrado", + serverAddress: "Direccion del servidor", + serverAddressPlaceholder: "wss://192.168.0.69:3000", + connectToServer: "Conectar", + disconnect: "Desconectar", + mailbox: "Buz\xF3n", + requestMailbox: "Activar", + deleteMailbox: "Desactivar", + mailboxExplanation: "Cuando est\xE1s sin conexion, los mensajes se guardar\xE1n en el servidor temporalmente", + primaryChannel: "Canal principal", + primaryChannelPlaceholder: "mi-canal", + addChat: "A\xF1adir", + // messages + showChatOptions: "Mostrar opciones del chat", + configureChatTitle: "Configurar", + secondaryChannel: "Canal segundario", + secondaryChannelPlaceholder: "A\xF1adir canal segundario", + addSecondaryChannel: "A\xF1adir canal segundario", + removeSecondaryChannel: "Eliminar canal segundario", + encryptionKey: "Clave de cifrado", + encryptionKeyPlaceholder: "n10d2482dg283hg", + removeChat: "Eliminar chat", + clearChatMessages: "Eliminar todos mensajes", + noChatSelected: "Selecciona un chat", + composerPlaceholder: "Escribe un mensaje...", + sendMessage: "Enviar", + decryptMessage: "Decifrar mensaje", + copyMessage: "Copiar mensaje", + deleteMessage: "Eliminar este mensaje" + }, + de: { + // general + set: "OK", + back: "Zur\xFCck", + undoChanges: "\xC4nderungen verwerfen", + close: "Schlie\xDFen", + // overview + overview: "\xDCbersicht", + connection: "Verbindung", + chats: "Chats", + yourName: "Dein Name", + namePlaceholder: "Max Mustermann", + encryptionUnavailableTitle: "Verschl\xFCsselung nicht m\xF6glich", + encryptionUnavailableMessage: "Lade diese app \xFCber HTTPS, um Nachrichten zu verschl\xFCsseln", + serverAddress: "Serveradresse", + serverAddressPlaceholder: "wss://192.168.0.69:3000", + connectToServer: "Verbinden", + disconnect: "Trennen", + mailbox: "Briefkasten", + requestMailbox: "Aktivieren", + deleteMailbox: "Deaktivieren", + mailboxExplanation: "Wenn du offline bist, werden Nachrichten auf dem Server gelagert", + primaryChannel: "Hauptkanal", + primaryChannelPlaceholder: "mein-kanal", + addChat: "Hinzuf\xFCgen", + // messages + showChatOptions: "Chatoptionen einblenden", + configureChatTitle: "Chat konfigurieren", + secondaryChannel: "Zweitkanal", + secondaryChannelPlaceholder: "Zweitkanal hinzuf\xFCgen", + addSecondaryChannel: "Zweitkanal hinzuf\xFCgen", + removeSecondaryChannel: "Zweitkanal entfernen", + encryptionKey: "Schl\xFCssel", + encryptionKeyPlaceholder: "n10d2482dg283hg", + removeChat: "Chat l\xF6schen", + clearChatMessages: "Nachrichtenverlauf leeren", + noChatSelected: "Kein Chat ausgew\xE4hlt", + composerPlaceholder: "Neue Nachricht...", + sendMessage: "Senden", + decryptMessage: "Nachricht entschl\xFCsseln", + copyMessage: "Nachricht kopieren", + deleteMessage: "Nachricht l\xF6schen" + } }; var language = navigator.language.substring(0, 2); var translation = allTranslations[language] ?? allTranslations.en; @@ -963,11 +1046,11 @@ return /* @__PURE__ */ createElement("button", { class: "tile", "on:click": select, "toggle:selected": isSelected }, /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("b", { class: "ellipsis", "subscribe:innerText": chat.primaryChannel })), /* @__PURE__ */ createElement("span", { class: "icon" }, "arrow_forward")); }; function ChatListSection() { - return /* @__PURE__ */ createElement("div", { class: "flex-column" }, /* @__PURE__ */ createElement("h2", null, translation.chats), /* @__PURE__ */ createElement("label", { class: "tile" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "forum"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translation.newChatPrimaryChannel), /* @__PURE__ */ createElement( + return /* @__PURE__ */ createElement("div", { class: "flex-column" }, /* @__PURE__ */ createElement("h2", null, translation.chats), /* @__PURE__ */ createElement("label", { class: "tile" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "forum"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translation.primaryChannel), /* @__PURE__ */ createElement( "input", { "bind:value": newChatName, - placeholder: translation.newChatNamePlaceholder, + placeholder: translation.primaryChannelPlaceholder, "on:enter": createChat } ))), /* @__PURE__ */ createElement("div", { class: "flex-row justify-end" }, /* @__PURE__ */ createElement( @@ -1046,7 +1129,7 @@ // src/Views/personalSection.tsx function PersonalSection() { - return /* @__PURE__ */ createElement("div", { class: "flex-column" }, /* @__PURE__ */ createElement("h2", null, translation.personal), /* @__PURE__ */ createElement("label", { class: "tile" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "account_circle"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translation.yourName), /* @__PURE__ */ createElement( + return /* @__PURE__ */ createElement("div", { class: "flex-column" }, /* @__PURE__ */ createElement("label", { class: "tile" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "account_circle"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translation.yourName), /* @__PURE__ */ createElement( "input", { "bind:value": senderName, diff --git a/src/Views/chatListSection.tsx b/src/Views/chatListSection.tsx index 77c95788..0e0e30f4 100644 --- a/src/Views/chatListSection.tsx +++ b/src/Views/chatListSection.tsx @@ -1,6 +1,5 @@ import * as React from "bloatless-react"; -import { Chat, removeChat } from "../Model/chatModel"; import { cannotCreateChat, chats, @@ -10,6 +9,7 @@ import { selectedChat, } from "../Model/model"; +import { Chat } from "../Model/chatModel"; import { translation } from "../translations"; const chatConverter: React.ListItemConverter = (chat) => { @@ -40,10 +40,10 @@ export function ChatListSection() {