diff --git a/electron/.idea/statistic.xml b/electron/.idea/statistic.xml new file mode 100644 index 0000000..715443f --- /dev/null +++ b/electron/.idea/statistic.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/electron/index.js b/electron/index.js index 0bdbd96..451d1a0 100644 --- a/electron/index.js +++ b/electron/index.js @@ -6,6 +6,7 @@ const { nativeImage, session, desktopCapturer, + dialog, Notification, net, } = require("electron"); @@ -21,6 +22,9 @@ const Settings = require("./modules/settings"); const FrontendLibs = require("@hackthedev/frontend-libs").default; let libDir = path.join(path.resolve(), "web", "js", "libs"); +const AppUpdater = require("@hackthedev/app-updater").default; +//const AppUpdater = require("E:\\network-z-dev\\app-updater\\index.mjs").default; + let win = null; const applicationDataDir = path.join(app.getPath("documents"), "dcts"); @@ -76,10 +80,11 @@ function registerWindowBoundsPersistence(win) { async function createWindow(width, height) { // installing multiple packages const results = await FrontendLibs.installMultiple([ - { package: '@hackthedev/icons@1.0.6', path: libDir }, + { package: '@hackthedev/icons@latest', path: libDir }, { package: '@hackthedev/mobile-ui@latest', path: libDir }, { package: '@hackthedev/prompts@latest', path: libDir }, { package: '@hackthedev/rich-editor', path: libDir }, + { package: '@hackthedev/json-editor', path: libDir }, { package: '@hackthedev/chat-tools@1.0.0', path: libDir }, ]); @@ -166,14 +171,70 @@ app.commandLine.appendSwitch( ); app.whenReady().then(async () => { + // Check for fucking update + let updateCheck = await AppUpdater.check("https://dcts.community/app/electron", { + includeOsUrl: true, + version: app.getVersion() + }) + + + // if available show prompt + if(updateCheck?.available){ + let result = await dialog.showMessageBox({ + type: "question", + buttons: ["Update", "Not now"], + defaultId: 0, + cancelId: 1, + title: "DCTS Client Update available", + message: "It seems that a new version of the DCTS Client is available. Do you wanna download it?", + detail: `Current Version: ${updateCheck?.current}\nAvailable Version: ${updateCheck?.remote}` + }); + + if(result.response === 0){ + let progressWindow = new BrowserWindow({ + width: 400, + height: 160, + resizable: false, + minimizable: false, + maximizable: false, + autoHideMenuBar: true + }); + + await progressWindow.loadURL(`data:text/html, + +

DCTS Update

+
Starting download...
+ + `); + + if(updateCheck?.downloadUrl){ + let file = await AppUpdater.downloadFile( + updateCheck.downloadUrl, + (data) => { + progressWindow.webContents.executeJavaScript(` + document.getElementById("status").innerText = + "Downloading... ${data.progress}%"; + `); + } + ); + + progressWindow.webContents.executeJavaScript(` + document.getElementById("status").innerText = + "Finished: ${file.replaceAll("\\", "\\\\")}"; + `); + } + } + } + + await Settings.initSettings(applicationDataDir); // youtube embed fix — header spoofing session.defaultSession.webRequest.onBeforeSendHeaders( - { urls: ['*://*.youtube.com/*', '*://*.googlevideo.com/*', '*://*.ytimg.com/*'] }, + { urls: ['*://*.youtube-nocookie.com/*', '*://*.youtube.com/*', '*://*.googlevideo.com/*', '*://*.ytimg.com/*'] }, (details, callback) => { - details.requestHeaders['Referer'] = 'https://www.youtube.com' - details.requestHeaders['Origin'] = 'https://www.youtube.com' + details.requestHeaders['Referer'] = 'https://www.youtube-nocookie.com' + details.requestHeaders['Origin'] = 'https://www.youtube-nocookie.com' callback({ requestHeaders: details.requestHeaders }) } ) diff --git a/electron/modules/GetAlias.mjs b/electron/modules/GetAlias.mjs new file mode 100644 index 0000000..14811ad --- /dev/null +++ b/electron/modules/GetAlias.mjs @@ -0,0 +1,5 @@ +import Settings from "./settings.js"; + +export const GetAlias = async ({ signer, applicationDataDir }) => { + return await Settings.settings?.user?.alias ?? null; +} diff --git a/electron/modules/GetChatLastMessage.mjs b/electron/modules/GetChatLastMessage.mjs new file mode 100644 index 0000000..d93afa8 --- /dev/null +++ b/electron/modules/GetChatLastMessage.mjs @@ -0,0 +1,6 @@ +import Settings from "./settings.js"; + +export const GetChatLastMessage = async ({ signer, applicationDataDir }, chatId) => { + if(!chatId) throw new Error("No chatId for getting the chat message"); + return await Settings.Message.getChatLastMessage(chatId); +} diff --git a/electron/modules/GetChatMessages.mjs b/electron/modules/GetChatMessages.mjs index 8b79242..0fe5c84 100644 --- a/electron/modules/GetChatMessages.mjs +++ b/electron/modules/GetChatMessages.mjs @@ -1,6 +1,6 @@ import Settings from "./settings.js"; -export const GetChatMessages = async ({ signer, applicationDataDir }, chatId) => { +export const GetChatMessages = async ({ signer, applicationDataDir }, chatId, timestamp, desc) => { if(!chatId) throw new Error("No chatId for getting the chat messages"); - return await Settings.Message.getMessages(chatId); + return await Settings.Message.getMessages(chatId, timestamp, desc); } diff --git a/electron/modules/GetNickname.mjs b/electron/modules/GetNickname.mjs new file mode 100644 index 0000000..89da433 --- /dev/null +++ b/electron/modules/GetNickname.mjs @@ -0,0 +1,5 @@ +import Settings from "./settings.js"; + +export const GetNickname = async ({ signer, applicationDataDir }) => { + return await Settings.settings?.user?.nickname ?? null; +} diff --git a/electron/modules/GetUserConsistentSettings.mjs b/electron/modules/GetUserConsistentSettings.mjs new file mode 100644 index 0000000..858b5b7 --- /dev/null +++ b/electron/modules/GetUserConsistentSettings.mjs @@ -0,0 +1,5 @@ +import Settings from "./settings.js"; + +export const GetUserConsistentSettings = async ({ signer, applicationDataDir }) => { + return Settings?.settings?.user?.consistent ?? true; +} diff --git a/electron/modules/GetUserIcon.mjs b/electron/modules/GetUserIcon.mjs new file mode 100644 index 0000000..db23088 --- /dev/null +++ b/electron/modules/GetUserIcon.mjs @@ -0,0 +1,5 @@ +import Settings from "./settings.js"; + +export const GetUserIcon = async ({ signer, applicationDataDir }) => { + return await Settings.settings.user.icon ?? null; +} diff --git a/electron/modules/SetAlias.mjs b/electron/modules/SetAlias.mjs new file mode 100644 index 0000000..88f785f --- /dev/null +++ b/electron/modules/SetAlias.mjs @@ -0,0 +1,7 @@ +import Settings from "./settings.js"; + +export const SetAlias = async ({ signer, applicationDataDir }, aliasString) => { + if(aliasString === undefined) throw new Error("Nickname wasnt set") + Settings.settings.user.alias = aliasString; + await Settings.saveSettings() +} diff --git a/electron/modules/SetNickname.mjs b/electron/modules/SetNickname.mjs new file mode 100644 index 0000000..e49fb55 --- /dev/null +++ b/electron/modules/SetNickname.mjs @@ -0,0 +1,7 @@ +import Settings from "./settings.js"; + +export const SetNickname = async ({ signer, applicationDataDir }, nicknameString) => { + if(nicknameString === undefined) throw new Error("Nickname wasnt set") + Settings.settings.user.nickname = nicknameString; + await Settings.saveSettings() +} diff --git a/electron/modules/SetUserConsistentSettings.mjs b/electron/modules/SetUserConsistentSettings.mjs new file mode 100644 index 0000000..026abd5 --- /dev/null +++ b/electron/modules/SetUserConsistentSettings.mjs @@ -0,0 +1,7 @@ +import Settings from "./settings.js"; + +export const SetUserConsistentSettings = async ({ signer, applicationDataDir }, boolean) => { + if(boolean === undefined || typeof boolean !== "boolean") throw new Error("Bool not set") + Settings.settings.user.consistent = !!boolean; + await Settings.saveSettings() +} diff --git a/electron/modules/SetUserIcon.mjs b/electron/modules/SetUserIcon.mjs new file mode 100644 index 0000000..0582b69 --- /dev/null +++ b/electron/modules/SetUserIcon.mjs @@ -0,0 +1,7 @@ +import Settings from "./settings.js"; + +export const SetUserIcon = async ({ signer, applicationDataDir }, iconString) => { + if(iconString === undefined) throw new Error("Icon wasnt set") + Settings.settings.user.icon = iconString; + await Settings.saveSettings() +} diff --git a/electron/modules/settings.js b/electron/modules/settings.js index d190566..c6c27a3 100644 --- a/electron/modules/settings.js +++ b/electron/modules/settings.js @@ -8,6 +8,7 @@ class Settings { static settings = {} static _loaded = false static _writeQueue = Promise.resolve() + static runtimeData = {} static Client = class { static async getLastOnline() { @@ -51,23 +52,22 @@ class Settings { static Message = class { static async saveChat(chatId, data = {}) { if (!chatId) throw new Error("chatId is required") - if(Object.keys(data || {}).length === 0) throw new Error("Data was empty") + if (Object.keys(data || {}).length === 0) throw new Error("data was empty") - let chatPath = path.join(Settings.appDataDir, "chats", chatId) let chatMessagesPath = path.join(Settings.appDataDir, "chats", chatId, "messages") let chatConfigPath = path.join(Settings.appDataDir, "chats", chatId, "config.json") // if the directory doesnt exist create it. // we use the chatMessagePath here because that will also create the chatPath path // as we use "recursive: true" - if(!fsNormal.existsSync(chatMessagesPath)){ - fsNormal.mkdirSync(path.join(chatMessagesPath), { recursive: true }); + if (!fsNormal.existsSync(chatMessagesPath)) { + fsNormal.mkdirSync(chatMessagesPath, { recursive: true }); } - // also create the config file for the chat itself if it doesnt exist - if(!fsNormal.existsSync(chatConfigPath)){ - await fs.writeFile(chatConfigPath, JSON.stringify(data, null, 4)); - } + // always overwrite the config so it stays up to date + Settings.runtimeData.chats ??= {}; + Settings.runtimeData.chats[chatId] = data; + await fs.writeFile(chatConfigPath, JSON.stringify(data, null, 4)); Settings.settings.client ??= {} Settings.settings.client.lastOnline = new Date().getTime(); @@ -78,7 +78,9 @@ class Settings { if (!chatId) return null let chatConfigPath = path.join(Settings.appDataDir, "chats", chatId, "config.json") + Settings.runtimeData.chats ??= {}; if(fsNormal.existsSync(chatConfigPath)) { + if(Settings.runtimeData.chats[chatId]) return Settings.runtimeData.chats[chatId]; return JSON.parse(await fs.readFile(chatConfigPath, "utf8") ?? {}) } else{ @@ -90,12 +92,21 @@ class Settings { let chatsPath = path.join(Settings.appDataDir, "chats") // create it if it doesnt exist - if(!fsNormal.existsSync(chatsPath)) fsNormal.mkdirSync(chatsPath) + if(!fsNormal.existsSync(chatsPath)) fsNormal.mkdirSync(chatsPath, { recursive: true }) let chatIds = await fs.readdir(chatsPath); let chats = {} + Settings.runtimeData.chats ??= {}; + if(chatIds?.length > 0){ for(let chatId of chatIds){ + + // if already in ram use that instead + if(Settings.runtimeData.chats[chatId]){ + chats[chatId] = Settings.runtimeData.chats[chatId]; + continue; + } + let chatConfigPath = path.join(Settings.appDataDir, "chats", chatId, "config.json") let chatConfig = JSON.parse(await fs.readFile(chatConfigPath, "utf8") ?? {}) @@ -115,9 +126,9 @@ class Settings { if(!data) throw new Error("data is required") let messagesPath = path.join(Settings.appDataDir, "chats", chatId, "messages"); - if(!fsNormal.existsSync(messagesPath)) fsNormal.mkdirSync(messagesPath); + if(!fsNormal.existsSync(messagesPath)) fsNormal.mkdirSync(messagesPath, { recursive: true }); - fs.writeFile(path.join(messagesPath, `${messageId}.json`), JSON.stringify(data, null, 4)); + await fs.writeFile(path.join(messagesPath, `${messageId}.json`), JSON.stringify(data, null, 4)); Settings.settings.client ??= {} Settings.settings.client.lastOnline = new Date().getTime(); @@ -128,30 +139,62 @@ class Settings { if (!chatId || !messageId) return null } - static async getMessages(chatId) { + static async getChatLastMessage(chatId) { + if (!chatId) return null; + + let messages = await this.getMessages(chatId, new Date().getTime(), true); + messages = Object.values(messages); + + if(messages.length === 0) return null; + + messages.sort((a, b) => { + return (b?.timestamp ?? 0) - (a?.timestamp ?? 0); + }); + + return messages[0] ?? null; + } + + static async getMessages(chatId, timestamp = new Date().getTime(), desc = true) { if (!chatId) return {} + let limit = 50; let messagesPath = path.join(Settings.appDataDir, "chats", chatId, "messages") - // create it if it doesnt exist - if(!fsNormal.existsSync(messagesPath)) fsNormal.mkdirSync(messagesPath) + if(!fsNormal.existsSync(messagesPath)) fsNormal.mkdirSync(messagesPath, { recursive: true }) + let messageIds = await fs.readdir(messagesPath); - let messages = {} + let messages = [] - if(messageIds?.length > 0){ - for(let messageId of messageIds){ - let messageFile = path.join(messagesPath, `${messageId}`) + for(let messageId of messageIds){ + let messageFile = path.join(messagesPath, messageId) + let messageConfig = JSON.parse(await fs.readFile(messageFile, "utf8") ?? "{}") - let messageConfig = JSON.parse(await fs.readFile(messageFile, "utf8") ?? {}) + let createdAt = messageConfig?.timestamp ?? 0 - messages[messageId] = messageConfig; - } + if(desc && createdAt >= timestamp) continue + if(!desc && createdAt <= timestamp) continue - return messages; + messages.push({ + id: messageId, + timestamp: createdAt, + data: messageConfig + }) } - else{ - return {} + + messages.sort((a, b) => { + if(desc) return b.timestamp - a.timestamp + return a.timestamp - b.timestamp + }) + + messages = messages.slice(0, limit) + + let result = {} + + for(let message of messages){ + result[message.id] = message.data } + + return result } static async deleteMessage(chatId, messageId) { @@ -240,9 +283,10 @@ class Settings { } } - static initSettings(appDataDir) { + static async initSettings(appDataDir) { this.settingsPath = path.join(appDataDir, "settings.json") this.appDataDir = appDataDir + await this._ensureLoaded(); } static async _ensureLoaded() { @@ -250,6 +294,7 @@ class Settings { try { const data = await fs.readFile(this.settingsPath, "utf8") this.settings = JSON.parse(data) + this.settings.user ??= {} } catch { this.settings = {} } diff --git a/electron/package-lock.json b/electron/package-lock.json index 1c11dc8..3747d08 100644 --- a/electron/package-lock.json +++ b/electron/package-lock.json @@ -1,14 +1,15 @@ { "name": "DCTS", - "version": "1.0.0", + "version": "1.0.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "DCTS", - "version": "1.0.0", - "license": "ISC", + "version": "1.0.3", + "license": "UNLICENSED", "dependencies": { + "@hackthedev/app-updater": "^1.0.1", "@hackthedev/dsync-sign": "^1.0.14", "@hackthedev/frontend-libs": "^1.0.5", "cors": "^2.8.6", @@ -16,7 +17,7 @@ }, "devDependencies": { "electron": "^40.4.1", - "electron-builder": "^26.0.12" + "electron-builder": "^26.8.1" } }, "node_modules/@develar/schema-utils": { @@ -63,9 +64,9 @@ "license": "MIT" }, "node_modules/@electron/asar/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", "dev": true, "license": "MIT", "dependencies": { @@ -357,9 +358,9 @@ "license": "MIT" }, "node_modules/@electron/universal/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", "dev": true, "license": "MIT", "dependencies": { @@ -482,6 +483,12 @@ "node": ">= 10.0.0" } }, + "node_modules/@hackthedev/app-updater": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@hackthedev/app-updater/-/app-updater-1.0.1.tgz", + "integrity": "sha512-I04nyJNUFtJsiHrxGGg5KJuKQEhudyCn1Majp73PCQP1ubY8fm3mAcHpEksu7R07poLogwLMhGpk2KLU9el0DA==", + "license": "UNLICENSED" + }, "node_modules/@hackthedev/dsync-sign": { "version": "1.0.14", "resolved": "https://registry.npmjs.org/@hackthedev/dsync-sign/-/dsync-sign-1.0.14.tgz", @@ -888,9 +895,9 @@ } }, "node_modules/@xmldom/xmldom": { - "version": "0.8.11", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz", - "integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==", + "version": "0.8.13", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz", + "integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==", "dev": true, "license": "MIT", "engines": { @@ -1345,9 +1352,9 @@ "optional": true }, "node_modules/brace-expansion": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, "license": "MIT", "dependencies": { @@ -1517,9 +1524,9 @@ "license": "MIT" }, "node_modules/cacache/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", "dev": true, "license": "MIT", "dependencies": { @@ -2113,9 +2120,9 @@ "license": "MIT" }, "node_modules/dir-compare/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", "dev": true, "license": "MIT", "dependencies": { @@ -2292,9 +2299,9 @@ } }, "node_modules/electron": { - "version": "40.4.1", - "resolved": "https://registry.npmjs.org/electron/-/electron-40.4.1.tgz", - "integrity": "sha512-N1ZXybQZL8kYemO8vAeh9nrk4mSvqlAO8xs0QCHkXIvRnuB/7VGwEehjvQbsU5/f4bmTKpG+2GQERe/zmKpudQ==", + "version": "40.10.0", + "resolved": "https://registry.npmjs.org/electron/-/electron-40.10.0.tgz", + "integrity": "sha512-e7XVcAfyWoFQGS7ZhgxeNn0AijHaqgRCa6uA6TYOrvBWv8smI6JILvMR/8DYBIn07oqvxDLRC90tu/xa2cJCow==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -2792,9 +2799,9 @@ "license": "MIT" }, "node_modules/filelist/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", "dev": true, "license": "MIT", "dependencies": { @@ -3043,9 +3050,9 @@ "license": "MIT" }, "node_modules/glob/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", "dev": true, "license": "MIT", "dependencies": { @@ -3389,9 +3396,9 @@ "license": "ISC" }, "node_modules/ip-address": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", - "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", "dev": true, "license": "MIT", "engines": { @@ -3589,9 +3596,9 @@ "license": "MIT" }, "node_modules/lodash": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", - "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", "dev": true, "license": "MIT" }, @@ -4300,9 +4307,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "peer": true, diff --git a/electron/package.json b/electron/package.json index 838ac19..4fc734f 100644 --- a/electron/package.json +++ b/electron/package.json @@ -1,9 +1,9 @@ { "name": "DCTS", "productName": "DCTS", - "version": "1.0.0", + "version": "1.0.3", "description": "", - "license": "ISC", + "license": "UNLICENSED", "author": "", "type": "commonjs", "main": "index.js", @@ -13,6 +13,7 @@ "build": "electron-builder" }, "dependencies": { + "@hackthedev/app-updater": "^1.0.1", "@hackthedev/dsync-sign": "^1.0.14", "@hackthedev/frontend-libs": "^1.0.5", "cors": "^2.8.6", @@ -20,7 +21,7 @@ }, "devDependencies": { "electron": "^40.4.1", - "electron-builder": "^26.0.12" + "electron-builder": "^26.8.1" }, "build": { "appId": "com.dcts.client", diff --git a/electron/web/css/content/account/settings.css b/electron/web/css/content/account/settings.css new file mode 100644 index 0000000..9b86f0d --- /dev/null +++ b/electron/web/css/content/account/settings.css @@ -0,0 +1,101 @@ +.layout > .content-container > .content > .account-container *{ + box-sizing: border-box; +} +.layout > .content-container > .content > .account-container{ + display: flex; + flex-direction: column; + margin: auto; + + justify-content: start; + margin-top: 1rem; + + padding: 1rem; + border-radius: 8px; + max-width: 400px; + + background-color: hsl(from var(--main) h s calc(l * 2.5)); + border: 1.5px solid var(--border-color); +} + +.layout > .content-container > .content > .account-container > h1{ + display: flex; + align-content: center; + align-items: center; + gap: 0.5rem; + + margin: 0 0 0.5rem 0; + font-size: 22px; +} + +.layout > .content-container > .content > .account-container > p.hint{ + color: hsl(from var(--main) h s calc(l * 8)); + border-left: 3px solid hsl(from var(--main) h s calc(l * 8)); + background-color: hsl(from var(--main) h s calc(l * 8) / 10%); + padding: 0.5rem; + + font-style: italic; + font-size: 14px; +} + +.layout > .content-container > .content > .account-container > .settings > .json-editor-setting{ + padding: 0.5rem; + border-radius: 4px; + + + border: 1.5px solid var(--border-color); + background-color: hsl(from var(--main) h s calc(l * 2)); +} +.layout > .content-container > .content > .account-container > .settings > .json-editor-setting hr{ + display: none; +} +.layout > .content-container > .content > .account-container > .settings > .json-editor-setting .json-editor-setting-description{ + color: hsl(from var(--main) h s calc(l * 8)); + font-size: 12.5px; + margin: 4px 8px 0 0; +} +.layout > .content-container > .content > .account-container > .settings > .json-editor-setting .json-editor-setting-headline{ + margin: 0 1rem 0.5rem 0; +} +.layout > .content-container > .content > .account-container > .settings > .json-editor-setting input[disabled]{ + background-color: hsl(from var(--main) h s calc(l * 6)); + cursor: not-allowed; +} +.layout > .content-container > .content > .account-container > .settings > .json-editor-setting input{ + outline: none; + border: 1.5px solid var(--main); + padding: 0.25rem 0.25rem; + border-radius: 4px; + + color: hsl(from var(--main) h s calc(l * 1)); + background-color: hsl(from var(--main) h s calc(l * 10)); +} + +.json-editor-save-bar .btn-save{ + background-color: var(--accent); + color: var(--main); + + font-weight: bold; + border: 1.5px solid var(--border-color); + outline: none; + padding: 0.25rem 0.5rem; + border-radius: 4px; +} +.json-editor-save-bar { + position: fixed; + display: flex; + align-items: center; + gap: 1rem; + padding: 0.5rem 1rem; + border-radius: 8px; + border: 1.5px solid var(--border-color); + + background-color: hsl(from var(--main) h s calc(l * 3)); + + + bottom: 1.5rem; + left: 50%; + transform: translateX(-50%); + + width: fit-content; + min-width: 200px; +} \ No newline at end of file diff --git a/electron/web/css/content/messages.css b/electron/web/css/content/messages.css index c08d91e..518e22e 100644 --- a/electron/web/css/content/messages.css +++ b/electron/web/css/content/messages.css @@ -6,6 +6,9 @@ margin: -1rem; overflow: hidden; } +.layout > .content-container > .content > .message-page-container .chats.hide{ + display: none !important; +} .layout > .content-container > .content > .message-page-container .chats{ display: flex; flex-direction: column; @@ -17,6 +20,9 @@ overflow: hidden; text-overflow: ellipsis; } +.layout > .content-container > .content > .message-page-container .chat-content.mobile{ + display: none !important; +} .layout > .content-container > .content > .message-page-container .chat-content{ display: flex; flex-direction: column; @@ -58,7 +64,7 @@ box-sizing: border-box; } .layout > .content-container > .content > .message-page-container .chats > .header h1{ - font-size: 22px; + font-size: 18px; } .layout > .content-container > .content > .message-page-container .chats > .header span{ display: flex; @@ -106,12 +112,30 @@ width: fit-content; overflow: hidden; } + +.layout > .content-container > .content > .message-page-container .chats .chat .middle-section .meta{ + display: flex; + flex-direction: row; + align-items: center; +} +.layout > .content-container > .content > .message-page-container .chats .chat .middle-section .meta .date{ + display: flex; + justify-content: end; + margin-left: auto; + font-size: 12px; +} .layout > .content-container > .content > .message-page-container .chats .chat .latestMessage { display: flex; flex-direction: row; font-size: 14px; position: relative; } +.layout > .content-container > .content > .message-page-container .chats .chat .latestMessage img, +.layout > .content-container > .content > .message-page-container .chats .chat .latestMessage video, +.layout > .content-container > .content > .message-page-container .chats .chat .latestMessage audio, +.layout > .content-container > .content > .message-page-container .chats .chat .latestMessage iframe{ + display: none; +} .layout > .content-container > .content > .message-page-container .chats .chat .latestMessage p{ position: relative; margin: 0; @@ -155,6 +179,9 @@ gap: 1rem; padding: 0.5rem 1rem; } +.layout > .content-container > .content > .message-page-container .chat-content .header .back{ + display: flex; +} .layout > .content-container > .content > .message-page-container .chat-content .header h1{ font-size: 16px; margin: 0; diff --git a/electron/web/css/mobile.css b/electron/web/css/mobile.css index ec7c2e2..8a54679 100644 --- a/electron/web/css/mobile.css +++ b/electron/web/css/mobile.css @@ -17,21 +17,26 @@ } .layout > .content-container { - flex-direction: column; + flex-direction: column-reverse; } .layout > .content-container > .navigation{ width: 100%; flex-direction: row; justify-content: center; - border-bottom: 1.5px solid var(--border-color); + border-top: 1.5px solid var(--border-color); padding: 0.5rem; flex-wrap: wrap; } .layout > .content-container > .navigation .entry{ - padding: 12px; + padding: 0.5rem 1rem; + flex: 1 1 auto; } .layout > .content-container > .content > .serverList > .server-card { width: 100% !important; } + + .layout > .content-container > .content > .message-page-container .chats .chat{ + gap: 0.5rem !important; + } } \ No newline at end of file diff --git a/electron/web/css/nav.css b/electron/web/css/nav.css index a52c4d8..99f17de 100644 --- a/electron/web/css/nav.css +++ b/electron/web/css/nav.css @@ -1,4 +1,8 @@ +.layout > .content-container > .navigation.hide { + display: none !important; +} .layout > .content-container > .navigation { + box-sizing: border-box; display: flex; flex-direction: column; @@ -9,7 +13,7 @@ } .layout > .content-container > .navigation > .entry{ display: flex; - flex-direction: row; + flex-direction: column; align-items: center; gap: 0.5rem; @@ -18,6 +22,9 @@ border-radius: 6px; cursor: pointer; } +.layout > .content-container > .navigation > .entry span{ + font-size: 12px; +} .layout > .content-container > .navigation > .entry svg{ width: 20px; height: 20px; diff --git a/electron/web/css/themes/default/default.css b/electron/web/css/themes/default/default.css index 6071af7..5aed6d3 100644 --- a/electron/web/css/themes/default/default.css +++ b/electron/web/css/themes/default/default.css @@ -67,9 +67,16 @@ a { background-color: hsl(from var(--main) h s calc(l * 2)); } +.layout > .content-container > .content > .message-page-container .chat-content .header .back{ + color: hsl(from var(--main) h s calc(l * 6)); +} + .layout > .content-container > .content > .message-page-container .chats .chat .latestMessage { color: hsl(from var(--main) h s calc(l * 6)); } +.layout > .content-container > .content > .message-page-container .chats .chat .middle-section .meta .date{ + color: hsl(from var(--main) h s calc(l * 6)); +} .layout > .content-container > .content > .message-page-container .chats .chat .badge { background-color: hsl(from var(--main) h s calc(l * 10)); @@ -119,7 +126,10 @@ a { } .layout > .content-container > .content > .message-page-container > .chat-content > .content > .message-container { - background-color: hsl(from var(--accent) h s calc(l * 1)); + background-color: hsl(from var(--accent) h calc(s * 0.5) calc(l * 0.5)); +} +.layout > .content-container > .content > .message-page-container > .chat-content > .content > .message-container > .meta { + color: hsl(from var(--main) h s calc(l * 10) / 100%); } .layout > .content-container > .content > .message-page-container > .chat-content > .content > .message-container.mine{ background-color: hsl(from var(--main) h s calc(l * 3)); @@ -131,7 +141,7 @@ a { background-color: hsl(from var(--main) h s calc(l * 8) / 10%); } -.layout > .content-container > .content > .message-page-container > .chat-content > .content > .message-container > .meta { +.layout > .content-container > .content > .message-page-container > .chat-content > .content > .message-container.mine > .meta { color: hsl(from var(--main) h s calc(l * 6) / 100%); } diff --git a/electron/web/index.html b/electron/web/index.html index 5e4f4b0..a5335b7 100644 --- a/electron/web/index.html +++ b/electron/web/index.html @@ -38,6 +38,7 @@ + @@ -48,6 +49,8 @@ + + @@ -73,6 +76,7 @@ let prompts = null; document.addEventListener("DOMContentLoaded", function() { prompts = new Prompt(); + ChatTools.Sanitize.installDomPurifyHooks(); }) diff --git a/electron/web/js/content/account.js b/electron/web/js/content/account.js index 23f6dfd..3a351e7 100644 --- a/electron/web/js/content/account.js +++ b/electron/web/js/content/account.js @@ -1,17 +1,157 @@ +let originalUserData = { + +} + async function loadAccount(){ getContentElement().innerHTML = ` -
-

This feature isnt done yet!

-

A lot of work is going into this feature! Checkout our subreddit to stay up to date!

-

+
+

${Icon.display("account")}Account Settings

+

+ If you leave these settings empty they will be automatically set by the server. +

-

What is it going to be about?

- +
+ +
` -} \ No newline at end of file + + let nickname = await Client().GetNickname() ?? ""; + let alias = await Client().GetAlias() ?? ""; + let profileUrl = await Client().GetUserIcon() ?? ""; + let consistent = await Client().GetUserConsistentSettings() ?? true; + let disableInputs = !!consistent === true; + + originalUserData.nickname = nickname; + originalUserData.alias = alias; + originalUserData.icon = profileUrl; + originalUserData.consistent = consistent; + + if(typeof Client().SetNickname === "function" ) { + getAccountSettingsElement().insertAdjacentElement( + "beforeend", + JsonEditor.getSettingElement(nickname, "Display Name", "How others will see you", async (value) => { + if(originalUserData.nickname !== value){ + JsonEditor.showSaveButton("nickname", () => { + originalUserData.nickname = value + saveAccountChanges({ + name: value + }); + }); + } + else{ + JsonEditor.hideSaveButton(); + } + }, { + regexMatcher: /^[a-zA-Z0-9_.-]{1,30}$/, + canBeNull: true, + }) + ) + } + else{ + console.error("Setting 'Display Name' not shown because client doesnt support it") + } + + if(typeof Client().SetUserIcon === "function" ) { + getAccountSettingsElement().insertAdjacentElement( + "beforeend", + JsonEditor.getSettingElement(profileUrl, "Profile Picture", "Enter an URL starting with https://", async (value) => { + if (originalUserData.icon !== value) { + JsonEditor.showSaveButton("profile_url", () => { + originalUserData.icon = value + saveAccountChanges({ + icon: value + }); + }); + } else { + JsonEditor.hideSaveButton(); + } + }, { + regexMatcher: /^https?:\/\/[a-zA-Z0-9.-]+(?:\/[^\s]*)?$/, + canBeNull: true, + }) + ) + } + else{ + console.error("Setting 'Profile Picture' not shown because client doesnt support it") + } + + if(typeof Client().SetAlias === "function" ) { + getAccountSettingsElement().insertAdjacentElement( + "beforeend", + JsonEditor.getSettingElement(alias, "Messenger Alias", + ` + How people can reach you
+ Current: ${originalUserData?.alias ? `${originalUserData?.alias}@${getHomeSocket().host}` : "none"} + ` + , async (value) => { + if(originalUserData.alias !== value && value?.trim()?.length > 0){ + JsonEditor.showSaveButton("alias", () => { + originalUserData.alias = value + saveAccountChanges({ + vanity: value + }); + }); + } + else{ + JsonEditor.hideSaveButton(); + } + }, { + regexMatcher: /^[a-zA-Z0-9_.-]{1,30}$/, + }) + ) + } + else{ + console.error("Setting 'Display Name' not shown because client doesnt support it") + } + + // check if this feature is supported in whatever client + if(typeof Client().SetUserConsistentSettings === "function" ){ + getAccountSettingsElement().insertAdjacentElement( + "beforeend", + JsonEditor.getSettingElement(consistent, "Sync with servers?", "Automatically update server profiles on connection.", async (value) => { + if(originalUserData.consistent !== value){ + JsonEditor.showSaveButton("consistent", () => { + originalUserData.consistent = value + Client().SetUserConsistentSettings(!!value); + }); + } + else{ + JsonEditor.hideSaveButton(); + } + }) + ) + } + else{ + console.error("Setting 'Consistent?' not shown because client doesnt support it") + } +} + +async function saveAccountChanges({ + icon = undefined, + name = undefined, + vanity = undefined, + }){ + + let payload = {} + if(name !== undefined) payload.name = name; + if(icon !== undefined) payload.icon = icon; + if(vanity !== undefined) payload.vanity = vanity; + if(icon !== undefined) payload.icon = icon; + + let result = await socketHello(getHomeSocket(), getHomeSocket().host, {...payload}) + if(result?.error){ + alert(result?.error); + } + else{ + if(payload?.vanity) Client().SetAlias(vanity); + if(payload?.name) Client().SetNickname(name); + if(payload?.icon) Client().SetUserIcon(icon); + } +} + +function getAccountSettingsElement(){ + return getContentElement()?.querySelector(".account-container .settings"); +} + diff --git a/electron/web/js/content/messages.js b/electron/web/js/content/messages.js index d28f7cd..057cef6 100644 --- a/electron/web/js/content/messages.js +++ b/electron/web/js/content/messages.js @@ -1,3 +1,13 @@ +function sortMessagesByTimestamp(messages){ + return Object.values(messages) + .map(item => item.data ?? item) + .sort((a, b) => { + const aTimestamp = a?.timestamp ?? 0; + const bTimestamp = b?.timestamp ?? 0; + + return aTimestamp - bTimestamp; + }); +} async function fetchServerInbox(host) { host = extractHost(host); @@ -5,6 +15,8 @@ async function fetchServerInbox(host) { if (typeof Client().SaveChatMessage !== "function") throw new Error("Unsupported Client!") if (typeof Client().GetChat !== "function") throw new Error("Unsupported Client!") if (typeof Client().GetChatMessages !== "function") throw new Error("Unsupported Client!") + if (typeof Client().GetChatLastMessage !== "function") throw new Error("Unsupported Client!") + let sessionId = await getSessionIdFromHost(host); if (!sessionId) return console.warn("Session id not found for host ", host) @@ -14,18 +26,21 @@ async function fetchServerInbox(host) { // get stored shit let storedChat = await Client().GetChat(host) ?? {}; - let localMessages = await Client().GetChatMessages(host) ?? {}; + let localMessages = await Client().GetChatMessages(host, new Date().getTime(), true) ?? {}; let serverInfo = Object.keys(storedChat?.data?.serverinfo ?? {}).length > 0 ? storedChat.data.serverinfo : await fetchServerInfo(host) ?? null; let chatData = { + ...storedChat, + ...(storedChat?.data ?? {}), isServer: true, host, - title: serverInfo?.name ?? host, - serverinfo: serverInfo ?? {}, - icon: serverInfo?.icon ? getFixedUrl(host, serverInfo.icon) : null + title: serverInfo?.name ?? storedChat?.title ?? host, + serverinfo: serverInfo ?? storedChat?.serverinfo ?? {}, + icon: serverInfo?.icon ? getFixedUrl(host, serverInfo.icon) : storedChat?.icon ?? null, + lastRead: storedChat?.lastRead ?? storedChat?.data?.lastRead ?? 0 }; let mergedMessages = {...localMessages}; @@ -53,50 +68,13 @@ async function fetchServerInbox(host) { } // sorting - let messages = Object.values(mergedMessages) - .map(item => item.data ?? item) - .sort((a, b) => { - const aTimestamp = a?.timestamp ?? 0; - const bTimestamp = b?.timestamp ?? 0; - - return aTimestamp - bTimestamp; - }); + let messages = sortMessagesByTimestamp(mergedMessages) chatData.messages = messages; chatData.lastMessage = messages.at(-1) ?? null; await Client().SaveChat(host, chatData); - await addInboxEntry(chatData); - return chatData; - - async function addInboxEntry(item) { - if (!item) throw new Error("item not found for adding inbox element"); - - let chatId = item.host; - let chatName = item?.title ?? item?.host; - let latestMessage = item?.lastMessage?.message ?? item?.lastMessage?.text ?? `@${chatId}`; - let iconUrl = item?.icon ?? ""; - - let serverChatSelector = getContentElement().querySelector(`.chats .chat[data-gid="${chatId}"]`); - if (serverChatSelector) serverChatSelector.remove(); - - getContentElement().querySelector(`.chats`).insertAdjacentHTML("beforeend", ` -
-
-
-
${chatName}
- ${latestMessage ? `
${latestMessage}
` : ""} -
-
${messages.length}
-
- `) - - serverChatSelector = getContentElement().querySelector(`.chats .chat[data-gid="${chatId}"]`); - serverChatSelector?.addEventListener("click", async () => { - renderChat(null, item) - }) - } } function getFixedUrl(host, url) { @@ -165,11 +143,20 @@ async function fetchMessengerChats(timestamp = 0) { `) } + // make sure the chat exists before saving any message let existingChat = await Client().GetChat(authorGid) - if(!existingChat) await startNewChat({ - identifier: `${authorGid}@${authorHomeServer}`, - automate: true - }) + if (!existingChat) { + await Client().SaveChat(authorGid, { + publicKey: authorPublicKey, + gid: authorGid, + title: `@${authorGid}`, + host: authorHomeServer, + home_server: authorHomeServer, + lastMessage: null, + icon: null, + }); + } + await Client().SaveChatMessage(authorGid, message); } else{ @@ -189,8 +176,55 @@ async function fetchMessengerChats(timestamp = 0) { }) } -async function renderMessages() { - getContentElement().innerHTML = +async function refreshChatEntry(chatGid, latestMessageObj = null) { + let chatsElement = getContentElement().querySelector(`.chats .list`); + if (!chatsElement) return; + + let chat = await Client().GetChat(chatGid); + if (!chat) return; + + // sort messages by timestamp + let messages = Object.values(await Client().GetChatMessages(chatGid, new Date().getTime(), true) ?? {}) + .map(item => item.data ?? item) + .sort((a, b) => (a?.timestamp ?? 0) - (b?.timestamp ?? 0)); + + let gid = await getGid(); + let lastMessage = latestMessageObj ?? messages.at(-1) ?? null; + + // decrypt this shit + let decryptedLastMessage = null; + if (lastMessage) { + try { + decryptedLastMessage = await decryptUserMessage(lastMessage[gid]); + } catch { + decryptedLastMessage = null; + } + } + + let chatName = chat?.title ?? "Unknown"; + let latestMessage = decryptedLastMessage ?? `@${chat?.host ?? chat?.home_server}`; + + // remove old entry and re-insert at top so newest chat bubbles up + let existing = chatsElement.querySelector(`.chat[data-gid="${chatGid}"]`); + if (existing) existing.remove(); + + chatsElement.insertAdjacentHTML("afterbegin", ` +
+
+
+
${ChatTools.Sanitize.forRender(chatName)}
+ ${latestMessage ? `
${ChatTools.Sanitize.forRender(latestMessage)}
` : ""} +
+
${messages.length}
+
+ `); +} + +async function renderMessages(customElement = undefined) { + let renderElement = customElement !== undefined ? customElement : getContentElement(); + if(customElement === null) throw new Error("Custom Element was null!"); + + renderElement.innerHTML = `
@@ -201,8 +235,9 @@ async function renderMessages() { ${Icon.display("message_add")}
+
-
+
`; @@ -212,57 +247,86 @@ async function renderMessages() { (a, b) => (b?.lastMessage?.timestamp ?? 0) - (a?.lastMessage?.timestamp ?? 0) ); - let gid = await Client().GenerateGid(await Client().GetPublicKey()); - - addChatEntries(getContentElement().querySelector(`.chats`)) + addChatEntries(renderElement.querySelector(`.chats .list`)) async function addChatEntries(element) { if (!element) throw new Error("Element not found for adding chat element"); + if(typeof Client().GetChatLastMessage !== "function") throw new Error("Unsupported Client: GetChatLastMessage") + if(typeof Client().GetChatMessages !== "function") throw new Error("Unsupported Client: GetChatMessages") - for (let chat of Object.values(uniqueChats.reverse())) { + for (let chat of Object.values(uniqueChats)) { let chatId = chat?.gid ?? chat?.host if (!chatId) { console.warn("No chat id found for chat ", chat) continue; } - chat.messages = await Client().GetChatMessages(chatId) ?? {}; + chatId = ChatTools.Sanitize.stripHTML(chatId); - let messages = Object.values(chat.messages) - .map(item => item.data ?? item) - .sort((a, b) => { - const aTimestamp = a?.timestamp ?? 0; - const bTimestamp = b?.timestamp ?? 0; + chat.messages = await Client().GetChatMessages(chatId, new Date().getTime(), true) ?? {}; + chat.messages = sortMessagesByTimestamp(chat.messages); + chat.lastMessage = await getLastChatMessage(chatId); - return aTimestamp - bTimestamp; - }); - - chat.messages = messages; - chat.lastMessage = messages.at(-1) ?? null; - - if (chat.lastMessage) { - chat.lastMessage = chat.lastMessage[gid]; - chat.lastMessage = await decryptUserMessage(chat.lastMessage) - } + let unreadMessages = chat.messages.filter(message => { + return (message?.timestamp ?? 0) > (chat?.lastRead ?? 0); + }); let chatName = chat?.title ?? "Unkown" - let latestMessage = chat.lastMessage ?? `@${chat?.host ?? chat?.data?.host ?? chat?.data?.home_server}`// will need to actually decrypt this + let displayDate = new Date(chat?.lastMessage?.timestamp).toLocaleDateString(undefined, { + //year: "numeric", + month: "numeric", + day: "numeric", + hour: "numeric", + minute: "numeric", + }); + let iconUrl = getFixedUrl(chat?.host, chat?.icon); element.insertAdjacentHTML("beforeend", `
-
+
-
${chatName}
- ${latestMessage ? `
${latestMessage}
` : ""} +
+
${ChatTools.Sanitize.forRender(chatName)}
+ ${chat?.lastMessage?.timestamp ? displayDate : ""} +
+ + ${chat.lastMessage?.message ? `
${ChatTools.Sanitize.forRender(chat.lastMessage?.message)}
` : ""}
-
${messages?.length ?? ""}
+
${unreadMessages?.length ?? ""}
`) } } } +async function getGid(){ + return await Client().GenerateGid(await Client().GetPublicKey()); +} + +async function getLastChatMessage(chatId){ + let message = await Client().GetChatLastMessage(chatId); + + let text = null; + if(message?.isServer){ + text = message.message; + } + else{ + try{ + text = await decryptUserMessage(message[await getGid()]) + } + catch(lastChatMessageError){ + console.error(lastChatMessageError); + text = null; + } + } + + return { + timestamp: message?.timestamp, + message: text + } +} + function getChatContentElement() { return document.querySelector(`.message-page-container .chat-content`); } @@ -271,9 +335,14 @@ function getInnerChatContentElement() { return getChatContentElement().querySelector(`.content`); } +function getChatListElement() { + return getContentElement().querySelector(`.chats`); +} + + + async function renderChat(chatId, customChatObject = null) { let activeChat = customChatObject ?? await Client().GetChat(chatId); - if (!activeChat) throw new Error("Chat not found"); await setChatHeader(activeChat); @@ -283,8 +352,59 @@ async function renderChat(chatId, customChatObject = null) {
`; + // infinite scroll shit + // lets see how much pain this will be + await ChatTools.Scroll.registerMessageInfiniteLoad(getInnerChatContentElement(), async () => { + let messages = getInnerChatContentElement()?.querySelectorAll(`.message-container`); + let topMessage = messages[0]; + let timestamp = topMessage?.getAttribute("data-timestamp") ?? null; + + if(!timestamp) throw new Error("Invalid timestamp or not found"); + + // get older messages and display them + let messageBatch = await Client().GetChatMessages(chatId, timestamp, true); + let sortedMessages = sortMessagesByTimestamp(messageBatch); + + // dedup + sortedMessages = sortedMessages.filter(message => { + return !getInnerChatContentElement().querySelector(`.message-container[data-timestamp="${message?.timestamp}"]`); + }); + + if(sortedMessages.length > 0) { + ChatTools.Scroll.toggleSmoothScroll(getInnerChatContentElement(), false) + + let template = document.createElement("div"); + + let lastDate = null + for(let message of sortedMessages) { + + let timestamp = message?.timestamp; + let currentDate = new Date(timestamp).toDateString(); + + if (currentDate !== lastDate) { + lastDate = currentDate; + renderSystemDateInChat(timestamp, template, true) + } + + await renderUserMessage({ + item: message, + renderTop: false, + element: template + }) + } + + getInnerChatContentElement().prepend(...template.childNodes); + ChatTools.Scroll.toggleSmoothScroll(getInnerChatContentElement(), true) + } + }) + + ChatTools.Scroll.observeContainer(getInnerChatContentElement()); + + let chatHost = activeChat?.home_server ?? activeChat?.host ?? null; if (!chatHost) throw new Error("No chat host found!"); + + await setChatSocket(chatHost); await connectToSocketHost(chatHost); if (!activeChat?.isServer) { @@ -295,16 +415,42 @@ async function renderChat(chatId, customChatObject = null) { ["clean", "link", "image", "video"], ["code", "code-block", "blockquote"] ], - onImg: async (src) => { + onImg: (src, { insert }) => { + if(src?.constructor?.name === "File"){ + return console.log("Detected file") + } + else if(src?.constructor?.name === "String"){ + if(src.startsWith("data:image")){ + insert(""); + } + + return + } + + // remove base64 image + if(src.startsWith("data:image/")){ + insert(""); + } }, onSend: async (html) => { let messageResult = await sendMessage(html, activeChat.publicKey, chatHost); if (messageResult?.error) { return alert(`Error while sending message!\n\n${messageResult.error}`) } + else{ + let targetData = messageResult?.target; + let existingChat = await Client().GetChat(targetData.gid) - console.log(messageResult) + if(targetData && existingChat){ + let icon = targetData?.icon; + let name = targetData?.name; + + if(icon) existingChat.icon = icon; + if(name) existingChat.title = name; + await Client().SaveChat(targetData.gid, existingChat); + } + } editor.quill.setContents([{insert: "\n"}]); } @@ -312,13 +458,54 @@ async function renderChat(chatId, customChatObject = null) { } await renderInboxElementsInChat(activeChat, true); + + // some ui tricks for mobile + if(getChatContentElement()?.classList?.contains("mobile") && MobilePanel.isMobile()){ + getChatContentElement().classList.remove("mobile"); + getChatListElement().classList.add("hide"); + getNavElement().classList.add("hide"); + } +} + +function renderSystemDateInChat(timestamp, element = null, renderTop = false){ + if(!timestamp) throw new Error("Cant show system date as timestamp is missing!") + + let render = element ? element : getInnerChatContentElement(); + let displayDate = new Date(timestamp).toLocaleDateString("en-US", { + weekday: "long", + year: "numeric", + month: "long", + day: "numeric" + }); + + // this is mostly for checking on the system dates. + // IF we append, lets check the main container too so we can remove it if present. + // then we always check the render element too for the sake of it not being possibly + // doubled within the appending stuff + let existingDateMessage = null; + if(renderTop){ + existingDateMessage = getInnerChatContentElement().querySelector(`.system-message.date[data-display-date="${displayDate}"]`); + if(existingDateMessage) existingDateMessage.remove(); + } + existingDateMessage = render?.querySelector(`.system-message.date[data-display-date="${displayDate}"]`); + if(existingDateMessage) return; + + // then we obviously just render it. maybe its too much, idk, its 3am at the time of writing, gg. + render.insertAdjacentHTML("beforeend", ` +
+ + ${displayDate} + +
+
+ `); } async function renderInboxElementsInChat(chat, initial = false) { if (!chat) throw new Error("No chat for rendering inbox messages"); - let messages = await Client().GetChatMessages(chat.gid); - console.log(messages) + let gid = chat?.isServer ? chat.host : chat?.gid; + let messages = await Client().GetChatMessages(gid, new Date().getTime(), true); if (!Array.isArray(messages)) { messages = Object.values(messages).map(item => item.data ?? item); @@ -326,6 +513,12 @@ async function renderInboxElementsInChat(chat, initial = false) { let lastDate = null; + messages = sortMessagesByTimestamp(messages); + + // update some stuff + chat.lastRead = new Date().getTime(); + chat.lastMessage = await getLastChatMessage(gid) + for (let item of messages) { if (!item?.type) continue; @@ -334,45 +527,44 @@ async function renderInboxElementsInChat(chat, initial = false) { if (currentDate !== lastDate) { lastDate = currentDate; - - getInnerChatContentElement().insertAdjacentHTML("beforeend", ` -
- - ${new Date(timestamp).toLocaleDateString("en-US", { - weekday: "long", - year: "numeric", - month: "long", - day: "numeric" - })} - -
-
- `); + renderSystemDateInChat(lastDate) } if (item.type === "mention") { await renderMention(item); } else if (item.type === "user_message") { - await renderUserMessage(item); + await renderUserMessage({ + item + }); } else { console.warn("Didnt render chat because of unsupported type: ", item.type) } } // if we actually open a chat we will just scroll down - if (initial) { + if (initial && getInnerChatContentElement()) { ChatTools.Scroll.scrollDown(getInnerChatContentElement()) } + + await Client().SaveChat(gid, chat); } -async function renderUserMessage(item, element = null) { +async function renderUserMessage({ + item, + element = null, + renderTop = false, + } = {}) { let message = item?.data?.message ?? item; let authorGid = message?.author?.gid; // OUR gid comrade. - let gid = await Client().GenerateGid(await Client().GetPublicKey()); + let gid = await getGid(); let encryptedMessage = message[gid]; + // idk why this happens, lazy. + // seems to only happen on android + if(typeof encryptedMessage === "string" && encryptedMessage.startsWith("{")) encryptedMessage = JSON.parse(encryptedMessage ?? "{}"); + // skill issue if (!encryptedMessage?.method) throw new Error("Invalid message data", message); @@ -385,11 +577,16 @@ async function renderUserMessage(item, element = null) { return; } + // dedup + if(getInnerChatContentElement().querySelector(`.message-container[data-timestamp="${message?.timestamp}"]`)){ + return; + } + // handle markdown let markdownResult = await ChatTools.Media.markdown({ htmlInput: decryptedMessageText, identifier: item?.timestamp, - containerElement: getInnerChatContentElement(), + containerElement: element ? element : getInnerChatContentElement(), }) // if it was changed update the text @@ -407,13 +604,13 @@ async function renderUserMessage(item, element = null) { let isScrolledDown = ChatTools.Scroll.isScrolledToBottom(getInnerChatContentElement(), 50); let renderElement = element ? element : getInnerChatContentElement(); - renderElement.insertAdjacentHTML("beforeend", await getMessageHTML({ + renderElement.insertAdjacentHTML(renderTop ? "afterbegin" : "beforeend", await getMessageHTML({ text, timestamp: message?.timestamp, isMine: authorGid === gid, })) - if (isScrolledDown) ChatTools.Scroll.scrollDown(getInnerChatContentElement()) + if (isScrolledDown && !renderTop) ChatTools.Scroll.scrollDown(getInnerChatContentElement()) } async function renderMention(item, element = null) { @@ -436,7 +633,7 @@ async function renderMention(item, element = null) { let renderElement = element ? element : getInnerChatContentElement(); renderElement.insertAdjacentHTML("beforeend", await getMessageHTML({ text, - timestamp: message?.timestamp + timestamp: message?.timestamp, })) } @@ -448,7 +645,7 @@ async function getMessageHTML({ if (!text) throw new Error("No text for messages"); return ` -
+
${text}
@@ -472,8 +669,9 @@ async function setChatHeader(chat) { getChatContentElement().innerHTML = `
-
-

${chatTitle}

+ ${Icon.display("back")} +
+

${ChatTools.Sanitize.forRender(chatTitle)}

`; } @@ -570,11 +768,12 @@ async function startNewChat({ let newChat = { publicKey: testMessage.target?.publicKey, gid: targetGid, - title: `New Chat`, + title: testMessage?.name ?? `New Chat`, host: homeServer, home_server: homeServer, lastMessage: null, - icon: null, + lastRead: new Date().getTime(), + icon: testMessage?.icon ?? null, }; await Client().SaveChat(targetGid, newChat); diff --git a/electron/web/js/content/socket.js b/electron/web/js/content/socket.js index 4a98e28..532c645 100644 --- a/electron/web/js/content/socket.js +++ b/electron/web/js/content/socket.js @@ -25,73 +25,80 @@ async function waitForSocket(socket) { } const sockets = new Map(); - -function getChatSocket() { - return sockets.get("remote") - ?? [...sockets.values()][0] - ?? null; -} - -function getHomeSocket() { - return [...sockets.values()].find(socket => socket?.isHomeServer === true) - ?? null; -} +let activeChatHost = null; async function getSocket(host) { host = extractHost(host); + if (!host) throw new Error("host is required"); - let homeServer = extractHost(await Client().GetHomeServer()); - let socketKey = host === homeServer ? host : "remote"; - - if (sockets.has(socketKey)) { - let existingSocket = sockets.get(socketKey); - - if (socketKey === "remote" && existingSocket.host !== host) { - existingSocket.disconnect(); - sockets.delete(socketKey); - } else { - return existingSocket; - } - } + if (sockets.has(host)) return sockets.get(host); let socket = io.connect(`${getProtocol(host)}://${host}`, { reconnection: true }); socket.host = host; - socket.isHomeServer = host === homeServer; - - sockets.set(socketKey, socket); + socket.isHomeServer = extractHost(await Client().GetHomeServer()) === host; socket.on("connect", async () => { + console.log(`connected to ${host}`); await socketHello(socket, host); - console.log(`Connected to host ${host} via socket`); + + if (!socket.didRegisterSocketListeners) { + registerSocketListeners(socket); + socket.didRegisterSocketListeners = true; + } + }); + + socket.on("disconnect", () => { + sockets.delete(host); + if (activeChatHost === host) activeChatHost = null; }); + sockets.set(host, socket); + return socket; +} + +async function setChatSocket(host) { + host = extractHost(host); + let socket = await getSocket(host); + await waitForSocket(socket); + + activeChatHost = host; return socket; } -function terminateSocket(address) { - address = extractHost(address); +async function getChatSocket(host = null) { + if (host) return await getSocket(extractHost(host)); + if (activeChatHost) return sockets.get(activeChatHost) ?? null; + return null; +} - let socket = sockets.get(address); +function getHomeSocket() { + return [...sockets.values()].find(s => s?.isHomeServer === true) ?? null; +} + +function terminateSocket(host) { + host = extractHost(host); + let socket = sockets.get(host); if (!socket) return false; socket.disconnect(); - sockets.delete(address); + sockets.delete(host); + if (activeChatHost === host) activeChatHost = null; return true; } async function connectToSocketHost(address) { - if(typeof Client().GetHomeServer !== "function") { + if (typeof Client().GetHomeServer !== "function") { throw new Error("Socket Connection canceled due to unsupported client"); } let socket = await getSocket(address); socket.on("connect", async () => { - if(!socket.didRegisterSocketListeners) { + if (!socket.didRegisterSocketListeners) { registerSocketListeners(socket); socket.didRegisterSocketListeners = true; } @@ -102,35 +109,84 @@ async function connectToSocketHost(address) { return socket; } -async function socketHello(socket, address){ - if(typeof Client().GetHomeServer !== "function") throw new Error("Socket Connection canceled due to unsupported client") +async function socketHello(socket, address, { + name = null, + icon = null, + vanity = null, + } = {} +) { + if (typeof Client().GetHomeServer !== "function") throw new Error("Socket Connection canceled due to unsupported client") + if (typeof Client().GetPublicKey !== "function") throw new Error("Socket Connection canceled due to unsupported client") return new Promise(async (resolve, reject) => { socket.emit("/messenger/hello", { publicKey: await Client().GetPublicKey(), sessionId: await getSessionIdFromHost(address), - home_server: await Client().GetHomeServer() + home_server: await Client().GetHomeServer(), + name, + icon, + vanity, }, - async function (response){ - if(response?.error) console.error(response?.error); - resolve(true) + async function (response) { + if (response?.error) { + console.error(response?.error); + } + + resolve({ + response, + error: response?.error, + }); }) }) } -async function registerSocketListeners(socket){ +async function registerSocketListeners(socket) { socket.on("/messenger/receive", async (message) => { - let myMessage = message[await Client().GenerateGid(await Client().GetPublicKey())] message.messageId = message.timestamp; - await Client().SaveChatMessage(message?.author?.gid, message) - if(message?.type === "user_message") await renderUserMessage(message, getInnerChatContentElement()) + let authorGid = message?.author?.gid; + let authorPublicKey = message?.author?.publicKey; + let authorHomeServer = message?.author?.home_server; + + let ownGid = await getGid(); + + // if we are the author, save under the target — otherwise save under the author + let targetGid = message?.targetIdentifier + ? await Client().GenerateGid(message.targetIdentifier) + : null; + + let chatGid = authorGid === ownGid ? targetGid : authorGid; + if (!chatGid) return console.error("could not determine chat partner gid", message); + + // make sure the chat exists before saving any message + let existingChat = await Client().GetChat(chatGid) + if (!existingChat) { + await Client().SaveChat(chatGid, { + publicKey: authorGid === ownGid ? message.targetIdentifier : authorPublicKey, + gid: chatGid, + title: `@${chatGid}`, + host: authorHomeServer, + home_server: authorHomeServer, + lastMessage: null, + icon: null, + }); + } + + // update the chat entry in the sidebar so the latest message and badge show up + await refreshChatEntry(chatGid, message); + await Client().SaveChatMessage(chatGid, message) + + if (message?.type === "user_message" && getInnerChatContentElement()) await renderUserMessage({ + item: message, + element: getInnerChatContentElement() + }) }) } -async function decryptUserMessage(message){ - if(!message) throw new Error("Message was not set"); +async function decryptUserMessage(message) { + if (!message) throw new Error("Message was not set"); + if (!message?.method) throw new Error("Message method not found"); let decryptedMessageText = await Client().DecryptData( message.method, @@ -145,10 +201,10 @@ async function decryptUserMessage(message){ return decryptedMessageText; } -async function sendMessage(text, targetPublicKey, host, test = false){ - if(text?.trim()?.length === 0) throw new Error("no text found to send"); - if(!targetPublicKey) throw new Error("target gid not found"); - if(!host) throw new Error("host not found to send"); +async function sendMessage(text, targetPublicKey, host, test = false) { + if (text?.trim()?.length === 0) throw new Error("no text found to send"); + if (!targetPublicKey) throw new Error("target gid not found"); + if (!host) throw new Error("host not found to send"); // prepair some shit let ownGid = await Client().GenerateGid(await Client().GetPublicKey()) @@ -180,18 +236,10 @@ async function sendMessage(text, targetPublicKey, host, test = false){ payload.message = await Client().SignJson(payload.message) return await new Promise(async (resolve, reject) => { - (await getChatSocket()).emit("/messenger/send", {message: payload.message, sessionId}, async function (response){ - - if(!response?.error){ - let existingChat = await Client().GetChat(targetGid); - if(!existingChat) await startNewChat({ - identifier: `${targetGid}@${host}`, - automate: true - }) - - await Client().SaveChatMessage(targetGid, payload.message) - } - + (await getChatSocket(host)).emit("/messenger/send", { + message: payload.message, + sessionId + }, async function (response) { resolve(response) }) }) diff --git a/electron/web/js/general.js b/electron/web/js/general.js index ed8f707..61f9eaa 100644 --- a/electron/web/js/general.js +++ b/electron/web/js/general.js @@ -1,5 +1,87 @@ function Client() { - return window?.dcts; + const client = window?.dcts; + if (!client) return null; + + if (typeof client.getPlatform === "function" && client.getPlatform() === "android") { + const wrapper = {}; + + + for (const key in client) { + const value = client[key]; + + if (typeof value === "function") { + wrapper[key] = (...args) => { + try { + return client[key](...args); + } catch (e) { + console.error("ANDROID METHOD FAILED:", key); + console.error("ARGS:", args); + console.error("ERROR:", e); + throw e; + } + }; + } else { + wrapper[key] = value; + } + } + + wrapper.SignJson = async obj => { + const res = client.SignJson(JSON.stringify(obj)); + return res ? JSON.parse(res) : null; + }; + + wrapper.VerifyJson = async (obj, publicKey) => { + const res = client.VerifyJson(JSON.stringify(obj), publicKey); + return res === "true" || res === true; + }; + + wrapper.SaveServer = async (address, isFav) => { + return client.SaveServer(String(address), Boolean(isFav)); + }; + + wrapper.GetServer = async address => { + const res = client.GetServer(String(address)); + return res ? JSON.parse(res) : null; + }; + + // Messenger functions + wrapper.GetChatMessages = async (chatId, timestamp, desc) => { + const res = client.GetChatMessages( + String(chatId), + timestamp == null ? "null" : String(timestamp), + desc == null ? "true" : String(desc) + ); + + return res ? JSON.parse(res) : null; + }; + + wrapper.GetChats = async () => { + const res = client.GetChats(); + return res ? JSON.parse(res) : null; + }; + + wrapper.GetChat = async (chatId) => { + const res = client.GetChat(String(chatId)); + return res ? JSON.parse(res) : null; + }; + + wrapper.GetChatLastMessage = async (chatId) => { + const res = client.GetChatLastMessage(String(chatId)); + return res ? JSON.parse(res) : null; + }; + + wrapper.SaveChatMessage = async (chatId, messageObj) => { + return client.SaveChatMessage(String(chatId), JSON.stringify(messageObj)); + }; + + wrapper.GetUserConsistentSettings = async () => { + return client.GetUserConsistentSettings() === "true"; + }; + + return wrapper; + } + + return client; } function isLauncher() { diff --git a/electron/web/js/libs/chat-tools/ChatTools.js b/electron/web/js/libs/chat-tools/ChatTools.js index 2b0cdce..bd5f473 100644 --- a/electron/web/js/libs/chat-tools/ChatTools.js +++ b/electron/web/js/libs/chat-tools/ChatTools.js @@ -1,5 +1,19 @@ class ChatTools { static Scroll = class { + static async registerMessageInfiniteLoad(element, callback = null) { + if(!element) throw new Error("Element for infinite scroll not found"); + + if(!element.getAttribute("data-scroll-init")){ + element.addEventListener("scroll", async function () { + if (element.scrollTop === 0) { + if(callback && typeof callback === "function") await callback(element); + } + }); + + element.setAttribute("data-scroll-init", true) + } + } + static scrollDown(containerElement, opts = {}) { const el = containerElement if (!el) return; @@ -247,11 +261,11 @@ class ChatTools { if (t) embed += "?start=" + parseInt(t); return ` -
- ${url}
+
+ ${ChatTools.Sanitize.forRender(url)}