Compare commits

...
16 changed files with 957 additions and 12975 deletions

26
.gitignore vendored
View file

@ -1,5 +1,29 @@
.DS_Store
/node_modules
/out
/.log
crash.log
# local env files
.env.local
.env.*.local
.env
# Log files
npm-debug.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# Dist
*.app
*.sublime-workspace
*.sublime-project

2
.npmrc
View file

@ -1,2 +1,2 @@
@crimata:registry=https://gitlab.com/api/v4/projects/28849281/packages/npm/
//gitlab.com/api/v4/projects/28849281/packages/npm
//gitlab.com/api/v4/projects/28849281/packages/npm/:_authToken=${API_TOKEN}

66
README.md Normal file
View file

@ -0,0 +1,66 @@
# crimata-electron
## Project setup
```
yarn install
```
### Compiles and hot-reloads for development
```
yarn electron:serve
```
### Compiles and minifies for production
```
yarn electron:build
```
<!-- ### Run your unit tests
```
yarn test:unit
```
### Lints and fixes files
```
yarn lint
``` -->
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).
## Sign and Notarize
Prerequisites:
* Apple Developer Account ($99)
* Valid Apple Developer ID Application Certificate on keychain
Clear debris from app before signing:
xattr -cr Electron.app
Sign and Notarize:
node notarize.js
Debug electron-osx-sign:
export DEBUG=electron-osx-sign*
### Some things to note:
Notarize only when distributing outside the Mac App Store.
#### Gatekeeper Assess
Electron-osx-sign seems to have a bug where the sign will fail if "gatekeeper-assess" is true (default).
#### Entitlements.plist
Not to be confused with Info.plist, this file specifies entitlements the app can have in hardened runtime (e.g. Can I use the microphone?). Only including the ones Electron reccommends seems to work (including audio of course and omitting allow-unsigned-executable-memory).
#### The Benifit of Electron OSX Sign
Without this package, we would have to manually go in and figure out how to sign each level of the app. While it didn't work out of the box (gatekeeper-assess) it seems to be good otherwise. However, there is a bug where it puts multiple runtime flags on each command - not catastrophic though.
#### Apple Password
This must be an app specific password, not your normal Apple ID password.
#### --Signiture-Flags OSX Sign
Not exactly sure what this does or why its important, but Electron includes it.

View file

@ -1,8 +0,0 @@
{
"folders":
[
{
"path": "."
}
]
}

52
notarize.js Normal file
View file

@ -0,0 +1,52 @@
const exec = require('child_process').exec;
const sign = require("electron-osx-sign").signAsync;
const notarize = require("electron-notarize").notarize;
const app = "Crimata.app";
const dir = `${app}/Contents/Resources/app`;
const runShellCommand = (cmd) => (new Promise((resolve, reject) => {
exec(cmd, (error, stdout, stderr) => {
error ? reject(stderr) : resolve(stdout);
});
}));
const signConfig = {
"app": app,
"hardened-runtime": true,
"gatekeeper-assess": false,
"signature-flags": "library",
"entitlements": "entitlements.plist",
"entitlements-inherit": "entitlements.plist",
};
const notarizeConfig = {
appPath: app,
appleId: "[email protected]",
appBundleId: "com.crimata.CrimataMessenger",
appleIdPassword: process.env["AC_PASSWORD"]
};
(async function () {
try
{
console.log("Building...");
await runShellCommand(`rm -rf ${dir} && mkdir ${dir} && cp -r {package.json,src,node_modules} ${dir}
`);
console.log("Cleaning...");
await runShellCommand(`xattr -cr ${app}`);
console.log("Signing...");
await sign(signConfig);
console.log("Notarizing...");
await notarize(notarizeConfig);
}
catch (e)
{
console.log(e);
}
})();

12592
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,54 +1,25 @@
{
"name": "crimata",
"name": "Crimata",
"productName": "crimata-messenger",
"version": "1.0.0",
"private": true,
"description": "Cross-platform messenger app (Electron, Vue3)",
"author": {
"name": "Andrew Gundersen"
},
"main": "src/main.js",
"scripts": {
"start": "electron-forge start",
"test": "echo \"Error: no test specified\" && exit 1",
"package": "electron-forge package",
"make": "electron-forge make"
},
"author": "Andrew Gundersen",
"license": "ISC",
"devDependencies": {
"@electron-forge/cli": "^6.0.0-beta.63",
"@electron-forge/maker-deb": "^6.0.0-beta.63",
"@electron-forge/maker-rpm": "^6.0.0-beta.63",
"@electron-forge/maker-squirrel": "^6.0.0-beta.63",
"@electron-forge/maker-zip": "^6.0.0-beta.63",
"electron": "^18.2.3"
"start": "electron ."
},
"dependencies": {
"@crimata/nodeaudio": "0.0.0",
"axios": "^0.21.1",
"base64-arraybuffer": "^1.0.1",
"electron-store": "^8.0.0",
"ws": "^7.3.1",
"electron-squirrel-startup": "^1.0.0"
"ws": "^7.3.1"
},
"config": {
"forge": {
"packagerConfig": {},
"makers": [
{
"name": "@electron-forge/maker-squirrel",
"config": {}
},
{
"name": "@electron-forge/maker-zip",
"platforms": [
"darwin"
]
},
{
"name": "@electron-forge/maker-deb",
"config": {}
},
{
"name": "@electron-forge/maker-rpm",
"config": {}
}
]
}
"devDependencies": {
"electron-notarize": "^1.1.1",
"electron-osx-sign": "^0.6.0"
}
}

View file

@ -5,8 +5,6 @@ const store = require("./utils/store");
const { launchSession, endSession } = require("./session");
const { backgroundMitt, ipcEmit } = require("./utils/emitter");
store.set("account", process.env.ACCOUNT);
async function auth(_e, creds)
{
const { error, data } = await post("/auth", creds);

View file

@ -2,10 +2,10 @@ const { app } = require("electron");
const DOMAIN = "crimata.com";
/* If PROD env variable exists */
const prod = process.env.PROD;
const prod = !process.defaultApp;
// const prod = true;
module.exports = {
PLATFORM: prod ? `https://app.${DOMAIN}` : `http://localhost:5000`,
API: prod ? `https://${DOMAIN}/api` : `http://localhost:5001`
PLATFORM: prod ? `https://app.${DOMAIN}` : `http://localhost:8760`,
API: prod ? `https://${DOMAIN}/api` : `http://localhost:8761`
}

View file

@ -5,7 +5,7 @@ const store = require("./utils/store");
const { initTray } = require("./tray");
const { createWin } = require("./window");
const { initAccount } = require("./account");
// const { terminateAudio } = require("./audio");
const { terminateAudio } = require("./audio");
// Assert a light theme
nativeTheme.themeSource = "light";
@ -19,7 +19,7 @@ app.on("ready", () =>
initAccount();
});
// app.on("will-quit", terminateAudio);
app.on("will-quit", terminateAudio);
// When user clicks app icon (re-open)
app.on("activate", createWin);

View file

@ -1,23 +0,0 @@
export const isEmail = (value) => {
console.log(value);
if (!/(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])/.test(value)) {
return "Not a valid email."
}
}
export const isPassword = (value) => {
if (!/^(?=.*[0-9])(?=.*[!@#$%^&*])[a-zA-Z0-9!@#$%^&*]{6,16}$/.test(value)) {
return "Password must have symbol, number, uppercase and be 6-16 length.";
}
}
/**
* Name is optional and therefore doesn't return error if no value.
*/
export const isName = (value) => {
if (value) {
if (!/^([a-zA-Z ]){5,30}$/.test(value)) {
return "Name must be at least 5 long.";
}
}
}

View file

@ -1,86 +1,83 @@
// Basic debounce implentation
const debounce = (fn, delay) => {
let timeoutId;
return (...args) => {
if (timeoutId) clearTimeout(timeoutId);
const wrapper = (...args) => {
if (timeoutId) {
clearTimeout(timeoutId);
}
timeoutId = setTimeout(() => {
fn(...args);
}, delay);
}
return wrapper;
}
/**
* Dynamic form component with auto submission and validation.
*
* props
* .fields => List of field objects
* .channel => e.g. "/login"
* .modifier => Form description
*/
const SmartForm =
const SmartForm =
{
props: ["fields", "channel", "modifier"],
setup(props)
{
// Error log below form
const log = Vue.ref(props.modifier);
let logsEl;
/**
* Valid and submit form data to channel.
*/
const submit = async () => {
const submit = debounce(async (data) => {
// Validate each field
for (let field of props.fields) {
const error = field.valid(field.value);
if (error) return error;
let object = {};
data.forEach(function(value, key){
object[key] = value;
});
const err = await window.mainApi.invoke(props.channel, object)
if (err) {
logsEl.innerText = err;
logsEl.style.color = "red";
return;
}
// Reduce fields into the form
const form = props.fields.reduce((v, n) => {
v[n.name] = n.value;
return v;
}, {});
console.log("Submitting =>", form);
return await window.mainApi.invoke(props.channel, form);
}
/**
* Wrapper for submit that is debounced and updates UI elements
*/
const preSub = debounce(async () => {
const error = await submit();
log.value = error ? error : props.modifier
logsEl.style.color = "green";
logsEl.innerText = props.modifier;
}, 2000);
return { preSub, log };
Vue.onMounted(() => {
const el = document.getElementById("smart-form");
logsEl = el.querySelector(".smart-form-logs");
logsEl.innerText = props.modifier;
el.addEventListener("input", () => {
submit(new FormData(el))
});
});
},
template: `
<form id="smart-form" class="smart-form" v-on:input="preSub">
<form id="smart-form" class="smart-form">
<div v-for="field in fields" style="display:flex">
<input type="text" class="text-field"
v-model="field.value"
<input type="text" class="text-field"
:name="field.name"
:value="field.value"
:placeholder="field.placeholder"
/>
</div>
<div class="smart-form-logs">{{ log }}</div>
<div class="smart-form-logs"></div>
</form>`
}
export default SmartForm;

View file

@ -1,5 +1,4 @@
import SmartForm from "./form.js";
import { isEmail, isPassword, isName } from "./control/valid.js";
const Login =
{
@ -9,7 +8,7 @@ const Login =
setup()
{
return { isEmail, isPassword, isName };
return;
},
template: `
@ -20,19 +19,16 @@ const Login =
name: 'email',
value: null,
placeholder: 'Email',
valid: isEmail
},
{
name: 'password',
value: null,
placeholder: 'Password',
valid: isPassword
placeholder: 'Password'
},
{
name: 'name',
value: null,
placeholder: 'Name',
valid: isName
placeholder: 'Name'
}]"
:channel="'auth'"
:modifier="'Login or enter name to register.'"
@ -41,4 +37,4 @@ const Login =
</div>`
}
export default Login;
export default Login;

View file

@ -24,7 +24,7 @@ const Settings =
},
template: `
<div v-if="active" id="settings">
<div v-if="active" id="settings" >
<button class="settings-logout-button button" @click="logout">
Logout

View file

@ -36,7 +36,6 @@ html, body {
-webkit-font-smoothing: antialiased;
height: 100vh; /* 100% for website */
width: 100vw; /* 100% for website */
/* Does not work on Linux */
border-radius: 15px;
}
@ -93,8 +92,6 @@ html, body {
margin-left: 20px;
margin-top: 20px;
z-index: 2;
/* Needed for Linux even with higher Z index */
-webkit-app-region: no-drag;
}
.header-menu-button {
@ -230,6 +227,7 @@ html, body {
border-radius: 50%;
/* Set opacity here to not affect child. */
background-color: rgba(235, 235, 235, 0.75);
cursor: pointer;
box-shadow: 0px 0px 10px 0px rgba(0,0,0,0.15);
}
@ -296,8 +294,10 @@ html, body {
margin-top: 19px;
z-index: 2;
background-color: Transparent;
/* Needed for Linux */
-webkit-app-region: no-drag;
}
.settings-icon:hover {
cursor: pointer;
}
.settings-account {
@ -314,6 +314,10 @@ html, body {
font-size: 14px;
}
.settings-logout-button:hover {
cursor: pointer;
}
.settings-version {
position: absolute;
left: 50%;

View file

@ -3,7 +3,7 @@ const { Notification, ipcMain } = require("electron");
const store = require("./utils/store");
const { backgroundMitt, ipcEmit } = require("./utils/emitter");
const { connectToPlatform, disconnectFromPlatform } = require("./io");
// const { initAudio, terminateAudio, playback, streamState } = require("./audio");
const { initAudio, terminateAudio, playback, streamState } = require("./audio");
const util = require('util');
@ -25,12 +25,10 @@ backgroundMitt.on("message", (message) => messageQueue.push(message));
function handleMessageQueue()
{
// if (!streamState.pb && !streamState.rec)
// {
if (!streamState.pb && !streamState.rec)
{
const update = messageQueue.shift();
console.log(update);
if (update)
{
if (update.person)
@ -68,31 +66,30 @@ function handleMessageQueue()
body: content.text
}).show();
/* Ideally, we want a more native solution than this */
// if (current.modifier == "ai" && content.category == "audio")
// {
// playback(content.blob, current.id);
// }
if (content.category == "audio")
{
playback(current.id, content.blob);
}
return;
}
ipcEmit("message", update);
}
// }
}
}
function launchSession(account)
{
connectToPlatform(account);
// initAudio();
initAudio();
processContentId = setInterval(handleMessageQueue, 100);
}
function endSession()
{
clearInterval(processContentId);
// terminateAudio();
terminateAudio();
disconnectFromPlatform();
ui = null;
}