mirror of
https://github.com/MarekWo/mc-webui.git
synced 2026-08-09 10:12:54 +02:00
feat: configurable route popup + toast display time and position
Users complained that the route popup under group-chat messages and the top-of-page notification toasts auto-close before they can read them, and some users wanted to move the toasts out of the top-left corner. Adds to Settings modal: - Group Chat tab: route popup auto-close timeout + "don't close" switch (applies to both channel popups and DM route popups) - New Interface tab: toast auto-close timeout, "don't close" switch, and five position options (top-left/top-right/bottom-left/bottom-right/center) Persisted as chat_settings (extended) and a new ui_settings row in the app_settings table, with /api/chat/settings and /api/ui/settings endpoints. Default toast delay bumped from 1.5s to 2s. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+98
-4
@@ -272,9 +272,26 @@ DM_RETRY_DEFAULTS = {
|
||||
}
|
||||
|
||||
CHAT_SETTINGS_DEFAULTS = {
|
||||
'quote_max_bytes': 20, # max UTF-8 bytes for truncated quote
|
||||
'quote_max_bytes': 20, # max UTF-8 bytes for truncated quote
|
||||
'path_popup_timeout_sec': 8, # auto-close timeout for route popup (channel + DM)
|
||||
'path_popup_no_autoclose': False, # when True, route popup stays open until click-outside
|
||||
}
|
||||
|
||||
# Keys in chat_settings that carry a boolean value
|
||||
CHAT_SETTINGS_BOOL_KEYS = {'path_popup_no_autoclose'}
|
||||
|
||||
# =============================================================================
|
||||
# UI Settings (app-wide interface behavior — toast notifications, etc.)
|
||||
# =============================================================================
|
||||
|
||||
UI_SETTINGS_DEFAULTS = {
|
||||
'toast_timeout_sec': 2.0, # auto-hide delay for notification toasts
|
||||
'toast_no_autoclose': False, # when True, toasts stay until dismissed
|
||||
'toast_position': 'top-left', # one of TOAST_POSITIONS
|
||||
}
|
||||
|
||||
TOAST_POSITIONS = {'top-left', 'top-right', 'bottom-left', 'bottom-right', 'center'}
|
||||
|
||||
|
||||
def get_dm_retry_settings() -> dict:
|
||||
"""Get DM retry settings from database."""
|
||||
@@ -320,6 +337,28 @@ def save_chat_settings(settings: dict) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def get_ui_settings() -> dict:
|
||||
"""Get UI (interface) settings from database."""
|
||||
db = _get_db()
|
||||
if db:
|
||||
saved = db.get_setting_json('ui_settings', {})
|
||||
return {**UI_SETTINGS_DEFAULTS, **saved}
|
||||
return dict(UI_SETTINGS_DEFAULTS)
|
||||
|
||||
|
||||
def save_ui_settings(settings: dict) -> bool:
|
||||
"""Save UI settings to database."""
|
||||
db = _get_db()
|
||||
if not db:
|
||||
return False
|
||||
try:
|
||||
db.set_setting_json('ui_settings', settings)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to save UI settings: {e}")
|
||||
return False
|
||||
|
||||
|
||||
@api_bp.route('/messages', methods=['GET'])
|
||||
def get_messages():
|
||||
"""
|
||||
@@ -2560,9 +2599,17 @@ def set_chat_config():
|
||||
valid_keys = set(CHAT_SETTINGS_DEFAULTS.keys())
|
||||
settings = {}
|
||||
for key in valid_keys:
|
||||
if key in data:
|
||||
val = data[key]
|
||||
if not isinstance(val, (int, float)) or val < 1:
|
||||
if key not in data:
|
||||
continue
|
||||
val = data[key]
|
||||
if key in CHAT_SETTINGS_BOOL_KEYS:
|
||||
if not isinstance(val, bool):
|
||||
return jsonify({'success': False, 'error': f'Invalid value for {key}'}), 400
|
||||
settings[key] = val
|
||||
else:
|
||||
if isinstance(val, bool) or not isinstance(val, (int, float)) or val < 1:
|
||||
return jsonify({'success': False, 'error': f'Invalid value for {key}'}), 400
|
||||
if key == 'path_popup_timeout_sec' and val > 60:
|
||||
return jsonify({'success': False, 'error': f'Invalid value for {key}'}), 400
|
||||
settings[key] = int(val)
|
||||
|
||||
@@ -2576,6 +2623,53 @@ def set_chat_config():
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
@api_bp.route('/ui/settings', methods=['GET'])
|
||||
def get_ui_config():
|
||||
"""Get UI (interface) settings."""
|
||||
try:
|
||||
return jsonify(get_ui_settings()), 200
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
@api_bp.route('/ui/settings', methods=['POST'])
|
||||
def set_ui_config():
|
||||
"""Update UI (interface) settings."""
|
||||
try:
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
return jsonify({'success': False, 'error': 'Missing JSON body'}), 400
|
||||
|
||||
settings = {}
|
||||
|
||||
if 'toast_timeout_sec' in data:
|
||||
val = data['toast_timeout_sec']
|
||||
if not isinstance(val, (int, float)) or isinstance(val, bool) or val < 1 or val > 60:
|
||||
return jsonify({'success': False, 'error': 'Invalid value for toast_timeout_sec'}), 400
|
||||
settings['toast_timeout_sec'] = float(val)
|
||||
|
||||
if 'toast_no_autoclose' in data:
|
||||
val = data['toast_no_autoclose']
|
||||
if not isinstance(val, bool):
|
||||
return jsonify({'success': False, 'error': 'Invalid value for toast_no_autoclose'}), 400
|
||||
settings['toast_no_autoclose'] = val
|
||||
|
||||
if 'toast_position' in data:
|
||||
val = data['toast_position']
|
||||
if val not in TOAST_POSITIONS:
|
||||
return jsonify({'success': False, 'error': 'Invalid value for toast_position'}), 400
|
||||
settings['toast_position'] = val
|
||||
|
||||
if not settings:
|
||||
return jsonify({'success': False, 'error': 'No valid settings provided'}), 400
|
||||
|
||||
if save_ui_settings(settings):
|
||||
return jsonify({**get_ui_settings(), 'success': True}), 200
|
||||
return jsonify({'success': False, 'error': 'Failed to save settings'}), 500
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
@api_bp.route('/dm/updates', methods=['GET'])
|
||||
def get_dm_updates():
|
||||
"""
|
||||
|
||||
+141
-10
@@ -1541,9 +1541,15 @@ function showPathsPopup(element, encodedPaths) {
|
||||
popup.style.left = '0';
|
||||
}
|
||||
|
||||
// Auto-dismiss after 8 seconds or on outside tap
|
||||
// Auto-dismiss after configured timeout (unless disabled) or on outside tap
|
||||
const dismiss = () => popup.remove();
|
||||
setTimeout(dismiss, 8000);
|
||||
const cfg = window.chatSettingsCache || {};
|
||||
const noAutoclose = !!cfg.path_popup_no_autoclose;
|
||||
const timeoutSec = parseInt(cfg.path_popup_timeout_sec, 10);
|
||||
if (!noAutoclose) {
|
||||
const ms = (isFinite(timeoutSec) && timeoutSec > 0 ? timeoutSec : 8) * 1000;
|
||||
setTimeout(dismiss, ms);
|
||||
}
|
||||
document.addEventListener('click', function handler(e) {
|
||||
if (!element.contains(e.target)) {
|
||||
dismiss();
|
||||
@@ -2077,20 +2083,32 @@ function openCoordPicker() {
|
||||
// --- Chat Settings ---
|
||||
|
||||
const CHAT_SETTINGS_DEFAULTS = {
|
||||
quote_max_bytes: 20
|
||||
quote_max_bytes: 20,
|
||||
path_popup_timeout_sec: 8,
|
||||
path_popup_no_autoclose: false
|
||||
};
|
||||
|
||||
const CHAT_SETTINGS_FIELDS = {
|
||||
quote_max_bytes: 'settQuoteMaxBytes'
|
||||
const CHAT_SETTINGS_INT_FIELDS = {
|
||||
quote_max_bytes: 'settQuoteMaxBytes',
|
||||
path_popup_timeout_sec: 'settPathPopupTimeout'
|
||||
};
|
||||
|
||||
const CHAT_SETTINGS_BOOL_FIELDS = {
|
||||
path_popup_no_autoclose: 'settPathPopupNoAutoclose'
|
||||
};
|
||||
|
||||
let chatSettingsCache = { ...CHAT_SETTINGS_DEFAULTS };
|
||||
window.chatSettingsCache = chatSettingsCache;
|
||||
|
||||
function populateChatSettingsForm(data) {
|
||||
for (const [key, elId] of Object.entries(CHAT_SETTINGS_FIELDS)) {
|
||||
for (const [key, elId] of Object.entries(CHAT_SETTINGS_INT_FIELDS)) {
|
||||
const el = document.getElementById(elId);
|
||||
if (el) el.value = data[key] ?? CHAT_SETTINGS_DEFAULTS[key];
|
||||
}
|
||||
for (const [key, elId] of Object.entries(CHAT_SETTINGS_BOOL_FIELDS)) {
|
||||
const el = document.getElementById(elId);
|
||||
if (el) el.checked = !!(data[key] ?? CHAT_SETTINGS_DEFAULTS[key]);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadChatSettings() {
|
||||
@@ -2099,6 +2117,7 @@ async function loadChatSettings() {
|
||||
if (resp.ok) {
|
||||
const data = await resp.json();
|
||||
chatSettingsCache = { ...CHAT_SETTINGS_DEFAULTS, ...data };
|
||||
window.chatSettingsCache = chatSettingsCache;
|
||||
populateChatSettingsForm(chatSettingsCache);
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -2108,7 +2127,7 @@ async function loadChatSettings() {
|
||||
|
||||
async function saveChatSettings() {
|
||||
const payload = {};
|
||||
for (const [key, elId] of Object.entries(CHAT_SETTINGS_FIELDS)) {
|
||||
for (const [key, elId] of Object.entries(CHAT_SETTINGS_INT_FIELDS)) {
|
||||
const el = document.getElementById(elId);
|
||||
const val = parseInt(el.value, 10);
|
||||
if (isNaN(val) || val < parseInt(el.min) || val > parseInt(el.max)) {
|
||||
@@ -2118,6 +2137,10 @@ async function saveChatSettings() {
|
||||
}
|
||||
payload[key] = val;
|
||||
}
|
||||
for (const [key, elId] of Object.entries(CHAT_SETTINGS_BOOL_FIELDS)) {
|
||||
const el = document.getElementById(elId);
|
||||
if (el) payload[key] = !!el.checked;
|
||||
}
|
||||
try {
|
||||
const resp = await fetch('/api/chat/settings', {
|
||||
method: 'POST',
|
||||
@@ -2127,6 +2150,95 @@ async function saveChatSettings() {
|
||||
if (resp.ok) {
|
||||
const data = await resp.json();
|
||||
chatSettingsCache = { ...CHAT_SETTINGS_DEFAULTS, ...data };
|
||||
window.chatSettingsCache = chatSettingsCache;
|
||||
showNotification('Settings saved', 'success');
|
||||
} else {
|
||||
const err = await resp.json();
|
||||
showNotification(err.error || 'Failed to save', 'danger');
|
||||
}
|
||||
} catch (e) {
|
||||
showNotification('Failed to save settings', 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
// --- UI (Interface) Settings ---
|
||||
|
||||
const UI_SETTINGS_DEFAULTS = {
|
||||
toast_timeout_sec: 2,
|
||||
toast_no_autoclose: false,
|
||||
toast_position: 'top-left'
|
||||
};
|
||||
|
||||
const TOAST_POSITION_CLASSES = {
|
||||
'top-left': ['top-0', 'start-0'],
|
||||
'top-right': ['top-0', 'end-0'],
|
||||
'bottom-left': ['bottom-0', 'start-0'],
|
||||
'bottom-right': ['bottom-0', 'end-0'],
|
||||
'center': ['top-50', 'start-50', 'translate-middle']
|
||||
};
|
||||
|
||||
const ALL_POSITION_CLASSES = ['top-0', 'top-50', 'start-0', 'start-50', 'bottom-0', 'end-0', 'translate-middle'];
|
||||
|
||||
let uiSettingsCache = { ...UI_SETTINGS_DEFAULTS };
|
||||
window.uiSettingsCache = uiSettingsCache;
|
||||
|
||||
function applyToastPosition(position) {
|
||||
const classes = TOAST_POSITION_CLASSES[position] || TOAST_POSITION_CLASSES['top-left'];
|
||||
document.querySelectorAll('[data-toast-container]').forEach(el => {
|
||||
ALL_POSITION_CLASSES.forEach(c => el.classList.remove(c));
|
||||
classes.forEach(c => el.classList.add(c));
|
||||
});
|
||||
}
|
||||
window.applyToastPosition = applyToastPosition;
|
||||
|
||||
function populateUiSettingsForm(data) {
|
||||
const t = document.getElementById('settToastTimeout');
|
||||
if (t) t.value = data.toast_timeout_sec ?? UI_SETTINGS_DEFAULTS.toast_timeout_sec;
|
||||
const noClose = document.getElementById('settToastNoAutoclose');
|
||||
if (noClose) noClose.checked = !!(data.toast_no_autoclose ?? UI_SETTINGS_DEFAULTS.toast_no_autoclose);
|
||||
const pos = document.getElementById('settToastPosition');
|
||||
if (pos) pos.value = data.toast_position ?? UI_SETTINGS_DEFAULTS.toast_position;
|
||||
}
|
||||
|
||||
async function loadUiSettings() {
|
||||
try {
|
||||
const resp = await fetch('/api/ui/settings');
|
||||
if (resp.ok) {
|
||||
const data = await resp.json();
|
||||
uiSettingsCache = { ...UI_SETTINGS_DEFAULTS, ...data };
|
||||
window.uiSettingsCache = uiSettingsCache;
|
||||
applyToastPosition(uiSettingsCache.toast_position);
|
||||
populateUiSettingsForm(uiSettingsCache);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load UI settings:', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function saveUiSettings() {
|
||||
const timeoutEl = document.getElementById('settToastTimeout');
|
||||
const timeout = parseFloat(timeoutEl.value);
|
||||
if (isNaN(timeout) || timeout < 1 || timeout > 60) {
|
||||
showNotification('Invalid auto-close duration', 'danger');
|
||||
timeoutEl.focus();
|
||||
return;
|
||||
}
|
||||
const payload = {
|
||||
toast_timeout_sec: timeout,
|
||||
toast_no_autoclose: !!document.getElementById('settToastNoAutoclose').checked,
|
||||
toast_position: document.getElementById('settToastPosition').value
|
||||
};
|
||||
try {
|
||||
const resp = await fetch('/api/ui/settings', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
if (resp.ok) {
|
||||
const data = await resp.json();
|
||||
uiSettingsCache = { ...UI_SETTINGS_DEFAULTS, ...data };
|
||||
window.uiSettingsCache = uiSettingsCache;
|
||||
applyToastPosition(uiSettingsCache.toast_position);
|
||||
showNotification('Settings saved', 'success');
|
||||
} else {
|
||||
const err = await resp.json();
|
||||
@@ -2212,6 +2324,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
loadDeviceConfig();
|
||||
loadDmRetrySettings();
|
||||
loadChatSettings();
|
||||
loadUiSettings();
|
||||
});
|
||||
settingsModal.addEventListener('shown.bs.modal', () => {
|
||||
settingsModal.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(el => {
|
||||
@@ -2244,6 +2357,18 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
populateChatSettingsForm(CHAT_SETTINGS_DEFAULTS);
|
||||
});
|
||||
|
||||
const uiSettingsForm = document.getElementById('uiSettingsForm');
|
||||
if (uiSettingsForm) {
|
||||
uiSettingsForm.addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
saveUiSettings();
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('uiSettingsResetBtn')?.addEventListener('click', () => {
|
||||
populateUiSettingsForm(UI_SETTINGS_DEFAULTS);
|
||||
});
|
||||
|
||||
// --- Device Settings ---
|
||||
const devicePublicInfoForm = document.getElementById('devicePublicInfoForm');
|
||||
if (devicePublicInfoForm) {
|
||||
@@ -2279,8 +2404,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
bootstrap.Modal.getInstance(document.getElementById('coordPickerModal'))?.hide();
|
||||
});
|
||||
|
||||
// Load chat settings cache on startup (for quote dialog)
|
||||
// Load settings caches on startup (for quote dialog, path popup, toast behavior)
|
||||
loadChatSettings();
|
||||
loadUiSettings();
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -2648,9 +2774,14 @@ function showNotification(message, type = 'info') {
|
||||
toastBody.textContent = message;
|
||||
toastEl.className = `toast bg-${type} text-white`;
|
||||
|
||||
const cfg = window.uiSettingsCache || {};
|
||||
const noAutoclose = !!cfg.toast_no_autoclose;
|
||||
const timeoutSec = parseFloat(cfg.toast_timeout_sec);
|
||||
const delay = isFinite(timeoutSec) && timeoutSec > 0 ? Math.round(timeoutSec * 1000) : 2000;
|
||||
|
||||
const toast = new bootstrap.Toast(toastEl, {
|
||||
autohide: true,
|
||||
delay: 1500
|
||||
autohide: !noAutoclose,
|
||||
delay: delay
|
||||
});
|
||||
toast.show();
|
||||
}
|
||||
|
||||
@@ -10,6 +10,44 @@
|
||||
* - Mobile-first design
|
||||
*/
|
||||
|
||||
// --- UI settings bootstrap (for standalone contact pages that don't load app.js) ---
|
||||
|
||||
(function initContactsUiSettings() {
|
||||
const TOAST_POSITION_CLASSES = {
|
||||
'top-left': ['top-0', 'start-0'],
|
||||
'top-right': ['top-0', 'end-0'],
|
||||
'bottom-left': ['bottom-0', 'start-0'],
|
||||
'bottom-right': ['bottom-0', 'end-0'],
|
||||
'center': ['top-50', 'start-50', 'translate-middle']
|
||||
};
|
||||
const ALL_CLASSES = ['top-0', 'top-50', 'start-0', 'start-50', 'bottom-0', 'end-0', 'translate-middle'];
|
||||
|
||||
function apply(position) {
|
||||
const classes = TOAST_POSITION_CLASSES[position] || TOAST_POSITION_CLASSES['top-left'];
|
||||
document.querySelectorAll('[data-toast-container]').forEach(el => {
|
||||
ALL_CLASSES.forEach(c => el.classList.remove(c));
|
||||
classes.forEach(c => el.classList.add(c));
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
// If app.js is on the page, it owns settings loading and will update all
|
||||
// [data-toast-container] elements (including ours) once its fetch resolves.
|
||||
if (typeof window.applyToastPosition === 'function') return;
|
||||
|
||||
try {
|
||||
const resp = await fetch('/api/ui/settings');
|
||||
if (resp.ok) {
|
||||
const data = await resp.json();
|
||||
window.uiSettingsCache = data;
|
||||
apply(data.toast_position || 'top-left');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load UI settings:', e);
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
// =============================================================================
|
||||
// Global Navigation Helper
|
||||
// =============================================================================
|
||||
@@ -1783,10 +1821,15 @@ function showToast(message, type = 'info') {
|
||||
toastEl.classList.add('bg-info', 'text-white');
|
||||
}
|
||||
|
||||
// Show toast
|
||||
// Show toast (honors ui_settings: timeout + no-autoclose; position handled by toast-container classes)
|
||||
const cfg = window.uiSettingsCache || {};
|
||||
const noAutoclose = !!cfg.toast_no_autoclose;
|
||||
const timeoutSec = parseFloat(cfg.toast_timeout_sec);
|
||||
const delay = isFinite(timeoutSec) && timeoutSec > 0 ? Math.round(timeoutSec * 1000) : 2000;
|
||||
|
||||
const toast = new bootstrap.Toast(toastEl, {
|
||||
autohide: true,
|
||||
delay: 1500
|
||||
autohide: !noAutoclose,
|
||||
delay: delay
|
||||
});
|
||||
toast.show();
|
||||
}
|
||||
|
||||
+76
-3
@@ -3,6 +3,64 @@
|
||||
* Full-page DM view functionality
|
||||
*/
|
||||
|
||||
// --- Settings caches (standalone page — no app.js on this route) ---
|
||||
|
||||
const DM_CHAT_SETTINGS_DEFAULTS = {
|
||||
path_popup_timeout_sec: 8,
|
||||
path_popup_no_autoclose: false
|
||||
};
|
||||
|
||||
const DM_UI_SETTINGS_DEFAULTS = {
|
||||
toast_timeout_sec: 2,
|
||||
toast_no_autoclose: false,
|
||||
toast_position: 'top-left'
|
||||
};
|
||||
|
||||
const DM_TOAST_POSITION_CLASSES = {
|
||||
'top-left': ['top-0', 'start-0'],
|
||||
'top-right': ['top-0', 'end-0'],
|
||||
'bottom-left': ['bottom-0', 'start-0'],
|
||||
'bottom-right': ['bottom-0', 'end-0'],
|
||||
'center': ['top-50', 'start-50', 'translate-middle']
|
||||
};
|
||||
const DM_ALL_POSITION_CLASSES = ['top-0', 'top-50', 'start-0', 'start-50', 'bottom-0', 'end-0', 'translate-middle'];
|
||||
|
||||
window.chatSettingsCache = window.chatSettingsCache || { ...DM_CHAT_SETTINGS_DEFAULTS };
|
||||
window.uiSettingsCache = window.uiSettingsCache || { ...DM_UI_SETTINGS_DEFAULTS };
|
||||
|
||||
function dmApplyToastPosition(position) {
|
||||
const classes = DM_TOAST_POSITION_CLASSES[position] || DM_TOAST_POSITION_CLASSES['top-left'];
|
||||
document.querySelectorAll('[data-toast-container]').forEach(el => {
|
||||
DM_ALL_POSITION_CLASSES.forEach(c => el.classList.remove(c));
|
||||
classes.forEach(c => el.classList.add(c));
|
||||
});
|
||||
}
|
||||
|
||||
async function loadDmChatSettings() {
|
||||
try {
|
||||
const resp = await fetch('/api/chat/settings');
|
||||
if (resp.ok) {
|
||||
const data = await resp.json();
|
||||
window.chatSettingsCache = { ...DM_CHAT_SETTINGS_DEFAULTS, ...data };
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load chat settings:', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDmUiSettings() {
|
||||
try {
|
||||
const resp = await fetch('/api/ui/settings');
|
||||
if (resp.ok) {
|
||||
const data = await resp.json();
|
||||
window.uiSettingsCache = { ...DM_UI_SETTINGS_DEFAULTS, ...data };
|
||||
dmApplyToastPosition(window.uiSettingsCache.toast_position);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load UI settings:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// State variables
|
||||
let currentConversationId = null;
|
||||
let currentRecipient = null;
|
||||
@@ -202,6 +260,10 @@ document.addEventListener('DOMContentLoaded', async function() {
|
||||
// Force reflow to ensure proper layout calculation
|
||||
document.body.offsetHeight;
|
||||
|
||||
// Load settings caches (path popup timeout + toast behavior/position)
|
||||
loadDmChatSettings();
|
||||
loadDmUiSettings();
|
||||
|
||||
// Load last seen timestamps from server
|
||||
await loadDmLastSeenTimestampsFromServer();
|
||||
|
||||
@@ -1478,7 +1540,13 @@ function showDmRoutePopup(element, hexPath, hashSize) {
|
||||
element.appendChild(popup);
|
||||
|
||||
const dismiss = () => popup.remove();
|
||||
setTimeout(dismiss, 8000);
|
||||
const cfg = window.chatSettingsCache || {};
|
||||
const noAutoclose = !!cfg.path_popup_no_autoclose;
|
||||
const timeoutSec = parseInt(cfg.path_popup_timeout_sec, 10);
|
||||
if (!noAutoclose) {
|
||||
const ms = (isFinite(timeoutSec) && timeoutSec > 0 ? timeoutSec : 8) * 1000;
|
||||
setTimeout(dismiss, ms);
|
||||
}
|
||||
document.addEventListener('click', function handler(e) {
|
||||
if (!element.contains(e.target)) {
|
||||
dismiss();
|
||||
@@ -1725,9 +1793,14 @@ function showNotification(message, type = 'info') {
|
||||
}
|
||||
}
|
||||
|
||||
const cfg = window.uiSettingsCache || {};
|
||||
const noAutoclose = !!cfg.toast_no_autoclose;
|
||||
const timeoutSec = parseFloat(cfg.toast_timeout_sec);
|
||||
const delay = isFinite(timeoutSec) && timeoutSec > 0 ? Math.round(timeoutSec * 1000) : 2000;
|
||||
|
||||
const toast = new bootstrap.Toast(toastEl, {
|
||||
autohide: true,
|
||||
delay: 1500
|
||||
autohide: !noAutoclose,
|
||||
delay: delay
|
||||
});
|
||||
toast.show();
|
||||
}
|
||||
|
||||
+62
-2
@@ -379,6 +379,9 @@
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#tabSettingsChat" type="button">Group Chat</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#tabSettingsInterface" type="button">Interface</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#tabSettingsAppearance" type="button">Appearance</button>
|
||||
</li>
|
||||
@@ -563,12 +566,69 @@
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h6 class="text-muted mb-2">Route popup</h6>
|
||||
<p class="text-muted small mb-2">The popup shown when tapping "SNR | Hops" under a message. Also applies to DMs.</p>
|
||||
<table class="table table-sm table-borderless mb-3 align-middle">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="ps-0">Auto-close after (s) <span class="badge rounded-pill text-muted" data-bs-toggle="tooltip" title="Seconds before the route popup closes automatically"><i class="bi bi-info-circle"></i></span></td>
|
||||
<td class="pe-0" style="width:5rem"><input type="number" class="form-control form-control-sm" id="settPathPopupTimeout" min="1" max="60" value="8"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="ps-0">Don't close automatically <span class="badge rounded-pill text-muted" data-bs-toggle="tooltip" title="Popup stays open until you tap outside it"><i class="bi bi-info-circle"></i></span></td>
|
||||
<td class="pe-0">
|
||||
<div class="form-check form-switch mb-0">
|
||||
<input class="form-check-input" type="checkbox" id="settPathPopupNoAutoclose">
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary btn-sm">Save</button>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" id="chatSettingsResetBtn">Reset to defaults</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="tabSettingsInterface">
|
||||
<form id="uiSettingsForm">
|
||||
<h6 class="text-muted mb-2">Notifications</h6>
|
||||
<p class="text-muted small mb-2">Controls the small toasts shown after actions (e.g. "Advert Sent", errors).</p>
|
||||
<table class="table table-sm table-borderless mb-3 align-middle">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="ps-0">Auto-close after (s) <span class="badge rounded-pill text-muted" data-bs-toggle="tooltip" title="Seconds before a notification closes automatically"><i class="bi bi-info-circle"></i></span></td>
|
||||
<td class="pe-0" style="width:5rem"><input type="number" class="form-control form-control-sm" id="settToastTimeout" min="1" max="60" step="0.5" value="2"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="ps-0">Don't close automatically <span class="badge rounded-pill text-muted" data-bs-toggle="tooltip" title="Notifications stay until dismissed via their close button"><i class="bi bi-info-circle"></i></span></td>
|
||||
<td class="pe-0">
|
||||
<div class="form-check form-switch mb-0">
|
||||
<input class="form-check-input" type="checkbox" id="settToastNoAutoclose">
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="ps-0">Position on screen</td>
|
||||
<td class="pe-0">
|
||||
<select class="form-select form-select-sm" id="settToastPosition">
|
||||
<option value="top-left">Top left</option>
|
||||
<option value="top-right">Top right</option>
|
||||
<option value="bottom-left">Bottom left</option>
|
||||
<option value="bottom-right">Bottom right</option>
|
||||
<option value="center">Center</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary btn-sm">Save</button>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" id="uiSettingsResetBtn">Reset to defaults</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="tabSettingsAppearance">
|
||||
<h6 class="text-muted mb-3">Theme</h6>
|
||||
<div class="d-flex flex-column gap-2">
|
||||
@@ -798,8 +858,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toast container for notifications -->
|
||||
<div class="toast-container position-fixed top-0 start-0 p-3">
|
||||
<!-- Toast container for notifications (position classes applied by JS from ui_settings) -->
|
||||
<div class="toast-container position-fixed top-0 start-0 p-3" data-toast-container>
|
||||
<div id="notificationToast" class="toast" role="alert">
|
||||
<div class="toast-header">
|
||||
<strong class="me-auto">mc-webui</strong>
|
||||
|
||||
@@ -145,8 +145,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toast container for notifications -->
|
||||
<div class="toast-container position-fixed bottom-0 end-0 p-3">
|
||||
<!-- Toast container for notifications (position classes applied by JS from ui_settings) -->
|
||||
<div class="toast-container position-fixed bottom-0 end-0 p-3" data-toast-container>
|
||||
<div id="contactToast" class="toast" role="alert">
|
||||
<div class="toast-header">
|
||||
<strong class="me-auto">Contact Management</strong>
|
||||
|
||||
@@ -277,8 +277,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toast container for notifications (shared across all contact pages) -->
|
||||
<div class="toast-container position-fixed top-0 start-0 p-3">
|
||||
<!-- Toast container for notifications (shared across all contact pages; position via JS from ui_settings) -->
|
||||
<div class="toast-container position-fixed top-0 start-0 p-3" data-toast-container>
|
||||
<div id="contactToast" class="toast" role="alert">
|
||||
<div class="toast-header">
|
||||
<strong class="me-auto">Contact Management</strong>
|
||||
|
||||
@@ -331,8 +331,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toast container for notifications -->
|
||||
<div class="toast-container position-fixed top-0 start-0 p-3">
|
||||
<!-- Toast container for notifications (position classes applied by JS from ui_settings) -->
|
||||
<div class="toast-container position-fixed top-0 start-0 p-3" data-toast-container>
|
||||
<div id="notificationToast" class="toast" role="alert">
|
||||
<div class="toast-header">
|
||||
<strong class="me-auto">mc-webui</strong>
|
||||
|
||||
Reference in New Issue
Block a user