Implemented features: - High-performance Rust extension with Redis persistence - Actor/player management with loadout, position, and state tracking - Banking system with deposit, withdraw, and transfer operations - Physical and virtual garage/locker systems for vehicle and equipment storage - Organization management with member tracking and permissions - Client-side UI with React-like state management - Server-side event-driven architecture with CBA Events - Security: Self-transfer prevention at multiple layers - Logging system with per-module log files - ICOM module for inter-server communication Co-Authored-By: Warp <agent@warp.dev>
263 lines
9.3 KiB
JavaScript
263 lines
9.3 KiB
JavaScript
//=============================================================================
|
|
// #region ACTIONS
|
|
//=============================================================================
|
|
|
|
const NotificationActionTypes = {
|
|
ADD_NOTIFICATION: 'ADD_NOTIFICATION',
|
|
REMOVE_NOTIFICATION: 'REMOVE_NOTIFICATION',
|
|
CLEAR_NOTIFICATIONS: 'CLEAR_NOTIFICATIONS',
|
|
UPDATE_NOTIFICATION: 'UPDATE_NOTIFICATION'
|
|
};
|
|
|
|
const notificationActions = {
|
|
addNotification: (notification) => ({
|
|
type: NotificationActionTypes.ADD_NOTIFICATION,
|
|
payload: {
|
|
id: Date.now() + Math.random(),
|
|
timestamp: Date.now(),
|
|
type: 'info',
|
|
title: 'Notification',
|
|
message: 'Default message',
|
|
duration: 0,
|
|
status: 'showing',
|
|
...notification
|
|
}
|
|
}),
|
|
removeNotification: (id) => ({
|
|
type: NotificationActionTypes.REMOVE_NOTIFICATION,
|
|
payload: { id }
|
|
}),
|
|
clearNotifications: () => ({
|
|
type: NotificationActionTypes.CLEAR_NOTIFICATIONS
|
|
}),
|
|
updateNotification: (id, updates) => ({
|
|
type: NotificationActionTypes.UPDATE_NOTIFICATION,
|
|
payload: { id, updates }
|
|
})
|
|
};
|
|
|
|
//=============================================================================
|
|
// #region REDUCER
|
|
//=============================================================================
|
|
|
|
const notificationInitialState = {
|
|
notifications: [],
|
|
maxNotifications: 5
|
|
};
|
|
|
|
function notificationReducer(state = notificationInitialState, action = {}) {
|
|
switch (action.type) {
|
|
case NotificationActionTypes.ADD_NOTIFICATION: {
|
|
if (!action.payload) return state;
|
|
let newNotifications = [...state.notifications];
|
|
if (newNotifications.length >= state.maxNotifications) {
|
|
newNotifications = newNotifications.slice(1);
|
|
}
|
|
return { ...state, notifications: [...newNotifications, action.payload] };
|
|
}
|
|
case NotificationActionTypes.REMOVE_NOTIFICATION: {
|
|
if (!action.payload || !action.payload.id) return state;
|
|
return {
|
|
...state,
|
|
notifications: state.notifications.filter(n => n.id !== action.payload.id)
|
|
};
|
|
}
|
|
case NotificationActionTypes.CLEAR_NOTIFICATIONS:
|
|
return { ...state, notifications: [] };
|
|
case NotificationActionTypes.UPDATE_NOTIFICATION: {
|
|
if (!action.payload || !action.payload.id || !action.payload.updates) return state;
|
|
return {
|
|
...state,
|
|
notifications: state.notifications.map(n =>
|
|
n.id === action.payload.id ? { ...n, ...action.payload.updates } : n
|
|
)
|
|
};
|
|
}
|
|
default:
|
|
return state;
|
|
}
|
|
}
|
|
|
|
//=============================================================================
|
|
// #region STORE
|
|
//=============================================================================
|
|
|
|
class Store {
|
|
constructor(reducer, initialState) {
|
|
this.reducer = reducer;
|
|
this.state = initialState;
|
|
this.listeners = [];
|
|
}
|
|
|
|
getState() {
|
|
return this.state;
|
|
}
|
|
|
|
dispatch(action) {
|
|
this.state = this.reducer(this.state, action);
|
|
this.listeners.forEach(listener => listener(this.state));
|
|
return action;
|
|
}
|
|
|
|
subscribe(listener) {
|
|
this.listeners.push(listener);
|
|
return () => {
|
|
this.listeners = this.listeners.filter(l => l !== listener);
|
|
};
|
|
}
|
|
}
|
|
|
|
const notificationStore = new Store(notificationReducer, notificationInitialState);
|
|
|
|
//=============================================================================
|
|
// #region SELECTORS
|
|
//=============================================================================
|
|
|
|
const notificationSelectors = {
|
|
getNotifications: (state) => state.notifications,
|
|
getMaxNotifications: (state) => state.maxNotifications
|
|
};
|
|
|
|
//=============================================================================
|
|
// #region UI COMPONENT
|
|
//=============================================================================
|
|
|
|
class NotificationUI {
|
|
constructor(store) {
|
|
this.store = store;
|
|
this.unsubscribe = null;
|
|
this.container = document.getElementById('notification-container');
|
|
this.renderedNotifications = new Map();
|
|
}
|
|
|
|
init() {
|
|
if (!this.container) {
|
|
console.error('Notification container not found');
|
|
return;
|
|
}
|
|
this.unsubscribe = this.store.subscribe((state) => this.render(state));
|
|
this.render(this.store.getState());
|
|
}
|
|
|
|
destroy() {
|
|
if (this.unsubscribe) this.unsubscribe();
|
|
this.renderedNotifications.forEach(el => {
|
|
if (el.parentNode) el.parentNode.removeChild(el);
|
|
});
|
|
this.renderedNotifications.clear();
|
|
}
|
|
|
|
render(state) {
|
|
const notifications = notificationSelectors.getNotifications(state);
|
|
|
|
// Remove notifications no longer present
|
|
const currentIds = new Set(notifications.map(n => n.id));
|
|
for (const [id, el] of this.renderedNotifications.entries()) {
|
|
if (!currentIds.has(id)) {
|
|
if (el.parentNode) el.parentNode.removeChild(el);
|
|
this.renderedNotifications.delete(id);
|
|
}
|
|
}
|
|
|
|
// Add or update notifications
|
|
notifications.forEach(notification => {
|
|
if (!notification || !notification.id) return;
|
|
if (!this.renderedNotifications.has(notification.id)) {
|
|
this.createNotificationElement(notification);
|
|
} else {
|
|
this.updateNotificationElement(notification);
|
|
}
|
|
});
|
|
}
|
|
|
|
createNotificationElement(notification) {
|
|
const el = document.createElement('div');
|
|
el.className = `notification ${notification.type || 'info'}`;
|
|
el.dataset.id = notification.id;
|
|
el.innerHTML = `
|
|
<div class="notification-header">
|
|
<div class="notification-title">${notification.title || 'Notification'}</div>
|
|
</div>
|
|
<div class="notification-message">${notification.message || 'No message'}</div>
|
|
${notification.duration > 0 ? '<div class="notification-progress"><div class="notification-progress-bar"></div></div>' : ''}
|
|
`;
|
|
this.container.appendChild(el);
|
|
this.renderedNotifications.set(notification.id, el);
|
|
setTimeout(() => el.classList.add('show'), 10);
|
|
|
|
// Set progress bar animation duration
|
|
if (notification.duration > 0) {
|
|
const progressBar = el.querySelector('.notification-progress-bar');
|
|
if (progressBar) {
|
|
progressBar.style.transition = `width ${notification.duration}ms linear`;
|
|
progressBar.style.width = '100%';
|
|
setTimeout(() => {
|
|
progressBar.style.width = '0%';
|
|
}, 20);
|
|
}
|
|
|
|
setTimeout(() => {
|
|
notificationStore.dispatch(notificationActions.updateNotification(notification.id, { status: 'hiding' }));
|
|
setTimeout(() => {
|
|
notificationStore.dispatch(notificationActions.removeNotification(notification.id));
|
|
}, 300);
|
|
}, notification.duration);
|
|
}
|
|
}
|
|
|
|
updateNotificationElement(notification) {
|
|
const el = this.renderedNotifications.get(notification.id);
|
|
if (!el) return;
|
|
if (notification.status === 'hiding') el.classList.add('hide');
|
|
}
|
|
}
|
|
|
|
//=============================================================================
|
|
// #region GLOBAL API & EVENT HANDLING
|
|
//=============================================================================
|
|
|
|
let notificationUI = null;
|
|
let notificationUIInitialized = false;
|
|
|
|
function notifyArmaNotificationReady() {
|
|
if (window.parent && window.parent !== window && typeof window.parent.postMessage === "function") {
|
|
window.parent.postMessage({ event: "notifications::ready" }, "*");
|
|
}
|
|
if (typeof A3API !== "undefined" && typeof A3API.SendAlert === "function") {
|
|
A3API.SendAlert(JSON.stringify({ event: "notifications::ready" }));
|
|
}
|
|
}
|
|
|
|
function initializeNotifications() {
|
|
if (notificationUIInitialized) {
|
|
console.log('Notification system already initialized, skipping...');
|
|
return;
|
|
}
|
|
notificationUI = new NotificationUI(notificationStore);
|
|
notificationUI.init();
|
|
notificationUIInitialized = true;
|
|
console.log('Notification system is ready!');
|
|
notifyArmaNotificationReady();
|
|
}
|
|
|
|
// Expose global notification API
|
|
const showNotification = (type, title, message, duration) => {
|
|
return notificationStore.dispatch(notificationActions.addNotification({ type, title, message, duration }));
|
|
};
|
|
const clearAllNotifications = () => {
|
|
return notificationStore.dispatch(notificationActions.clearNotifications());
|
|
};
|
|
|
|
// Listen for global notification events (for Arma/SQF or other scripts)
|
|
window.addEventListener('forge:notify', function (e) {
|
|
if (!e || !e.detail) return;
|
|
const { type, title, message, duration } = e.detail;
|
|
showNotification(type, title, message, duration);
|
|
});
|
|
|
|
// Auto-initialize if DOM is already loaded when script executes
|
|
if (document.readyState !== 'loading') {
|
|
initializeNotifications();
|
|
} else {
|
|
document.addEventListener('DOMContentLoaded', initializeNotifications, { once: true });
|
|
} |