Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions front-end/src/renderer/composables/useFilterNotifications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import useNotificationsStore from '@renderer/stores/storeNotifications.ts';

export default function useFilterNotifications(
transactionNode: Ref<ITransactionNode>,
notificationType: Ref<NotificationType|null>,
notificationTypes: Ref<NotificationType[]>,
) {

/* Stores */
Expand All @@ -22,16 +22,18 @@ export default function useFilterNotifications(
const filteredNotificationIds = computed(() => {
return filteredNotifications.value.map(candidate => candidate.id);
});

const notificationsKey = computed(() => user.selectedOrganization?.serverUrl ?? '');

const candidateNotifications = computed(() => {
let result: INotificationReceiver[];
if (notificationType.value !== null) {
if (notificationTypes.value.length > 0) {
const serverNotifications = notifications.notifications[notificationsKey.value] ?? [];
result = serverNotifications.filter((n: INotificationReceiver) => {
return n.notification.type === notificationType.value;
return notificationTypes.value.includes(n.notification.type);
});
} else {
result = []
result = [];
}
return result;
});
Expand Down Expand Up @@ -85,7 +87,7 @@ export default function useFilterNotifications(
/* Mount */
onMounted(() => {
watch(
[user.selectedOrganization, notifications.notifications, transactionNode, notificationType],
[user.selectedOrganization, notifications.notifications, transactionNode, notificationTypes],
updateFilteredNotifications,
{ immediate: true },
);
Expand Down
35 changes: 35 additions & 0 deletions front-end/src/renderer/composables/useWebsocketSubscription.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { onBeforeMount } from 'vue';
import useUserStore from '@renderer/stores/storeUser';
import useWebsocketConnection from '@renderer/stores/storeWebsocketConnection';
import useDisposableWs from '@renderer/composables/useDisposableWs';

/**
* Subscribe to a WebSocket event for the current organization
* Automatically handles initial connection and cleanup
*
* @param eventName - The WebSocket event to listen for
* @param callback - Function to call when event is received
*/
export default function useWebsocketSubscription(
eventName: string,
callback: () => void | Promise<void>,
) {
const user = useUserStore();
const wsStore = useWebsocketConnection();
const ws = useDisposableWs();

/* Functions */
const subscribe = () => {
if (!user.selectedOrganization?.serverUrl) return;
ws.on(user.selectedOrganization.serverUrl, eventName, callback);
};

/* Subscribe immediately on mount (in case WS is already connected) */
onBeforeMount(subscribe);

/* Handle WebSocket reconnections (for future reconnects) */
wsStore.$onAction(ctx => {
if (ctx.name !== 'setup') return;
ctx.after(() => subscribe());
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,10 @@ import { CommonNetwork } from '@shared/enums';
import useUserStore from '@renderer/stores/storeUser';
import useNetwork from '@renderer/stores/storeNetwork';
import useContactsStore from '@renderer/stores/storeContacts';
import useWebsocketConnection from '@renderer/stores/storeWebsocketConnection';
import useNextTransactionStore from '@renderer/stores/storeNextTransaction';

import useDisposableWs from '@renderer/composables/useDisposableWs';
import useSetDynamicLayout, { LOGGED_IN_LAYOUT } from '@renderer/composables/useSetDynamicLayout';
import useWebsocketSubscription from '@renderer/composables/useWebsocketSubscription';

import { getApiGroupById, getTransactionById } from '@renderer/services/organization';
import { getTransaction } from '@renderer/services/transactionService';
Expand Down Expand Up @@ -58,12 +57,20 @@ import TransactionId from '@renderer/components/ui/TransactionId.vue';
const user = useUserStore();
const network = useNetwork();
const contacts = useContactsStore();
const wsStore = useWebsocketConnection();
const nextTransaction = useNextTransactionStore();

/* Composables */
const router = useRouter();
const ws = useDisposableWs();
useWebsocketSubscription(TRANSACTION_ACTION, async () => {
await fetchTransaction();
const id = formattedId.value!;
nextId.value = await nextTransaction.getNext(
isLoggedInOrganization(user.selectedOrganization) ? Number(id) : id,
);
prevId.value = await nextTransaction.getPrevious(
isLoggedInOrganization(user.selectedOrganization) ? Number(id) : id,
);
});
useSetDynamicLayout(LOGGED_IN_LAYOUT);
const route = useRoute();

Expand Down Expand Up @@ -158,20 +165,6 @@ async function fetchTransaction() {
feePayer.value = getTransactionPayerId(sdkTransaction.value);
}

const subscribeToTransactionAction = () => {
if (!user.selectedOrganization?.serverUrl) return;
ws.on(user.selectedOrganization?.serverUrl, TRANSACTION_ACTION, async () => {
await fetchTransaction();
const id = formattedId.value!;
nextId.value = await nextTransaction.getNext(
isLoggedInOrganization(user.selectedOrganization) ? Number(id) : id,
);
prevId.value = await nextTransaction.getPrevious(
isLoggedInOrganization(user.selectedOrganization) ? Number(id) : id,
);
});
};

const formattedId = computed(() => {
const id = router.currentRoute.value.params.id;
return id ? (Array.isArray(id) ? id[0] : id) : null;
Expand All @@ -189,8 +182,6 @@ onBeforeMount(async () => {
const keepNextTransaction = router.currentRoute.value.query[KEEP_NEXT_QUERY_KEY];
if (!keepNextTransaction) nextTransaction.reset();

subscribeToTransactionAction();

const result = await Promise.all([
fetchTransaction(),
nextTransaction.getNext(
Expand All @@ -215,11 +206,6 @@ onBeforeRouteLeave(to => {
});

/* Watchers */
wsStore.$onAction(ctx => {
if (ctx.name !== 'setup') return;
ctx.after(() => subscribeToTransactionAction());
});

watch(() => user.selectedOrganization, router.back);

watch(feePayer, async newFeePayer => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,12 @@ import { historyTitle, TRANSACTION_ACTION } from '@shared/constants';

import useUserStore from '@renderer/stores/storeUser';
import useNetwork from '@renderer/stores/storeNetwork';
import useWebsocketConnection from '@renderer/stores/storeWebsocketConnection';
import useNextTransactionStore from '@renderer/stores/storeNextTransaction';

import useDisposableWs from '@renderer/composables/useDisposableWs';
import usePersonalPassword from '@renderer/composables/usePersonalPassword';
import useSetDynamicLayout, { LOGGED_IN_LAYOUT } from '@renderer/composables/useSetDynamicLayout';
import useCreateTooltips from '@renderer/composables/useCreateTooltips';
import useWebsocketSubscription from '@renderer/composables/useWebsocketSubscription';

import { areByteArraysEqual } from '@shared/utils/byteUtils';

Expand Down Expand Up @@ -86,15 +85,18 @@ const buttonsDataTestIds: { [key: string]: string } = {
/* Stores */
const user = useUserStore();
const network = useNetwork();
const wsStore = useWebsocketConnection();
const nextTransaction = useNextTransactionStore();
const contacts = useContactsStore();

/* Composables */
const router = useRouter();
const route = useRoute();
const toast = useToast();
const ws = useDisposableWs();
useWebsocketSubscription(TRANSACTION_ACTION, async () => {
const id = router.currentRoute.value.params.id;
await fetchGroup(Array.isArray(id) ? id[0] : id);
setGetTransactionsFunction();
});
useSetDynamicLayout(LOGGED_IN_LAYOUT);
const { getPassword, passwordModalOpened } = usePersonalPassword();
const createTooltips = useCreateTooltips();
Expand Down Expand Up @@ -482,17 +484,11 @@ onBeforeMount(async () => {
return;
}

subscribeToTransactionAction();
await fetchGroup(Array.isArray(id) ? id[0] : id);
setGetTransactionsFunction();
});

/* Watchers */
wsStore.$onAction(ctx => {
if (ctx.name !== 'setup') return;
ctx.after(() => subscribeToTransactionAction());
});

watch(
() => user.selectedOrganization,
() => {
Expand Down Expand Up @@ -576,15 +572,6 @@ const isTransactionInProgress = (transaction: ITransactionFull) => {
].includes(transaction.status);
};

const subscribeToTransactionAction = () => {
if (!user.selectedOrganization?.serverUrl) return;
ws.on(user.selectedOrganization?.serverUrl, TRANSACTION_ACTION, async () => {
const id = router.currentRoute.value.params.id;
await fetchGroup(Array.isArray(id) ? id[0] : id);
setGetTransactionsFunction();
});
};

function setGetTransactionsFunction() {
nextTransaction.setGetTransactionsFunction(async () => {
const transactions = group.value?.groupItems.map(t => t.transaction);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { ITransaction } from '@shared/interfaces';
import type { Transaction } from '@prisma/client';

import { computed, onBeforeMount, reactive, ref, watch } from 'vue';
import { useRouter } from 'vue-router';
import { Prisma } from '@prisma/client';

import { Transaction as SDKTransaction } from '@hashgraph/sdk';
Expand All @@ -13,12 +14,10 @@ import { TRANSACTION_ACTION } from '@shared/constants';
import useUserStore from '@renderer/stores/storeUser';
import useNetworkStore from '@renderer/stores/storeNetwork';
import useNotificationsStore from '@renderer/stores/storeNotifications';
import useWebsocketConnection from '@renderer/stores/storeWebsocketConnection';
import useNextTransactionStore from '@renderer/stores/storeNextTransaction';

import { useRouter } from 'vue-router';
import useDisposableWs from '@renderer/composables/useDisposableWs';
import useMarkNotifications from '@renderer/composables/useMarkNotifications';
import useNextTransactionStore from '@renderer/stores/storeNextTransaction';
import useWebsocketSubscription from '@renderer/composables/useWebsocketSubscription';

import { getTransactions, getTransactionsCount } from '@renderer/services/transactionService';
import { getHistoryTransactions } from '@renderer/services/organization';
Expand Down Expand Up @@ -46,12 +45,11 @@ import TransactionId from '@renderer/components/ui/TransactionId.vue';
const user = useUserStore();
const network = useNetworkStore();
const notifications = useNotificationsStore();
const wsStore = useWebsocketConnection();
const nextTransaction = useNextTransactionStore();

/* Composables */
const router = useRouter();
const ws = useDisposableWs();
useWebsocketSubscription(TRANSACTION_ACTION, fetchTransactions);
const { oldNotifications } = useMarkNotifications([
NotificationType.TRANSACTION_INDICATOR_EXECUTED,
NotificationType.TRANSACTION_INDICATOR_EXPIRED,
Expand Down Expand Up @@ -243,26 +241,13 @@ function setPreviousTransactionsIds(id: string | number) {
}
}

const subscribeToTransactionAction = () => {
if (!user.selectedOrganization?.serverUrl) return;
ws.on(user.selectedOrganization?.serverUrl, TRANSACTION_ACTION, async () => {
await fetchTransactions();
});
};

/* Hooks */
onBeforeMount(async () => {
subscribeToTransactionAction();
setGetTransactionsFunction();
await fetchTransactions();
});

/* Watchers */
wsStore.$onAction(ctx => {
if (ctx.name !== 'setup') return;
ctx.after(() => subscribeToTransactionAction());
});

watch([currentPage, pageSize, () => user.selectedOrganization, orgFilters], async () => {
setGetTransactionsFunction();
await fetchTransactions();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
<script setup lang="ts">
import type { INotificationReceiver } from '@shared/interfaces';

import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
import { useRouter } from 'vue-router';

import useNotificationsStore from '@renderer/stores/storeNotifications.ts';
import useFilterNotifications from '@renderer/composables/useFilterNotifications.ts';
import { getTransactionTypeFromBackendType } from '@renderer/utils/sdk/transactions.ts';
Expand All @@ -23,6 +26,7 @@ const props = defineProps<{
collection: TransactionNodeCollection;
node: ITransactionNode;
index: number;
oldNotifications?: INotificationReceiver[];
}>();

/* Emits */
Expand All @@ -44,33 +48,62 @@ const router = useRouter();
const createTooltips = useCreateTooltips();

/* Computed */
const hasNotifications = computed(() => {
return notificationMonitor.filteredNotifications.value.length > 0;
});
const filteringNotificationType = computed(() => {
let result: NotificationType | null;
const filteringNotificationTypes = computed(() => {
let result: NotificationType[];
switch (props.collection) {
case TransactionNodeCollection.READY_FOR_REVIEW:
result = NotificationType.TRANSACTION_INDICATOR_APPROVE;
result = [NotificationType.TRANSACTION_INDICATOR_APPROVE];
break;
case TransactionNodeCollection.READY_TO_SIGN:
result = NotificationType.TRANSACTION_INDICATOR_SIGN;
result = [NotificationType.TRANSACTION_INDICATOR_SIGN];
break;
case TransactionNodeCollection.READY_FOR_EXECUTION:
result = NotificationType.TRANSACTION_INDICATOR_EXECUTABLE;
result = [NotificationType.TRANSACTION_INDICATOR_EXECUTABLE];
break;
case TransactionNodeCollection.IN_PROGRESS:
case TransactionNodeCollection.HISTORY:
result = null;
result = [
NotificationType.TRANSACTION_INDICATOR_EXECUTED,
NotificationType.TRANSACTION_INDICATOR_EXPIRED,
NotificationType.TRANSACTION_INDICATOR_ARCHIVED,
NotificationType.TRANSACTION_INDICATOR_CANCELLED,
NotificationType.TRANSACTION_INDICATOR_FAILED,
];
break;
case TransactionNodeCollection.IN_PROGRESS:
result = [];
break;
}
return result;
});

const notificationMonitor = useFilterNotifications(
computed(() => props.node),
filteringNotificationType,
filteringNotificationTypes,
);

const hasOldNotifications = computed(() => {
if (!props.oldNotifications || props.oldNotifications.length === 0) {
return false;
}

const notificationTypes = filteringNotificationTypes.value;

if (notificationTypes.length === 0) {
return false;
}

// Check if any old notifications match this node
return props.oldNotifications.some(n => {
const matchesType = notificationTypes.includes(n.notification.type);
const matchesEntity = n.notification.entityId === (props.node.transactionId || props.node.groupId);
return matchesType && matchesEntity;
});
});

const hasNotifications = computed(() => {
return notificationMonitor.filteredNotifications.value.length > 0 || hasOldNotifications.value;
});

const transactionType = computed(() => {
let result: string;
if (props.node.transactionType) {
Expand Down
Loading
Loading