Browse Source

bug-fixes

imwald
Silberengel 2 weeks ago
parent
commit
0e21663433
  1. 168
      electron/main.cjs
  2. 4
      package-lock.json
  3. 4
      package.json
  4. 83
      src/components/Note/MarkdownArticle/MarkdownArticle.tsx
  5. 6
      src/components/YoutubeEmbeddedPlayer/index.tsx
  6. 7
      src/constants.ts

168
electron/main.cjs

@ -2,11 +2,153 @@ @@ -2,11 +2,153 @@
const { app, BrowserWindow, ipcMain, shell, Menu } = require('electron')
const fs = require('fs')
const http = require('http')
const path = require('path')
/** True when running from source (`electron .`); false when packaged. */
const isDev = !app.isPackaged
/** Stable loopback origin for packaged builds so YouTube embeds get a real `origin` (see YoutubeEmbeddedPlayer). */
const PACKAGED_STATIC_PORT_START = 45279
const PACKAGED_STATIC_PORT_TRIES = 40
const MIME = {
'.html': 'text/html; charset=utf-8',
'.js': 'application/javascript; charset=utf-8',
'.mjs': 'application/javascript; charset=utf-8',
'.css': 'text/css; charset=utf-8',
'.json': 'application/json',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.gif': 'image/gif',
'.webp': 'image/webp',
'.svg': 'image/svg+xml',
'.ico': 'image/x-icon',
'.woff': 'font/woff',
'.woff2': 'font/woff2',
'.webmanifest': 'application/manifest+json',
'.txt': 'text/plain; charset=utf-8',
'.wasm': 'application/wasm',
'.map': 'application/json'
}
let packagedStaticServer = null
/** @type {string | null} */
let packagedStaticBaseUrl = null
/** @type {Promise<string> | null} */
let packagedStaticStartPromise = null
function resolveUnderDist(distDir, pathname) {
let rel = pathname
try {
rel = decodeURIComponent(pathname)
} catch {
return null
}
rel = rel.replace(/^\/+/, '')
const candidate = path.resolve(distDir, rel)
const root = path.resolve(distDir)
const prefix = root.endsWith(path.sep) ? root : root + path.sep
if (candidate !== root && !candidate.startsWith(prefix)) return null
return candidate
}
function createPackagedStaticHandler(distDir) {
const indexPath = path.join(distDir, 'index.html')
return function handleRequest(req, res) {
if (req.method !== 'GET' && req.method !== 'HEAD') {
res.writeHead(405).end()
return
}
let pathname = '/'
try {
pathname = new URL(req.url || '/', 'http://127.0.0.1').pathname
} catch {
pathname = '/'
}
const sendIndex = () => {
fs.readFile(indexPath, (err, data) => {
if (err) {
res.writeHead(500).end('Missing index.html')
return
}
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Content-Length': data.length })
res.end(req.method === 'HEAD' ? undefined : data)
})
}
const filePath = resolveUnderDist(distDir, pathname)
if (!filePath) {
res.writeHead(403).end()
return
}
fs.stat(filePath, (err, st) => {
if (!err && st.isFile()) {
const ext = path.extname(filePath).toLowerCase()
const ct = MIME[ext] || 'application/octet-stream'
res.writeHead(200, { 'Content-Type': ct, 'Content-Length': st.size })
if (req.method === 'HEAD') {
res.end()
return
}
fs.createReadStream(filePath).pipe(res)
return
}
sendIndex()
})
}
}
function startPackagedStaticServer(distDir) {
return new Promise((resolve, reject) => {
const handler = createPackagedStaticHandler(distDir)
const server = http.createServer(handler)
const listenOn = (port, attempt) => {
if (attempt >= PACKAGED_STATIC_PORT_TRIES) {
reject(new Error('No free port for packaged static server'))
return
}
const onErr = (err) => {
server.removeListener('error', onErr)
if (err && err.code === 'EADDRINUSE') {
listenOn(port + 1, attempt + 1)
} else {
reject(err)
}
}
server.on('error', onErr)
server.listen(port, '127.0.0.1', () => {
server.removeListener('error', onErr)
packagedStaticServer = server
const addr = server.address()
const p = typeof addr === 'object' && addr ? addr.port : port
packagedStaticBaseUrl = `http://127.0.0.1:${p}`
resolve(packagedStaticBaseUrl)
})
}
listenOn(PACKAGED_STATIC_PORT_START, 0)
})
}
function ensurePackagedStaticBaseUrl() {
if (packagedStaticBaseUrl) return Promise.resolve(packagedStaticBaseUrl)
if (!packagedStaticStartPromise) {
const distDir = path.join(__dirname, '..', 'dist')
packagedStaticStartPromise = startPackagedStaticServer(distDir).catch((err) => {
packagedStaticStartPromise = null
throw err
})
}
return packagedStaticStartPromise
}
function resolveWindowIcon() {
const candidates = isDev
? [path.join(__dirname, '..', 'public', 'favicon.png')]
@ -30,8 +172,21 @@ function loadRenderer(win) { @@ -30,8 +172,21 @@ function loadRenderer(win) {
}
return
}
void ensurePackagedStaticBaseUrl()
.then((baseUrl) => {
if (win.isDestroyed()) return
const u = new URL('/', baseUrl)
u.search = ''
u.hash = ''
void win.loadURL(u.href)
})
.catch((err) => {
console.error('Packaged static server failed, falling back to file://', err)
if (!win.isDestroyed()) {
void win.loadFile(path.join(__dirname, '..', 'dist', 'index.html'))
}
})
}
function createWindow() {
const win = new BrowserWindow({
@ -107,6 +262,19 @@ app.whenReady().then(() => { @@ -107,6 +262,19 @@ app.whenReady().then(() => {
})
})
app.on('will-quit', () => {
if (packagedStaticServer) {
try {
packagedStaticServer.close()
} catch {
// ignore
}
packagedStaticServer = null
packagedStaticBaseUrl = null
packagedStaticStartPromise = null
}
})
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit()
})

4
package-lock.json generated

@ -1,12 +1,12 @@ @@ -1,12 +1,12 @@
{
"name": "imwald",
"version": "22.5.4",
"version": "22.5.6",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "imwald",
"version": "22.5.4",
"version": "22.5.6",
"license": "MIT",
"dependencies": {
"@asciidoctor/core": "^3.0.4",

4
package.json

@ -1,6 +1,6 @@ @@ -1,6 +1,6 @@
{
"name": "imwald",
"version": "22.5.4",
"version": "22.5.6",
"description": "Imwald — a user-friendly Nostr client focused on relay feed browsing, publications, and relay discovery",
"private": true,
"type": "module",
@ -27,7 +27,7 @@ @@ -27,7 +27,7 @@
"i18n:gaps": "npx tsx scripts/export-en-parity-gaps.ts",
"i18n:translate-de": "PYTHONUNBUFFERED=1 .venv-i18n/bin/python scripts/auto_translate_i18n.py de",
"electron:dev": "concurrently -k -n vite,electron -c blue,green \"vite --host\" \"wait-on http://127.0.0.1:5173 && cross-env NODE_ENV=development electron .\"",
"build:electron": "tsc -b && vite build --base ./",
"build:electron": "tsc -b && vite build",
"electron:pack": "npm run build:electron && electron-builder",
"og:image": "node scripts/generate-og-png.mjs"
},

83
src/components/Note/MarkdownArticle/MarkdownArticle.tsx

@ -33,6 +33,7 @@ import { @@ -33,6 +33,7 @@ import {
} from '@/constants'
import { isSpotifyOpenUrl } from '@/lib/spotify-url'
import { canonicalZapStreamWatchUrl, isZapStreamWatchUrl } from '@/lib/zap-stream-url'
import { isEmbeddableYoutubeUrl } from '@/lib/youtube-url'
import { EMOJI_SHORT_CODE_REGEX, NOSTR_URI_INLINE_REGEX } from '@/lib/content-patterns'
import { replaceStandardEmojiShortcodesInContent } from '@/lib/emoji-content'
import { TEmoji, TImetaInfo } from '@/types'
@ -3408,7 +3409,7 @@ function parseMarkdownContentMarked( @@ -3408,7 +3409,7 @@ function parseMarkdownContentMarked(
if (/^https?:\/\/\S+$/i.test(line)) {
const cleaned = cleanUrl(line)
if (cleaned) {
if (isYouTubeUrl(cleaned)) {
if (isEmbeddableYoutubeUrl(cleaned)) {
return (
<div key={`${key}-line-youtube-${lineIdx}`} className="my-2">
<YoutubeEmbeddedPlayer
@ -3573,7 +3574,7 @@ function parseMarkdownContentMarked( @@ -3573,7 +3574,7 @@ function parseMarkdownContentMarked(
if (/^https?:\/\/\S+$/i.test(paragraphText)) {
const cleaned = cleanUrl(paragraphText)
if (cleaned) {
if (isYouTubeUrl(cleaned)) {
if (isEmbeddableYoutubeUrl(cleaned)) {
return (
<div key={`${key}-youtube-url`} className="my-2">
<YoutubeEmbeddedPlayer
@ -3646,6 +3647,39 @@ function parseMarkdownContentMarked( @@ -3646,6 +3647,39 @@ function parseMarkdownContentMarked(
}
const paragraphTokens = lexInlineProtected(rawParagraphText)
// One GFM autolink only: embed using `href` even when `paragraphText`/`token.text` do not match
// `^https?://…$` (marked quirks, odd whitespace, or autolink vs raw mismatch).
if (Array.isArray(paragraphTokens) && paragraphTokens.length === 1 && paragraphTokens[0]?.type === 'link') {
const soleHref = cleanUrl(String(paragraphTokens[0].href ?? ''))
if (soleHref && isEmbeddableYoutubeUrl(soleHref)) {
return (
<div key={`${key}-youtube-sole-link`} className="my-2">
<YoutubeEmbeddedPlayer url={soleHref} className="max-w-[400px]" mustLoad={!lazyMedia} />
</div>
)
}
if (soleHref && isSpotifyUrl(soleHref)) {
return (
<div key={`${key}-spotify-sole-link`} className="my-2">
<SpotifyEmbeddedPlayer url={soleHref} className="max-w-[400px]" mustLoad={!lazyMedia} />
</div>
)
}
if (soleHref && isZapStreamUrl(soleHref)) {
return (
<div key={`${key}-zapstream-sole-link`} className="my-2">
<ZapStreamLiveEventEmbed
url={soleHref}
className="max-w-[400px]"
containingEvent={containingEvent}
showFull={!lazyMedia}
/>
</div>
)
}
}
const parseNostrHref = (href: string): string | null => {
if (!href.toLowerCase().startsWith('nostr:')) return null
const raw = href.slice(6).trim()
@ -3677,6 +3711,47 @@ function parseMarkdownContentMarked( @@ -3677,6 +3711,47 @@ function parseMarkdownContentMarked(
let segmentIdx = 0
paragraphTokens.forEach((t: any, idx: number) => {
// Same paragraph can mix ![](video) with GFM autolinks (single line break → one <p> in marked).
// Without this, YouTube/Spotify/zap.stream links are pushed into inlineSegment and render as plain <a>.
if (t?.type === 'link') {
const cleaned = cleanUrl(String(t.href ?? ''))
if (cleaned && isEmbeddableYoutubeUrl(cleaned)) {
flushInlineSegment(segmentIdx++)
nodes.push(
<div key={`${key}-inline-yt-with-media-${idx}`} className="my-2">
<YoutubeEmbeddedPlayer
url={cleaned}
className="max-w-[400px]"
mustLoad={!lazyMedia}
/>
</div>
)
return
}
if (cleaned && isSpotifyUrl(cleaned)) {
flushInlineSegment(segmentIdx++)
nodes.push(
<div key={`${key}-inline-spotify-with-media-${idx}`} className="my-2">
<SpotifyEmbeddedPlayer url={cleaned} className="max-w-[400px]" mustLoad={!lazyMedia} />
</div>
)
return
}
if (cleaned && isZapStreamUrl(cleaned)) {
flushInlineSegment(segmentIdx++)
nodes.push(
<div key={`${key}-inline-zapstream-with-media-${idx}`} className="my-2">
<ZapStreamLiveEventEmbed
url={cleaned}
className="max-w-[400px]"
containingEvent={containingEvent}
showFull={!lazyMedia}
/>
</div>
)
return
}
}
if (t?.type !== 'image') {
inlineSegment.push(t)
return
@ -3766,7 +3841,7 @@ function parseMarkdownContentMarked( @@ -3766,7 +3841,7 @@ function parseMarkdownContentMarked(
const hasInlineYouTubeLink = paragraphTokens.some((t: any) => {
if (t?.type !== 'link') return false
const cleaned = cleanUrl(String(t.href ?? ''))
return !!cleaned && isYouTubeUrl(cleaned)
return !!cleaned && isEmbeddableYoutubeUrl(cleaned)
})
if (hasInlineYouTubeLink) {
const nodes: React.ReactNode[] = []
@ -3788,7 +3863,7 @@ function parseMarkdownContentMarked( @@ -3788,7 +3863,7 @@ function parseMarkdownContentMarked(
return
}
const cleaned = cleanUrl(String(t.href ?? ''))
if (!cleaned || !isYouTubeUrl(cleaned)) {
if (!cleaned || !isEmbeddableYoutubeUrl(cleaned)) {
inlineSegment.push(t)
return
}

6
src/components/YoutubeEmbeddedPlayer/index.tsx

@ -38,9 +38,9 @@ export default function YoutubeEmbeddedPlayer({ @@ -38,9 +38,9 @@ export default function YoutubeEmbeddedPlayer({
* YouTube in Electron:
* - **Iframe API** (`YT.Player`) against `http(s)://localhost` often ends in error **153** (player configuration)
* in recent Chromium/Electron builds; it worked more reliably in plain browsers only.
* - **Native `/embed/` iframe** works if the `origin` query param matches the real page origin. Use
* `window.location.origin` for dev (`http://127.0.0.1:5173`, etc.). On **`file:`** there is no valid https
* origin omit `origin` (a fake `https://…` origin caused **150**).
* - **Native `/embed/` iframe** works if the `origin` query param matches the real page origin (dev server,
* or packaged app: loopback static server see `electron/main.cjs`). On raw **`file:`** omit `origin`
* (a fake `https://…` origin caused **150**).
* Non-Electron: keep the Iframe API (unchanged from preElectron-split behavior).
*/
const useNativeEmbed = isImwaldElectron()

7
src/constants.ts

@ -42,11 +42,8 @@ const _moduleHref: string = import.meta.url @@ -42,11 +42,8 @@ const _moduleHref: string = import.meta.url
/**
* URL for a file from `public/` (banner, favicon, payto logos, etc.).
* Uses Vite `base`: `/` on the web, `./` when built for Electron (`loadFile` + `file:`).
*
* Electron packaged builds use `file:` + client-side history paths like `/notes/…`, which replace
* the document URL with `file:///notes/…`. Relative `BASE_URL` links would then resolve next to that
* bogus path and 404.
* Uses Vite `base`: `/` for web and packaged Electron (renderer is served from `http://127.0.0.1:*`; see
* `electron/main.cjs`). The `file:` branch remains for opening `dist/index.html` directly from disk.
*
* For `file:` we derive the `dist/` root from the chunk's own URL. The chunk lives at
* `dist/assets/*.js`, so `/assets/` marks the boundary: everything before it is the dist root.

Loading…
Cancel
Save