mirror of
https://github.com/basicswap/basicswap.git
synced 2025-11-05 10:28:10 +01:00
GUI: Updated toasts and added notifications history + Various fixes.
This commit is contained in:
@@ -1784,61 +1784,86 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
|
||||
f"Invalid swap type for: {coin_from.name} -> {coin_to.name}"
|
||||
)
|
||||
|
||||
def notify(self, event_type, event_data, cursor=None) -> None:
|
||||
show_event = event_type not in self._disabled_notification_types
|
||||
if event_type == NT.OFFER_RECEIVED:
|
||||
offer_id: bytes = bytes.fromhex(event_data["offer_id"])
|
||||
self.log.debug(f"Received new offer {self.log.id(offer_id)}")
|
||||
if self.ws_server and show_event:
|
||||
event_data["event"] = "new_offer"
|
||||
self.ws_server.send_message_to_all(json.dumps(event_data))
|
||||
elif event_type == NT.BID_RECEIVED:
|
||||
offer_id: bytes = bytes.fromhex(event_data["offer_id"])
|
||||
offer_type: str = event_data["type"]
|
||||
bid_id: bytes = bytes.fromhex(event_data["bid_id"])
|
||||
self.log.info(
|
||||
f"Received valid bid {self.log.id(bid_id)} for {offer_type} offer {self.log.id(offer_id)}"
|
||||
)
|
||||
if self.ws_server and show_event:
|
||||
event_data["event"] = "new_bid"
|
||||
self.ws_server.send_message_to_all(json.dumps(event_data))
|
||||
elif event_type == NT.BID_ACCEPTED:
|
||||
bid_id: bytes = bytes.fromhex(event_data["bid_id"])
|
||||
self.log.info(f"Received valid bid accept for {self.log.id(bid_id)}")
|
||||
if self.ws_server and show_event:
|
||||
event_data["event"] = "bid_accepted"
|
||||
self.ws_server.send_message_to_all(json.dumps(event_data))
|
||||
else:
|
||||
self.log.warning(f"Unknown notification {event_type}")
|
||||
|
||||
def _process_notification_safe(self, event_type, event_data) -> None:
|
||||
try:
|
||||
show_event = event_type not in self._disabled_notification_types
|
||||
if event_type == NT.OFFER_RECEIVED:
|
||||
offer_id: bytes = bytes.fromhex(event_data["offer_id"])
|
||||
self.log.debug(f"Received new offer {self.log.id(offer_id)}")
|
||||
if self.ws_server and show_event:
|
||||
event_data["event"] = "new_offer"
|
||||
self.ws_server.send_message_to_all(json.dumps(event_data))
|
||||
elif event_type == NT.BID_RECEIVED:
|
||||
offer_id: bytes = bytes.fromhex(event_data["offer_id"])
|
||||
offer_type: str = event_data["type"]
|
||||
bid_id: bytes = bytes.fromhex(event_data["bid_id"])
|
||||
self.log.info(
|
||||
f"Received valid bid {self.log.id(bid_id)} for {offer_type} offer {self.log.id(offer_id)}"
|
||||
)
|
||||
if self.ws_server and show_event:
|
||||
event_data["event"] = "new_bid"
|
||||
self.ws_server.send_message_to_all(json.dumps(event_data))
|
||||
elif event_type == NT.BID_ACCEPTED:
|
||||
bid_id: bytes = bytes.fromhex(event_data["bid_id"])
|
||||
self.log.info(f"Received valid bid accept for {self.log.id(bid_id)}")
|
||||
if self.ws_server and show_event:
|
||||
event_data["event"] = "bid_accepted"
|
||||
self.ws_server.send_message_to_all(json.dumps(event_data))
|
||||
elif event_type == NT.SWAP_COMPLETED:
|
||||
bid_id: bytes = bytes.fromhex(event_data["bid_id"])
|
||||
self.log.info(f"Swap completed for bid {self.log.id(bid_id)}")
|
||||
event_data["event"] = "swap_completed"
|
||||
|
||||
if self.ws_server and show_event:
|
||||
self.ws_server.send_message_to_all(json.dumps(event_data))
|
||||
else:
|
||||
self.log.warning(f"Unknown notification {event_type}")
|
||||
|
||||
now: int = self.getTime()
|
||||
use_cursor = self.openDB(cursor)
|
||||
self.add(
|
||||
Notification(
|
||||
active_ind=1,
|
||||
created_at=now,
|
||||
event_type=int(event_type),
|
||||
event_data=bytes(json.dumps(event_data), "UTF-8"),
|
||||
),
|
||||
use_cursor,
|
||||
)
|
||||
use_cursor = self.openDB(None)
|
||||
try:
|
||||
self.add(
|
||||
Notification(
|
||||
active_ind=1,
|
||||
created_at=now,
|
||||
event_type=int(event_type),
|
||||
event_data=bytes(json.dumps(event_data), "UTF-8"),
|
||||
),
|
||||
use_cursor,
|
||||
)
|
||||
|
||||
use_cursor.execute(
|
||||
"DELETE FROM notifications WHERE record_id NOT IN (SELECT record_id FROM notifications WHERE active_ind=1 ORDER BY created_at ASC LIMIT ?)",
|
||||
(self._keep_notifications,),
|
||||
)
|
||||
use_cursor.execute(
|
||||
"DELETE FROM notifications WHERE record_id NOT IN (SELECT record_id FROM notifications WHERE active_ind=1 ORDER BY created_at ASC LIMIT ?)",
|
||||
(self._keep_notifications,),
|
||||
)
|
||||
|
||||
if show_event:
|
||||
self._notifications_cache[now] = (event_type, event_data)
|
||||
while len(self._notifications_cache) > self._show_notifications:
|
||||
# dicts preserve insertion order in Python 3.7+
|
||||
self._notifications_cache.pop(next(iter(self._notifications_cache)))
|
||||
if show_event:
|
||||
self._notifications_cache[now] = (event_type, event_data)
|
||||
while len(self._notifications_cache) > self._show_notifications:
|
||||
# dicts preserve insertion order in Python 3.7+
|
||||
self._notifications_cache.pop(next(iter(self._notifications_cache)))
|
||||
|
||||
finally:
|
||||
if cursor is None:
|
||||
finally:
|
||||
self.closeDB(use_cursor)
|
||||
|
||||
except Exception as ex:
|
||||
self.log.error(
|
||||
f"Notification processing failed for event_type {event_type}: {ex}"
|
||||
)
|
||||
|
||||
def notify(self, event_type, event_data, cursor=None) -> None:
|
||||
"""Submit notification for processing in isolated thread."""
|
||||
try:
|
||||
self.thread_pool.submit(
|
||||
self._process_notification_safe, event_type, event_data
|
||||
)
|
||||
except Exception as ex:
|
||||
self.log.error(f"Failed to submit notification to thread pool: {ex}")
|
||||
try:
|
||||
self._process_notification_safe(event_type, event_data)
|
||||
except Exception as ex2:
|
||||
self.log.error(f"Notification fallback also failed: {ex2}")
|
||||
|
||||
def buildNotificationsCache(self, cursor):
|
||||
self._notifications_cache.clear()
|
||||
q = cursor.execute(
|
||||
@@ -2093,6 +2118,12 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
|
||||
"Rate mismatch.",
|
||||
)
|
||||
|
||||
def validateMessageNets(self, message_nets: str) -> None:
|
||||
try:
|
||||
self.expandMessageNets(message_nets)
|
||||
except Exception as e:
|
||||
raise ValueError(f"Invalid message networks: {e}")
|
||||
|
||||
def ensureWalletCanSend(
|
||||
self, ci, swap_type, ensure_balance: int, estimated_fee: int, for_offer=True
|
||||
) -> None:
|
||||
@@ -6112,6 +6143,7 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
|
||||
bid.setState(BidStates.SWAP_COMPLETED)
|
||||
self.saveBidInSession(bid_id, bid, cursor, xmr_swap)
|
||||
self.commitDB()
|
||||
self.notify(NT.SWAP_COMPLETED, {"bid_id": bid_id.hex()})
|
||||
elif state == BidStates.XMR_SWAP_SCRIPT_TX_PREREFUND:
|
||||
if TxTypes.XMR_SWAP_A_LOCK_REFUND in bid.txns:
|
||||
refund_tx = bid.txns[TxTypes.XMR_SWAP_A_LOCK_REFUND]
|
||||
@@ -6381,6 +6413,18 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
|
||||
|
||||
bid.setState(BidStates.SWAP_COMPLETED)
|
||||
self.saveBid(bid_id, bid)
|
||||
try:
|
||||
self.notify(
|
||||
NT.SWAP_COMPLETED,
|
||||
{
|
||||
"bid_id": bid_id.hex(),
|
||||
},
|
||||
)
|
||||
except Exception as ex:
|
||||
self.log.warning(
|
||||
f"Failed to send swap completion notification: {ex}"
|
||||
)
|
||||
|
||||
return True # Mark bid for archiving
|
||||
|
||||
if save_bid:
|
||||
@@ -6673,6 +6717,17 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
|
||||
|
||||
if not was_received:
|
||||
bid.setState(BidStates.SWAP_COMPLETED)
|
||||
try:
|
||||
self.notify(
|
||||
NT.SWAP_COMPLETED,
|
||||
{
|
||||
"bid_id": bid_id.hex(),
|
||||
},
|
||||
)
|
||||
except Exception as ex:
|
||||
self.log.warning(
|
||||
f"Failed to send swap completion notification: {ex}"
|
||||
)
|
||||
else:
|
||||
# Could already be processed if spend was detected in the mempool
|
||||
self.log.warning(
|
||||
|
||||
@@ -243,6 +243,7 @@ class NotificationTypes(IntEnum):
|
||||
OFFER_RECEIVED = auto()
|
||||
BID_RECEIVED = auto()
|
||||
BID_ACCEPTED = auto()
|
||||
SWAP_COMPLETED = auto()
|
||||
|
||||
|
||||
class ConnectionRequestTypes(IntEnum):
|
||||
|
||||
@@ -7,6 +7,7 @@ const NotificationManager = (function() {
|
||||
showBidAccepted: true,
|
||||
showBalanceChanges: true,
|
||||
showOutgoingTransactions: true,
|
||||
showSwapCompleted: true,
|
||||
notificationDuration: 20000
|
||||
};
|
||||
|
||||
@@ -34,8 +35,166 @@ const NotificationManager = (function() {
|
||||
}
|
||||
|
||||
let config = loadConfig();
|
||||
let notificationHistory = [];
|
||||
const MAX_HISTORY_ITEMS = 10;
|
||||
|
||||
function ensureToastContainer() {
|
||||
function loadNotificationHistory() {
|
||||
try {
|
||||
const saved = localStorage.getItem('notification_history');
|
||||
if (saved) {
|
||||
notificationHistory = JSON.parse(saved);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error loading notification history:', e);
|
||||
notificationHistory = [];
|
||||
}
|
||||
}
|
||||
|
||||
function saveNotificationHistory() {
|
||||
try {
|
||||
localStorage.setItem('notification_history', JSON.stringify(notificationHistory));
|
||||
} catch (e) {
|
||||
console.error('Error saving notification history:', e);
|
||||
}
|
||||
}
|
||||
|
||||
function addToHistory(title, type, options) {
|
||||
const historyItem = {
|
||||
id: Date.now(),
|
||||
title: title,
|
||||
type: type,
|
||||
subtitle: options.subtitle || '',
|
||||
coinSymbol: options.coinSymbol || '',
|
||||
coinFrom: options.coinFrom || null,
|
||||
coinTo: options.coinTo || null,
|
||||
timestamp: new Date().toLocaleString(),
|
||||
timestampMs: Date.now()
|
||||
};
|
||||
|
||||
notificationHistory.unshift(historyItem);
|
||||
|
||||
if (notificationHistory.length > MAX_HISTORY_ITEMS) {
|
||||
notificationHistory = notificationHistory.slice(0, MAX_HISTORY_ITEMS);
|
||||
}
|
||||
|
||||
saveNotificationHistory();
|
||||
updateHistoryDropdown();
|
||||
}
|
||||
|
||||
function updateHistoryDropdown() {
|
||||
const dropdown = document.getElementById('notification-history-dropdown');
|
||||
const mobileDropdown = document.getElementById('notification-history-dropdown-mobile');
|
||||
|
||||
const emptyMessage = '<div class="px-4 py-3 text-sm text-gray-500 dark:text-gray-400">No notifications yet</div>';
|
||||
const emptyMessageMobile = '<div class="px-4 py-3 text-sm text-gray-400">No notifications yet</div>';
|
||||
|
||||
if (notificationHistory.length === 0) {
|
||||
if (dropdown) dropdown.innerHTML = emptyMessage;
|
||||
if (mobileDropdown) mobileDropdown.innerHTML = emptyMessageMobile;
|
||||
return;
|
||||
}
|
||||
|
||||
const clearAllButton = `
|
||||
<div class="px-4 py-2 border-t border-gray-100 dark:border-gray-400 text-center">
|
||||
<button onclick="clearAllNotifications()"
|
||||
class="text-xs text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300">
|
||||
Clear All
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
let historyHTML = '';
|
||||
let mobileHistoryHTML = '';
|
||||
|
||||
notificationHistory.forEach(item => {
|
||||
let coinIconHtml = '';
|
||||
if (item.coinSymbol) {
|
||||
const coinIcon = getCoinIcon(item.coinSymbol);
|
||||
coinIconHtml = `<img src="/static/images/coins/${coinIcon}" class="w-5 h-5 mr-2 flex-shrink-0" alt="${item.coinSymbol}" onerror="this.style.display='none'">`;
|
||||
}
|
||||
|
||||
const typeIcon = getToastIcon(item.type);
|
||||
const typeColor = getToastColor(item.type, item);
|
||||
const typeIconHtml = `<div class="inline-flex flex-shrink-0 justify-center items-center w-8 h-8 ${typeColor} rounded-lg text-white mr-3">${typeIcon}</div>`;
|
||||
|
||||
let enhancedTitle = item.title;
|
||||
if ((item.type === 'new_offer' || item.type === 'new_bid') && item.coinFrom && item.coinTo) {
|
||||
const coinFromIcon = getCoinIcon(getCoinDisplayName(item.coinFrom));
|
||||
const coinToIcon = getCoinIcon(getCoinDisplayName(item.coinTo));
|
||||
const coinFromName = getCoinDisplayName(item.coinFrom);
|
||||
const coinToName = getCoinDisplayName(item.coinTo);
|
||||
|
||||
enhancedTitle = item.title
|
||||
.replace(new RegExp(`(\\d+\\.\\d+)\\s+${coinFromName}`, 'g'), `<img src="/static/images/coins/${coinFromIcon}" class="w-4 h-4 inline mr-1" alt="${coinFromName}" onerror="this.style.display='none'">$1 ${coinFromName}`)
|
||||
.replace(new RegExp(`(\\d+\\.\\d+)\\s+${coinToName}`, 'g'), `<img src="/static/images/coins/${coinToIcon}" class="w-4 h-4 inline mr-1" alt="${coinToName}" onerror="this.style.display='none'">$1 ${coinToName}`);
|
||||
}
|
||||
|
||||
const clickAction = getNotificationClickAction(item);
|
||||
const itemHTML = `
|
||||
<div class="block py-4 px-4 hover:bg-gray-100 dark:hover:bg-gray-700 dark:text-white cursor-pointer transition-colors" ${clickAction ? `onclick="${clickAction}"` : ''}>
|
||||
<div class="flex items-center">
|
||||
${typeIconHtml}
|
||||
${coinIconHtml}
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="text-sm font-medium text-gray-900 dark:text-white break-words">${enhancedTitle}</div>
|
||||
${item.subtitle ? `<div class="text-xs text-gray-500 dark:text-gray-400 break-words">${item.subtitle}</div>` : ''}
|
||||
<div class="text-xs text-gray-400 dark:text-gray-500">${item.timestamp}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
historyHTML += itemHTML;
|
||||
|
||||
const mobileItemHTML = `
|
||||
<div class="block py-4 px-4 hover:bg-gray-700 text-white cursor-pointer transition-colors" ${clickAction ? `onclick="${clickAction}"` : ''}>
|
||||
<div class="flex items-center">
|
||||
${typeIconHtml}
|
||||
${coinIconHtml}
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="text-sm font-medium text-gray-100 break-words">${enhancedTitle}</div>
|
||||
${item.subtitle ? `<div class="text-xs text-gray-300 break-words">${item.subtitle}</div>` : ''}
|
||||
<div class="text-xs text-gray-400">${item.timestamp}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
mobileHistoryHTML += mobileItemHTML;
|
||||
});
|
||||
|
||||
historyHTML += clearAllButton;
|
||||
mobileHistoryHTML += clearAllButton;
|
||||
|
||||
if (dropdown) dropdown.innerHTML = historyHTML;
|
||||
if (mobileDropdown) mobileDropdown.innerHTML = mobileHistoryHTML;
|
||||
}
|
||||
|
||||
function getNotificationClickAction(item) {
|
||||
if (item.type === 'balance_change' && item.coinSymbol) {
|
||||
return `window.location.href='/wallet/${item.coinSymbol.toLowerCase()}'`;
|
||||
}
|
||||
|
||||
if (item.type === 'new_offer') {
|
||||
return `window.location.href='/offers'`;
|
||||
}
|
||||
|
||||
if (item.type === 'new_bid' || item.type === 'bid_accepted') {
|
||||
return `window.location.href='/bids'`;
|
||||
}
|
||||
|
||||
if (item.title.includes('offer') || item.title.includes('Offer')) {
|
||||
return `window.location.href='/offers'`;
|
||||
}
|
||||
|
||||
if (item.title.includes('bid') || item.title.includes('Bid') || item.title.includes('swap') || item.title.includes('Swap')) {
|
||||
return `window.location.href='/bids'`;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function ensureToastContainer() {
|
||||
let container = document.getElementById('ul_updates');
|
||||
if (!container) {
|
||||
const floating_div = document.createElement('div');
|
||||
@@ -66,6 +225,9 @@ const NotificationManager = (function() {
|
||||
'bid_accepted': `<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"></path>
|
||||
</svg>`,
|
||||
'swap_completed': `<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"></path>
|
||||
</svg>`,
|
||||
'balance_change': `<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M4 4a2 2 0 00-2 2v4a2 2 0 002 2V6h10a2 2 0 00-2-2H4zm2 6a2 2 0 012-2h8a2 2 0 012 2v4a2 2 0 01-2 2H8a2 2 0 01-2-2v-4zm6 4a2 2 0 100-4 2 2 0 000 4z" clip-rule="evenodd"></path>
|
||||
</svg>`,
|
||||
@@ -81,6 +243,7 @@ const NotificationManager = (function() {
|
||||
'new_offer': 'bg-blue-500',
|
||||
'new_bid': 'bg-green-500',
|
||||
'bid_accepted': 'bg-purple-500',
|
||||
'swap_completed': 'bg-green-600',
|
||||
'balance_change': 'bg-yellow-500',
|
||||
'success': 'bg-blue-500'
|
||||
};
|
||||
@@ -96,20 +259,67 @@ const NotificationManager = (function() {
|
||||
return colors[type] || colors['success'];
|
||||
}
|
||||
|
||||
// Todo: Remove later and use global.
|
||||
function getCoinDisplayName(coinId) {
|
||||
const coinMap = {
|
||||
1: 'PART',
|
||||
2: 'BTC',
|
||||
3: 'LTC',
|
||||
4: 'DCR',
|
||||
5: 'NMC',
|
||||
6: 'XMR',
|
||||
7: 'PART (Blind)',
|
||||
8: 'PART (Anon)',
|
||||
9: 'WOW',
|
||||
11: 'PIVX',
|
||||
12: 'DASH',
|
||||
13: 'FIRO',
|
||||
14: 'NAV',
|
||||
15: 'LTC (MWEB)',
|
||||
17: 'BCH',
|
||||
18: 'DOGE'
|
||||
};
|
||||
return coinMap[coinId] || `Coin ${coinId}`;
|
||||
}
|
||||
|
||||
// Todo: Remove later.
|
||||
function formatCoinAmount(amount, coinId) {
|
||||
const divisors = {
|
||||
1: 100000000, // PART - 8 decimals
|
||||
2: 100000000, // BTC - 8 decimals
|
||||
3: 100000000, // LTC - 8 decimals
|
||||
4: 100000000, // DCR - 8 decimals
|
||||
5: 100000000, // NMC - 8 decimals
|
||||
6: 1000000000000, // XMR - 12 decimals
|
||||
7: 100000000, // PART (Blind) - 8 decimals
|
||||
8: 100000000, // PART (Anon) - 8 decimals
|
||||
9: 100000000000, // WOW - 11 decimals
|
||||
11: 100000000, // PIVX - 8 decimals
|
||||
12: 100000000, // DASH - 8 decimals
|
||||
13: 100000000, // FIRO - 8 decimals
|
||||
14: 100000000, // NAV - 8 decimals
|
||||
15: 100000000, // LTC (MWEB) - 8 decimals
|
||||
17: 100000000, // BCH - 8 decimals
|
||||
18: 100000000 // DOGE - 8 decimals
|
||||
};
|
||||
|
||||
const divisor = divisors[coinId] || 100000000;
|
||||
const displayAmount = amount / divisor;
|
||||
|
||||
return displayAmount.toFixed(8).replace(/\.?0+$/, '');
|
||||
}
|
||||
|
||||
const publicAPI = {
|
||||
initialize: function(options = {}) {
|
||||
Object.assign(config, options);
|
||||
|
||||
loadNotificationHistory();
|
||||
updateHistoryDropdown();
|
||||
|
||||
this.initializeBalanceTracking();
|
||||
|
||||
if (window.CleanupManager) {
|
||||
window.CleanupManager.registerResource('notificationManager', this, (mgr) => {
|
||||
|
||||
if (this.balanceTimeouts) {
|
||||
Object.values(this.balanceTimeouts).forEach(timeout => clearTimeout(timeout));
|
||||
}
|
||||
console.log('NotificationManager disposed');
|
||||
mgr.dispose();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -121,6 +331,16 @@ const NotificationManager = (function() {
|
||||
return this;
|
||||
},
|
||||
|
||||
getConfig: function() {
|
||||
return { ...config };
|
||||
},
|
||||
|
||||
clearAllNotifications: function() {
|
||||
notificationHistory = [];
|
||||
localStorage.removeItem('notification_history');
|
||||
updateHistoryDropdown();
|
||||
},
|
||||
|
||||
getSettings: function() {
|
||||
return { ...config };
|
||||
},
|
||||
@@ -170,24 +390,45 @@ const NotificationManager = (function() {
|
||||
|
||||
setTimeout(() => {
|
||||
this.createToast(
|
||||
'New network offer',
|
||||
'<img src="/static/images/coins/bitcoin.svg" class="w-4 h-4 inline mr-1" alt="BTC" onerror="this.style.display=\'none\'">1.00000000 BTC → <img src="/static/images/coins/monero.svg" class="w-4 h-4 inline mr-1" alt="XMR" onerror="this.style.display=\'none\'">15.50000000 XMR',
|
||||
'new_offer',
|
||||
{ offerId: '000000006873f4ef17d4f220730400f4fdd57157492289c5d414ea66', subtitle: 'Click to view offer' }
|
||||
{
|
||||
offerId: '000000006873f4ef17d4f220730400f4fdd57157492289c5d414ea66',
|
||||
subtitle: 'New offer • Rate: 1 BTC = 15.50000000 XMR',
|
||||
coinFrom: 2,
|
||||
coinTo: 6
|
||||
}
|
||||
);
|
||||
}, 3000);
|
||||
|
||||
setTimeout(() => {
|
||||
this.createToast(
|
||||
'New bid received',
|
||||
'<img src="/static/images/coins/bitcoin.svg" class="w-4 h-4 inline mr-1" alt="BTC" onerror="this.style.display=\'none\'">0.50000000 BTC → <img src="/static/images/coins/monero.svg" class="w-4 h-4 inline mr-1" alt="XMR" onerror="this.style.display=\'none\'">7.75000000 XMR',
|
||||
'new_bid',
|
||||
{ bidId: '000000006873f4ef17d4f220730400f4fdd57157492289c5d414ea66', subtitle: 'Click to view bid' }
|
||||
{
|
||||
bidId: '000000006873f4ef17d4f220730400f4fdd57157492289c5d414ea66',
|
||||
subtitle: 'New bid • Rate: 1 BTC = 15.50000000 XMR',
|
||||
coinFrom: 2,
|
||||
coinTo: 6
|
||||
}
|
||||
);
|
||||
}, 3500);
|
||||
|
||||
setTimeout(() => {
|
||||
this.createToast(
|
||||
'Swap completed successfully',
|
||||
'swap_completed',
|
||||
{
|
||||
bidId: '000000006873f4ef17d4f220730400f4fdd57157492289c5d414ea66',
|
||||
subtitle: 'Click to view details'
|
||||
}
|
||||
);
|
||||
}, 4000);
|
||||
|
||||
},
|
||||
|
||||
|
||||
|
||||
initializeBalanceTracking: function() {
|
||||
this.checkAndResetStaleBalanceTracking();
|
||||
|
||||
fetch('/json/walletbalances')
|
||||
.then(response => response.json())
|
||||
@@ -201,14 +442,17 @@ const NotificationManager = (function() {
|
||||
const storageKey = `prev_balance_${coinKey}`;
|
||||
const pendingStorageKey = `prev_pending_${coinKey}`;
|
||||
|
||||
|
||||
if (!localStorage.getItem(storageKey)) {
|
||||
localStorage.setItem(storageKey, balance.toString());
|
||||
localStorage.setItem(`${storageKey}_timestamp`, Date.now().toString());
|
||||
}
|
||||
if (!localStorage.getItem(pendingStorageKey)) {
|
||||
localStorage.setItem(pendingStorageKey, pending.toString());
|
||||
localStorage.setItem(`${pendingStorageKey}_timestamp`, Date.now().toString());
|
||||
}
|
||||
});
|
||||
|
||||
localStorage.setItem('last_balance_fetch', Date.now().toString());
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
@@ -216,14 +460,50 @@ const NotificationManager = (function() {
|
||||
});
|
||||
},
|
||||
|
||||
checkAndResetStaleBalanceTracking: function() {
|
||||
const lastFetch = localStorage.getItem('last_balance_fetch');
|
||||
const now = Date.now();
|
||||
const staleThreshold = 10 * 60 * 1000;
|
||||
|
||||
if (!lastFetch || (now - parseInt(lastFetch)) > staleThreshold) {
|
||||
console.log('Resetting stale balance tracking to prevent false notifications');
|
||||
this.resetBalanceTracking();
|
||||
}
|
||||
},
|
||||
|
||||
resetBalanceTracking: function() {
|
||||
const keys = Object.keys(localStorage);
|
||||
keys.forEach(key => {
|
||||
if (key.startsWith('prev_balance_') || key.startsWith('prev_pending_') || key.startsWith('last_notification_') || key.startsWith('balance_change_')) {
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
getNotificationHistory: function() {
|
||||
return notificationHistory;
|
||||
},
|
||||
|
||||
clearNotificationHistory: function() {
|
||||
notificationHistory = [];
|
||||
localStorage.removeItem('notification_history');
|
||||
updateHistoryDropdown();
|
||||
},
|
||||
|
||||
updateHistoryDropdown: function() {
|
||||
updateHistoryDropdown();
|
||||
},
|
||||
|
||||
createToast: function(title, type = 'success', options = {}) {
|
||||
const plainTitle = title.replace(/<[^>]*>/g, '');
|
||||
addToHistory(plainTitle, type, options);
|
||||
|
||||
const messages = ensureToastContainer();
|
||||
const message = document.createElement('li');
|
||||
const toastId = `toast-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
const toastId = `toast-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
|
||||
const iconColor = getToastColor(type, options);
|
||||
const icon = getToastIcon(type);
|
||||
|
||||
|
||||
let coinIconHtml = '';
|
||||
if (options.coinSymbol) {
|
||||
const coinIcon = getCoinIcon(options.coinSymbol);
|
||||
@@ -278,7 +558,6 @@ const NotificationManager = (function() {
|
||||
`;
|
||||
messages.appendChild(message);
|
||||
|
||||
|
||||
setTimeout(() => {
|
||||
if (message.parentNode) {
|
||||
message.classList.add('toast-slide-out');
|
||||
@@ -286,6 +565,7 @@ const NotificationManager = (function() {
|
||||
if (message.parentNode) {
|
||||
message.parentNode.removeChild(message);
|
||||
}
|
||||
|
||||
}, 300);
|
||||
}
|
||||
}, config.notificationDuration);
|
||||
@@ -298,16 +578,42 @@ const NotificationManager = (function() {
|
||||
|
||||
switch (data.event) {
|
||||
case 'new_offer':
|
||||
toastTitle = `New network offer`;
|
||||
if (data.coin_from && data.coin_to && data.amount_from && data.amount_to) {
|
||||
const coinFromName = getCoinDisplayName(data.coin_from);
|
||||
const coinToName = getCoinDisplayName(data.coin_to);
|
||||
const amountFrom = formatCoinAmount(data.amount_from, data.coin_from);
|
||||
const amountTo = formatCoinAmount(data.amount_to, data.coin_to);
|
||||
const coinFromIcon = getCoinIcon(coinFromName);
|
||||
const coinToIcon = getCoinIcon(coinToName);
|
||||
toastTitle = `<img src="/static/images/coins/${coinFromIcon}" class="w-4 h-4 inline mr-1" alt="${coinFromName}" onerror="this.style.display='none'">${amountFrom} ${coinFromName} → <img src="/static/images/coins/${coinToIcon}" class="w-4 h-4 inline mr-1" alt="${coinToName}" onerror="this.style.display='none'">${amountTo} ${coinToName}`;
|
||||
toastOptions.subtitle = `New offer • Rate: 1 ${coinFromName} = ${(data.amount_to / data.amount_from).toFixed(8)} ${coinToName}`;
|
||||
toastOptions.coinFrom = data.coin_from;
|
||||
toastOptions.coinTo = data.coin_to;
|
||||
} else {
|
||||
toastTitle = `New network offer`;
|
||||
toastOptions.subtitle = 'Click to view offer';
|
||||
}
|
||||
toastType = 'new_offer';
|
||||
toastOptions.offerId = data.offer_id;
|
||||
toastOptions.subtitle = 'Click to view offer';
|
||||
shouldShowToast = config.showNewOffers;
|
||||
break;
|
||||
case 'new_bid':
|
||||
toastTitle = `New bid received`;
|
||||
if (data.coin_from && data.coin_to && data.bid_amount && data.bid_amount_to) {
|
||||
const coinFromName = getCoinDisplayName(data.coin_from);
|
||||
const coinToName = getCoinDisplayName(data.coin_to);
|
||||
const bidAmountFrom = formatCoinAmount(data.bid_amount, data.coin_from);
|
||||
const bidAmountTo = formatCoinAmount(data.bid_amount_to, data.coin_to);
|
||||
const coinFromIcon = getCoinIcon(coinFromName);
|
||||
const coinToIcon = getCoinIcon(coinToName);
|
||||
toastTitle = `<img src="/static/images/coins/${coinFromIcon}" class="w-4 h-4 inline mr-1" alt="${coinFromName}" onerror="this.style.display='none'">${bidAmountFrom} ${coinFromName} → <img src="/static/images/coins/${coinToIcon}" class="w-4 h-4 inline mr-1" alt="${coinToName}" onerror="this.style.display='none'">${bidAmountTo} ${coinToName}`;
|
||||
toastOptions.subtitle = `New bid • Rate: 1 ${coinFromName} = ${(data.bid_amount_to / data.bid_amount).toFixed(8)} ${coinToName}`;
|
||||
toastOptions.coinFrom = data.coin_from;
|
||||
toastOptions.coinTo = data.coin_to;
|
||||
} else {
|
||||
toastTitle = `New bid received`;
|
||||
toastOptions.subtitle = 'Click to view bid';
|
||||
}
|
||||
toastOptions.bidId = data.bid_id;
|
||||
toastOptions.subtitle = 'Click to view bid';
|
||||
toastType = 'new_bid';
|
||||
shouldShowToast = config.showNewBids;
|
||||
break;
|
||||
@@ -318,6 +624,15 @@ const NotificationManager = (function() {
|
||||
toastType = 'bid_accepted';
|
||||
shouldShowToast = config.showBidAccepted;
|
||||
break;
|
||||
|
||||
case 'swap_completed':
|
||||
toastTitle = `Swap completed successfully`;
|
||||
toastOptions.bidId = data.bid_id;
|
||||
toastOptions.subtitle = 'Click to view details';
|
||||
toastType = 'swap_completed';
|
||||
shouldShowToast = config.showSwapCompleted;
|
||||
break;
|
||||
|
||||
case 'coin_balance_updated':
|
||||
if (data.coin && config.showBalanceChanges) {
|
||||
this.handleBalanceUpdate(data);
|
||||
@@ -350,7 +665,6 @@ const NotificationManager = (function() {
|
||||
},
|
||||
|
||||
fetchAndShowBalanceChange: function(coinSymbol) {
|
||||
|
||||
fetch('/json/walletbalances')
|
||||
.then(response => response.json())
|
||||
.then(balanceData => {
|
||||
@@ -371,6 +685,8 @@ const NotificationManager = (function() {
|
||||
coinsToCheck.forEach(coinData => {
|
||||
this.checkSingleCoinBalance(coinData, coinSymbol);
|
||||
});
|
||||
|
||||
localStorage.setItem('last_balance_fetch', Date.now().toString());
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
@@ -385,15 +701,48 @@ const NotificationManager = (function() {
|
||||
const coinKey = coinData.name.replace(/\s+/g, '_');
|
||||
const storageKey = `prev_balance_${coinKey}`;
|
||||
const pendingStorageKey = `prev_pending_${coinKey}`;
|
||||
const lastNotificationKey = `last_notification_${coinKey}`;
|
||||
|
||||
const prevBalance = parseFloat(localStorage.getItem(storageKey)) || 0;
|
||||
const prevPending = parseFloat(localStorage.getItem(pendingStorageKey)) || 0;
|
||||
|
||||
const lastNotificationTime = parseInt(localStorage.getItem(lastNotificationKey)) || 0;
|
||||
|
||||
const balanceIncrease = currentBalance - prevBalance;
|
||||
const pendingIncrease = currentPending - prevPending;
|
||||
const pendingDecrease = prevPending - currentPending;
|
||||
|
||||
|
||||
|
||||
const totalChange = Math.abs(balanceIncrease) + Math.abs(pendingIncrease);
|
||||
const maxReasonableChange = Math.max(currentBalance, prevBalance) * 0.5;
|
||||
|
||||
if (totalChange > maxReasonableChange && totalChange > 1.0) {
|
||||
console.log(`Detected potentially stale balance data for ${coinData.name}, resetting tracking`);
|
||||
localStorage.setItem(storageKey, currentBalance.toString());
|
||||
localStorage.setItem(pendingStorageKey, currentPending.toString());
|
||||
return;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const minTimeBetweenNotifications = 30000;
|
||||
const balanceChangeKey = `balance_change_${coinKey}`;
|
||||
const lastBalanceChange = localStorage.getItem(balanceChangeKey);
|
||||
|
||||
const currentChangeSignature = `${currentBalance}_${currentPending}`;
|
||||
|
||||
if (lastBalanceChange === currentChangeSignature) {
|
||||
localStorage.setItem(storageKey, currentBalance.toString());
|
||||
localStorage.setItem(pendingStorageKey, currentPending.toString());
|
||||
return;
|
||||
}
|
||||
|
||||
if (now - lastNotificationTime < minTimeBetweenNotifications) {
|
||||
localStorage.setItem(storageKey, currentBalance.toString());
|
||||
localStorage.setItem(pendingStorageKey, currentPending.toString());
|
||||
localStorage.setItem(balanceChangeKey, currentChangeSignature);
|
||||
return;
|
||||
}
|
||||
|
||||
const isPendingToConfirmed = pendingDecrease > 0.00000001 && balanceIncrease > 0.00000001;
|
||||
|
||||
|
||||
@@ -408,7 +757,9 @@ const NotificationManager = (function() {
|
||||
variantInfo = ` (${coinData.name.replace('Litecoin ', '')})`;
|
||||
}
|
||||
|
||||
if (balanceIncrease > 0.00000001) {
|
||||
let notificationShown = false;
|
||||
|
||||
if (balanceIncrease > 0.00000001 && config.showBalanceChanges) {
|
||||
const displayAmount = balanceIncrease.toFixed(8).replace(/\.?0+$/, '');
|
||||
const subtitle = isPendingToConfirmed ? 'Funds confirmed' : 'Incoming funds confirmed';
|
||||
this.createToast(
|
||||
@@ -419,6 +770,7 @@ const NotificationManager = (function() {
|
||||
subtitle: subtitle
|
||||
}
|
||||
);
|
||||
notificationShown = true;
|
||||
}
|
||||
|
||||
if (balanceIncrease < -0.00000001 && config.showOutgoingTransactions) {
|
||||
@@ -431,6 +783,7 @@ const NotificationManager = (function() {
|
||||
subtitle: 'Funds sent'
|
||||
}
|
||||
);
|
||||
notificationShown = true;
|
||||
}
|
||||
|
||||
if (pendingIncrease > 0.00000001) {
|
||||
@@ -443,6 +796,7 @@ const NotificationManager = (function() {
|
||||
subtitle: 'Incoming funds pending'
|
||||
}
|
||||
);
|
||||
notificationShown = true;
|
||||
}
|
||||
|
||||
if (pendingIncrease < -0.00000001 && config.showOutgoingTransactions && !isPendingToConfirmed) {
|
||||
@@ -455,9 +809,9 @@ const NotificationManager = (function() {
|
||||
subtitle: 'Funds sending'
|
||||
}
|
||||
);
|
||||
notificationShown = true;
|
||||
}
|
||||
|
||||
|
||||
if (pendingDecrease > 0.00000001 && !isPendingToConfirmed) {
|
||||
const displayAmount = pendingDecrease.toFixed(8).replace(/\.?0+$/, '');
|
||||
this.createToast(
|
||||
@@ -468,11 +822,16 @@ const NotificationManager = (function() {
|
||||
subtitle: 'Pending funds confirmed'
|
||||
}
|
||||
);
|
||||
notificationShown = true;
|
||||
}
|
||||
|
||||
|
||||
localStorage.setItem(storageKey, currentBalance.toString());
|
||||
localStorage.setItem(pendingStorageKey, currentPending.toString());
|
||||
localStorage.setItem(balanceChangeKey, currentChangeSignature);
|
||||
|
||||
if (notificationShown) {
|
||||
localStorage.setItem(lastNotificationKey, now.toString());
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -480,6 +839,20 @@ const NotificationManager = (function() {
|
||||
updateConfig: function(newConfig) {
|
||||
Object.assign(config, newConfig);
|
||||
return this;
|
||||
},
|
||||
|
||||
manualResetBalanceTracking: function() {
|
||||
this.resetBalanceTracking();
|
||||
this.initializeBalanceTracking();
|
||||
},
|
||||
|
||||
dispose: function() {
|
||||
if (this.balanceTimeouts) {
|
||||
Object.values(this.balanceTimeouts).forEach(timeout => {
|
||||
clearTimeout(timeout);
|
||||
});
|
||||
this.balanceTimeouts = {};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -488,7 +861,9 @@ const NotificationManager = (function() {
|
||||
while (element.nodeName !== "BUTTON") {
|
||||
element = element.parentNode;
|
||||
}
|
||||
element.parentNode.parentNode.removeChild(element.parentNode);
|
||||
const toastElement = element.parentNode;
|
||||
|
||||
toastElement.parentNode.removeChild(toastElement);
|
||||
};
|
||||
|
||||
return publicAPI;
|
||||
@@ -496,13 +871,21 @@ const NotificationManager = (function() {
|
||||
|
||||
window.NotificationManager = NotificationManager;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
window.resetBalanceTracking = function() {
|
||||
if (window.NotificationManager && window.NotificationManager.manualResetBalanceTracking) {
|
||||
window.NotificationManager.manualResetBalanceTracking();
|
||||
}
|
||||
};
|
||||
|
||||
window.testNotification = function() {
|
||||
if (window.NotificationManager) {
|
||||
window.NotificationManager.createToast('Test Notification', 'success', { subtitle: 'This is a test notification' });
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
if (!window.notificationManagerInitialized) {
|
||||
window.NotificationManager.initialize(window.notificationConfig || {});
|
||||
window.notificationManagerInitialized = true;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
console.log('NotificationManager initialized');
|
||||
|
||||
@@ -45,6 +45,32 @@
|
||||
};
|
||||
})();
|
||||
|
||||
function toggleNotificationDropdown(event) {
|
||||
if (event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
const dropdown = document.getElementById('notification-history-dropdown');
|
||||
const mobileDropdown = document.getElementById('notification-history-dropdown-mobile');
|
||||
|
||||
if (window.innerWidth >= 768) {
|
||||
if (dropdown) {
|
||||
dropdown.classList.toggle('hidden');
|
||||
}
|
||||
} else {
|
||||
if (mobileDropdown) {
|
||||
mobileDropdown.classList.toggle('hidden');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function clearAllNotifications() {
|
||||
if (window.NotificationManager && typeof window.NotificationManager.clearAllNotifications === 'function') {
|
||||
window.NotificationManager.clearAllNotifications();
|
||||
}
|
||||
}
|
||||
|
||||
(function() {
|
||||
const isDarkMode = localStorage.getItem('color-theme') === 'dark' ||
|
||||
(!localStorage.getItem('color-theme') && window.matchMedia('(prefers-color-scheme: dark)').matches);
|
||||
@@ -159,11 +185,10 @@
|
||||
|
||||
<!-- Place New Offer -->
|
||||
<li>
|
||||
<a class="flex rounded-full flex-wrap justify-center w-full px-4 py-2.5 bg-blue-500
|
||||
hover:bg-green-600 hover:border-green-600 font-medium text-sm text-white border
|
||||
border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none" href="/newoffer">
|
||||
<a class="flex mr-10 items-center py-2.5 text-gray-50 hover:text-gray-100 text-sm"
|
||||
href="/newoffer">
|
||||
{{ new_offer_svg | safe }}
|
||||
<span>Place new Offer</span>
|
||||
<span>Place New Offer</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -173,7 +198,7 @@
|
||||
<li>
|
||||
<a href="/donation" class="flex items-center py-2 pr-4 pl-3 text-gray-50 text-sm hover:text-gray-100">
|
||||
{{ love_svg | safe }}
|
||||
<span class="ml-2">Donate</span>
|
||||
<span class="ml-2">Donations</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -398,7 +423,18 @@
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
<!-- Theme Toggle -->
|
||||
<div class="relative">
|
||||
<button id="notification-history-button" onclick="toggleNotificationDropdown(event)" type="button"
|
||||
class="relative text-green-500 dark:text-green-400 focus:outline-none rounded-lg text-sm ml-5 flex items-center justify-center">
|
||||
{{ notifications_svg | safe }}
|
||||
</button>
|
||||
<div id="notification-history-dropdown" class="absolute left-1/2 transform -translate-x-1/2 top-full mt-2 z-50 hidden bg-white divide-y divide-gray-100 shadow w-80 dark:bg-gray-500 dark:divide-gray-400">
|
||||
<div class="px-4 py-3 text-sm text-gray-500 dark:text-gray-400">
|
||||
No notifications yet
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button data-tooltip-target="tooltip-darkmode" id="theme-toggle" type="button"
|
||||
class="text-gray-500 dark:text-gray-400 focus:outline-none rounded-lg text-sm ml-5">
|
||||
{{ sun_svg | safe }}
|
||||
@@ -585,9 +621,21 @@
|
||||
<a class="flex items-center pl-3 py-3 pr-4 text-gray-50 hover:bg-gray-900 rounded"
|
||||
href="/donation">
|
||||
{{ mobile_love_svg | safe }}
|
||||
<span>Donate</span>
|
||||
<span>Donations</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<button id="notification-history-button-mobile" onclick="toggleNotificationDropdown(event)" ontouchstart="toggleNotificationDropdown(event)"
|
||||
class="flex items-center pl-3 py-3 pr-4 text-gray-50 hover:bg-gray-900 rounded w-full text-left focus:outline-none">
|
||||
{{ notifications_svg | safe }}
|
||||
<span>Notifications</span>
|
||||
</button>
|
||||
<div id="notification-history-dropdown-mobile" class="hidden bg-gray-500 rounded-lg shadow w-full mt-2">
|
||||
<div class="px-4 py-3 text-sm text-gray-400">
|
||||
No notifications yet
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
|
||||
@@ -767,3 +815,34 @@
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
function testNotificationSystem() {
|
||||
if (window.NotificationManager) {
|
||||
window.NotificationManager.createToast('Test Withdrawal', 'balance_change', {
|
||||
coinSymbol: 'PART',
|
||||
subtitle: 'Funds sent'
|
||||
});
|
||||
console.log('Test notification created');
|
||||
} else {
|
||||
console.error('NotificationManager not available');
|
||||
}
|
||||
}
|
||||
|
||||
function handleOutsideClick(event) {
|
||||
const button = document.getElementById('notification-history-button');
|
||||
const mobileButton = document.getElementById('notification-history-button-mobile');
|
||||
const dropdown = document.getElementById('notification-history-dropdown');
|
||||
const mobileDropdown = document.getElementById('notification-history-dropdown-mobile');
|
||||
|
||||
if (dropdown && !dropdown.contains(event.target) && button && !button.contains(event.target)) {
|
||||
dropdown.classList.add('hidden');
|
||||
}
|
||||
if (mobileDropdown && !mobileDropdown.contains(event.target) && mobileButton && !mobileButton.contains(event.target)) {
|
||||
mobileDropdown.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('click', handleOutsideClick);
|
||||
document.addEventListener('touchstart', handleOutsideClick);
|
||||
</script>
|
||||
|
||||
@@ -495,6 +495,16 @@
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 ml-7 mt-1">Show notifications for sent funds</p>
|
||||
</div>
|
||||
|
||||
<div class="py-2">
|
||||
<div class="flex items-center">
|
||||
<input type="checkbox" id="notifications_swap_completed" name="notifications_swap_completed" value="true" class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-600 dark:border-gray-500"{% if notification_settings.notifications_swap_completed %} checked{% endif %}>
|
||||
<label for="notifications_swap_completed" class="ml-3 text-sm font-medium text-gray-700 dark:text-gray-300">Swap Completed</label>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 ml-7 mt-1">Show notifications when swaps complete successfully</p>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -553,13 +563,13 @@
|
||||
<script>
|
||||
function syncNotificationSettings() {
|
||||
if (window.NotificationManager && typeof window.NotificationManager.updateSettings === 'function') {
|
||||
|
||||
const backendSettings = {
|
||||
showNewOffers: document.getElementById('notifications_new_offers').checked,
|
||||
showNewBids: document.getElementById('notifications_new_bids').checked,
|
||||
showBidAccepted: document.getElementById('notifications_bid_accepted').checked,
|
||||
showBalanceChanges: document.getElementById('notifications_balance_changes').checked,
|
||||
showOutgoingTransactions: document.getElementById('notifications_outgoing_transactions').checked,
|
||||
showSwapCompleted: document.getElementById('notifications_swap_completed').checked,
|
||||
notificationDuration: parseInt(document.getElementById('notifications_duration').value) * 1000
|
||||
};
|
||||
|
||||
|
||||
@@ -150,13 +150,13 @@
|
||||
' %}
|
||||
{% set new_offer_svg = '
|
||||
<svg class="text-gray-500 w-5 h-5 mr-2" xmlns="http://www.w3.org/2000/svg" height="18" width="18" viewBox="0 0 24 24">
|
||||
<g stroke-linecap="round" stroke-width="2" fill="none" stroke="#ffffff" stroke-linejoin="round">
|
||||
<g stroke-linecap="round" stroke-width="2" fill="none" stroke="#6b7280" stroke-linejoin="round">
|
||||
<circle cx="5" cy="5" r="4"></circle>
|
||||
<circle cx="19" cy="19" r="4"></circle>
|
||||
<polyline data-cap="butt" points="13,5 21,5 21,11 " stroke="#ffffff"></polyline>
|
||||
<polyline data-cap="butt" points="11,19 3,19 3,13 " stroke="#ffffff"></polyline>
|
||||
<polyline points=" 16,2 13,5 16,8 " stroke="#ffffff"></polyline>
|
||||
<polyline points=" 8,16 11,19 8,22 " stroke="#ffffff"></polyline>
|
||||
<polyline data-cap="butt" points="13,5 21,5 21,11 " stroke="#6b7280"></polyline>
|
||||
<polyline data-cap="butt" points="11,19 3,19 3,13 " stroke="#6b7280"></polyline>
|
||||
<polyline points=" 16,2 13,5 16,8 " stroke="#6b7280"></polyline>
|
||||
<polyline points=" 8,16 11,19 8,22 " stroke="#6b7280"></polyline>
|
||||
</g>
|
||||
</svg>
|
||||
' %}
|
||||
@@ -273,8 +273,11 @@
|
||||
</svg>
|
||||
' %}
|
||||
{% set notifications_svg = '
|
||||
<svg class="h-5 w-5" viewBox="0 0 16 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14 11.18V8C13.9986 6.58312 13.4958 5.21247 12.5806 4.13077C11.6655 3.04908 10.3971 2.32615 9 2.09V1C9 0.734784 8.89464 0.48043 8.70711 0.292893C8.51957 0.105357 8.26522 0 8 0C7.73478 0 7.48043 0.105357 7.29289 0.292893C7.10536 0.48043 7 0.734784 7 1V2.09C5.60294 2.32615 4.33452 3.04908 3.41939 4.13077C2.50425 5.21247 2.00144 6.58312 2 8V11.18C1.41645 11.3863 0.910998 11.7681 0.552938 12.2729C0.194879 12.7778 0.00173951 13.3811 0 14V16C0 16.2652 0.105357 16.5196 0.292893 16.7071C0.48043 16.8946 0.734784 17 1 17H4.14C4.37028 17.8474 4.873 18.5954 5.5706 19.1287C6.26819 19.6621 7.1219 19.951 8 19.951C8.8781 19.951 9.73181 19.6621 10.4294 19.1287C11.127 18.5954 11.6297 17.8474 11.86 17H15C15.2652 17 15.5196 16.8946 15.7071 16.7071C15.8946 16.5196 16 16.2652 16 16V14C15.9983 13.3811 15.8051 12.7778 15.4471 12.2729C15.089 11.7681 14.5835 11.3863 14 11.18ZM4 8C4 6.93913 4.42143 5.92172 5.17157 5.17157C5.92172 4.42143 6.93913 4 8 4C9.06087 4 10.0783 4.42143 10.8284 5.17157C11.5786 5.92172 12 6.93913 12 8V11H4V8ZM8 18C7.65097 17.9979 7.30857 17.9045 7.00683 17.7291C6.70509 17.5536 6.45451 17.3023 6.28 17H9.72C9.54549 17.3023 9.29491 17.5536 8.99317 17.7291C8.69143 17.9045 8.34903 17.9979 8 18ZM14 15H2V14C2 13.7348 2.10536 13.4804 2.29289 13.2929C2.48043 13.1054 2.73478 13 3 13H13C13.2652 13 13.5196 13.1054 13.7071 13.2929C13.8946 13.4804 14 13.7348 14 14V15Z" fill="#6b7280"></path>
|
||||
<svg class="h-5 w-5 mr-2" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g stroke-linecap="round" stroke-width="2" fill="none" stroke="#2ad167" stroke-linejoin="round">
|
||||
<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"></path>
|
||||
<path d="M13.73 21a2 2 0 0 1-3.46 0"></path>
|
||||
</g>
|
||||
</svg>
|
||||
' %}
|
||||
{% set debug_nerd_svg = '
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{% include 'header.html' %}
|
||||
{% from 'style.html' import select_box_arrow_svg, select_box_class, circular_arrows_svg, circular_error_svg, circular_info_svg, cross_close_svg, breadcrumb_line_svg, withdraw_svg, utxo_groups_svg, create_utxo_svg, red_cross_close_svg, blue_cross_close_svg, circular_update_messages_svg, circular_error_messages_svg %}
|
||||
{% from 'style.html' import select_box_arrow_svg, select_box_class, circular_arrows_svg, circular_error_svg, circular_info_svg, cross_close_svg, breadcrumb_line_svg, withdraw_svg, utxo_groups_svg, create_utxo_svg, red_cross_close_svg, blue_cross_close_svg, circular_update_messages_svg, circular_error_messages_svg, love_svg %}
|
||||
<script src="/static/js/libs//qrcode.js"></script>
|
||||
|
||||
<section class="py-3 px-4 mt-6">
|
||||
@@ -580,6 +580,28 @@ function copyToClipboardFallback(text) {
|
||||
|
||||
document.body.removeChild(textArea);
|
||||
}
|
||||
|
||||
function fillDonationAddress(address, type) {
|
||||
const addressInput = document.querySelector('input[name^="to_"]');
|
||||
if (addressInput) {
|
||||
addressInput.value = address;
|
||||
addressInput.focus();
|
||||
|
||||
const typeIndicator = document.getElementById('donation-type-indicator');
|
||||
if (typeIndicator && type) {
|
||||
typeIndicator.textContent = type;
|
||||
}
|
||||
|
||||
const withdrawTypeSelect = document.getElementById('withdraw_type');
|
||||
if (withdrawTypeSelect && type) {
|
||||
if (type === 'LTC') {
|
||||
withdrawTypeSelect.value = 'plain';
|
||||
} else if (type === 'MWEB') {
|
||||
withdrawTypeSelect.value = 'mweb';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<section class="p-6">
|
||||
@@ -643,6 +665,54 @@ function copyToClipboardFallback(text) {
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if donation_info %}
|
||||
<tr>
|
||||
<td colspan="2" class="py-3 px-6">
|
||||
<div class="p-4 bg-coolGray-100 dark:bg-gray-500 border border-coolGray-200 dark:border-gray-400 rounded-lg">
|
||||
<div class="flex items-start">
|
||||
<div class="flex-shrink-0 mt-1">
|
||||
{{ love_svg | safe }}
|
||||
</div>
|
||||
<div class="ml-3 flex-1">
|
||||
<h4 class="text-sm font-semibold text-gray-900 dark:text-white mb-2">Support BasicSwap Development!</h4>
|
||||
<p class="text-sm text-gray-700 dark:text-gray-200 mb-3">
|
||||
Consider donating {{ donation_info.coin_name }} to help fund development.
|
||||
</p>
|
||||
|
||||
{% if donation_info.mweb_address %}
|
||||
<div class="space-y-2">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button type="button" onclick="fillDonationAddress('{{ donation_info.address }}', 'LTC')"
|
||||
class="px-3 py-1 bg-blue-500 hover:bg-blue-600 text-white text-xs rounded-md transition-colors">
|
||||
Use LTC Address
|
||||
</button>
|
||||
<button type="button" onclick="fillDonationAddress('{{ donation_info.mweb_address }}', 'MWEB')"
|
||||
class="px-3 py-1 bg-purple-500 hover:bg-purple-600 text-white text-xs rounded-md transition-colors">
|
||||
Use MWEB Address
|
||||
</button>
|
||||
</div>
|
||||
<div class="text-xs text-gray-600 dark:text-gray-300">
|
||||
Current type: <span id="donation-type-indicator">LTC</span>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<button type="button" onclick="fillDonationAddress('{{ donation_info.address }}', '{{ donation_info.coin_name }}')"
|
||||
class="px-3 py-1 bg-blue-500 hover:bg-blue-600 text-white text-xs rounded-md transition-colors">
|
||||
Auto-fill Donation Address
|
||||
</button>
|
||||
{% endif %}
|
||||
|
||||
<div class="mt-3 pt-2 border-t border-coolGray-200 dark:border-gray-400">
|
||||
<a href="/donation" class="text-xs text-blue-600 dark:text-blue-400 hover:underline">
|
||||
Learn more about donations ->
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr class="opacity-100 text-gray-500 dark:text-gray-100">
|
||||
<td class="py-4 pl-6 bold"> {{ w.name }} Address: </td>
|
||||
<td class="py-3 px-6"> <input placeholder="{{ w.ticker }} Address" class="hover:border-blue-500 bg-gray-50 text-gray-900 appearance-none pr-10 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-400 text-lg lg:text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-0" type="text" name="to_{{ w.cid }}" value="{{ w.wd_address }}"> </td>
|
||||
|
||||
@@ -86,6 +86,11 @@ def page_settings(self, url_split, post_string):
|
||||
form_data, "notifications_outgoing_transactions", "false"
|
||||
)
|
||||
),
|
||||
"notifications_swap_completed": toBool(
|
||||
get_data_entry_or(
|
||||
form_data, "notifications_swap_completed", "false"
|
||||
)
|
||||
),
|
||||
"notifications_duration": int(
|
||||
get_data_entry_or(form_data, "notifications_duration", "20")
|
||||
),
|
||||
@@ -234,6 +239,9 @@ def page_settings(self, url_split, post_string):
|
||||
"notifications_outgoing_transactions": swap_client.settings.get(
|
||||
"notifications_outgoing_transactions", True
|
||||
),
|
||||
"notifications_swap_completed": swap_client.settings.get(
|
||||
"notifications_swap_completed", True
|
||||
),
|
||||
"notifications_duration": swap_client.settings.get(
|
||||
"notifications_duration", 20
|
||||
),
|
||||
|
||||
@@ -21,6 +21,15 @@ from basicswap.chainparams import (
|
||||
getCoinIdFromTicker,
|
||||
)
|
||||
|
||||
# Todo: Move at JS
|
||||
DONATION_ADDRESSES = {
|
||||
"XMR": "8BuQsYBNdfhfoWsvVR1unE7YuZEoTkC4hANaPm2fD6VR5VM2DzQoJhq2CHHXUN1UCWQfH3dctJgorSRxksVa5U4RNTJkcAc",
|
||||
"LTC": "ltc1qevlumv48nz2afl0re9ml4tdewc56svxq3egkyt",
|
||||
"LTC_MWEB": "ltcmweb1qqt9rwznnxzkghv4s5wgtwxs0m0ry6n3atp95f47slppapxljde3xyqmdlnrc8ag7y2k354jzdc4pc4ks0kr43jehr77lngdecgh6689nn5mgv5yn",
|
||||
"BTC": "bc1q72j07vkn059xnmsrkk8x9up9lgvd9h9xjf8cq8",
|
||||
"PART": "pw1qf59ef0zjdckldjs8smfhv4j04gsjv302w7pdpz",
|
||||
}
|
||||
|
||||
|
||||
def format_wallet_data(swap_client, ci, w):
|
||||
wf = {
|
||||
@@ -376,6 +385,21 @@ def page_wallet(self, url_split, post_string):
|
||||
|
||||
checkAddressesOwned(swap_client, ci, wallet_data)
|
||||
|
||||
donation_info = None
|
||||
ticker = wallet_data.get("ticker", "").upper()
|
||||
|
||||
if ticker == "LTC":
|
||||
donation_info = {
|
||||
"address": DONATION_ADDRESSES["LTC"],
|
||||
"coin_name": wallet_data.get("name", "Litecoin"),
|
||||
"mweb_address": DONATION_ADDRESSES["LTC_MWEB"],
|
||||
}
|
||||
elif ticker in DONATION_ADDRESSES:
|
||||
donation_info = {
|
||||
"address": DONATION_ADDRESSES[ticker],
|
||||
"coin_name": wallet_data.get("name", ticker),
|
||||
}
|
||||
|
||||
template = server.env.get_template("wallet.html")
|
||||
return self.render_template(
|
||||
template,
|
||||
@@ -385,5 +409,6 @@ def page_wallet(self, url_split, post_string):
|
||||
"w": wallet_data,
|
||||
"summary": summary,
|
||||
"block_unknown_seeds": swap_client._restrict_unknown_seed_wallets,
|
||||
"donation_info": donation_info,
|
||||
},
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user