Browse Source

fix media note

cleaned up terminal
imwald
Silberengel 3 weeks ago
parent
commit
dcd22ea496
  1. 74
      src/components/PostEditor/PostContent.tsx
  2. 74
      src/lib/error-suppression.ts
  3. 21
      src/lib/languagetool-client.ts
  4. 8
      src/lib/logger.ts
  5. 146
      vite.config.ts

74
src/components/PostEditor/PostContent.tsx

@ -383,6 +383,9 @@ export default function PostContent({ @@ -383,6 +383,9 @@ export default function PostContent({
/** Accumulates imeta tags across uploads (short note or multi-attachment) so files are not dropped. */
const composerImetaTagsRef = useRef<string[][]>([])
const mediaNoteKindRef = useRef<number | null>(null)
/** True when the hidden uploader was opened from Note type → Media Note (not toolbar paste/drop). */
const mediaNoteUploaderIntentRef = useRef(false)
const [mediaNoteUploadPending, setMediaNoteUploadPending] = useState(false)
/** Stable auto d-tag when the field is left empty; `{ slug, value }` resets when article subtype changes. */
const articleDTagFallbackRef = useRef<{ slug: string; value: string } | null>(null)
@ -1520,8 +1523,14 @@ export default function PostContent({ @@ -1520,8 +1523,14 @@ export default function PostContent({
}
}
const handlePlainNoteMode = () => {
if (parentEvent) return
const clearMediaNoteUploadIntent = useCallback(() => {
mediaNoteUploaderIntentRef.current = false
setMediaNoteUploadPending(false)
}, [])
const isMediaNoteComposerMode = mediaNoteKind !== null || mediaNoteUploadPending
const clearNonMediaNoteComposerModes = () => {
setIsPoll(false)
setIsPublicMessage(false)
setIsHighlight(false)
@ -1534,6 +1543,21 @@ export default function PostContent({ @@ -1534,6 +1543,21 @@ export default function PostContent({
setIsCitationHardcopy(false)
setIsCitationPrompt(false)
setIsDiscussionThread(false)
}
const beginMediaNoteUpload = () => {
if (parentEvent) return
clearNonMediaNoteComposerModes()
clearMediaNoteUploadIntent()
setMediaNoteUploadPending(true)
mediaNoteUploaderIntentRef.current = true
mediaUploaderBtnRef.current?.click()
}
const handlePlainNoteMode = () => {
if (parentEvent) return
clearNonMediaNoteComposerModes()
clearMediaNoteUploadIntent()
// Short note (kind 1) still supports NIP-94 imeta; only Clear should drop uploads/tags.
setMediaNoteKind(null)
}
@ -1657,7 +1681,7 @@ export default function PostContent({ @@ -1657,7 +1681,7 @@ export default function PostContent({
!isCitationHardcopy &&
!isCitationPrompt &&
!isDiscussionThread &&
mediaNoteKind === null,
!isMediaNoteComposerMode,
[
parentEvent,
isPoll,
@ -1672,7 +1696,7 @@ export default function PostContent({ @@ -1672,7 +1696,7 @@ export default function PostContent({
isCitationHardcopy,
isCitationPrompt,
isDiscussionThread,
mediaNoteKind
isMediaNoteComposerMode
]
)
@ -1888,6 +1912,7 @@ export default function PostContent({ @@ -1888,6 +1912,7 @@ export default function PostContent({
selectedKind?: number,
opts?: { skipComposerUrlAppend?: boolean }
) => {
const fromMediaNoteMenu = mediaNoteUploaderIntentRef.current
try {
let resolvedKind: number
if (selectedKind !== undefined) {
@ -1896,8 +1921,11 @@ export default function PostContent({ @@ -1896,8 +1921,11 @@ export default function PostContent({
resolvedKind = await getMediaKindFromFile(uploadingFile, false)
}
// New-post composer: images stay kind 1 (short text + imeta + URL), not kind 20 picture notes.
if (resolvedKind === ExtendedKind.PICTURE) {
// Toolbar/drop: images stay kind 1 (short text + imeta). Media Note menu: use NIP-94 media kinds (20/21/22/1222).
if (fromMediaNoteMenu) {
setMediaNoteKind(resolvedKind)
setMediaUrl(url)
} else if (resolvedKind === ExtendedKind.PICTURE) {
setMediaNoteKind(null)
setMediaUrl('')
} else {
@ -1964,6 +1992,11 @@ export default function PostContent({ @@ -1964,6 +1992,11 @@ export default function PostContent({
if (mediaNoteKindRef.current !== null) {
setMediaUrl((prev) => prev || url)
}
} finally {
if (fromMediaNoteMenu) {
mediaNoteUploaderIntentRef.current = false
setMediaNoteUploadPending(false)
}
}
}
@ -1992,6 +2025,10 @@ export default function PostContent({ @@ -1992,6 +2025,10 @@ export default function PostContent({
}
if (!uploadingFile) {
logger.warn('Media upload succeeded but file not found')
if (mediaNoteUploaderIntentRef.current) {
mediaNoteUploaderIntentRef.current = false
setMediaNoteUploadPending(false)
}
return
}
@ -2120,19 +2157,9 @@ export default function PostContent({ @@ -2120,19 +2157,9 @@ export default function PostContent({
// Don't throw - just log the error so the upload doesn't fail completely
}
// Clear other note types when media is selected
setIsPoll(false)
setIsPublicMessage(false)
setIsHighlight(false)
setIsLongFormArticle(false)
setIsWikiArticle(false)
setIsNostrSpecification(false)
setIsPublicationContent(false)
setIsCitationInternal(false)
setIsCitationExternal(false)
setIsCitationHardcopy(false)
setIsCitationPrompt(false)
setIsDiscussionThread(false)
if (!mediaNoteUploaderIntentRef.current) {
clearNonMediaNoteComposerModes()
}
// Clear uploaded file map (upload finished). Keep composerImetaTagsRef in sync with mediaImetaTags — do not wipe here.
uploadedMediaFileMap.current.clear()
@ -2241,6 +2268,7 @@ export default function PostContent({ @@ -2241,6 +2268,7 @@ export default function PostContent({
setText('')
setMediaNoteKind(null)
setMediaUrl('')
clearMediaNoteUploadIntent()
setMediaImetaTags([])
setMentions([])
setExtractedMentions([])
@ -3043,7 +3071,7 @@ export default function PostContent({ @@ -3043,7 +3071,7 @@ export default function PostContent({
isPublicMessage ? MessageCircle :
isPoll ? ListTodo :
isDiscussionThread ? MessagesSquare :
mediaNoteKind !== null ? Upload :
isMediaNoteComposerMode ? Upload :
StickyNote
const activeLabel =
isLongFormArticle ? t('Long-form Article') :
@ -3058,7 +3086,7 @@ export default function PostContent({ @@ -3058,7 +3086,7 @@ export default function PostContent({
isPublicMessage ? t('Public Message') :
isPoll ? t('Poll') :
isDiscussionThread ? t('Thread') :
mediaNoteKind !== null ? t('Media Note') :
isMediaNoteComposerMode ? t('Media Note') :
t('Short Note')
return (
<div className="flex flex-wrap items-center justify-end gap-1.5">
@ -3111,13 +3139,13 @@ export default function PostContent({ @@ -3111,13 +3139,13 @@ export default function PostContent({
</div>
{isPlainShortNoteToolbar && <Check className="h-4 w-4 shrink-0 text-primary" />}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => mediaUploaderBtnRef.current?.click()} className="gap-3 py-2 cursor-pointer">
<DropdownMenuItem onClick={beginMediaNoteUpload} className="gap-3 py-2 cursor-pointer">
<Upload className="h-4 w-4 shrink-0 text-muted-foreground" />
<div className="flex flex-col flex-1 min-w-0">
<span className="font-medium leading-none">{t('Media Note')}</span>
<span className="text-xs text-muted-foreground mt-0.5">{t('Attach image, audio, or video')}</span>
</div>
{mediaNoteKind !== null && <Check className="h-4 w-4 shrink-0 text-primary" />}
{isMediaNoteComposerMode && <Check className="h-4 w-4 shrink-0 text-primary" />}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={handleHighlightToggle} className="gap-3 py-2 cursor-pointer">

74
src/lib/error-suppression.ts

@ -44,6 +44,68 @@ function isExpectedFaviconNetworkNoise(message: string): boolean { @@ -44,6 +44,68 @@ function isExpectedFaviconNetworkNoise(message: string): boolean {
)
}
/** Expected dev-only noise: optional proxies, profile cache misses, wallet timeouts, editor quirks. */
function isExpectedDevAppNoise(message: string): boolean {
if (
message.includes('[ReplaceableEventService] Profile batch network load timed out') ||
message.includes('[ReplaceableEventService] fetchProfilesForPubkeys exceeded wall timeout')
) {
return true
}
if (
message.includes('[LanguageTool] HTTP error') ||
message.includes('LanguageTool: 5') ||
message.includes('[Translate] Optional translate proxy offline') ||
message.includes('[Translate] /languages skipped') ||
message.includes('[Optional proxy] Sites proxy returned')
) {
return true
}
if (
message.includes('Failed to request get_info') ||
message.includes('Using minimal getInfo') ||
message.includes('reply timeout: event')
) {
return true
}
if (message.includes('NIP-04 encryption is about to be deprecated')) {
return true
}
if (
message.includes('TextSelection endpoint not pointing into a node with inline content') ||
message.includes('scroll-verknüpften Positionierungseffekt') ||
message.includes('scroll-linked positioning effect')
) {
return true
}
if (
message.includes('Cookie') &&
(message.includes('abgelehnt') || message.includes('rejected')) &&
message.includes('SameSite')
) {
return true
}
if (
message.includes('Cross-Origin-Resource-Policy') ||
(message.includes('CORP') && message.includes('blockiert'))
) {
return true
}
if (
message.includes('[QueryService] req_end') ||
message.includes('[relay-req]') ||
message.includes('[FeedPaint]') ||
message.includes('[LiveActivities] poll done') ||
message.includes('[RelayPoolIdle]')
) {
return true
}
if (message.includes('[vite]') && (message.includes('connected') || message.includes('connecting'))) {
return true
}
return false
}
function isExpectedRelayWebSocketNoise(message: string): boolean {
if (message.includes('WebSocket connection to') || message.includes('Close received after close')) {
return true
@ -92,6 +154,10 @@ function suppressExpectedErrors() { @@ -92,6 +154,10 @@ function suppressExpectedErrors() {
return
}
if (import.meta.env.DEV && isExpectedDevAppNoise(message)) {
return
}
if (message.includes('NS_BINDING_ABORTED')) {
return
}
@ -242,6 +308,10 @@ function suppressExpectedErrors() { @@ -242,6 +308,10 @@ function suppressExpectedErrors() {
return
}
if (import.meta.env.DEV && isExpectedDevAppNoise(message)) {
return
}
// Suppress invalid URI / failed media resource (e.g. empty img src)
if (message.includes('Ungültige URI') ||
message.includes('Invalid URI') ||
@ -356,6 +426,10 @@ function suppressExpectedErrors() { @@ -356,6 +426,10 @@ function suppressExpectedErrors() {
return
}
if (import.meta.env.DEV && isExpectedDevAppNoise(message)) {
return
}
// Firefox ORB: cross-origin favicon / relay icon requests often hit HTML or wrong MIME; not actionable in-app.
if (
message.includes('OpaqueResponseBlocking') ||

21
src/lib/languagetool-client.ts

@ -2,6 +2,14 @@ import { LANGUAGE_TOOL_URL } from '@/constants' @@ -2,6 +2,14 @@ import { LANGUAGE_TOOL_URL } from '@/constants'
import { electronAwareFetch } from '@/lib/electron-aware-fetch'
import logger from '@/lib/logger'
/** After proxy/backend 502/503/504, skip further grammar HTTP this tab (optional dev proxy). */
let languageToolBackendGoneThisSession = false
let languageToolOptionalLogged = false
export function isLanguageToolBackendUnreachableThisSession(): boolean {
return languageToolBackendGoneThisSession
}
export type LanguageToolMatch = {
offset: number
length: number
@ -28,7 +36,7 @@ export async function languageToolCheck( @@ -28,7 +36,7 @@ export async function languageToolCheck(
signal?: AbortSignal
): Promise<LanguageToolCheckResponse> {
const url = checkUrl()
if (!url) {
if (!url || languageToolBackendGoneThisSession) {
return { matches: [] }
}
const body = new URLSearchParams()
@ -44,6 +52,17 @@ export async function languageToolCheck( @@ -44,6 +52,17 @@ export async function languageToolCheck(
})
if (!res.ok) {
const errText = await res.text().catch(() => '')
if ([502, 503, 504].includes(res.status)) {
languageToolBackendGoneThisSession = true
if (import.meta.env.DEV && !languageToolOptionalLogged) {
languageToolOptionalLogged = true
logger.debug(
'[LanguageTool] Optional grammar proxy offline; skipping further checks this session.',
{ status: res.status, errText: errText.slice(0, 120) }
)
}
return { matches: [] }
}
logger.warn('[LanguageTool] HTTP error', { status: res.status, errText: errText.slice(0, 200) })
throw new Error(`LanguageTool: ${res.status}`)
}

8
src/lib/logger.ts

@ -2,8 +2,8 @@ @@ -2,8 +2,8 @@
* Centralized logging utility.
*
* Level matrix:
* dev (default) debug / info / warn / error (full formatted output; `logger.debug` on)
* dev + opt-out info / warn / error (set `imwald-debug` or `jumble-debug` to `false`)
* dev (default) info / warn / error (quiet console; relay/query traces off)
* dev + opt-in debug / info / warn / error (`imwald-debug` / `jumble-debug` `true`, or `VITE_DEBUG=true`)
* production warn / error only (bare console no timestamp string built)
*
* Opt out of debug in dev: `localStorage.setItem('imwald-debug', 'false')` then reload.
@ -27,8 +27,8 @@ class Logger { @@ -27,8 +27,8 @@ class Logger {
localStorage.getItem('imwald-debug') === 'true' ||
localStorage.getItem('jumble-debug') === 'true' ||
import.meta.env.VITE_DEBUG === 'true'
// `npm run dev`: debug on by default so relay/query/cache traces are visible without localStorage.
this.enableDebug = this.isDev && (explicitOn || !explicitOff)
// `npm run dev`: quiet by default; opt in via localStorage / `VITE_DEBUG` / `imwaldDebug.enable()`.
this.enableDebug = this.isDev && explicitOn && !explicitOff
// In production only warn/error reach the console — info is noise for end-users.
this.minLevel = this.enableDebug ? 'debug' : this.isDev ? 'info' : 'warn'

146
vite.config.ts

@ -45,35 +45,50 @@ function fullReloadOnProvidersAndPages(): Plugin { @@ -45,35 +45,50 @@ function fullReloadOnProvidersAndPages(): Plugin {
}
}
/**
* `http-proxy` logs `Error: connect ECONNREFUSED …` via `console.error`, bypassing Vite's `logger.error`.
*/
function isOptionalDevProxyConnRefusedNoise(args: unknown[]): boolean {
const blob = args
/** Loopback targets in `server.proxy` — optional in dev; see PROXY_SETUP.md. */
const OPTIONAL_DEV_PROXY_LOOPBACK_PORTS = [4000, 5000, 8010, 8090, 9876] as const
function blobFromLogArgs(args: unknown[]): string {
return args
.map((a) => {
if (typeof a === 'string') return a
if (a instanceof Error) return `${a.message}\n${a.stack ?? ''}`
return ''
})
.join('\n')
}
/**
* `http-proxy` logs `Error: connect ECONNREFUSED …` via `console.error`, bypassing Vite's `logger.error`.
*/
function isOptionalDevProxyConnRefusedNoise(args: unknown[]): boolean {
const blob = blobFromLogArgs(args)
if (!blob.includes('ECONNREFUSED')) return false
return (
blob.includes('127.0.0.1:5000') ||
blob.includes('127.0.0.1:8090') ||
/\b:5000\b/.test(blob) ||
/\b:8090\b/.test(blob)
)
if (blob.includes('127.0.0.1:') || blob.includes('localhost:')) return true
return OPTIONAL_DEV_PROXY_LOOPBACK_PORTS.some((port) => new RegExp(`\\b:${port}\\b`).test(blob))
}
function isOptionalDevProxyHttpError(text: string): boolean {
if (!text.includes('http proxy error')) return false
if (!text.includes('ECONNREFUSED')) return false
if (
text.includes('/api/languagetool') ||
text.includes('/v2/') ||
text.includes('/api/piper-tts') ||
text.includes('/api/translate') ||
text.includes('/sites') ||
text.includes('/dev-index-relay') ||
text.includes('/api/events')
) {
return true
}
return OPTIONAL_DEV_PROXY_LOOPBACK_PORTS.some((port) => text.includes(`127.0.0.1:${port}`))
}
/**
* When optional localhost backends are down, `http-proxy` otherwise logs a multiline stack per request.
* Throttle to one hint per category (cooldown), matching paths only real misconfigurations still log.
*/
function quietOptionalDevProxyErrors(devIndexRelayTarget: string): Plugin {
let lastIndexRelaySuppressed = 0
let lastTranslateSitesSuppressed = 0
const COOLDOWN_MS = 60_000
function quietOptionalDevProxyErrors(): Plugin {
return {
name: 'quiet-optional-dev-proxy-errors',
apply: 'serve',
@ -91,34 +106,30 @@ function quietOptionalDevProxyErrors(devIndexRelayTarget: string): Plugin { @@ -91,34 +106,30 @@ function quietOptionalDevProxyErrors(devIndexRelayTarget: string): Plugin {
const prevError = config.logger.error.bind(config.logger)
config.logger.error = (msg, options) => {
const text = typeof msg === 'string' ? msg : ''
if (text.includes('http proxy error') && text.includes('ECONNREFUSED')) {
if (text.includes('/api/events') || text.includes('/dev-index-relay')) {
const now = Date.now()
if (now - lastIndexRelaySuppressed >= COOLDOWN_MS) {
lastIndexRelaySuppressed = now
config.logger.warn(
`[vite] Dev index relay not reachable (${devIndexRelayTarget}). Start it or set VITE_DEV_INDEX_RELAY_TARGET. Suppressing duplicate proxy errors for ${COOLDOWN_MS / 1000}s.`
)
}
return
}
if (text.includes('/api/translate') || text.includes('/sites')) {
const now = Date.now()
if (now - lastTranslateSitesSuppressed >= COOLDOWN_MS) {
lastTranslateSitesSuppressed = now
config.logger.warn(
`[vite] Optional dev proxies unreachable (LibreTranslate /api/translate → :5000, OG /sites → :8090). Start them or ignore — see PROXY_SETUP.md. Suppressing duplicate proxy errors for ${COOLDOWN_MS / 1000}s.`
)
}
return
}
}
if (isOptionalDevProxyHttpError(text)) return
prevError(msg, options)
}
}
}
}
function jsonProxyErrorHandler(status: number, body: Record<string, unknown>) {
return (proxy: { on: (event: string, handler: (...args: unknown[]) => void) => void }) => {
proxy.on('error', (_err, _req, res) => {
const r = res as {
headersSent?: boolean
writeHead?: (c: number, h: Record<string, string>) => void
end?: (b: string) => void
}
if (r.headersSent) return
if (typeof r?.writeHead === 'function' && typeof r?.end === 'function') {
r.writeHead(status, { 'Content-Type': 'application/json' })
r.end(JSON.stringify(body))
}
})
}
}
/**
* Loopback / RFC1918 / ULA mirrors `isLocalNetworkUrl` in `src/lib/url.ts` without importing it
* (Vite's config bundle does not resolve `@/` for transitive deps).
@ -202,52 +213,41 @@ export default defineConfig(({ mode }) => { @@ -202,52 +213,41 @@ export default defineConfig(({ mode }) => {
// Read-aloud Piper: same path as production Apache → aitherboard (avoid cross-origin CORS in dev).
'/api/piper-tts': {
target: 'http://127.0.0.1:9876',
changeOrigin: true
changeOrigin: true,
configure: jsonProxyErrorHandler(503, {
ok: false,
error: 'piper_proxy_unreachable',
hint: 'Start Piper TTS on :9876 — see PROXY_SETUP.md'
})
},
'/api/languagetool': {
target: 'http://127.0.0.1:8010',
changeOrigin: true,
rewrite: (p) => p.replace(/^\/api\/languagetool/u, '') || '/'
rewrite: (p) => p.replace(/^\/api\/languagetool/u, '') || '/',
configure: jsonProxyErrorHandler(503, {
ok: false,
error: 'languagetool_proxy_unreachable',
hint: 'Start LanguageTool on :8010 — see PROXY_SETUP.md'
})
},
'/api/translate': {
target: 'http://127.0.0.1:5000',
changeOrigin: true,
rewrite: (p) => p.replace(/^\/api\/translate/u, '') || '/',
/** Match `/sites`: when LibreTranslate is not running, return JSON instead of a broken proxy response. */
configure(proxy) {
proxy.on('error', (_err, _req, res) => {
const r = res as {
headersSent?: boolean
writeHead?: (c: number, h: Record<string, string>) => void
end?: (b: string) => void
}
if (r.headersSent) return
if (typeof r?.writeHead === 'function' && typeof r?.end === 'function') {
r.writeHead(503, { 'Content-Type': 'application/json' })
r.end(
JSON.stringify({
ok: false,
error: 'translate_proxy_unreachable',
hint: 'Start LibreTranslate (or compatible API) on :5000 — see PROXY_SETUP.md'
})
)
}
})
}
configure: jsonProxyErrorHandler(503, {
ok: false,
error: 'translate_proxy_unreachable',
hint: 'Start LibreTranslate (or compatible API) on :5000 — see PROXY_SETUP.md'
})
},
'/sites': {
target: 'http://127.0.0.1:8090',
changeOrigin: true,
/** Without OG proxy on :8090, Node was returning 500 HTML; return JSON so callers fail softly in dev. */
configure(proxy) {
proxy.on('error', (_err, _req, res) => {
const r = res as { writeHead?: (c: number, h: Record<string, string>) => void; end?: (b: string) => void }
if (typeof r?.writeHead === 'function' && typeof r?.end === 'function') {
r.writeHead(502, { 'Content-Type': 'application/json' })
r.end(JSON.stringify({ ok: false, error: 'og_proxy_unreachable', hint: 'Start OG scraper on :8090 (see PROXY_SETUP.md)' }))
}
})
}
configure: jsonProxyErrorHandler(502, {
ok: false,
error: 'og_proxy_unreachable',
hint: 'Start OG scraper on :8090 (see PROXY_SETUP.md)'
})
},
// Loopback HTTP index relay: `import.meta.env.DEV` rewrites kind 10243 URLs through this path.
'/dev-index-relay': {
@ -437,7 +437,7 @@ export default defineConfig(({ mode }) => { @@ -437,7 +437,7 @@ export default defineConfig(({ mode }) => {
plugins: [
react(),
fullReloadOnProvidersAndPages(),
quietOptionalDevProxyErrors(devIndexRelayTarget),
quietOptionalDevProxyErrors(),
VitePWA({
// Prompt mode + virtual:pwa-register (main bundle) — do not auto-activate; VersionUpdateBanner calls updateSW().
registerType: 'prompt',

Loading…
Cancel
Save