From 6d005387d6e173ed14605b41fa26b2c562f179f8 Mon Sep 17 00:00:00 2001 From: EzhikRuinit <89244602+EzhikRuinit@users.noreply.github.com> Date: Wed, 3 Dec 2025 11:48:50 +0300 Subject: [PATCH 01/15] update webkit2gtk requirement from 4.0 to 4.1 updated because fedora doesn't uses webkit2gtk <4.1 anymore --- src-tauri/src/core/platform/linux.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-tauri/src/core/platform/linux.rs b/src-tauri/src/core/platform/linux.rs index cd6aed9..4a8fa3c 100644 --- a/src-tauri/src/core/platform/linux.rs +++ b/src-tauri/src/core/platform/linux.rs @@ -2,7 +2,7 @@ use crate::core::error::StartupError; pub fn check_platform_dependencies() -> Result<(), StartupError> { let result = std::process::Command::new("pkg-config") - .args(["--print-errors", "webkit2gtk-4.0"]) + .args(["--print-errors", "webkit2gtk-4.1"]) .output(); match result { From ecdc243144edb1338b6db0397ae8f78711526fe5 Mon Sep 17 00:00:00 2001 From: dest4590 Date: Wed, 10 Dec 2025 20:16:15 +0200 Subject: [PATCH 02/15] feat: fixed presets modals --- .../social/presets/CreatePresetModal.vue | 22 +++++++++---------- .../social/presets/ShareLocalPresetModal.vue | 22 +++++++++---------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/components/modals/social/presets/CreatePresetModal.vue b/src/components/modals/social/presets/CreatePresetModal.vue index 33075e5..0d7a814 100644 --- a/src/components/modals/social/presets/CreatePresetModal.vue +++ b/src/components/modals/social/presets/CreatePresetModal.vue @@ -110,30 +110,30 @@ const save = async () => { id: props.editingPreset.id, name: form.name.trim(), description: form.description.trim() || undefined, - custom_css: props.editingPreset.custom_css, - enable_custom_css: props.editingPreset.enable_custom_css, + customCSS: props.editingPreset.customCSS, + enableCustomCSS: props.editingPreset.enableCustomCSS, base100: props.editingPreset.base100, base200: props.editingPreset.base200, base300: props.editingPreset.base300, - base_content: props.editingPreset.base_content, + baseContent: props.editingPreset.baseContent, primary: props.editingPreset.primary, - primary_content: props.editingPreset.primary_content, + primaryContent: props.editingPreset.primaryContent, secondary: props.editingPreset.secondary, - secondary_content: props.editingPreset.secondary_content, + secondaryContent: props.editingPreset.secondaryContent, accent: props.editingPreset.accent, - accent_content: props.editingPreset.accent_content, + accentContent: props.editingPreset.accentContent, neutral: props.editingPreset.neutral, - neutral_content: props.editingPreset.neutral_content, + neutralContent: props.editingPreset.neutralContent, info: props.editingPreset.info, - info_content: props.editingPreset.info_content, + infoContent: props.editingPreset.infoContent, success: props.editingPreset.success, - success_content: props.editingPreset.success_content, + successContent: props.editingPreset.successContent, warning: props.editingPreset.warning, - warning_content: props.editingPreset.warning_content, + warningContent: props.editingPreset.warningContent, error: props.editingPreset.error, - error_content: props.editingPreset.error_content, + errorContent: props.editingPreset.errorContent, }); } else { emit('save', { diff --git a/src/components/modals/social/presets/ShareLocalPresetModal.vue b/src/components/modals/social/presets/ShareLocalPresetModal.vue index f31a89d..58a39b2 100644 --- a/src/components/modals/social/presets/ShareLocalPresetModal.vue +++ b/src/components/modals/social/presets/ShareLocalPresetModal.vue @@ -81,28 +81,28 @@ async function share() { title: title.value.trim(), description: description.value.trim(), preset_data: { - custom_css: s.custom_css, - enable_custom_css: s.enable_custom_css, + custom_css: s.customCSS, + enable_custom_css: s.enableCustomCSS, primary: s.primary, base100: s.base100, base200: s.base200, base300: s.base300, - base_content: s.base_content, - primary_content: s.primary_content, + base_content: s.baseContent, + primary_content: s.primaryContent, secondary: s.secondary, - secondary_content: s.secondary_content, + secondary_content: s.secondaryContent, accent: s.accent, - accent_content: s.accent_content, + accent_content: s.accentContent, neutral: s.neutral, - neutral_content: s.neutral_content, + neutral_content: s.neutralContent, info: s.info, - info_content: s.info_content, + info_content: s.infoContent, success: s.success, - success_content: s.success_content, + success_content: s.successContent, warning: s.warning, - warning_content: s.warning_content, + warning_content: s.warningContent, error: s.error, - error_content: s.error_content, + error_content: s.errorContent, }, is_public: isPublic.value, }; From ca600de8f921b0f6cb945f8bbb9f608883768e79 Mon Sep 17 00:00:00 2001 From: dest4590 Date: Thu, 11 Dec 2025 20:37:36 +0200 Subject: [PATCH 03/15] feat: removed race condition because it occurs problem with server initialization --- src/composables/useUser.ts | 40 +++++++---------- src/main.ts | 13 +++--- src/services/apiClient.ts | 92 ++++++++++++++++++++++++++++++++++++-- 3 files changed, 111 insertions(+), 34 deletions(-) diff --git a/src/composables/useUser.ts b/src/composables/useUser.ts index 024254e..36e0cc2 100644 --- a/src/composables/useUser.ts +++ b/src/composables/useUser.ts @@ -58,9 +58,6 @@ export function useUser() { const email = computed(() => globalUserState.info?.email || ''); const nickname = computed(() => globalUserState.profile?.nickname || ''); - // race condition fix - let loadPromise: Promise | null = null; - const loadUserData = async (forceRefresh = false): Promise => { if (!isAuthenticated.value) { clearUserData(); @@ -71,33 +68,28 @@ export function useUser() { return; } - if (loadPromise) { - return loadPromise; + if (globalUserState.isLoading) { + return; } globalUserState.isLoading = true; - loadPromise = (async () => { - try { - const initData = await userService.initializeUser(); + try { + const initData = await userService.initializeUser(); - globalUserState.profile = initData.profile; - globalUserState.info = initData.user_info; - globalUserState.adminStatus = initData.admin_status; - globalUserState.syncStatus = initData.sync_status; - globalUserState.lastUpdated = new Date().toISOString(); - globalUserState.isLoaded = true; - - console.log('Global user data loaded successfully'); - } catch (error) { - console.error('Failed to load global user data:', error); - } finally { - globalUserState.isLoading = false; - loadPromise = null; - } - })(); + globalUserState.profile = initData.profile; + globalUserState.info = initData.user_info; + globalUserState.adminStatus = initData.admin_status; + globalUserState.syncStatus = initData.sync_status; + globalUserState.lastUpdated = new Date().toISOString(); + globalUserState.isLoaded = true; - return loadPromise; + console.log('Global user data loaded successfully'); + } catch (error) { + console.error('Failed to load global user data:', error); + } finally { + globalUserState.isLoading = false; + } }; const updateUserProfile = async (newNickname: string): Promise => { diff --git a/src/main.ts b/src/main.ts index 1ab2a15..3eb2eec 100644 --- a/src/main.ts +++ b/src/main.ts @@ -11,9 +11,10 @@ loader.config({ }, }) -initializeAuthUrl(); - -createApp(App) - .use(Vue3Lottie) - .use(i18n) - .mount('#app'); +initializeAuthUrl() + .finally(() => { + createApp(App) + .use(Vue3Lottie) + .use(i18n) + .mount('#app'); + }); diff --git a/src/services/apiClient.ts b/src/services/apiClient.ts index cf50b95..9e21abe 100644 --- a/src/services/apiClient.ts +++ b/src/services/apiClient.ts @@ -1,5 +1,5 @@ -import axios, { AxiosRequestConfig } from 'axios'; -import { getAuthUrl, ensureAuthUrl } from '../config'; +import axios, { AxiosResponse, AxiosRequestConfig } from 'axios'; +import { getAuthUrl } from '../config'; import { getCurrentLanguage } from '../i18n'; interface CacheEntry { @@ -9,6 +9,16 @@ interface CacheEntry { etag?: string; } +interface RequestMetrics { + totalRequests: number; + cacheHits: number; + cacheMisses: number; + avgResponseTime: number; + errorRate: number; + requestTimes: number[]; + errors: number; +} + interface BatchConfig { maxBatchSize: number; batchTimeout: number; @@ -20,6 +30,15 @@ class ApiClient { private cache = new Map(); private pendingRequests = new Map>(); private requestTimings = new Map(); + private metrics: RequestMetrics = { + totalRequests: 0, + cacheHits: 0, + cacheMisses: 0, + avgResponseTime: 0, + errorRate: 0, + requestTimes: [], + errors: 0 + }; private batchConfig: BatchConfig = { maxBatchSize: 10, @@ -33,8 +52,7 @@ class ApiClient { } private setupInterceptors() { - this.client.interceptors.request.use(async (config) => { - await ensureAuthUrl(); + this.client.interceptors.request.use((config) => { const baseUrl = getAuthUrl(); if (config.url?.startsWith('/')) { @@ -61,14 +79,42 @@ class ApiClient { this.client.interceptors.response.use( (response) => { + this.updateMetrics(response); return response; }, (error) => { + this.updateErrorMetrics(); return Promise.reject(error); } ); } + private updateMetrics(response: AxiosResponse) { + const timingKey = (response.config as any).__timingKey; + if (timingKey && this.requestTimings.has(timingKey)) { + const startTime = this.requestTimings.get(timingKey)!; + const responseTime = Date.now() - startTime; + this.metrics.requestTimes.push(responseTime); + + this.requestTimings.delete(timingKey); + + if (this.metrics.requestTimes.length > 100) { + this.metrics.requestTimes.shift(); + } + + this.metrics.avgResponseTime = + this.metrics.requestTimes.reduce((a, b) => a + b, 0) / this.metrics.requestTimes.length; + } + + this.metrics.totalRequests++; + } + + private updateErrorMetrics() { + this.metrics.errors++; + this.metrics.totalRequests++; + this.metrics.errorRate = this.metrics.errors / this.metrics.totalRequests; + } + private setupCacheCleanup() { setInterval(() => { const now = Date.now(); @@ -136,6 +182,8 @@ class ApiClient { if (this.shouldCache(url, method)) { if (cached && Date.now() - cached.timestamp < cached.ttl) { + this.metrics.cacheHits++; + console.log(`Cache hit for ${url}`); return cached.data; } } @@ -145,6 +193,7 @@ class ApiClient { return this.pendingRequests.get(cacheKey) as Promise; } + // If we have an expired cached entry with an ETag, add conditional header const requestConfig: AxiosRequestConfig = { ...(config || {}), method }; if (cached && cached.etag) { requestConfig.headers = { ...(requestConfig.headers || {}) }; @@ -155,10 +204,13 @@ class ApiClient { try { const response = await this.executeRequest(url, requestConfig); + // axios response handled in executeRequest now returns AxiosResponse const axiosResp: any = response as any; if (axiosResp.status === 304 && cached) { + // server indicates not modified cached.timestamp = Date.now(); + this.metrics.cacheHits++; return cached.data as T; } @@ -172,6 +224,7 @@ class ApiClient { ttl: this.getCacheTTL(url), etag: etag }); + this.metrics.cacheMisses++; } return result; @@ -294,11 +347,40 @@ class ApiClient { return false; } + + getMetrics(): RequestMetrics { + return { ...this.metrics }; + } + + + resetMetrics() { + this.metrics = { + totalRequests: 0, + cacheHits: 0, + cacheMisses: 0, + avgResponseTime: 0, + errorRate: 0, + requestTimes: [], + errors: 0 + }; + } + + clearCache() { this.cache.clear(); console.log('API cache cleared'); } + getCacheStats() { + return { + size: this.cache.size, + hitRate: this.metrics.cacheHits / (this.metrics.cacheHits + this.metrics.cacheMisses) || 0, + avgResponseTime: this.metrics.avgResponseTime, + totalRequests: this.metrics.totalRequests + }; + } + + async preloadCriticalData(): Promise { const criticalEndpoints = [ '/auth/init/', @@ -339,6 +421,8 @@ export const apiDelete = apiClient.delete.bind(apiClient); export const apiBatchGet = apiClient.batchGet.bind(apiClient); export const apiPreload = apiClient.preloadCriticalData.bind(apiClient); +export const apiMetrics = apiClient.getMetrics.bind(apiClient); +export const apiCacheStats = apiClient.getCacheStats.bind(apiClient); export const apiHeartbeat = apiClient.heartbeat.bind(apiClient); export const apiInvalidateProfile = apiClient.invalidateProfileCaches.bind(apiClient); From 7840edf7027511b520a727991a689db4feb6ca5d Mon Sep 17 00:00:00 2001 From: dest4590 Date: Fri, 12 Dec 2025 15:23:43 +0200 Subject: [PATCH 04/15] feat: server checking system rewrite & sentry integration --- package-lock.json | 98 ++++++++++++++ package.json | 1 + src-tauri/src/commands/clients.rs | 3 +- src-tauri/src/core/network/servers.rs | 128 ++++++------------ .../features/social/InlineIRCChat.vue | 7 +- src/composables/useIrcChat.ts | 2 +- src/main.ts | 22 ++- 7 files changed, 165 insertions(+), 96 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4743553..2465b7d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.2.4", "dependencies": { "@guolao/vue-monaco-editor": "1.6.0", + "@sentry/vue": "^10.30.0", "@tauri-apps/api": "2.9.1", "@tauri-apps/plugin-dialog": "2.4.2", "@tauri-apps/plugin-fs": "2.4.4", @@ -1230,6 +1231,103 @@ "win32" ] }, + "node_modules/@sentry-internal/browser-utils": { + "version": "10.30.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-10.30.0.tgz", + "integrity": "sha512-dVsHTUbvgaLNetWAQC6yJFnmgD0xUbVgCkmzNB7S28wIP570GcZ4cxFGPOkXbPx6dEBUfoOREeXzLqjJLtJPfg==", + "license": "MIT", + "dependencies": { + "@sentry/core": "10.30.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry-internal/feedback": { + "version": "10.30.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-10.30.0.tgz", + "integrity": "sha512-+bnQZ6SNF265nTXrRlXTmq5Ila1fRfraDOAahlOT/VM4j6zqCvNZzmeDD9J6IbxiAdhlp/YOkrG3zbr5vgYo0A==", + "license": "MIT", + "dependencies": { + "@sentry/core": "10.30.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry-internal/replay": { + "version": "10.30.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-10.30.0.tgz", + "integrity": "sha512-Pj/fMIZQkXzIw6YWpxKWUE5+GXffKq6CgXwHszVB39al1wYz1gTIrTqJqt31IBLIihfCy8XxYddglR2EW0BVIQ==", + "license": "MIT", + "dependencies": { + "@sentry-internal/browser-utils": "10.30.0", + "@sentry/core": "10.30.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry-internal/replay-canvas": { + "version": "10.30.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-10.30.0.tgz", + "integrity": "sha512-RIlIz+XQ4DUWaN60CjfmicJq2O2JRtDKM5lw0wB++M5ha0TBh6rv+Ojf6BDgiV3LOQ7lZvCM57xhmNUtrGmelg==", + "license": "MIT", + "dependencies": { + "@sentry-internal/replay": "10.30.0", + "@sentry/core": "10.30.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/browser": { + "version": "10.30.0", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-10.30.0.tgz", + "integrity": "sha512-7M/IJUMLo0iCMLNxDV/OHTPI0WKyluxhCcxXJn7nrCcolu8A1aq9R8XjKxm0oTCO8ht5pz8bhGXUnYJj4eoEBA==", + "license": "MIT", + "dependencies": { + "@sentry-internal/browser-utils": "10.30.0", + "@sentry-internal/feedback": "10.30.0", + "@sentry-internal/replay": "10.30.0", + "@sentry-internal/replay-canvas": "10.30.0", + "@sentry/core": "10.30.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/core": { + "version": "10.30.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.30.0.tgz", + "integrity": "sha512-IfNuqIoGVO9pwphwbOptAEJJI1SCAfewS5LBU1iL7hjPBHYAnE8tCVzyZN+pooEkQQ47Q4rGanaG1xY8mjTT1A==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/vue": { + "version": "10.30.0", + "resolved": "https://registry.npmjs.org/@sentry/vue/-/vue-10.30.0.tgz", + "integrity": "sha512-bGGchq1iMgd5qK/Z4czgEHBMJpoATIkMHz5JTRg3NjfSvl2IthigJvk83CviLqN/6TZ21/ycnqUxvT/jNJLr+w==", + "license": "MIT", + "dependencies": { + "@sentry/browser": "10.30.0", + "@sentry/core": "10.30.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "pinia": "2.x || 3.x", + "vue": "2.x || 3.x" + }, + "peerDependenciesMeta": { + "pinia": { + "optional": true + } + } + }, "node_modules/@tailwindcss/node": { "version": "4.1.17", "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.17.tgz", diff --git a/package.json b/package.json index f0264b1..0a24940 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ }, "dependencies": { "@guolao/vue-monaco-editor": "1.6.0", + "@sentry/vue": "^10.30.0", "@tauri-apps/api": "2.9.1", "@tauri-apps/plugin-dialog": "2.4.2", "@tauri-apps/plugin-fs": "2.4.4", diff --git a/src-tauri/src/commands/clients.rs b/src-tauri/src/commands/clients.rs index f64c3db..eeb9642 100644 --- a/src-tauri/src/commands/clients.rs +++ b/src-tauri/src/commands/clients.rs @@ -82,8 +82,9 @@ pub fn initialize_rpc() -> Result<(), String> { } #[tauri::command] -pub fn get_server_connectivity_status() -> ServerConnectivityStatus { +pub async fn get_server_connectivity_status() -> ServerConnectivityStatus { let servers = &SERVERS; + servers.wait_for_initial_check().await; servers.connectivity_status.lock().unwrap().clone() } diff --git a/src-tauri/src/core/network/servers.rs b/src-tauri/src/core/network/servers.rs index 73d61e4..12bd88b 100644 --- a/src-tauri/src/core/network/servers.rs +++ b/src-tauri/src/core/network/servers.rs @@ -2,60 +2,16 @@ use crate::{ core::utils::globals::{AUTH_SERVERS, CDN_SERVERS}, log_info, log_warn, }; -use backoff::{future::retry, ExponentialBackoff}; use reqwest::Client; -use std::sync::atomic::{AtomicUsize, Ordering}; -use std::sync::{Arc, LazyLock, Mutex, RwLock}; -use std::time::{Duration, Instant}; +use std::sync::{LazyLock, Mutex, RwLock}; +use std::time::Duration; +use tokio::sync::watch; -const REQUEST_TIMEOUT: Duration = Duration::from_secs(5); -const CB_MAX_FAILURES: usize = 3; -const CB_RESET_WINDOW: Duration = Duration::from_secs(60); -const BACKOFF_MAX_ELAPSED: Duration = Duration::from_secs(10); - -#[derive(Debug)] -pub struct CircuitBreaker { - failures: AtomicUsize, - last_failure: Mutex>, -} - -impl CircuitBreaker { - fn new() -> Self { - Self { - failures: AtomicUsize::new(0), - last_failure: Mutex::new(None), - } - } - - fn record_failure(&self) { - self.failures.fetch_add(1, Ordering::SeqCst); - let mut last = self.last_failure.lock().unwrap(); - *last = Some(Instant::now()); - } - - fn record_success(&self) { - self.failures.store(0, Ordering::SeqCst); - } - - fn is_open(&self) -> bool { - if self.failures.load(Ordering::SeqCst) < CB_MAX_FAILURES { - return false; - } - let last = self.last_failure.lock().unwrap(); - if let Some(time) = *last { - if time.elapsed() < CB_RESET_WINDOW { - return true; - } - } - false - } -} +const REQUEST_TIMEOUT: Duration = Duration::from_secs(10); #[derive(Debug, Clone, serde::Serialize)] pub struct Server { pub url: String, - #[serde(skip)] - pub circuit_breaker: Arc, } #[derive(Debug, Clone, serde::Serialize)] @@ -71,13 +27,14 @@ pub struct Servers { pub selected_cdn: RwLock>, pub selected_auth: RwLock>, pub connectivity_status: Mutex, + pub check_complete_tx: watch::Sender, + pub check_complete_rx: watch::Receiver, } impl Server { pub fn new(url: &str) -> Self { Self { url: url.to_string(), - circuit_breaker: Arc::new(CircuitBreaker::new()), } } } @@ -99,6 +56,8 @@ impl Servers { None }; + let (tx, rx) = watch::channel(false); + Self { cdns, auths, @@ -108,6 +67,8 @@ impl Servers { cdn_online: false, auth_online: false, }), + check_complete_tx: tx, + check_complete_rx: rx, } } @@ -123,6 +84,15 @@ impl Servers { .await; self.set_status(); + let _ = self.check_complete_tx.send(true); + } + + pub async fn wait_for_initial_check(&self) { + let mut rx = self.check_complete_rx.clone(); + if *rx.borrow_and_update() { + return; + } + let _ = rx.changed().await; } async fn check_group( @@ -133,51 +103,33 @@ impl Servers { name: &str, ) { for server in servers { - if server.circuit_breaker.is_open() { - log_warn!("Skipping {} Server {}", name, server.url); - continue; - } - - let op = || async { - let resp = client.head(&server.url).send().await.map_err(|e| { - backoff::Error::>::transient(Box::new( - e, - )) - })?; - if !resp.status().is_success() { - return Err( - backoff::Error::>::transient( - format!("Status not success: {}", resp.status()).into(), - ), - ); - } - Ok(resp) - }; - - let backoff = ExponentialBackoff { - max_elapsed_time: Some(BACKOFF_MAX_ELAPSED), - ..Default::default() - }; - - match retry(backoff, op).await { - Ok(response) => { - log_info!( - "{} Server {} responded with: {}", - name, - server.url, - response.status() - ); - server.circuit_breaker.record_success(); - let mut lock = selected.write().unwrap(); - *lock = Some(server.clone()); - return; + match client.head(&server.url).send().await { + Ok(resp) => { + if resp.status().is_success() { + log_info!( + "{} Server {} responded with: {}", + name, + server.url, + resp.status() + ); + let mut lock = selected.write().unwrap(); + *lock = Some(server.clone()); + return; + } else { + log_warn!( + "{} Server {} returned status: {}", + name, + server.url, + resp.status() + ); + } } Err(e) => { log_warn!("Failed to connect to {} Server {}: {}", name, server.url, e); - server.circuit_breaker.record_failure(); } } } + let mut lock = selected.write().unwrap(); *lock = None; } diff --git a/src/components/features/social/InlineIRCChat.vue b/src/components/features/social/InlineIRCChat.vue index 32b365c..436028b 100644 --- a/src/components/features/social/InlineIRCChat.vue +++ b/src/components/features/social/InlineIRCChat.vue @@ -1,5 +1,6 @@ diff --git a/src/services/syncService.ts b/src/services/syncService.ts index 6105818..99e17f8 100644 --- a/src/services/syncService.ts +++ b/src/services/syncService.ts @@ -190,7 +190,14 @@ class SyncService { if (cloudData.favorites_data && Array.isArray(cloudData.favorites_data)) { try { - await invoke('set_all_favorites', { clientIds: cloudData.favorites_data }); + const currentFavorites = await invoke('get_favorite_clients'); + const favoritesChanged = + cloudData.favorites_data.length !== currentFavorites.length || + !cloudData.favorites_data.every((id: number) => currentFavorites.includes(id)); + + if (favoritesChanged) { + await invoke('set_all_favorites', { clientIds: cloudData.favorites_data }); + } } catch (e) { console.warn('Failed to set all favorites, falling back to loop', e); const currentFavorites = await invoke('get_favorite_clients'); From e0e261a493f7443ee3df9b764904521f206605db Mon Sep 17 00:00:00 2001 From: dest4590 Date: Fri, 12 Dec 2025 21:37:10 +0200 Subject: [PATCH 06/15] feat: i18n typo fix & linux permission JPS fix --- src-tauri/src/core/utils/process.rs | 23 +++++++++++++++++++++++ src/i18n/locales/en.json | 2 +- src/i18n/locales/ru.json | 2 +- src/i18n/locales/ua.json | 2 +- src/i18n/locales/zh_cn.json | 4 ++-- 5 files changed, 28 insertions(+), 5 deletions(-) diff --git a/src-tauri/src/core/utils/process.rs b/src-tauri/src/core/utils/process.rs index c98fbe7..5f027d0 100644 --- a/src-tauri/src/core/utils/process.rs +++ b/src-tauri/src/core/utils/process.rs @@ -28,6 +28,29 @@ pub fn execute_jps() -> Result { } let jps_path = get_jps_path(); + + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + if jps_path.exists() { + if let Ok(mut perms) = std::fs::metadata(&jps_path).map(|m| m.permissions()) { + let mode = perms.mode() & 0o777; + if mode != 0o755 { + perms.set_mode(0o755); + if let Err(e) = std::fs::set_permissions(&jps_path, perms) { + log_warn!( + "Failed to set exec perm on jps {}: {}", + jps_path.display(), + e + ); + } else { + log_debug!("Set exec perm on jps {}", jps_path.display()); + } + } + } + } + } + let mut command = Command::new(jps_path); #[cfg(target_os = "windows")] diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 34168ae..676749a 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -618,7 +618,7 @@ "irc": { "inline": { "title": "Global Chat", - "latest_activity_connected": "Connected and listening", + "latest_activity_connected": "Connected to IRC chat", "tap_to_connect": "Tap to connect to IRC", "placeholder": "Type a message...", "send_failed": "Failed to send message", diff --git a/src/i18n/locales/ru.json b/src/i18n/locales/ru.json index 0a40616..49efadd 100644 --- a/src/i18n/locales/ru.json +++ b/src/i18n/locales/ru.json @@ -618,7 +618,7 @@ "irc": { "inline": { "title": "Глобальный чат", - "latest_activity_connected": "Подключено и слушаем", + "latest_activity_connected": "Подключено к IRC серверу.", "tap_to_connect": "Нажмите, чтобы подключиться к IRC", "placeholder": "Введите сообщение...", "send_failed": "Не удалось отправить сообщение", diff --git a/src/i18n/locales/ua.json b/src/i18n/locales/ua.json index a2cf47e..250a639 100644 --- a/src/i18n/locales/ua.json +++ b/src/i18n/locales/ua.json @@ -618,7 +618,7 @@ "irc": { "inline": { "title": "Глобальний чат", - "latest_activity_connected": "Підключено та слухаємо", + "latest_activity_connected": "Підключено до IRC чату", "tap_to_connect": "Торкніться, щоб підключитись до IRC", "placeholder": "Введіть повідомлення...", "send_failed": "Не вдалося відправити повідомлення", diff --git a/src/i18n/locales/zh_cn.json b/src/i18n/locales/zh_cn.json index f682298..81bdc20 100644 --- a/src/i18n/locales/zh_cn.json +++ b/src/i18n/locales/zh_cn.json @@ -607,7 +607,7 @@ "irc": { "inline": { "title": "全局聊天", - "latest_activity_connected": "已连接并在监听", + "latest_activity_connected": "连接到IRC聊天", "tap_to_connect": "点击以连接到 IRC", "placeholder": "输入消息...", "send_failed": "发送消息失败", @@ -1103,4 +1103,4 @@ "download_complete": "下载完成", "installation_complete": "安装完成" } -} +} \ No newline at end of file From fa7fca823491d119d136d7efa01f4e40bdc48952 Mon Sep 17 00:00:00 2001 From: dest4590 Date: Sat, 13 Dec 2025 11:57:29 +0200 Subject: [PATCH 07/15] feat: IRC chat improve (Room State, Message Parse) --- .../features/social/InlineIRCChat.vue | 91 +++++++++++++------ src/composables/useIrcChat.ts | 29 +++++- 2 files changed, 91 insertions(+), 29 deletions(-) diff --git a/src/components/features/social/InlineIRCChat.vue b/src/components/features/social/InlineIRCChat.vue index 65b150c..d0f00ef 100644 --- a/src/components/features/social/InlineIRCChat.vue +++ b/src/components/features/social/InlineIRCChat.vue @@ -27,7 +27,7 @@
-
+
{{ statusMeta.label }}
+ + +
+
+ + {{ onlineUsers }} +
+
+ + {{ onlineGuests }} +
+
+
[{{ msg.time }}] - {{ - part.text }} + + +
@@ -90,7 +118,7 @@ import { useIrcChat } from '../../../composables/useIrcChat'; import { useI18n } from 'vue-i18n'; const attrs = useAttrs(); -const { messages, connected, status, sendIrcMessage, forceReconnect, ensureIrcConnection } = useIrcChat(); +const { messages, connected, status, sendIrcMessage, forceReconnect, ensureIrcConnection, onlineUsers, onlineGuests } = useIrcChat(); const { t } = useI18n(); const inputMessage = ref(''); const ircInput = ref(null); @@ -101,6 +129,15 @@ const isExpanded = ref(false); const messagesContainer = ref(null); const { addToast } = useToast(); +const getRoleColor = (role: string) => { + switch (role.toLowerCase()) { + case 'admin': return '#AA0000'; + case 'developer': return '#AA00AA'; + case 'moderator': return '#00AA00'; + default: return undefined; + } +}; + const parseMessage = (msg: string, type?: string) => { const colorMap: Record = { '0': '#000000', '1': '#0000AA', '2': '#00AA00', '3': '#00AAAA', @@ -109,6 +146,21 @@ const parseMessage = (msg: string, type?: string) => { 'c': '#FF5555', 'd': '#FF55FF', 'e': '#FFFF55', 'f': '#FFFFFF' }; + let contentToParse = msg; + + if (type !== 'system') { + const nameStripRegex = /^.*?(?= §7\(| \[§)/; + + if (nameStripRegex.test(msg)) { + contentToParse = msg.replace(nameStripRegex, ''); + } else { + const colonIndex = msg.indexOf(': '); + if (colonIndex !== -1 && colonIndex < 50 && !msg.toLowerCase().startsWith('system')) { + contentToParse = msg.substring(colonIndex + 2); + } + } + } + const parts: { text: string; color?: string; isName?: boolean; bold?: boolean }[] = []; let currentColor: string | undefined = undefined; let currentBold = false; @@ -117,23 +169,10 @@ const parseMessage = (msg: string, type?: string) => { let lastIndex = 0; let match; - const colonIndex = msg.indexOf(':'); - const headerEnd = colonIndex === -1 ? -1 : colonIndex; - const headerRaw = colonIndex === -1 ? '' : msg.substring(0, colonIndex); - const headerStripped = stripColorCodes(headerRaw).trim(); - const headerStrippedLower = headerStripped.toLowerCase(); - const headerFirstToken = headerStripped.split(/\s+/)[0] || ''; - const headerLooksLikeNick = (headerStripped.includes('(') || headerStripped.includes('[') || /^[A-Za-z0-9_\-]+$/.test(headerFirstToken)) - && !headerStrippedLower.startsWith('@') && !headerStrippedLower.includes('profile') && !/\b(id|ip|name):?/i.test(headerStripped); - let namePartMarked = false; - - while ((match = regex.exec(msg)) !== null) { - const text = msg.substring(lastIndex, match.index); + while ((match = regex.exec(contentToParse)) !== null) { + const text = contentToParse.substring(lastIndex, match.index); if (text) { - const isInHeader = headerEnd !== -1 && match.index <= headerEnd && lastIndex < headerEnd; - const isName = isInHeader && !namePartMarked && text.trim().length > 0 && type !== 'system' && headerStrippedLower !== 'system' && headerLooksLikeNick; - if (isName) namePartMarked = true; - parts.push({ text, color: currentColor, isName: !!isName, bold: currentBold }); + parts.push({ text, color: currentColor, isName: false, bold: currentBold }); } const code = match[1].toLowerCase(); @@ -149,11 +188,9 @@ const parseMessage = (msg: string, type?: string) => { lastIndex = regex.lastIndex; } - const remaining = msg.substring(lastIndex); + const remaining = contentToParse.substring(lastIndex); if (remaining) { - const isInHeader = headerEnd !== -1 && lastIndex < headerEnd; - const isName = isInHeader && !namePartMarked && remaining.trim().length > 0 && type !== 'system' && headerStrippedLower !== 'system' && headerLooksLikeNick; - parts.push({ text: remaining, color: currentColor, isName: !!isName, bold: currentBold }); + parts.push({ text: remaining, color: currentColor, isName: false, bold: currentBold }); } return parts; diff --git a/src/composables/useIrcChat.ts b/src/composables/useIrcChat.ts index abc96f5..4872729 100644 --- a/src/composables/useIrcChat.ts +++ b/src/composables/useIrcChat.ts @@ -3,11 +3,23 @@ import { invoke } from '@tauri-apps/api/core'; import { listen } from '@tauri-apps/api/event'; import { useToast } from '../services/toastService'; +interface SenderInfo { + username: string; + role: string; +} + +interface RoomState { + online_users: number; + online_guests: number; +} + interface IrcMessage { time: string; content: string; type?: string; isHistory?: boolean; + sender?: SenderInfo; + roomState?: RoomState; } interface IncomingIrcPayload { @@ -15,6 +27,8 @@ interface IncomingIrcPayload { time?: string; content?: string; history?: boolean; + sender?: SenderInfo; + room_state?: RoomState; } type IrcStatus = 'disconnected' | 'connecting' | 'connected' | 'reconnecting' | 'error'; @@ -25,6 +39,8 @@ const messages = ref([]); const connected = ref(false); const isConnecting = ref(false); const status = ref('disconnected'); +const onlineUsers = ref(0); +const onlineGuests = ref(0); let connectionPromise: Promise | null = null; let listenersRegistered = false; @@ -61,6 +77,8 @@ const parseIrcPayload = (payload: unknown): IrcMessage | null => { content: parsed.content || '', type: parsed.type, isHistory: Boolean(parsed.history), + sender: parsed.sender, + roomState: parsed.room_state, }; } catch { return { time: fallbackTime, content: payload, type: 'system' }; @@ -88,7 +106,12 @@ const registerListeners = async (): Promise => { await listen('irc-message', (event) => { const msg = parseIrcPayload(event.payload); if (msg) { - messages.value.push(msg); + if (msg.type === 'room_state' && msg.roomState) { + onlineUsers.value = msg.roomState.online_users; + onlineGuests.value = msg.roomState.online_guests; + } else { + messages.value.push(msg); + } } }); @@ -192,8 +215,10 @@ export function useIrcChat() { connected, isConnecting, status, + onlineUsers, + onlineGuests, ensureIrcConnection, forceReconnect, sendIrcMessage }; -} \ No newline at end of file +} From c9bd2948180a77cf75724262e0fb25b261c0ebb2 Mon Sep 17 00:00:00 2001 From: dest4590 Date: Sat, 13 Dec 2025 13:33:16 +0200 Subject: [PATCH 08/15] feat: added profile view in IRC Chat --- .../features/social/InlineIRCChat.vue | 52 ++++++++++++++++++- src/composables/useIrcChat.ts | 16 +++++- src/views/FriendsView.vue | 2 +- src/views/Home.vue | 2 +- 4 files changed, 67 insertions(+), 5 deletions(-) diff --git a/src/components/features/social/InlineIRCChat.vue b/src/components/features/social/InlineIRCChat.vue index d0f00ef..d6de853 100644 --- a/src/components/features/social/InlineIRCChat.vue +++ b/src/components/features/social/InlineIRCChat.vue @@ -60,6 +60,7 @@