Add session modal and book tabs
This commit is contained in:
+420
-49
@@ -25,6 +25,10 @@ type TimelineEntry = {
|
||||
type StudySession = {
|
||||
id: string
|
||||
title: string
|
||||
baseTranslation: string
|
||||
footnotesEnabled: boolean
|
||||
strongsEnabled: boolean
|
||||
timelineEnabled: boolean
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
selectedId: number | null
|
||||
@@ -49,6 +53,10 @@ function createSession(title = 'Study Session'): StudySession {
|
||||
return {
|
||||
id,
|
||||
title,
|
||||
baseTranslation: 'kjv-eng-kjv2006',
|
||||
footnotesEnabled: true,
|
||||
strongsEnabled: true,
|
||||
timelineEnabled: true,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
selectedId: null,
|
||||
@@ -68,7 +76,83 @@ function formatTimelineTime(value: string) {
|
||||
return new Date(value).toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })
|
||||
}
|
||||
|
||||
function abbreviateBook(book: string) {
|
||||
const abbreviations: Record<string, string> = {
|
||||
Genesis: 'Gen',
|
||||
Exodus: 'Ex',
|
||||
Leviticus: 'Lev',
|
||||
Numbers: 'Num',
|
||||
Deuteronomy: 'Deut',
|
||||
Joshua: 'Josh',
|
||||
Judges: 'Judg',
|
||||
Ruth: 'Ruth',
|
||||
'1 Samuel': '1 Sam',
|
||||
'2 Samuel': '2 Sam',
|
||||
'1 Kings': '1 Kgs',
|
||||
'2 Kings': '2 Kgs',
|
||||
'1 Chronicles': '1 Chr',
|
||||
'2 Chronicles': '2 Chr',
|
||||
Ezra: 'Ezra',
|
||||
Nehemiah: 'Neh',
|
||||
Esther: 'Est',
|
||||
Job: 'Job',
|
||||
Psalms: 'Ps',
|
||||
Proverbs: 'Prov',
|
||||
Ecclesiastes: 'Eccl',
|
||||
'Song of Solomon': 'Song',
|
||||
Isaiah: 'Isa',
|
||||
Jeremiah: 'Jer',
|
||||
Lamentations: 'Lam',
|
||||
Ezekiel: 'Ezek',
|
||||
Daniel: 'Dan',
|
||||
Hosea: 'Hos',
|
||||
Joel: 'Joel',
|
||||
Amos: 'Amos',
|
||||
Obadiah: 'Ob',
|
||||
Jonah: 'Jon',
|
||||
Micah: 'Mic',
|
||||
Nahum: 'Nah',
|
||||
Habakkuk: 'Hab',
|
||||
Zephaniah: 'Zeph',
|
||||
Haggai: 'Hag',
|
||||
Zechariah: 'Zech',
|
||||
Malachi: 'Mal',
|
||||
Matthew: 'Matt',
|
||||
Mark: 'Mark',
|
||||
Luke: 'Luke',
|
||||
John: 'John',
|
||||
Acts: 'Acts',
|
||||
Romans: 'Rom',
|
||||
'1 Corinthians': '1 Cor',
|
||||
'2 Corinthians': '2 Cor',
|
||||
Galatians: 'Gal',
|
||||
Ephesians: 'Eph',
|
||||
Philippians: 'Phil',
|
||||
Colossians: 'Col',
|
||||
'1 Thessalonians': '1 Th',
|
||||
'2 Thessalonians': '2 Th',
|
||||
'1 Timothy': '1 Tim',
|
||||
'2 Timothy': '2 Tim',
|
||||
Titus: 'Titus',
|
||||
Philemon: 'Phlm',
|
||||
Hebrews: 'Heb',
|
||||
James: 'Jas',
|
||||
'1 Peter': '1 Pet',
|
||||
'2 Peter': '2 Pet',
|
||||
'1 John': '1 Jn',
|
||||
'2 John': '2 Jn',
|
||||
'3 John': '3 Jn',
|
||||
Jude: 'Jude',
|
||||
Revelation: 'Rev',
|
||||
}
|
||||
|
||||
return abbreviations[book] ?? book.slice(0, 4)
|
||||
}
|
||||
|
||||
function App() {
|
||||
const [railOpen, setRailOpen] = useState(false)
|
||||
const [sessionModalOpen, setSessionModalOpen] = useState(false)
|
||||
const [aboutModalOpen, setAboutModalOpen] = useState(false)
|
||||
const [viewMode, setViewMode] = useState<ViewMode>('notes')
|
||||
const [query, setQuery] = useState('')
|
||||
const [libraryVerses, setLibraryVerses] = useState<Verse[]>([])
|
||||
@@ -81,6 +165,10 @@ function App() {
|
||||
const [noteDraft, setNoteDraft] = useState('')
|
||||
const [sessions, setSessions] = useState<StudySession[]>([])
|
||||
const [activeSessionId, setActiveSessionId] = useState('')
|
||||
const [newSessionTitle, setNewSessionTitle] = useState('New KJV Study')
|
||||
const [newSessionFootnotes, setNewSessionFootnotes] = useState(true)
|
||||
const [newSessionStrongs, setNewSessionStrongs] = useState(true)
|
||||
const [newSessionTimeline, setNewSessionTimeline] = useState(true)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const restoredSessionId = useRef('')
|
||||
@@ -125,7 +213,10 @@ function App() {
|
||||
noteDraft,
|
||||
...patch,
|
||||
updatedAt: at,
|
||||
timeline: [nextEntry, ...session.timeline].slice(0, timelineLimit),
|
||||
timeline:
|
||||
session.timelineEnabled === false
|
||||
? session.timeline
|
||||
: [nextEntry, ...session.timeline].slice(0, timelineLimit),
|
||||
}
|
||||
: session,
|
||||
),
|
||||
@@ -188,6 +279,15 @@ function App() {
|
||||
nextSessions = [createSession('KJV Study Session')]
|
||||
}
|
||||
|
||||
nextSessions = nextSessions.map((session) => ({
|
||||
...createSession(session.title),
|
||||
...session,
|
||||
baseTranslation: session.baseTranslation ?? 'kjv-eng-kjv2006',
|
||||
footnotesEnabled: session.footnotesEnabled ?? true,
|
||||
strongsEnabled: session.strongsEnabled ?? true,
|
||||
timelineEnabled: session.timelineEnabled ?? true,
|
||||
}))
|
||||
|
||||
setSessions(nextSessions)
|
||||
setActiveSessionId(
|
||||
nextSessions.some((session) => session.id === savedActiveSessionId)
|
||||
@@ -444,9 +544,38 @@ function App() {
|
||||
}
|
||||
|
||||
function startNewSession() {
|
||||
const nextSession = createSession(`Study Session ${sessions.length + 1}`)
|
||||
const nextSession = {
|
||||
...createSession(newSessionTitle.trim() || `Study Session ${sessions.length + 1}`),
|
||||
footnotesEnabled: newSessionFootnotes,
|
||||
strongsEnabled: newSessionStrongs,
|
||||
timelineEnabled: newSessionTimeline,
|
||||
}
|
||||
setSessions((current) => [nextSession, ...current])
|
||||
setActiveSessionId(nextSession.id)
|
||||
setSessionModalOpen(false)
|
||||
}
|
||||
|
||||
function setActiveSessionOption(patch: Partial<StudySession>) {
|
||||
patchActiveSession(patch)
|
||||
if (patch.strongsEnabled === false) {
|
||||
setStrongDisplay('off')
|
||||
}
|
||||
if (typeof patch.strongsEnabled === 'boolean' || typeof patch.footnotesEnabled === 'boolean') {
|
||||
recordTimeline({ type: 'setting', label: 'Updated session resources' }, patch)
|
||||
}
|
||||
}
|
||||
|
||||
function exportActiveSession() {
|
||||
if (!activeSession) return
|
||||
const blob = new Blob([JSON.stringify(activeSession, null, 2)], {
|
||||
type: 'application/json',
|
||||
})
|
||||
const url = URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = `${activeSession.title.toLowerCase().replace(/[^a-z0-9]+/g, '-') || 'study-session'}.json`
|
||||
link.click()
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
function restoreTimelineEntry(entry: TimelineEntry) {
|
||||
@@ -467,7 +596,7 @@ function App() {
|
||||
}
|
||||
|
||||
function renderVerseText(verse: Verse) {
|
||||
if (!verse.strongs.length || strongDisplay === 'off') return verse.text
|
||||
if (!activeSession?.strongsEnabled || !verse.strongs.length || strongDisplay === 'off') return verse.text
|
||||
|
||||
const parts: ReactNode[] = []
|
||||
let cursor = 0
|
||||
@@ -597,11 +726,15 @@ function App() {
|
||||
<aside className="study-margin" aria-label="Study margin">
|
||||
<div className="panel-header">
|
||||
<h2>Study Margin</h2>
|
||||
<span>{selectedVerse.strongs.length} links</span>
|
||||
<span>{activeSession?.strongsEnabled === false ? 'disabled' : `${selectedVerse.strongs.length} links`}</span>
|
||||
</div>
|
||||
<p className="margin-reference">{reference(selectedVerse)}</p>
|
||||
{renderOriginalLanguageLine()}
|
||||
<div className="linked-word-list" aria-label="Linked Strong's words">
|
||||
{activeSession?.strongsEnabled === false ? (
|
||||
<p className="resource-disabled">Strong's links are disabled for this session.</p>
|
||||
) : (
|
||||
<>
|
||||
{renderOriginalLanguageLine()}
|
||||
<div className="linked-word-list" aria-label="Linked Strong's words">
|
||||
{selectedVerse.strongs.map((entry) => (
|
||||
<button
|
||||
key={`${entry.strong}-${entry.position}`}
|
||||
@@ -614,26 +747,37 @@ function App() {
|
||||
<small>{entry.strong}</small>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{selectedStrongEntry ? (
|
||||
<article className="strong-focus">
|
||||
<div>
|
||||
<strong>{selectedStrongEntry.strong}</strong>
|
||||
<span>{selectedStrongEntry.language}</span>
|
||||
</div>
|
||||
<h4>{selectedStrongEntry.lemma}</h4>
|
||||
<p className="translit">{selectedStrongEntry.transliteration}</p>
|
||||
<p>{selectedStrongEntry.definition}</p>
|
||||
<button type="button" onClick={() => searchStrong(selectedStrongEntry.strong)}>
|
||||
Show matching verses
|
||||
</button>
|
||||
</article>
|
||||
) : null}
|
||||
{selectedStrongEntry ? (
|
||||
<article className="strong-focus">
|
||||
<div>
|
||||
<strong>{selectedStrongEntry.strong}</strong>
|
||||
<span>{selectedStrongEntry.language}</span>
|
||||
</div>
|
||||
<h4>{selectedStrongEntry.lemma}</h4>
|
||||
<p className="translit">{selectedStrongEntry.transliteration}</p>
|
||||
<p>{selectedStrongEntry.definition}</p>
|
||||
<button type="button" onClick={() => searchStrong(selectedStrongEntry.strong)}>
|
||||
Show matching verses
|
||||
</button>
|
||||
</article>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
|
||||
function renderFootnoteStrip() {
|
||||
if (activeSession?.footnotesEnabled === false) {
|
||||
return (
|
||||
<section className="footnote-strip">
|
||||
<strong>Footnotes</strong>
|
||||
<span>Footnotes are disabled for this session.</span>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
const visibleFootnotes = selectedFootnotes.length ? selectedFootnotes : pageFootnotes.slice(0, 4)
|
||||
return (
|
||||
<section className="footnote-strip">
|
||||
@@ -665,14 +809,261 @@ function App() {
|
||||
)
|
||||
}
|
||||
|
||||
function renderBookTabs() {
|
||||
if (!selectedVerse) return null
|
||||
|
||||
return (
|
||||
<nav className="book-tabs" aria-label="Bible book tabs">
|
||||
{books.map((book) => (
|
||||
<button
|
||||
key={book}
|
||||
type="button"
|
||||
className={book === selectedVerse.book ? 'active' : ''}
|
||||
title={book}
|
||||
onClick={() => jumpTo(book, 1, 1)}
|
||||
>
|
||||
{abbreviateBook(book)}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
function renderSessionModal() {
|
||||
if (!sessionModalOpen) return null
|
||||
|
||||
return (
|
||||
<div className="modal-backdrop" role="presentation" onMouseDown={() => setSessionModalOpen(false)}>
|
||||
<section
|
||||
className="modal-panel session-modal"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="Session management"
|
||||
onMouseDown={(event) => event.stopPropagation()}
|
||||
>
|
||||
<header>
|
||||
<div>
|
||||
<span className="eyebrow">Session</span>
|
||||
<h2>Manage Study Session</h2>
|
||||
</div>
|
||||
<button type="button" className="close-button" onClick={() => setSessionModalOpen(false)}>
|
||||
Close
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div className="modal-grid">
|
||||
<section className="modal-section">
|
||||
<h3>Current Session</h3>
|
||||
<label>
|
||||
Active session
|
||||
<select value={activeSessionId} onChange={(event) => setActiveSessionId(event.target.value)}>
|
||||
{sessions.map((session) => (
|
||||
<option key={session.id} value={session.id}>
|
||||
{session.title}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Session title
|
||||
<input
|
||||
value={activeSession?.title ?? ''}
|
||||
onChange={(event) => renameActiveSession(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<dl className="session-facts">
|
||||
<div>
|
||||
<dt>Translation</dt>
|
||||
<dd>KJV</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Current page</dt>
|
||||
<dd>{pageRange}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Timeline</dt>
|
||||
<dd>{activeSession?.timeline.length ?? 0} events</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Started</dt>
|
||||
<dd>{activeSession ? new Date(activeSession.createdAt).toLocaleString() : 'Not started'}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<div className="resource-toggles">
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={activeSession?.strongsEnabled ?? true}
|
||||
onChange={(event) => setActiveSessionOption({ strongsEnabled: event.target.checked })}
|
||||
/>
|
||||
Strong's links
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={activeSession?.footnotesEnabled ?? true}
|
||||
onChange={(event) => setActiveSessionOption({ footnotesEnabled: event.target.checked })}
|
||||
/>
|
||||
Footnotes
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={activeSession?.timelineEnabled ?? true}
|
||||
onChange={(event) => setActiveSessionOption({ timelineEnabled: event.target.checked })}
|
||||
/>
|
||||
Record timeline
|
||||
</label>
|
||||
</div>
|
||||
<button type="button" onClick={exportActiveSession}>
|
||||
Export session
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<section className="modal-section">
|
||||
<h3>New Session</h3>
|
||||
<label>
|
||||
Session title
|
||||
<input value={newSessionTitle} onChange={(event) => setNewSessionTitle(event.target.value)} />
|
||||
</label>
|
||||
<label>
|
||||
Base translation
|
||||
<select value="kjv-eng-kjv2006" onChange={() => null}>
|
||||
<option value="kjv-eng-kjv2006">King James Version</option>
|
||||
</select>
|
||||
</label>
|
||||
<div className="resource-toggles">
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={newSessionStrongs}
|
||||
onChange={(event) => setNewSessionStrongs(event.target.checked)}
|
||||
/>
|
||||
Include Strong's links
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={newSessionFootnotes}
|
||||
onChange={(event) => setNewSessionFootnotes(event.target.checked)}
|
||||
/>
|
||||
Include footnotes
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={newSessionTimeline}
|
||||
onChange={(event) => setNewSessionTimeline(event.target.checked)}
|
||||
/>
|
||||
Record timeline
|
||||
</label>
|
||||
</div>
|
||||
<button type="button" onClick={startNewSession}>
|
||||
Create session
|
||||
</button>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function renderAboutModal() {
|
||||
if (!aboutModalOpen) return null
|
||||
|
||||
return (
|
||||
<div className="modal-backdrop" role="presentation" onMouseDown={() => setAboutModalOpen(false)}>
|
||||
<section
|
||||
className="modal-panel about-modal"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="About Libre Study"
|
||||
onMouseDown={(event) => event.stopPropagation()}
|
||||
>
|
||||
<header>
|
||||
<div>
|
||||
<span className="eyebrow">About</span>
|
||||
<h2>Libre Study</h2>
|
||||
</div>
|
||||
<button type="button" className="close-button" onClick={() => setAboutModalOpen(false)}>
|
||||
Close
|
||||
</button>
|
||||
</header>
|
||||
<p>
|
||||
Libre Study is a free Bible study application built by Christ IT. The project
|
||||
started in July 2026 as an open study tool focused on readable Bible pages,
|
||||
linked study data, notes, and reusable public Bible packages.
|
||||
</p>
|
||||
<dl className="about-list">
|
||||
<div>
|
||||
<dt>Project</dt>
|
||||
<dd>Christ IT / Libre Study</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Current Bible package</dt>
|
||||
<dd>King James Version, public domain KJV text package with Strong's-linked study data.</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Software stack</dt>
|
||||
<dd>Tauri, TypeScript, React, SQLite, Rust, Vite.</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Source data catalog</dt>
|
||||
<dd>LibreBible packages are maintained separately so this app and future sites can consume the same resources.</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<div className="about-links">
|
||||
<a href="https://christit.com">ChristIT.com</a>
|
||||
<a href="https://git.christit.com/libre-study/libre-study">Libre Study Git</a>
|
||||
<a href="https://git.christit.com/libre-study/libre-bible-data">LibreBible Data Git</a>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="app-shell">
|
||||
<aside className="tool-rail" aria-label="Main tools">
|
||||
<main className={railOpen ? 'app-shell menu-open' : 'app-shell'}>
|
||||
<aside
|
||||
className="tool-rail"
|
||||
aria-label="Main tools"
|
||||
onMouseEnter={() => setRailOpen(true)}
|
||||
onMouseLeave={() => setRailOpen(false)}
|
||||
onFocus={() => setRailOpen(true)}
|
||||
onBlur={(event) => {
|
||||
if (!event.currentTarget.contains(event.relatedTarget)) setRailOpen(false)
|
||||
}}
|
||||
>
|
||||
<button type="button" className="menu-button" title="Menu" onClick={() => setRailOpen((current) => !current)}>
|
||||
<span />
|
||||
<span />
|
||||
<span />
|
||||
</button>
|
||||
<span className="brand-mark">LS</span>
|
||||
<button type="button" title="Bible page" onClick={() => setWorkspaceView('page')}>B</button>
|
||||
<button type="button" title="Study margin" onClick={() => setWorkspaceView('study')}>M</button>
|
||||
<button type="button" title="Notes workspace" onClick={() => setWorkspaceView('notes')}>N</button>
|
||||
<button type="button" title="Compare translations" onClick={() => setWorkspaceView('compare')}>C</button>
|
||||
<button type="button" title="Bible page" onClick={() => setWorkspaceView('page')}>
|
||||
<span className="rail-icon">B</span>
|
||||
<span className="rail-copy"><strong>Bible</strong><small>Page-only reading</small></span>
|
||||
</button>
|
||||
<button type="button" title="Study margin" onClick={() => setWorkspaceView('study')}>
|
||||
<span className="rail-icon">M</span>
|
||||
<span className="rail-copy"><strong>Margin</strong><small>References and linked words</small></span>
|
||||
</button>
|
||||
<button type="button" title="Notes workspace" onClick={() => setWorkspaceView('notes')}>
|
||||
<span className="rail-icon">N</span>
|
||||
<span className="rail-copy"><strong>Notes</strong><small>Study notes and timeline</small></span>
|
||||
</button>
|
||||
<button type="button" title="Compare translations" onClick={() => setWorkspaceView('compare')}>
|
||||
<span className="rail-icon">C</span>
|
||||
<span className="rail-copy"><strong>Compare</strong><small>Translation comparison</small></span>
|
||||
</button>
|
||||
<button type="button" title="Session management" onClick={() => setSessionModalOpen(true)}>
|
||||
<span className="rail-icon">S</span>
|
||||
<span className="rail-copy"><strong>Sessions</strong><small>Save, export, and configure</small></span>
|
||||
</button>
|
||||
<button type="button" className="rail-bottom" title="About Libre Study" onClick={() => setAboutModalOpen(true)}>
|
||||
<span className="rail-icon">?</span>
|
||||
<span className="rail-copy"><strong>About</strong><small>Project, licenses, and links</small></span>
|
||||
</button>
|
||||
</aside>
|
||||
|
||||
<section className="reader">
|
||||
@@ -797,6 +1188,7 @@ function App() {
|
||||
{renderBiblePage('KJV')}
|
||||
{viewMode !== 'page' ? renderStudyMargin() : null}
|
||||
{renderFootnoteStrip()}
|
||||
{renderBookTabs()}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -806,29 +1198,6 @@ function App() {
|
||||
<span className="eyebrow">Notes</span>
|
||||
<h2>Study Notes</h2>
|
||||
</header>
|
||||
<div className="session-manager" aria-label="Study session manager">
|
||||
<label>
|
||||
Session
|
||||
<select
|
||||
value={activeSessionId}
|
||||
onChange={(event) => setActiveSessionId(event.target.value)}
|
||||
>
|
||||
{sessions.map((session) => (
|
||||
<option key={session.id} value={session.id}>
|
||||
{session.title}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<input
|
||||
value={activeSession?.title ?? ''}
|
||||
onChange={(event) => renameActiveSession(event.target.value)}
|
||||
placeholder="Session title"
|
||||
/>
|
||||
<button type="button" onClick={startNewSession}>
|
||||
New
|
||||
</button>
|
||||
</div>
|
||||
<textarea
|
||||
value={noteDraft}
|
||||
onChange={(event) => updateNoteDraft(event.target.value)}
|
||||
@@ -889,6 +1258,8 @@ function App() {
|
||||
<div className="status-panel">No verses matched that search.</div>
|
||||
)}
|
||||
</section>
|
||||
{renderSessionModal()}
|
||||
{renderAboutModal()}
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user