Skip to content

Commit

Permalink
Alert refactor (#425)
Browse files Browse the repository at this point in the history
* Alert refactor

* Initial alert refactor

* Use createAlert function instead of AlertController

* Fix alert animation

* Prettier

* Clean up and improve key

* Add alert test

* Change key to improve animations

* Prettier

* Remove console.logs

* Cap alert array at 1000
  • Loading branch information
Duddino authored Oct 18, 2024
1 parent 9b3766f commit 388b63e
Show file tree
Hide file tree
Showing 30 changed files with 394 additions and 119 deletions.
2 changes: 1 addition & 1 deletion index.template.html
Original file line number Diff line number Diff line change
Expand Up @@ -607,7 +607,7 @@ <h2 id="mnLastSeen" class="stake-balances" style="overflow-wrap: anywhere; top:
<!--
Alert
-->
<div class="alertPositioning"></div>
<div class="alertPositioning" id="Alerts"></div>
<footer id="foot" style="padding-bottom: 32px;">
<div class="footer container">

Expand Down
37 changes: 37 additions & 0 deletions scripts/alerts/Alert.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<script setup>
import { computed, toRefs } from 'vue';
const props = defineProps({
message: String,
level: String,
});
const { message, level } = toRefs(props);
const icon = computed(() => {
switch (level.value) {
case 'warning':
return 'fa-exclamation';
case 'info':
return 'fa-info';
case 'success':
return 'fa-check';
default:
throw new Error('Invalid type');
}
});
</script>

<template>
<div
class="notifyWrapper"
@click="$emit('click')"
:class="{ [level]: true }"
:style="{ opacity: 1 }"
>
<div class="notifyIcon" :class="{ ['notify-' + level]: true }">
<i class="fas fa-xl" :class="{ [icon]: true }"> </i>
</div>
<div class="notifyText" v-html="message"></div>
</div>
</template>
74 changes: 74 additions & 0 deletions scripts/alerts/Alerts.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<script setup>
import { useAlerts } from '../composables/use_alerts';
import { computed, watch, ref } from 'vue';
import Alert from './Alert.vue';
const alerts = useAlerts();
const foldedAlerts = ref([]);
watch(alerts, () => {
const res = [];
let previousAlert;
let count = 1;
const pushAlert = () => {
if (previousAlert) {
const countStr = count === 1 ? '' : ` (x${count})`;
const timeout =
previousAlert.created + previousAlert.timeout - Date.now();
const show = timeout > 0;
if (!show) return;
const alert = ref({
...previousAlert,
message: `${previousAlert.message}${countStr}`,
show,
// Store original message so we can use it as key.
// This skips the animation in case of multiple errors
original: previousAlert.message,
});
res.push(alert);
if (timeout > 0) {
setTimeout(() => {
alert.value.show = false;
}, timeout);
}
}
};
for (const alert of alerts.alerts) {
if (previousAlert && previousAlert?.message === alert.message) {
count++;
} else {
pushAlert();
count = 1;
}
previousAlert = alert;
}
pushAlert();
foldedAlerts.value = res;
});
</script>
<template>
<transition-group name="alert">
<div
v-for="alert of foldedAlerts.filter((a) => a.value.show)"
:key="alert.value.original"
>
<Alert
:message="alert.value.message"
:level="alert.value.level"
@click="alert.value.show = false"
/>
</div>
</transition-group>
</template>
<style>
.alert-enter-active,
.alert-leave-active {
transition: all 0.5s ease;
}
.alert-enter-from,
.alert-leave-to {
opacity: 0;
}
</style>
101 changes: 101 additions & 0 deletions scripts/alerts/alert.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
export class Alert {
/**
* @type{string} Message of the alert. Can contain <html>
* and must be properly escaped if it contains untrusted input
*/
message;

/**
* @type{'success'|'info'|'warning'} Alert level
*/
level;

/**
* @type{number} timeout of the alert, in milliseconds
*/
timeout;

/**
* @type{number} Time of creation in ms since unix epoch. Defaults to `Date.now()`
*/
created;

constructor({ message, level, timeout = 0, created = Date.now() }) {
this.message = message;
this.level = level;
this.timeout = timeout;
this.created = created;
}
}

export class AlertController {
/**
* @type{Alert[]}
*/
#alerts = [];

/**
* @param{((alert: Alert)=>void)[]} array of subscribers
*/
#subscribers = [];

/**
* @returns the array of alerts
* DO NOT PUSH TO THIS ARRAY. Use `createAlert` or `addAlert` instead
*/
getAlerts() {
return this.#alerts;
}

/**
* Create a custom GUI Alert popup
*
* ### Do NOT display arbitrary / external errors:
* - The use of `.innerHTML` allows for input styling at this cost.
* @param {'success'|'info'|'warning'} type - The alert level
* @param {string} message - The message to relay to the user
* @param {number?} timeout - The time in `ms` until the alert expires (Defaults to never expiring)
*/
createAlert(level, message, timeout = 0) {
this.addAlert(new Alert({ level, message, timeout }));
}

/**
* @param {Alert} alert - alert to add
*/
addAlert(alert) {
this.#alerts.push(alert);
this.#alerts.splice(0, this.#alerts.length - 1000);
for (const sub of this.#subscribers) {
// Notify subscribers of the new alert
sub(alert);
}
}

/**
* @param {(alert: Alert) => void} When a new alert is created, calls the `fn` callback
*/
subscribe(fn) {
this.#subscribers.push(fn);
}

static #instance = new AlertController();

static getInstance() {
return this.#instance;
}
}

/**
* Create a custom GUI Alert popup
*
* ### Do NOT display arbitrary / external errors:
* - The use of `.innerHTML` allows for input styling at this cost.
* @param {'success'|'info'|'warning'} type - The alert level
* @param {string} message - The message to relay to the user
* @param {number?} [timeout] - The time in `ms` until the alert expires (Defaults to never expiring)
*/
export function createAlert(type, message, timeout = 0) {
const alertController = AlertController.getInstance();
return alertController.createAlert(type, message, timeout);
}
31 changes: 31 additions & 0 deletions scripts/composables/use_alerts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { ref } from 'vue';
import { defineStore } from 'pinia';
import { AlertController } from '../alerts/alert.js';

export const useAlerts = defineStore('alerts', () => {
const alerts = ref([]);

const alertController = AlertController.getInstance();

/**
* Create a custom GUI Alert popup
*
* ### Do NOT display arbitrary / external errors:
* - The use of `.innerHTML` allows for input styling at this cost.
* @param {'success'|'info'|'warning'} type - The alert level
* @param {string} message - The message to relay to the user
* @param {number?} timeout - The time in `ms` until the alert expires (Defaults to never expiring)
*/
const createAlert = (type, message, timeout) => {
alertController.createAlert(type, message, timeout);
};

alertController.subscribe(() => {
alerts.value = [...alertController.getAlerts()];
});

return {
alerts,
createAlert,
};
});
2 changes: 1 addition & 1 deletion scripts/contacts-book.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { doms, toClipboard } from './global.js';
import { ALERTS, tr, translation } from './i18n.js';
import {
confirmPopup,
createAlert,
createQR,
getImageFile,
isValidPIVXAddress,
Expand All @@ -17,6 +16,7 @@ import { useWallet } from './composables/use_wallet.js';
import pIconCopy from '../assets/icons/icon-copy.svg';
import pIconCamera from '../assets/icons/icon-camera.svg';
import pIconBin from '../assets/icons/icon-bin.svg';
import { createAlert } from './alerts/alert.js';

/**
* Represents an Account contact
Expand Down
4 changes: 2 additions & 2 deletions scripts/dashboard/Dashboard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import TransferMenu from './TransferMenu.vue';
import ExportPrivKey from './ExportPrivKey.vue';
import RestoreWallet from './RestoreWallet.vue';
import {
createAlert,
isExchangeAddress,
isShieldAddress,
isValidPIVXAddress,
Expand Down Expand Up @@ -41,7 +40,8 @@ import pIconCamera from '../../assets/icons/icon-camera.svg';
import { ParsedSecret } from '../parsed_secret.js';
import { storeToRefs } from 'pinia';
import { Account } from '../accounts';
import { useAlerts } from '../composables/use_alerts.js';
const { createAlert } = useAlerts();
const wallet = useWallet();
const activity = ref(null);
Expand Down
3 changes: 2 additions & 1 deletion scripts/dashboard/GenKeyWarning.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import { ref } from 'vue';
import Modal from '../Modal.vue';
import Password from '../Password.vue';
import { MIN_PASS_LENGTH } from '../chain_params.js';
import { createAlert } from '../misc';
import { useAlerts } from '../composables/use_alerts.js';
const { createAlert } = useAlerts();
import pLock from '../../assets/icons/icon-lock-locked.svg';
Expand Down
3 changes: 2 additions & 1 deletion scripts/dashboard/RestoreWallet.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import Password from '../Password.vue';
import { ALERTS, translation } from '../i18n.js';
import { Database } from '../database.js';
import { decrypt } from '../aes-gcm';
import { createAlert } from '../misc';
import { useAlerts } from '../composables/use_alerts.js';
const { createAlert } = useAlerts();
const props = defineProps({
show: Boolean,
Expand Down
4 changes: 3 additions & 1 deletion scripts/dashboard/VanityGen.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import vanityWalletIcon from '../../assets/icons/icon-vanity-wallet.svg';
import { ALERTS, translation, tr } from '../i18n.js';
import { ref, computed, watch, nextTick } from 'vue';
import { cChainParams } from '../chain_params.js';
import { MAP_B58, createAlert } from '../misc.js';
import { MAP_B58 } from '../misc.js';
import { useAlerts } from '../composables/use_alerts.js';
const { createAlert } = useAlerts();
const addressPrefix = ref('');
const addressPrefixElement = ref({});
const isGenerating = ref(false);
Expand Down
9 changes: 2 additions & 7 deletions scripts/database.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,8 @@ import { openDB } from 'idb';
import Masternode from './masternode.js';
import { Settings } from './settings.js';
import { cChainParams } from './chain_params.js';
import {
confirmPopup,
sanitizeHTML,
createAlert,
isSameType,
isEmpty,
} from './misc.js';
import { confirmPopup, sanitizeHTML, isSameType, isEmpty } from './misc.js';

Check warning on line 5 in scripts/database.js

View workflow job for this annotation

GitHub Actions / Run linters

'confirmPopup' is defined but never used

Check warning on line 5 in scripts/database.js

View workflow job for this annotation

GitHub Actions / Run linters

'sanitizeHTML' is defined but never used
import { createAlert } from './alerts/alert.js';
import { PromoWallet } from './promos.js';
import { ALERTS, translation } from './i18n.js';

Check warning on line 8 in scripts/database.js

View workflow job for this annotation

GitHub Actions / Run linters

'ALERTS' is defined but never used

Check warning on line 8 in scripts/database.js

View workflow job for this annotation

GitHub Actions / Run linters

'translation' is defined but never used
import { Account } from './accounts.js';
Expand Down
5 changes: 4 additions & 1 deletion scripts/global.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import {
fAdvancedMode,
} from './settings.js';
import { createAndSendTransaction } from './legacy.js';
import { createAlert, confirmPopup, sanitizeHTML } from './misc.js';
import { createAlert } from './alerts/alert.js';
import { confirmPopup, sanitizeHTML } from './misc.js';
import { cChainParams, COIN } from './chain_params.js';
import { sleep } from './utils.js';
import { registerWorker } from './native.js';
Expand All @@ -21,6 +22,7 @@ import { checkForUpgrades } from './changelog.js';
import { FlipDown } from './flipdown.js';
import { createApp } from 'vue';
import Dashboard from './dashboard/Dashboard.vue';
import Alerts from './alerts/Alerts.vue';
import { loadDebug, debugLog, DebugTopics, debugError } from './debug.js';
import Stake from './stake/Stake.vue';
import { createPinia } from 'pinia';
Expand Down Expand Up @@ -49,6 +51,7 @@ const pinia = createPinia();
export const dashboard = createApp(Dashboard).use(pinia).mount('#DashboardTab');
createApp(Stake).use(pinia).mount('#StakingTab');
createApp(SideNavbar).use(pinia).mount('#SideNavbar');
createApp(Alerts).use(pinia).mount('#Alerts');

export async function start() {
doms = {
Expand Down
3 changes: 2 additions & 1 deletion scripts/ledger.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import createXpub from 'create-xpub';
import { ALERTS, tr } from './i18n.js';
import { confirmPopup, createAlert } from './misc.js';
import { confirmPopup } from './misc.js';
import { getNetwork } from './network.js';
import { Transaction } from './transaction.js';
import { COIN, cChainParams } from './chain_params.js';
import { hexToBytes, bytesToHex } from './utils.js';
import { OP } from './script.js';
import { createAlert } from './alerts/alert.js';
import { debugError, DebugTopics } from './debug.js';

/**
Expand Down
7 changes: 2 additions & 5 deletions scripts/legacy.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,11 @@ import { ALERTS, translation, tr } from './i18n.js';
import { doms, restoreWallet } from './global.js';
import { wallet, getNewAddress } from './wallet.js';
import { cChainParams, COIN, COIN_DECIMALS } from './chain_params.js';
import {
createAlert,
generateMasternodePrivkey,
confirmPopup,
} from './misc.js';
import { generateMasternodePrivkey, confirmPopup } from './misc.js';
import { Database } from './database.js';
import { getNetwork } from './network.js';
import { ledgerSignTransaction } from './ledger.js';
import { createAlert } from './alerts/alert.js';

/**
* @deprecated use the new wallet method instead
Expand Down
Loading

0 comments on commit 388b63e

Please sign in to comment.