fix: /local-workspace — chemin du header + clic sur un dossier du sidebar (v7.69.9)
Corrige deux régressions de /local-workspace :
- le chemin du header restait bloqué sur « Home / <workspace> » quel que
soit le dossier affiché : la route rend désormais breadcrumb_items
(Home / <workspace> / <dossier> / <sous-dossier>, niveaux cliquables,
collapse « … » au-delà de 4) et la navigation sans rechargement recalcule
le chemin via l'event flowdeck:breadcrumb-changed ;
- le clic sur un dossier du sidebar affichait TOUS les composants à la
fois : Alpine.data('wsInitData') retournait le même objet singleton, le
2e montage (navigation partielle) levait « Cannot redefine property:
\ » et initTree abandonnait, laissant tout le contenu au state
brut. La factory retourne désormais une enveloppe fraîche par montage
qui délègue à l'état réactif partagé. #lw-config est aussi relu à chaque
exécution (le 2e montage gardait le folder_id du 1er chargement).
Inclus également le travail en cours de l'arbre : Library (colonnes Last
visited/Source, ordre d'en-tête, favoris à icônes Workspace), Meeting
Notes (bloc, CSS, routes, docs), coloration de code hljs, badges
favori/publié dans l'arbre local-workspace, docs (DATA_MODEL,
architectures) et tests associés.
This commit is contained in:
+221
-25
@@ -361,13 +361,28 @@
|
||||
</div>
|
||||
<div class="sidebar-section-actions">
|
||||
{% if has_active_workspace %}
|
||||
<button class="section-action-btn" title="Add" @click.stop="fdCreatePage()">+</button>
|
||||
<button class="section-action-btn" title="New meeting note" @click.stop="fdCreateMeeting()">+</button>
|
||||
{% endif %}
|
||||
<button class="section-action-btn section-menu-btn" title="Section options" @click.stop="openSectionMenu($event, 'meetings')">⋮</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sidebar-section-items" x-show="sectionsOpen.meetings" x-transition>
|
||||
<ul class="sidebar-items"><li class="sidebar-item empty-hint"><span class="page-icon page-icon-svg">{{ fd_icon("calendar",16) }}</span><span class="page-name text-dim">No meetings</span></li></ul>
|
||||
<ul class="sidebar-items" id="sidebar-meetings-items">
|
||||
<template x-for="n in meetingNotes" :key="'mn-' + n.id">
|
||||
<li class="sidebar-item" @click="meetingNoteOpen(n)"
|
||||
@contextmenu.prevent="showWsContext($event, n.id, n.title, false)">
|
||||
<span class="page-icon page-icon-svg">{{ fd_icon("calendar",14) }}</span>
|
||||
<span style="flex:1;min-width:0;">
|
||||
<span class="page-name" style="display:block;" x-text="n.title"></span>
|
||||
<span class="sb-item-sub" x-text="n.has_summary ? 'Summary ready' : (n.status && n.status !== 'idle' ? n.status : 'Ready to record')"></span>
|
||||
</span>
|
||||
</li>
|
||||
</template>
|
||||
<li class="sidebar-item empty-hint" x-show="!meetingNotes.length">
|
||||
<span class="page-icon page-icon-svg">{{ fd_icon("calendar",16) }}</span>
|
||||
<span class="page-name text-dim">No meetings</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -662,6 +677,10 @@
|
||||
<div class="sb-filter-row">
|
||||
<button type="button" class="sb-filter-chip" :class="{ active: meetingFilter === 'upcoming' }" @click="meetingFilter = 'upcoming'">Upcoming</button>
|
||||
<button type="button" class="sb-filter-chip" :class="{ active: meetingFilter === 'today' }" @click="meetingFilter = 'today'">Today</button>
|
||||
<button type="button" class="sb-filter-chip" :class="{ active: meetingFilter === 'past' }" @click="meetingFilter = 'past'; loadMeetingHistory()">Past</button>
|
||||
</div>
|
||||
<div class="sb-search-row">
|
||||
<input type="search" class="sb-search-input" placeholder="Search meetings…" x-model="meetingSearch" @input="if (meetingFilter === 'past') loadMeetingHistory()" aria-label="Search meetings">
|
||||
</div>
|
||||
|
||||
<div class="sb-quick-actions">
|
||||
@@ -671,15 +690,18 @@
|
||||
</div>
|
||||
|
||||
<ul class="sidebar-items">
|
||||
<template x-for="ev in meetingVisible" :key="ev.collection_id + '-' + ev.id">
|
||||
<template x-for="ev in meetingVisible" :key="(ev.collection_id || 'h') + '-' + ev.id">
|
||||
<li class="sidebar-item" @click="meetingOpen(ev)">
|
||||
<span class="page-icon page-icon-svg">{{ fd_icon("calendar",14) }}</span>
|
||||
<span class="page-name" x-text="ev.title"></span>
|
||||
<span class="sb-item-meta" x-text="ev.today ? 'Today' : ev.date"></span>
|
||||
<span style="flex:1;min-width:0;">
|
||||
<span class="page-name" style="display:block;" x-text="ev.title"></span>
|
||||
<span class="sb-item-sub" x-text="meetingCta(ev)"></span>
|
||||
</span>
|
||||
<span class="sb-item-meta" x-text="ev.today ? 'Today' : (ev.date || '')"></span>
|
||||
</li>
|
||||
</template>
|
||||
<li class="sidebar-item empty-hint" x-show="!meetingsLoading && !meetingVisible.length">
|
||||
<span class="page-name text-dim" x-text="meetingFilter === 'today' ? 'Nothing scheduled today' : 'No upcoming meetings'"></span>
|
||||
<span class="page-name text-dim" x-text="meetingEmptyHint()"></span>
|
||||
</li>
|
||||
<li class="sidebar-item empty-hint" x-show="meetingsLoading">
|
||||
<span class="page-name text-dim">Loading…</span>
|
||||
@@ -719,18 +741,35 @@
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<!-- Footer : bouton "New" + popover "Create new" (vers le haut) -->
|
||||
<div class="sidebar-footer">
|
||||
<div style="display:flex; gap:6px;">
|
||||
<button class="sidebar-footer-btn" style="flex:1; justify-content:center; background:var(--bg-hover); border-radius:20px;">
|
||||
<span style="margin-right:4px;">{{ fd_icon("message-square",14) }}</span>New chat
|
||||
<span class="shortcut">Ctrl+O</span>
|
||||
<div class="fd-new-wrap" @click.outside="closeNewMenu()">
|
||||
<div class="fd-new-popover" x-show="newMenuOpen" x-cloak
|
||||
x-transition:enter="transition ease-out duration-150"
|
||||
x-transition:enter-start="opacity-0 translate-y-1"
|
||||
x-transition:enter-end="opacity-100 translate-y-0"
|
||||
x-transition:leave="transition ease-in duration-100"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0"
|
||||
role="menu" aria-label="Create new">
|
||||
<div class="fd-new-pop-header">Create new</div>
|
||||
<button type="button" class="fd-new-pop-item" role="menuitem" @click="newMenuAction('page')">
|
||||
<span>{{ fd_icon("file",16) }}</span><span>Page</span>
|
||||
</button>
|
||||
<button type="button" class="fd-new-pop-item" role="menuitem" @click="newMenuAction('chat')">
|
||||
<span>{{ fd_icon("message-square",16) }}</span><span>Chat</span>
|
||||
</button>
|
||||
<button type="button" class="fd-new-pop-item" role="menuitem" @click="newMenuAction('meeting')">
|
||||
<span><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="2" width="6" height="12" rx="3"/><path d="M5 10a7 7 0 0 0 14 0"/><line x1="12" y1="17" x2="12" y2="22"/><line x1="8" y1="22" x2="16" y2="22"/></svg></span><span>AI Meeting Notes</span>
|
||||
</button>
|
||||
<button type="button" class="fd-new-pop-item" role="menuitem" @click="newMenuAction('database')">
|
||||
<span>{{ fd_icon("grid",16) }}</span><span>Database</span>
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" class="fd-new-btn" :class="{ open: newMenuOpen }"
|
||||
@click="toggleNewMenu()" :aria-expanded="newMenuOpen" title="Create new">
|
||||
<span>{{ fd_icon("edit",16) }}</span><span>New</span>
|
||||
</button>
|
||||
{% if has_active_workspace %}
|
||||
<button class="sidebar-footer-btn" style="width:36px; justify-content:center; background:var(--bg-hover); border-radius:8px;" onclick="window.FlowDeck.createPage()" title="New page">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="12" y1="18" x2="12" y2="12"/><line x1="9" y1="15" x2="15" y2="15"/></svg>
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- /sidebar-scroll -->
|
||||
@@ -899,6 +938,9 @@
|
||||
</div>
|
||||
</template>
|
||||
<div class="context-menu-section">
|
||||
<div class="context-menu-item" @click="wsCtxAction('open')">
|
||||
<span class="menu-icon">{{ fd_icon("external-link",14) }}</span> Open
|
||||
</div>
|
||||
<div class="context-menu-item" @click="wsCtxAction('rename')">
|
||||
<span class="menu-icon">{{ fd_icon("edit",14) }}</span> Rename
|
||||
</div>
|
||||
@@ -1294,6 +1336,8 @@
|
||||
this.loadTeamspaces();
|
||||
// Badge counts on the collapsed tabs (lazy fetch, no blocking).
|
||||
this.loadInbox();
|
||||
// v7.1.1 : notes de réunion (section Meetings, pas Workspace).
|
||||
this.loadMeetingNotes();
|
||||
this.initSidebarWidth();
|
||||
document.addEventListener('fd-toggle-sidebar', () => this.toggleSidebar());
|
||||
this.startClipAutoRefresh();
|
||||
@@ -1310,7 +1354,7 @@
|
||||
});
|
||||
document.addEventListener('flowdeck:workspace-changed', () => {
|
||||
clearTimeout(self._clipRefreshTimer);
|
||||
self._clipRefreshTimer = setTimeout(() => self.refreshSidebarTree(), 400);
|
||||
self._clipRefreshTimer = setTimeout(() => { self.refreshSidebarTree(); self.loadMeetingNotes(); }, 400);
|
||||
});
|
||||
// Also refresh when tab becomes visible (user returns to FlowDeck after clipping)
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
@@ -1451,15 +1495,37 @@
|
||||
|
||||
// ── Meeting tab ──
|
||||
meetings: [],
|
||||
meetingPast: [],
|
||||
meetingsLoading: false,
|
||||
meetingFilter: 'upcoming',
|
||||
meetingSearch: '',
|
||||
meetingHistoryLoaded: false,
|
||||
get meetingVisible() {
|
||||
if (this.meetingFilter === 'today') {
|
||||
return this.meetings.filter(function(e) { return e.today; });
|
||||
var q = (this.meetingSearch || '').trim().toLowerCase();
|
||||
function matches(e) {
|
||||
if (!q) return true;
|
||||
return ((e.title || '') + ' ' + (e.date || '')).toLowerCase().indexOf(q) >= 0;
|
||||
}
|
||||
return this.meetings.filter(function(e) { return !e.today; });
|
||||
if (this.meetingFilter === 'today') {
|
||||
return this.meetings.filter(function(e) { return e.today && matches(e); });
|
||||
}
|
||||
if (this.meetingFilter === 'past') {
|
||||
return this.meetingPast.filter(matches);
|
||||
}
|
||||
return this.meetings.filter(function(e) { return !e.today && matches(e); });
|
||||
},
|
||||
get meetingToday() { return this.meetings.filter(function(e) { return e.today; }); },
|
||||
meetingEmptyHint() {
|
||||
if (this.meetingFilter === 'today') return 'Nothing scheduled today';
|
||||
if (this.meetingFilter === 'past') return 'No past meeting notes yet';
|
||||
return 'No upcoming meetings';
|
||||
},
|
||||
meetingCta(ev) {
|
||||
if (ev.status === 'done' || ev.summary) return 'View summary';
|
||||
if (ev.today) return 'Join & transcribe';
|
||||
if (ev.page_id) return 'Open note';
|
||||
return 'Prepare';
|
||||
},
|
||||
loadMeetings(force) {
|
||||
var self = this;
|
||||
if (!force && this.meetings.length) return Promise.resolve();
|
||||
@@ -1472,8 +1538,38 @@
|
||||
})
|
||||
.catch(function() { self.meetings = []; self.meetingsLoading = false; });
|
||||
},
|
||||
loadMeetingHistory() {
|
||||
var self = this;
|
||||
this.meetingsLoading = true;
|
||||
var q = encodeURIComponent((this.meetingSearch || '').trim());
|
||||
return fetch('/api/v2/meetings/history?query=' + q + '&limit=50', { credentials: 'same-origin' })
|
||||
.then(function(r) { return r.ok ? r.json() : { meetings: [] }; })
|
||||
.then(function(d) {
|
||||
self.meetingPast = (d && d.meetings) || [];
|
||||
self.meetingHistoryLoaded = true;
|
||||
self.meetingsLoading = false;
|
||||
})
|
||||
.catch(function() { self.meetingPast = []; self.meetingsLoading = false; });
|
||||
},
|
||||
meetingOpen(ev) {
|
||||
var target = ev.url || ('/db/' + ev.collection_id);
|
||||
var target = ev.url || (ev.page_id ? ('/pages/' + ev.page_id) : ('/db/' + ev.collection_id));
|
||||
if (ev.today) {
|
||||
try { this.toast('Opening today\u2019s meeting — press Start transcribing in the note.'); } catch (e) {}
|
||||
}
|
||||
if (window.fdNavigate) window.fdNavigate(target);
|
||||
else window.location.href = target;
|
||||
},
|
||||
// ── v7.1.1 : notes avec bloc AI Meeting Notes → section Meetings ──
|
||||
meetingNotes: [],
|
||||
loadMeetingNotes() {
|
||||
var self = this;
|
||||
return fetch('/api/v2/meetings/notes?limit=50', { credentials: 'same-origin' })
|
||||
.then(function(r) { return r.ok ? r.json() : { notes: [] }; })
|
||||
.then(function(d) { self.meetingNotes = (d && d.notes) || []; })
|
||||
.catch(function() { self.meetingNotes = []; });
|
||||
},
|
||||
meetingNoteOpen(n) {
|
||||
var target = '/pages/' + n.id;
|
||||
if (window.fdNavigate) window.fdNavigate(target);
|
||||
else window.location.href = target;
|
||||
},
|
||||
@@ -1614,6 +1710,15 @@
|
||||
this.wsCtx.visible = false;
|
||||
if (!id) return;
|
||||
switch(action) {
|
||||
case 'open':
|
||||
if (this.wsCtx.isFolder) {
|
||||
if (typeof this.navigateToFolder === 'function') this.navigateToFolder(id);
|
||||
} else {
|
||||
var target = '/pages/' + id;
|
||||
if (window.fdNavigate) window.fdNavigate(target);
|
||||
else window.location.href = target;
|
||||
}
|
||||
break;
|
||||
case 'new-file':
|
||||
this.newPageInFolder(id);
|
||||
break;
|
||||
@@ -1632,6 +1737,7 @@
|
||||
if (r.ok) {
|
||||
if (window.FlowDeck && window.FlowDeck.syncPageTitle) window.FlowDeck.syncPageTitle(id, newName.trim());
|
||||
self.refreshSidebarTree();
|
||||
if (typeof self.loadMeetingNotes === 'function') self.loadMeetingNotes();
|
||||
document.dispatchEvent(new CustomEvent('flowdeck:workspace-changed'));
|
||||
if (window._wsData && window._wsData._reloadAfterAction) {
|
||||
window._wsData._reloadAfterAction();
|
||||
@@ -1644,6 +1750,7 @@
|
||||
if (!id) break;
|
||||
if (!confirm('Delete "' + name + '"?')) break;
|
||||
this.deleteWorkspacePage(id, true);
|
||||
if (typeof this.loadMeetingNotes === 'function') this.loadMeetingNotes();
|
||||
break;
|
||||
}
|
||||
},
|
||||
@@ -1685,20 +1792,58 @@
|
||||
});
|
||||
},
|
||||
|
||||
// ── Sections collapsible ──
|
||||
// ── Sections collapsible (accordéon v7.70 : une seule ouverte à la fois) ──
|
||||
sectionsOpen: (function() {
|
||||
var def = { recents: true, private: true, meetings: true, favorites: true, agents: true, teamspaces: true, shared: true, published: true, apps: true, workspace: true, gitea: true };
|
||||
var order = ['workspace','gitea','meetings','recents','favorites','agents','teamspaces','shared','published','private','apps'];
|
||||
var def = { recents: false, private: false, meetings: false, favorites: false, agents: false, teamspaces: false, shared: false, published: false, apps: false, workspace: true, gitea: false };
|
||||
try {
|
||||
var saved = JSON.parse(localStorage.getItem('fd_sections') || '{}');
|
||||
return Object.assign(def, saved);
|
||||
var merged = Object.assign({}, def, saved);
|
||||
// Normalise : si plusieurs sections ouvertes (ancien état), ne garder que la première.
|
||||
var first = null;
|
||||
for (var i = 0; i < order.length; i++) {
|
||||
if (merged[order[i]]) { first = order[i]; break; }
|
||||
}
|
||||
if (first) {
|
||||
for (var j = 0; j < order.length; j++) merged[order[j]] = (order[j] === first);
|
||||
} else {
|
||||
merged.workspace = true;
|
||||
}
|
||||
return merged;
|
||||
} catch(e) { return def; }
|
||||
})(),
|
||||
toggleSection(key) {
|
||||
this.sectionsOpen[key] = !this.sectionsOpen[key];
|
||||
var isOpen = !!this.sectionsOpen[key];
|
||||
var order = ['workspace','gitea','meetings','recents','favorites','agents','teamspaces','shared','published','private','apps'];
|
||||
if (isOpen) {
|
||||
this.sectionsOpen[key] = false;
|
||||
} else {
|
||||
for (var i = 0; i < order.length; i++) this.sectionsOpen[order[i]] = false;
|
||||
this.sectionsOpen[key] = true;
|
||||
}
|
||||
this.sectionsOpen = Object.assign({}, this.sectionsOpen);
|
||||
try { localStorage.setItem('fd_sections', JSON.stringify(this.sectionsOpen)); }
|
||||
catch(e) {}
|
||||
},
|
||||
// ── Menu "New" (popover "Create new" vers le haut) ──
|
||||
newMenuOpen: false,
|
||||
toggleNewMenu() { this.newMenuOpen = !this.newMenuOpen; },
|
||||
closeNewMenu() { this.newMenuOpen = false; },
|
||||
newMenuAction(kind) {
|
||||
this.newMenuOpen = false;
|
||||
if (kind === 'page') {
|
||||
if (window.FlowDeck && window.FlowDeck.createPage) window.FlowDeck.createPage();
|
||||
} else if (kind === 'chat') {
|
||||
if (typeof this.chatNewConversation === 'function') this.chatNewConversation();
|
||||
else if (typeof this.agentNew === 'function') this.agentNew();
|
||||
} else if (kind === 'meeting') {
|
||||
if (typeof this.fdCreateMeeting === 'function') this.fdCreateMeeting();
|
||||
else if (typeof this.meetingNote === 'function') this.meetingNote();
|
||||
} else if (kind === 'database') {
|
||||
if (typeof this.createDatabase === 'function') this.createDatabase();
|
||||
else if (window.showToast) window.showToast('Database creation: use a Gitea repo as a database.', 'info');
|
||||
}
|
||||
},
|
||||
// ── Sidebar customization ──
|
||||
customizing: false,
|
||||
sidebarConfig: {},
|
||||
@@ -1933,6 +2078,50 @@
|
||||
});
|
||||
},
|
||||
fdCreatePage() { window.FlowDeck.createPage(); },
|
||||
fdCreateMeeting() {
|
||||
// v7.1.1 : crée une note de réunion vide avec son bloc
|
||||
// AI Meeting Notes déjà inséré (même action que +, New, /meet).
|
||||
var self = this;
|
||||
var iso = new Date().toISOString().slice(0, 10);
|
||||
var title = 'Meeting @' + iso;
|
||||
fetch('/api/local-workspace/items', {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': this.getCsrfToken() },
|
||||
body: JSON.stringify({ name: title, type: 'page' })
|
||||
})
|
||||
.then(function(r) { if (!r.ok) throw new Error('page creation (' + r.status + ')'); return r.json(); })
|
||||
.then(function(page) {
|
||||
var block = {
|
||||
id: 'b1_' + Date.now().toString(36), type: 'meeting', content: '',
|
||||
meeting: {
|
||||
v: 2, title: title, date: iso, state: 'idle', mode: 'mic_only',
|
||||
instruction: 'auto', lang: 'fr', consent: null, consentAudio: false,
|
||||
tab: 'notes', channels: [], segments: [], segSeq: 0, notes: '',
|
||||
manual: '', interim: '', startedAt: 0, elapsedBase: 0, pausedMs: 0,
|
||||
steps: [], summaryMd: '', completeness: '', genTitle: '',
|
||||
titleEdited: false, shareHidden: false, bannerHidden: false,
|
||||
transcriptId: null, error: '', remoteSilentWarned: false,
|
||||
menuOpen: false, settingsOpen: false
|
||||
}
|
||||
};
|
||||
// Amorçage du bloc : en cas d'échec la page s'ouvre quand même
|
||||
// (l'utilisateur ajoute le bloc via /meet).
|
||||
return fetch('/board/api/pages/' + page.id + '/blocks', {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': self.getCsrfToken() },
|
||||
body: JSON.stringify({ title: title, blocks: [block] })
|
||||
}).then(function() { return page; }).catch(function() { return page; });
|
||||
})
|
||||
.then(function(page) {
|
||||
if (typeof self.loadMeetingNotes === 'function') self.loadMeetingNotes();
|
||||
if (typeof self.refreshSidebarTree === 'function') self.refreshSidebarTree();
|
||||
if (window.fdNavigate) window.fdNavigate('/pages/' + page.id);
|
||||
else window.location.href = '/pages/' + page.id;
|
||||
})
|
||||
.catch(function(err) { self.toast('Meeting note creation failed: ' + err.message, 'error'); });
|
||||
},
|
||||
fdCreateFolder() { window.FlowDeck.showCreateFolderModal(); },
|
||||
fdGwRefresh() { if (window._gwData) window._gwData.refreshTree(); },
|
||||
sidebarSections() {
|
||||
@@ -2296,6 +2485,12 @@
|
||||
const manageLink = ul.querySelector('li:last-child');
|
||||
ul.innerHTML = html;
|
||||
if (manageLink) ul.appendChild(manageLink);
|
||||
// Le HTML injecté porte des directives Alpine (@contextmenu,
|
||||
// x-show des dossiers, drag&drop) : les monter explicitement,
|
||||
// sinon l'arbre perd menu contextuel et toggles après refresh.
|
||||
try {
|
||||
if (window.Alpine && window.Alpine.initTree) window.Alpine.initTree(ul);
|
||||
} catch (e) { console.error('Sidebar Alpine remount failed:', e); }
|
||||
})
|
||||
.catch(e => console.error('Sidebar refresh failed:', e));
|
||||
},
|
||||
@@ -2526,6 +2721,7 @@
|
||||
if (data.contextMenu) data.contextMenu.visible = false;
|
||||
if (data.wsCtx) data.wsCtx.visible = false;
|
||||
if (data.showNewPageMenu) data.showNewPageMenu = false;
|
||||
if (data.newMenuOpen) data.newMenuOpen = false;
|
||||
}
|
||||
}
|
||||
else if (e.ctrlKey && e.key === 's') {
|
||||
|
||||
Reference in New Issue
Block a user