refactor: remove electron-related code

This commit is contained in:
codytseng
2024-12-21 23:20:30 +08:00
parent bed8df06e8
commit 2b1e6fe8f5
200 changed files with 2771 additions and 8432 deletions

View File

@@ -1,9 +0,0 @@
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

View File

@@ -1,4 +0,0 @@
node_modules
dist
out
.gitignore

View File

@@ -1,14 +0,0 @@
module.exports = {
extends: [
'eslint:recommended',
'plugin:react/recommended',
'plugin:react/jsx-runtime',
'@electron-toolkit/eslint-config-ts/recommended',
'@electron-toolkit/eslint-config-prettier'
],
rules: {
'@typescript-eslint/explicit-function-return-type': 'off',
'react/prop-types': 'off',
'@typescript-eslint/no-explicit-any': 'off'
}
}

1
.github/FUNDING.yml vendored
View File

@@ -1 +0,0 @@
github: [CodyTseng]

View File

@@ -1,57 +0,0 @@
name: Build/release
on:
push:
tags:
- v*.*.*
permissions:
contents: write
jobs:
release:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-13, windows-latest]
steps:
- name: Check out Git repository
uses: actions/checkout@v4
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: Install Dependencies
run: npm install
- name: build-linux
if: matrix.os == 'ubuntu-latest'
run: npm run build:linux
- name: build-mac
if: matrix.os == 'macos-13'
run: npm run build:mac
- name: build-win
if: matrix.os == 'windows-latest'
run: npm run build:win
- name: release
uses: softprops/action-gh-release@v2
with:
draft: true
files: |
dist/*.exe
dist/*.zip
dist/*.dmg
dist/*.AppImage
dist/*.snap
dist/*.deb
dist/*.rpm
dist/*.tar.gz
env:
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}

23
.gitignore vendored
View File

@@ -1,5 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules node_modules
dist dist
out dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store .DS_Store
*.log* *.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@@ -2,5 +2,4 @@ out
dist dist
pnpm-lock.yaml pnpm-lock.yaml
LICENSE.md LICENSE.md
tsconfig.json *.json
tsconfig.*.json

View File

@@ -1,3 +0,0 @@
{
"recommendations": ["dbaeumer.vscode-eslint"]
}

39
.vscode/launch.json vendored
View File

@@ -1,39 +0,0 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug Main Process",
"type": "node",
"request": "launch",
"cwd": "${workspaceRoot}",
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite",
"windows": {
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite.cmd"
},
"runtimeArgs": ["--sourcemap"],
"env": {
"REMOTE_DEBUGGING_PORT": "9222"
}
},
{
"name": "Debug Renderer Process",
"port": 9222,
"request": "attach",
"type": "chrome",
"webRoot": "${workspaceFolder}/src/renderer",
"timeout": 60000,
"presentation": {
"hidden": true
}
}
],
"compounds": [
{
"name": "Debug All",
"configurations": ["Debug Main Process", "Debug Renderer Process"],
"presentation": {
"order": 1
}
}
]
}

11
.vscode/settings.json vendored
View File

@@ -1,11 +0,0 @@
{
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}

21
LICENSE
View File

@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2024 Cody Tseng
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -17,23 +17,8 @@ A beautiful nostr client focused on browsing relay feeds
- **Relay-Friendly Design:** Minimized and simplified requests ensure efficient communication with relays - **Relay-Friendly Design:** Minimized and simplified requests ensure efficient communication with relays
- **Relay Groups:** Easily manage and switch between relay groups - **Relay Groups:** Easily manage and switch between relay groups
- **Clean Interface:** Enjoy a minimalist design and intuitive interactions - **Clean Interface:** Enjoy a minimalist design and intuitive interactions
- **Cross-Platform:** Available on macOS, Windows, Linux, and web browsers
## Web Version ## Run Locally
You can use the web version of Jumble at [jumble.social](https://jumble.social).
## Desktop Version
You can download the desktop version from the [release page](https://github.com/CodyTseng/jumble/releases). If you want to use Apple Silicon version, you need to build it from the source code.
Because the app is not signed, you may need to allow it to run in the system settings.
## Build from source
You can also build the app from the source code.
> Note: Node.js >= 20 is required.
```bash ```bash
# Clone this repository # Clone this repository
@@ -45,15 +30,10 @@ cd jumble
# Install dependencies # Install dependencies
npm install npm install
# Build the app # Run the app
npm run build:mac npm run dev
# or npm run build:win
# or npm run build:linux
# or npm run build:web
``` ```
The executable file will be in the `dist` folder.
## Donate ## Donate
If you like this project, you can buy me a coffee :) ⚡️ codytseng@getalby.com ⚡️ If you like this project, you can buy me a coffee :) ⚡️ codytseng@getalby.com ⚡️

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

View File

@@ -1,17 +1,21 @@
{ {
"$schema": "https://ui.shadcn.com/schema.json", "$schema": "https://ui.shadcn.com/schema.json",
"style": "default", "style": "new-york",
"rsc": false, "rsc": false,
"tsx": true, "tsx": true,
"tailwind": { "tailwind": {
"config": "tailwind.config.js", "config": "tailwind.config.js",
"css": "src/renderer/src/assets/main.css", "css": "src/index.css",
"baseColor": "slate", "baseColor": "zinc",
"cssVariables": true, "cssVariables": true,
"prefix": "" "prefix": ""
}, },
"aliases": { "aliases": {
"components": "@renderer/components", "components": "@/components",
"utils": "@renderer/lib/utils" "utils": "@/lib/utils",
} "ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
} }

View File

@@ -1,34 +0,0 @@
appId: com.jumble.app
productName: jumble
directories:
buildResources: build
files:
- '!**/.vscode/*'
- '!src/*'
- '!electron.vite.config.{js,ts,mjs,cjs}'
- '!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}'
- '!{.env,.env.*,.npmrc,pnpm-lock.yaml}'
- '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}'
asarUnpack:
- resources/**
win:
executableName: jumble
nsis:
artifactName: ${name}-${version}-setup.${ext}
shortcutName: ${productName}
uninstallDisplayName: ${productName}
createDesktopShortcut: always
mac:
notarize: false
dmg:
artifactName: ${name}-${version}.${ext}
linux:
target:
- AppImage
- snap
- deb
maintainer: codytseng
category: Utility
appImage:
artifactName: ${name}-${version}.${ext}
npmRebuild: false

View File

@@ -1,21 +0,0 @@
import { resolve } from 'path'
import { defineConfig, externalizeDepsPlugin } from 'electron-vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
main: {
plugins: [externalizeDepsPlugin()]
},
preload: {
plugins: [externalizeDepsPlugin()]
},
renderer: {
resolve: {
alias: {
'@renderer': resolve('src/renderer/src'),
'@common': resolve('src/common')
}
},
plugins: [react()]
}
})

30
eslint.config.js Normal file
View File

@@ -0,0 +1,30 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
export default tseslint.config(
{ ignores: ['dist'] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
'@typescript-eslint/explicit-function-return-type': 'off',
'react/prop-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'react-refresh/only-export-components': 'off',
'react-hooks/exhaustive-deps': 'off'
}
}
)

View File

@@ -1,23 +1,34 @@
<!doctype html> <!doctype html>
<html> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <link
<link rel="icon" href="/src/assets/favicon-light.svg" media="(prefers-color-scheme: light)" type="image/svg+xml" /> rel="icon"
<link rel="icon" href="/src/assets/favicon-dark.svg" media="(prefers-color-scheme: dark)" type="image/svg+xml" /> href="/favicon-light.svg"
media="(prefers-color-scheme: light)"
type="image/svg+xml"
/>
<link
rel="icon"
href="/favicon-dark.svg"
media="(prefers-color-scheme: dark)"
type="image/svg+xml"
/>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta property="og:url" content="https://jumble.social" /> <meta property="og:url" content="https://jumble.social" />
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
<meta property="og:title" content="Jumble" /> <meta property="og:title" content="Jumble" />
<meta property="og:description" content="A beautiful nostr client focused on browsing relay feeds" /> <meta
property="og:description"
content="A beautiful nostr client focused on browsing relay feeds"
/>
<meta <meta
property="og:image" property="og:image"
content="https://github.com/CodyTseng/jumble/blob/master/resources/og-image.png?raw=true" content="https://github.com/CodyTseng/jumble/blob/master/resources/og-image.png?raw=true"
/> />
<title>Jumble</title>
</head> </head>
<title>Jumble</title>
<body> <body>
<div id="root"></div> <div id="root"></div>
<script type="module" src="/src/main.tsx"></script> <script type="module" src="/src/main.tsx"></script>

7777
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,8 @@
"name": "jumble", "name": "jumble",
"version": "0.1.0", "version": "0.1.0",
"description": "Yet another Nostr desktop client", "description": "Yet another Nostr desktop client",
"main": "./out/main/index.js", "private": true,
"type": "module",
"author": "codytseng", "author": "codytseng",
"license": "MIT", "license": "MIT",
"repository": { "repository": {
@@ -11,80 +12,63 @@
}, },
"homepage": "https://github.com/CodyTseng/jumble", "homepage": "https://github.com/CodyTseng/jumble",
"scripts": { "scripts": {
"dev": "vite --host",
"build": "tsc -b && vite build",
"lint": "eslint .",
"format": "prettier --write .", "format": "prettier --write .",
"lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix", "preview": "vite preview"
"typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false",
"typecheck:web": "tsc --noEmit -p tsconfig.web.json --composite false",
"typecheck": "npm run typecheck:node && npm run typecheck:web",
"start": "electron-vite preview",
"dev": "electron-vite dev",
"dev:web": "vite --config web.vite.config.ts",
"build": "npm run typecheck && electron-vite build",
"postinstall": "electron-builder install-app-deps",
"build:unpack": "npm run build && electron-builder --dir",
"build:win": "npm run build && electron-builder --win -p never",
"build:mac": "electron-vite build && electron-builder --mac -p never",
"build:linux": "electron-vite build && electron-builder --linux -p never",
"build:web": "vite build --config web.vite.config.ts"
}, },
"dependencies": { "dependencies": {
"@electron-toolkit/preload": "^3.0.1", "@nextui-org/image": "^2.2.3",
"@electron-toolkit/utils": "^3.0.0", "@radix-ui/react-avatar": "^1.1.2",
"@nextui-org/image": "^2.0.32", "@radix-ui/react-dialog": "^1.1.4",
"@nextui-org/system": "^2.2.6", "@radix-ui/react-dropdown-menu": "^2.1.4",
"@nextui-org/theme": "^2.2.11", "@radix-ui/react-hover-card": "^1.1.4",
"@radix-ui/react-alert-dialog": "^1.1.2", "@radix-ui/react-popover": "^1.1.4",
"@radix-ui/react-avatar": "^1.1.1", "@radix-ui/react-scroll-area": "^1.2.2",
"@radix-ui/react-dialog": "^1.1.2", "@radix-ui/react-separator": "^1.1.1",
"@radix-ui/react-dropdown-menu": "^2.1.2", "@radix-ui/react-slot": "^1.1.1",
"@radix-ui/react-hover-card": "^1.1.2", "@radix-ui/react-switch": "^1.1.2",
"@radix-ui/react-popover": "^1.1.2", "@radix-ui/react-toast": "^1.2.4",
"@radix-ui/react-radio-group": "^1.2.1", "class-variance-authority": "^0.7.1",
"@radix-ui/react-scroll-area": "^1.2.0",
"@radix-ui/react-select": "^2.1.2",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-switch": "^1.1.1",
"@radix-ui/react-toast": "^1.2.2",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "^1.0.0", "cmdk": "^1.0.0",
"dataloader": "^2.2.2", "dataloader": "^2.2.3",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"framer-motion": "^11.11.17", "framer-motion": "^11.15.0",
"i18next": "^23.16.5", "i18next": "^24.2.0",
"i18next-browser-languagedetector": "^8.0.0", "i18next-browser-languagedetector": "^8.0.2",
"lru-cache": "^11.0.1", "lru-cache": "^11.0.2",
"lucide-react": "^0.453.0", "lucide-react": "^0.469.0",
"nostr-tools": "^2.9.1", "nostr-tools": "^2.10.4",
"path-to-regexp": "^8.2.0", "path-to-regexp": "^8.2.0",
"qrcode.react": "^4.1.0", "qrcode.react": "^4.2.0",
"react-i18next": "^15.1.1",
"react-resizable-panels": "^2.1.5",
"react-string-replace": "^1.1.1",
"tailwind-merge": "^2.5.4",
"tailwindcss-animate": "^1.0.7",
"yet-another-react-lightbox": "^3.21.6"
},
"devDependencies": {
"@electron-toolkit/eslint-config-prettier": "^2.0.0",
"@electron-toolkit/eslint-config-ts": "^2.0.0",
"@electron-toolkit/tsconfig": "^1.0.1",
"@types/node": "^20.14.8",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react": "^4.3.1",
"autoprefixer": "^10.4.20",
"electron": "^31.0.2",
"electron-builder": "^24.13.3",
"electron-vite": "^2.3.0",
"eslint": "^8.57.0",
"eslint-plugin-react": "^7.34.3",
"prettier": "^3.3.2",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"tailwindcss": "^3.4.14", "react-i18next": "^15.2.0",
"typescript": "^5.5.2", "react-resizable-panels": "^2.1.7",
"vite": "^5.3.1" "react-string-replace": "^1.1.1",
"tailwind-merge": "^2.5.5",
"tailwindcss-animate": "^1.0.7",
"yet-another-react-lightbox": "^3.21.7",
"zod": "^3.24.1"
},
"devDependencies": {
"@eslint/js": "^9.17.0",
"@types/node": "^22.10.2",
"@types/react": "^18.3.17",
"@types/react-dom": "^18.3.5",
"@vitejs/plugin-react": "^4.3.4",
"autoprefixer": "^10.4.20",
"eslint": "^9.17.0",
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.16",
"globals": "^15.13.0",
"postcss": "^8.4.49",
"prettier": "3.4.2",
"tailwindcss": "^3.4.17",
"typescript": "~5.6.2",
"typescript-eslint": "^8.18.1",
"vite": "^6.0.3"
} }
} }

View File

@@ -1,3 +1,6 @@
module.exports = { export default {
plugins: [require('tailwindcss'), require('autoprefixer')] plugins: {
tailwindcss: {},
autoprefixer: {}
}
} }

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

View File

@@ -1 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 1080 1228" version="1.1" fill="currentColor" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"><path id="path1" d="M360.047,1225.75c-31.046,-3.901 -75.11,-14.46 -106.756,-25.58c-101.676,-35.727 -175.164,-93.066 -215.387,-168.055c-12.079,-22.521 -30.071,-71.422 -27.297,-74.195c0.736,-0.736 11.648,5.578 24.249,14.031c135.436,90.86 301.047,169.043 465.056,219.547l32.77,10.091l-20.27,7.416c-43.455,15.896 -105.159,22.678 -152.365,16.745Zm166.293,-59.234c-168.523,-50.004 -331.475,-126.514 -481.755,-226.196c-37.737,-25.031 -41.489,-28.372 -43.419,-38.663c-3.585,-19.109 1.498,-83.894 9.798,-124.886c7.343,-36.266 27.664,-106.034 32.278,-110.818c2.023,-2.099 217.924,48.207 221.274,51.557c0.975,0.975 -1.132,11.339 -4.682,23.032c-24.542,80.842 -27.217,127.586 -9.935,173.593c22.507,59.917 114.521,99.888 177.281,77.012c29.23,-10.654 56.593,-41.085 82.629,-91.894c29.288,-57.155 32.348,-64.988 196.483,-503.076c81.138,-216.562 148.499,-394.821 149.692,-396.131c2.1,-2.304 217.949,76.926 223.076,81.884c2.056,1.988 -262.476,712.505 -307.806,826.747c-18.422,46.426 -56.939,123.045 -77.918,154.993c-10.157,15.469 -30.753,40.901 -45.769,56.515c-27.821,28.93 -66.46,58.952 -75.447,58.621c-2.738,-0.106 -23.339,-5.631 -45.78,-12.29Z" style="fill-rule:nonzero;"/></svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -1,8 +1,8 @@
import 'yet-another-react-lightbox/styles.css' import 'yet-another-react-lightbox/styles.css'
import './assets/main.css' import './index.css'
import { Toaster } from '@renderer/components/ui/toaster' import { Toaster } from '@/components/ui/toaster'
import { ThemeProvider } from '@renderer/providers/ThemeProvider' import { ThemeProvider } from '@/providers/ThemeProvider'
import { PageManager } from './PageManager' import { PageManager } from './PageManager'
import NoteListPage from './pages/primary/NoteListPage' import NoteListPage from './pages/primary/NoteListPage'
import { FollowListProvider } from './providers/FollowListProvider' import { FollowListProvider } from './providers/FollowListProvider'

View File

@@ -1,11 +1,7 @@
import Sidebar from '@renderer/components/Sidebar' import Sidebar from '@/components/Sidebar'
import { import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '@/components/ui/resizable'
ResizableHandle, import { cn } from '@/lib/utils'
ResizablePanel, import HomePage from '@/pages/secondary/HomePage'
ResizablePanelGroup
} from '@renderer/components/ui/resizable'
import { cn } from '@renderer/lib/utils'
import HomePage from '@renderer/pages/secondary/HomePage'
import { cloneElement, createContext, useContext, useEffect, useState } from 'react' import { cloneElement, createContext, useContext, useEffect, useState } from 'react'
import { useScreenSize } from './providers/ScreenSizeProvider' import { useScreenSize } from './providers/ScreenSizeProvider'
import { routes } from './routes' import { routes } from './routes'
@@ -187,7 +183,9 @@ export function SecondaryPageLink({
<span <span
className={cn('cursor-pointer', className)} className={cn('cursor-pointer', className)}
onClick={(e) => { onClick={(e) => {
onClick && onClick(e) if (onClick) {
onClick(e)
}
push(to) push(to)
}} }}
> >

View File

@@ -1,59 +0,0 @@
import { ElectronAPI } from '@electron-toolkit/preload'
import { Event } from 'nostr-tools'
export type TRelayGroup = {
groupName: string
relayUrls: string[]
isActive: boolean
}
export type TConfig = {
relayGroups: TRelayGroup[]
theme: TThemeSetting
}
export type TThemeSetting = 'light' | 'dark' | 'system'
export type TTheme = 'light' | 'dark'
export type TDraftEvent = Pick<Event, 'content' | 'created_at' | 'kind' | 'tags'>
export interface ISigner {
getPublicKey: () => Promise<string | null>
signEvent: (draftEvent: TDraftEvent) => Promise<Event | null>
}
export type TElectronWindow = {
electron: ElectronAPI
api: {
system: {
isEncryptionAvailable: () => Promise<boolean>
getSelectedStorageBackend: () => Promise<string>
}
theme: {
addChangeListener: (listener: (theme: TTheme) => void) => void
removeChangeListener: () => void
current: () => Promise<TTheme>
}
storage: {
getItem: (key: string) => Promise<string>
setItem: (key: string, value: string) => Promise<void>
removeItem: (key: string) => Promise<void>
}
nostr: {
login: (nsec: string) => Promise<{
pubkey?: string
reason?: string
}>
logout: () => Promise<void>
}
}
nostr: ISigner
}
export type TAccount = {
pubkey: string
signerType: 'nsec' | 'browser-nsec' | 'nip-07' | 'bunker'
nsec?: string
bunker?: string
bunkerClientSecretKey?: string
}

View File

@@ -5,7 +5,7 @@ import {
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
DialogTrigger DialogTrigger
} from '@renderer/components/ui/dialog' } from '@/components/ui/dialog'
import Username from '../Username' import Username from '../Username'
export default function AboutInfoDialog({ children }: { children: React.ReactNode }) { export default function AboutInfoDialog({ children }: { children: React.ReactNode }) {
@@ -38,17 +38,6 @@ export default function AboutInfoDialog({ children }: { children: React.ReactNod
GitHub GitHub
</a> </a>
</div> </div>
<div>
Desktop app:{' '}
<a
href="https://github.com/CodyTseng/jumble/releases"
target="_blank"
rel="noreferrer"
className="text-primary hover:underline"
>
Download
</a>
</div>
<div> <div>
If you like this project, you can buy me a coffee <br /> If you like this project, you can buy me a coffee <br />
<div className="font-semibold"> codytseng@getalby.com </div> <div className="font-semibold"> codytseng@getalby.com </div>

View File

@@ -1,5 +1,5 @@
import { Button } from '@renderer/components/ui/button' import { Button } from '@/components/ui/button'
import { useNostr } from '@renderer/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import { LogIn } from 'lucide-react' import { LogIn } from 'lucide-react'
export default function LoginButton({ export default function LoginButton({

View File

@@ -1,16 +1,16 @@
import { Avatar, AvatarFallback, AvatarImage } from '@renderer/components/ui/avatar' import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { Button } from '@renderer/components/ui/button' import { Button } from '@/components/ui/button'
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger DropdownMenuTrigger
} from '@renderer/components/ui/dropdown-menu' } from '@/components/ui/dropdown-menu'
import { useFetchProfile } from '@renderer/hooks' import { useFetchProfile } from '@/hooks'
import { toProfile } from '@renderer/lib/link' import { toProfile } from '@/lib/link'
import { formatPubkey, generateImageByPubkey } from '@renderer/lib/pubkey' import { formatPubkey, generateImageByPubkey } from '@/lib/pubkey'
import { useSecondaryPage } from '@renderer/PageManager' import { useSecondaryPage } from '@/PageManager'
import { useNostr } from '@renderer/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
export default function ProfileButton({ export default function ProfileButton({
@@ -69,9 +69,7 @@ export default function ProfileButton({
return ( return (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger className="non-draggable" asChild> <DropdownMenuTrigger asChild>{triggerComponent}</DropdownMenuTrigger>
{triggerComponent}
</DropdownMenuTrigger>
<DropdownMenuContent> <DropdownMenuContent>
<DropdownMenuItem onClick={() => push(toProfile(pubkey))}>{t('Profile')}</DropdownMenuItem> <DropdownMenuItem onClick={() => push(toProfile(pubkey))}>{t('Profile')}</DropdownMenuItem>
<DropdownMenuItem className="text-destructive focus:text-destructive" onClick={logout}> <DropdownMenuItem className="text-destructive focus:text-destructive" onClick={logout}>

View File

@@ -1,4 +1,4 @@
import { useNostr } from '@renderer/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import LoginButton from './LoginButton' import LoginButton from './LoginButton'
import ProfileButton from './ProfileButton' import ProfileButton from './ProfileButton'

View File

@@ -1,5 +1,5 @@
import { Button } from '@renderer/components/ui/button' import { Button } from '@/components/ui/button'
import { useSecondaryPage } from '@renderer/PageManager' import { useSecondaryPage } from '@/PageManager'
import { ChevronLeft } from 'lucide-react' import { ChevronLeft } from 'lucide-react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'

View File

@@ -1,5 +1,5 @@
import { isNsfwEvent } from '@renderer/lib/event' import { isNsfwEvent } from '@/lib/event'
import { cn } from '@renderer/lib/utils' import { cn } from '@/lib/utils'
import { Event } from 'nostr-tools' import { Event } from 'nostr-tools'
import { memo } from 'react' import { memo } from 'react'
import { import {

View File

@@ -1,5 +1,5 @@
import { toNoteList } from '@renderer/lib/link' import { toNoteList } from '@/lib/link'
import { SecondaryPageLink } from '@renderer/PageManager' import { SecondaryPageLink } from '@/PageManager'
import { TEmbeddedRenderer } from './types' import { TEmbeddedRenderer } from './types'
export function EmbeddedHashtag({ hashtag }: { hashtag: string }) { export function EmbeddedHashtag({ hashtag }: { hashtag: string }) {

View File

@@ -1,6 +1,6 @@
import { useFetchEvent } from '@renderer/hooks' import { useFetchEvent } from '@/hooks'
import { toNoStrudelArticle, toNoStrudelNote, toNoStrudelStream } from '@renderer/lib/link' import { toNoStrudelArticle, toNoStrudelNote, toNoStrudelStream } from '@/lib/link'
import { cn } from '@renderer/lib/utils' import { cn } from '@/lib/utils'
import { kinds } from 'nostr-tools' import { kinds } from 'nostr-tools'
import ShortTextNoteCard from '../NoteCard/ShortTextNoteCard' import ShortTextNoteCard from '../NoteCard/ShortTextNoteCard'

View File

@@ -1,5 +1,5 @@
import { useSecondaryPage } from '@renderer/PageManager' import { useSecondaryPage } from '@/PageManager'
import { toNoteList } from '@renderer/lib/link' import { toNoteList } from '@/lib/link'
import { TEmbeddedRenderer } from './types' import { TEmbeddedRenderer } from './types'
export function EmbeddedWebsocketUrl({ url }: { url: string }) { export function EmbeddedWebsocketUrl({ url }: { url: string }) {

View File

@@ -1,7 +1,7 @@
import { Button } from '@renderer/components/ui/button' import { Button } from '@/components/ui/button'
import { useToast } from '@renderer/hooks' import { useToast } from '@/hooks'
import { useFollowList } from '@renderer/providers/FollowListProvider' import { useFollowList } from '@/providers/FollowListProvider'
import { useNostr } from '@renderer/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import { Loader } from 'lucide-react' import { Loader } from 'lucide-react'
import { useMemo, useState } from 'react' import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'

View File

@@ -1,6 +1,6 @@
import { Image } from '@nextui-org/image' import { Image } from '@nextui-org/image'
import { ScrollArea, ScrollBar } from '@renderer/components/ui/scroll-area' import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area'
import { cn } from '@renderer/lib/utils' import { cn } from '@/lib/utils'
import { useState } from 'react' import { useState } from 'react'
import Lightbox from 'yet-another-react-lightbox' import Lightbox from 'yet-another-react-lightbox'
import Zoom from 'yet-another-react-lightbox/plugins/zoom' import Zoom from 'yet-another-react-lightbox/plugins/zoom'

View File

@@ -1,6 +1,6 @@
import { Button } from '@renderer/components/ui/button' import { Button } from '@/components/ui/button'
import { Input } from '@renderer/components/ui/input' import { Input } from '@/components/ui/input'
import { useNostr } from '@renderer/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import { Loader } from 'lucide-react' import { Loader } from 'lucide-react'
import { useState } from 'react' import { useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'

View File

@@ -1,8 +1,7 @@
import { Button } from '@renderer/components/ui/button' import { Button } from '@/components/ui/button'
import { Input } from '@renderer/components/ui/input' import { Input } from '@/components/ui/input'
import { IS_ELECTRON, isElectron } from '@renderer/lib/env' import { useNostr } from '@/providers/NostrProvider'
import { useNostr } from '@renderer/providers/NostrProvider' import { useState } from 'react'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
export default function PrivateKeyLogin({ onLoginSuccess }: { onLoginSuccess: () => void }) { export default function PrivateKeyLogin({ onLoginSuccess }: { onLoginSuccess: () => void }) {
@@ -10,17 +9,6 @@ export default function PrivateKeyLogin({ onLoginSuccess }: { onLoginSuccess: ()
const { nsecLogin } = useNostr() const { nsecLogin } = useNostr()
const [nsec, setNsec] = useState('') const [nsec, setNsec] = useState('')
const [errMsg, setErrMsg] = useState<string | null>(null) const [errMsg, setErrMsg] = useState<string | null>(null)
const [storageBackend, setStorageBackend] = useState('unknown')
useEffect(() => {
const init = async () => {
if (!isElectron(window)) return
const backend = await window.api.system.getSelectedStorageBackend()
setStorageBackend(backend)
}
init()
}, [])
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setNsec(e.target.value) setNsec(e.target.value)
@@ -40,15 +28,9 @@ export default function PrivateKeyLogin({ onLoginSuccess }: { onLoginSuccess: ()
return ( return (
<> <>
<div className="text-orange-400"> <div className="text-orange-400">
{!IS_ELECTRON {t(
? t( 'Using private key login is insecure. It is recommended to use a browser extension for login, such as alby, nostr-keyx or nos2x.'
'Using private key login is insecure. It is recommended to use a browser extension for login, such as alby, nostr-keyx or nos2x.' )}
)
: ['unknown', 'basic_text'].includes(storageBackend)
? t('There are no secret keys stored on this device. Your nsec will be unprotected.')
: t('Your nsec will be encrypted using the {{backend}}.', {
backend: storageBackend
})}
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
<Input <Input

View File

@@ -1,13 +1,12 @@
import { Button } from '@renderer/components/ui/button' import { Button } from '@/components/ui/button'
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
DialogDescription, DialogDescription,
DialogHeader, DialogHeader,
DialogTitle DialogTitle
} from '@renderer/components/ui/dialog' } from '@/components/ui/dialog'
import { IS_ELECTRON } from '@renderer/lib/env' import { useNostr } from '@/providers/NostrProvider'
import { useNostr } from '@renderer/providers/NostrProvider'
import { ArrowLeft } from 'lucide-react' import { ArrowLeft } from 'lucide-react'
import { Dispatch, useState } from 'react' import { Dispatch, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@@ -54,7 +53,7 @@ export default function LoginDialog({
</> </>
) : ( ) : (
<> <>
{!IS_ELECTRON && !!window.nostr && ( {!!window.nostr && (
<Button onClick={() => nip07Login().then(() => setOpen(false))} className="w-full"> <Button onClick={() => nip07Login().then(() => setOpen(false))} className="w-full">
{t('Login with Browser Extension')} {t('Login with Browser Extension')}
</Button> </Button>

View File

@@ -1,4 +1,4 @@
import { useFetchNip05 } from '@renderer/hooks/useFetchNip05' import { useFetchNip05 } from '@/hooks/useFetchNip05'
import { BadgeAlert, BadgeCheck } from 'lucide-react' import { BadgeAlert, BadgeCheck } from 'lucide-react'
export default function Nip05({ nip05, pubkey }: { nip05?: string; pubkey: string }) { export default function Nip05({ nip05, pubkey }: { nip05?: string; pubkey: string }) {

View File

@@ -1,5 +1,5 @@
import { useSecondaryPage } from '@renderer/PageManager' import { useSecondaryPage } from '@/PageManager'
import { toNote } from '@renderer/lib/link' import { toNote } from '@/lib/link'
import { Event } from 'nostr-tools' import { Event } from 'nostr-tools'
import Content from '../Content' import Content from '../Content'
import { FormattedTimestamp } from '../FormattedTimestamp' import { FormattedTimestamp } from '../FormattedTimestamp'

View File

@@ -1,4 +1,4 @@
import client from '@renderer/services/client.service' import client from '@/services/client.service'
import { Event, kinds, verifyEvent } from 'nostr-tools' import { Event, kinds, verifyEvent } from 'nostr-tools'
import { useMemo } from 'react' import { useMemo } from 'react'
import ShortTextNoteCard from './ShortTextNoteCard' import ShortTextNoteCard from './ShortTextNoteCard'

View File

@@ -1,8 +1,8 @@
import { useFetchEvent } from '@renderer/hooks' import { useFetchEvent } from '@/hooks'
import { getParentEventId, getRootEventId } from '@renderer/lib/event' import { getParentEventId, getRootEventId } from '@/lib/event'
import { toNote } from '@renderer/lib/link' import { toNote } from '@/lib/link'
import { cn } from '@renderer/lib/utils' import { cn } from '@/lib/utils'
import { useSecondaryPage } from '@renderer/PageManager' import { useSecondaryPage } from '@/PageManager'
import { Repeat2 } from 'lucide-react' import { Repeat2 } from 'lucide-react'
import { Event } from 'nostr-tools' import { Event } from 'nostr-tools'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'

View File

@@ -1,5 +1,4 @@
import { Event } from 'nostr-tools' import { Event, kinds } from 'nostr-tools'
import { kinds } from 'nostr-tools'
import RepostNoteCard from './RepostNoteCard' import RepostNoteCard from './RepostNoteCard'
import ShortTextNoteCard from './ShortTextNoteCard' import ShortTextNoteCard from './ShortTextNoteCard'

View File

@@ -1,11 +1,11 @@
import { Button } from '@renderer/components/ui/button' import { Button } from '@/components/ui/button'
import { Switch } from '@renderer/components/ui/switch' import { Switch } from '@/components/ui/switch'
import { useFetchRelayInfos } from '@renderer/hooks' import { useFetchRelayInfos } from '@/hooks'
import { isReplyNoteEvent } from '@renderer/lib/event' import { isReplyNoteEvent } from '@/lib/event'
import { cn } from '@renderer/lib/utils' import { cn } from '@/lib/utils'
import { useNostr } from '@renderer/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import { useScreenSize } from '@renderer/providers/ScreenSizeProvider' import { useScreenSize } from '@/providers/ScreenSizeProvider'
import client from '@renderer/services/client.service' import client from '@/services/client.service'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { Event, Filter, kinds } from 'nostr-tools' import { Event, Filter, kinds } from 'nostr-tools'
import { useEffect, useMemo, useRef, useState } from 'react' import { useEffect, useMemo, useRef, useState } from 'react'
@@ -34,7 +34,6 @@ export default function NoteList({
const [hasMore, setHasMore] = useState<boolean>(true) const [hasMore, setHasMore] = useState<boolean>(true)
const [initialized, setInitialized] = useState(false) const [initialized, setInitialized] = useState(false)
const [displayReplies, setDisplayReplies] = useState(false) const [displayReplies, setDisplayReplies] = useState(false)
const observer = useRef<IntersectionObserver | null>(null)
const bottomRef = useRef<HTMLDivElement | null>(null) const bottomRef = useRef<HTMLDivElement | null>(null)
const noteFilter = useMemo(() => { const noteFilter = useMemo(() => {
return { return {
@@ -109,19 +108,21 @@ export default function NoteList({
threshold: 1 threshold: 1
} }
observer.current = new IntersectionObserver((entries) => { const observerInstance = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting && hasMore) { if (entries[0].isIntersecting && hasMore) {
loadMore() loadMore()
} }
}, options) }, options)
if (bottomRef.current) { const currentBottomRef = bottomRef.current
observer.current.observe(bottomRef.current)
if (currentBottomRef) {
observerInstance.observe(currentBottomRef)
} }
return () => { return () => {
if (observer.current && bottomRef.current) { if (observerInstance && currentBottomRef) {
observer.current.unobserve(bottomRef.current) observerInstance.unobserve(currentBottomRef)
} }
} }
}, [initialized, hasMore, events, timelineKey]) }, [initialized, hasMore, events, timelineKey])

View File

@@ -1,8 +1,8 @@
import { createReactionDraftEvent } from '@renderer/lib/draft-event' import { createReactionDraftEvent } from '@/lib/draft-event'
import { cn } from '@renderer/lib/utils' import { cn } from '@/lib/utils'
import { useNostr } from '@renderer/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import { useNoteStats } from '@renderer/providers/NoteStatsProvider' import { useNoteStats } from '@/providers/NoteStatsProvider'
import client from '@renderer/services/client.service' import client from '@/services/client.service'
import { Heart, Loader } from 'lucide-react' import { Heart, Loader } from 'lucide-react'
import { Event } from 'nostr-tools' import { Event } from 'nostr-tools'
import { useEffect, useMemo, useState } from 'react' import { useEffect, useMemo, useState } from 'react'
@@ -37,7 +37,7 @@ export default function LikeButton({
if (hasLiked === undefined) { if (hasLiked === undefined) {
fetchNoteLikedStatus(event) fetchNoteLikedStatus(event)
} }
}, []) }, [canFetch, event])
const like = async (e: React.MouseEvent) => { const like = async (e: React.MouseEvent) => {
e.stopPropagation() e.stopPropagation()

View File

@@ -4,8 +4,8 @@ import {
DialogDescription, DialogDescription,
DialogHeader, DialogHeader,
DialogTitle DialogTitle
} from '@renderer/components/ui/dialog' } from '@/components/ui/dialog'
import { ScrollArea, ScrollBar } from '@renderer/components/ui/scroll-area' import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area'
import { Event } from 'nostr-tools' import { Event } from 'nostr-tools'
export default function RawEventDialog({ export default function RawEventDialog({
@@ -25,9 +25,7 @@ export default function RawEventDialog({
<DialogDescription className="hidden" /> <DialogDescription className="hidden" />
</DialogHeader> </DialogHeader>
<ScrollArea className="h-full"> <ScrollArea className="h-full">
<pre className="text-sm overflow-x-auto text-muted-foreground"> <pre className="text-sm text-muted-foreground">{JSON.stringify(event, null, 2)}</pre>
{JSON.stringify(event, null, 2)}
</pre>
<ScrollBar orientation="horizontal" /> <ScrollBar orientation="horizontal" />
</ScrollArea> </ScrollArea>
</DialogContent> </DialogContent>

View File

@@ -3,8 +3,8 @@ import {
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger DropdownMenuTrigger
} from '@renderer/components/ui/dropdown-menu' } from '@/components/ui/dropdown-menu'
import { getSharableEventId } from '@renderer/lib/event' import { getSharableEventId } from '@/lib/event'
import { Code, Copy, Ellipsis } from 'lucide-react' import { Code, Copy, Ellipsis } from 'lucide-react'
import { Event } from 'nostr-tools' import { Event } from 'nostr-tools'
import { useState } from 'react' import { useState } from 'react'

View File

@@ -1,5 +1,5 @@
import { useNostr } from '@renderer/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import { useNoteStats } from '@renderer/providers/NoteStatsProvider' import { useNoteStats } from '@/providers/NoteStatsProvider'
import { MessageCircle } from 'lucide-react' import { MessageCircle } from 'lucide-react'
import { Event } from 'nostr-tools' import { Event } from 'nostr-tools'
import { useMemo, useState } from 'react' import { useMemo, useState } from 'react'

View File

@@ -3,13 +3,13 @@ import {
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger DropdownMenuTrigger
} from '@renderer/components/ui/dropdown-menu' } from '@/components/ui/dropdown-menu'
import { createRepostDraftEvent } from '@renderer/lib/draft-event' import { createRepostDraftEvent } from '@/lib/draft-event'
import { getSharableEventId } from '@renderer/lib/event' import { getSharableEventId } from '@/lib/event'
import { cn } from '@renderer/lib/utils' import { cn } from '@/lib/utils'
import { useNostr } from '@renderer/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import { useNoteStats } from '@renderer/providers/NoteStatsProvider' import { useNoteStats } from '@/providers/NoteStatsProvider'
import client from '@renderer/services/client.service' import client from '@/services/client.service'
import { Loader, PencilLine, Repeat } from 'lucide-react' import { Loader, PencilLine, Repeat } from 'lucide-react'
import { Event } from 'nostr-tools' import { Event } from 'nostr-tools'
import { useEffect, useMemo, useState } from 'react' import { useEffect, useMemo, useState } from 'react'
@@ -45,7 +45,7 @@ export default function RepostButton({
if (hasReposted === undefined) { if (hasReposted === undefined) {
fetchNoteRepostedStatus(event) fetchNoteRepostedStatus(event)
} }
}, []) }, [canFetch, event])
const repost = async (e: React.MouseEvent) => { const repost = async (e: React.MouseEvent) => {
e.stopPropagation() e.stopPropagation()

View File

@@ -1,4 +1,4 @@
import { cn } from '@renderer/lib/utils' import { cn } from '@/lib/utils'
import { Event } from 'nostr-tools' import { Event } from 'nostr-tools'
import LikeButton from './LikeButton' import LikeButton from './LikeButton'
import NoteOptions from './NoteOptions' import NoteOptions from './NoteOptions'

View File

@@ -1,6 +1,6 @@
import { Button } from '@renderer/components/ui/button' import { Button } from '@/components/ui/button'
import { toNotifications } from '@renderer/lib/link' import { toNotifications } from '@/lib/link'
import { useSecondaryPage } from '@renderer/PageManager' import { useSecondaryPage } from '@/PageManager'
import { Bell } from 'lucide-react' import { Bell } from 'lucide-react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'

View File

@@ -1,9 +1,9 @@
import { useFetchEvent } from '@renderer/hooks' import { useFetchEvent } from '@/hooks'
import { toNote } from '@renderer/lib/link' import { toNote } from '@/lib/link'
import { tagNameEquals } from '@renderer/lib/tag' import { tagNameEquals } from '@/lib/tag'
import { useSecondaryPage } from '@renderer/PageManager' import { useSecondaryPage } from '@/PageManager'
import { useNostr } from '@renderer/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import client from '@renderer/services/client.service' import client from '@/services/client.service'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { Heart, MessageCircle, Repeat } from 'lucide-react' import { Heart, MessageCircle, Repeat } from 'lucide-react'
import { Event, kinds, nip19, validateEvent } from 'nostr-tools' import { Event, kinds, nip19, validateEvent } from 'nostr-tools'
@@ -22,7 +22,6 @@ export default function NotificationList() {
const [notifications, setNotifications] = useState<Event[]>([]) const [notifications, setNotifications] = useState<Event[]>([])
const [until, setUntil] = useState<number | undefined>(dayjs().unix()) const [until, setUntil] = useState<number | undefined>(dayjs().unix())
const bottomRef = useRef<HTMLDivElement | null>(null) const bottomRef = useRef<HTMLDivElement | null>(null)
const observer = useRef<IntersectionObserver | null>(null)
useEffect(() => { useEffect(() => {
if (!pubkey) { if (!pubkey) {
@@ -74,19 +73,21 @@ export default function NotificationList() {
threshold: 1 threshold: 1
} }
observer.current = new IntersectionObserver((entries) => { const observerInstance = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) { if (entries[0].isIntersecting) {
loadMore() loadMore()
} }
}, options) }, options)
if (bottomRef.current) { const currentBottomRef = bottomRef.current
observer.current.observe(bottomRef.current)
if (currentBottomRef) {
observerInstance.observe(currentBottomRef)
} }
return () => { return () => {
if (observer.current && bottomRef.current) { if (observerInstance && currentBottomRef) {
observer.current.unobserve(bottomRef.current) observerInstance.unobserve(currentBottomRef)
} }
} }
}, [until, initialized, timelineKey]) }, [until, initialized, timelineKey])
@@ -141,7 +142,7 @@ function ReactionNotification({ notification }: { notification: Event }) {
return eventId return eventId
? nip19.neventEncode(author ? { id: eventId, author } : { id: eventId }) ? nip19.neventEncode(author ? { id: eventId, author } : { id: eventId })
: undefined : undefined
}, [notification.id]) }, [notification])
const { event } = useFetchEvent(bech32Id) const { event } = useFetchEvent(bech32Id)
if (!event || !bech32Id || event.kind !== kinds.ShortTextNote) return null if (!event || !bech32Id || event.kind !== kinds.ShortTextNote) return null
@@ -191,7 +192,7 @@ function RepostNotification({ notification }: { notification: Event }) {
} catch { } catch {
return null return null
} }
}, []) }, [notification.content])
if (!event) return null if (!event) return null
return ( return (

View File

@@ -1,4 +1,4 @@
import { cn } from '@renderer/lib/utils' import { cn } from '@/lib/utils'
import { useState } from 'react' import { useState } from 'react'
export default function NsfwOverlay({ className }: { className?: string }) { export default function NsfwOverlay({ className }: { className?: string }) {

View File

@@ -1,7 +1,7 @@
import { cn } from '@/lib/utils'
import { Event } from 'nostr-tools' import { Event } from 'nostr-tools'
import UserAvatar from '../UserAvatar'
import { cn } from '@renderer/lib/utils'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import UserAvatar from '../UserAvatar'
export default function ParentNotePreview({ export default function ParentNotePreview({
event, event,

View File

@@ -1,5 +1,5 @@
import PostDialog from '@renderer/components/PostDialog' import PostDialog from '@/components/PostDialog'
import { Button } from '@renderer/components/ui/button' import { Button } from '@/components/ui/button'
import { PencilLine } from 'lucide-react' import { PencilLine } from 'lucide-react'
import { useState } from 'react' import { useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'

View File

@@ -1,7 +1,7 @@
import { Button } from '@renderer/components/ui/button' import { Button } from '@/components/ui/button'
import { Popover, PopoverContent, PopoverTrigger } from '@renderer/components/ui/popover' import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import { extractMentions } from '@renderer/lib/event' import { extractMentions } from '@/lib/event'
import { useNostr } from '@renderer/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import { Event } from 'nostr-tools' import { Event } from 'nostr-tools'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import UserAvatar from '../UserAvatar' import UserAvatar from '../UserAvatar'
@@ -23,7 +23,7 @@ export default function Mentions({
extractMentions(content, parentEvent).then(({ pubkeys }) => extractMentions(content, parentEvent).then(({ pubkeys }) =>
setPubkeys(pubkeys.filter((p) => p !== pubkey)) setPubkeys(pubkeys.filter((p) => p !== pubkey))
) )
}, [content]) }, [content, parentEvent, pubkey])
return ( return (
<Popover> <Popover>

View File

@@ -1,4 +1,4 @@
import { Card } from '@renderer/components/ui/card' import { Card } from '@/components/ui/card'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import Content from '../Content' import Content from '../Content'

View File

@@ -1,8 +1,9 @@
import { Button } from '@renderer/components/ui/button' import { Button } from '@/components/ui/button'
import { useToast } from '@renderer/hooks/use-toast' import { useToast } from '@/hooks/use-toast'
import { useNostr } from '@renderer/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import { ImageUp, LoaderCircle } from 'lucide-react' import { ImageUp, LoaderCircle } from 'lucide-react'
import { useRef, useState } from 'react' import { useRef, useState } from 'react'
import { z } from 'zod'
export default function Uploader({ export default function Uploader({
setContent setContent
@@ -38,7 +39,8 @@ export default function Uploader({
} }
const data = await response.json() const data = await response.json()
const imageUrl = data.nip94_event?.tags.find(([tagName]) => tagName === 'url')?.[1] const tags = z.array(z.array(z.string())).parse(data.nip94_event?.tags ?? [])
const imageUrl = tags.find(([tagName]) => tagName === 'url')?.[1]
if (imageUrl) { if (imageUrl) {
setContent((prevContent) => `${prevContent}\n${imageUrl}`) setContent((prevContent) => `${prevContent}\n${imageUrl}`)
} else { } else {

View File

@@ -1,17 +1,17 @@
import { Button } from '@renderer/components/ui/button' import { Button } from '@/components/ui/button'
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
DialogDescription, DialogDescription,
DialogHeader, DialogHeader,
DialogTitle DialogTitle
} from '@renderer/components/ui/dialog' } from '@/components/ui/dialog'
import { ScrollArea } from '@renderer/components/ui/scroll-area' import { ScrollArea } from '@/components/ui/scroll-area'
import { Textarea } from '@renderer/components/ui/textarea' import { Textarea } from '@/components/ui/textarea'
import { useToast } from '@renderer/hooks/use-toast' import { useToast } from '@/hooks/use-toast'
import { createShortTextNoteDraftEvent } from '@renderer/lib/draft-event' import { createShortTextNoteDraftEvent } from '@/lib/draft-event'
import { useNostr } from '@renderer/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import client from '@renderer/services/client.service' import client from '@/services/client.service'
import { LoaderCircle } from 'lucide-react' import { LoaderCircle } from 'lucide-react'
import { Event } from 'nostr-tools' import { Event } from 'nostr-tools'
import { Dispatch, useState } from 'react' import { Dispatch, useState } from 'react'

View File

@@ -1,6 +1,6 @@
import { Image } from '@nextui-org/image' import { Image } from '@nextui-org/image'
import { generateImageByPubkey } from '@renderer/lib/pubkey' import { generateImageByPubkey } from '@/lib/pubkey'
import { cn } from '@renderer/lib/utils' import { cn } from '@/lib/utils'
import { useEffect, useMemo, useState } from 'react' import { useEffect, useMemo, useState } from 'react'
export default function ProfileBanner({ export default function ProfileBanner({

View File

@@ -1,6 +1,6 @@
import { Avatar, AvatarFallback, AvatarImage } from '@renderer/components/ui/avatar' import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { useFetchProfile } from '@renderer/hooks' import { useFetchProfile } from '@/hooks'
import { generateImageByPubkey } from '@renderer/lib/pubkey' import { generateImageByPubkey } from '@/lib/pubkey'
import { useMemo } from 'react' import { useMemo } from 'react'
import FollowButton from '../FollowButton' import FollowButton from '../FollowButton'
import Nip05 from '../Nip05' import Nip05 from '../Nip05'

View File

@@ -1,5 +1,5 @@
import { Button } from '@renderer/components/ui/button' import { Button } from '@/components/ui/button'
import { usePrimaryPage } from '@renderer/PageManager' import { usePrimaryPage } from '@/PageManager'
import { RefreshCcw } from 'lucide-react' import { RefreshCcw } from 'lucide-react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'

View File

@@ -1,12 +1,12 @@
import { Button } from '@renderer/components/ui/button' import { Button } from '@/components/ui/button'
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger DropdownMenuTrigger
} from '@renderer/components/ui/dropdown-menu' } from '@/components/ui/dropdown-menu'
import { Input } from '@renderer/components/ui/input' import { Input } from '@/components/ui/input'
import { useRelaySettings } from '@renderer/providers/RelaySettingsProvider' import { useRelaySettings } from '@/providers/RelaySettingsProvider'
import { Check, ChevronDown, Circle, CircleCheck, EllipsisVertical } from 'lucide-react' import { Check, ChevronDown, Circle, CircleCheck, EllipsisVertical } from 'lucide-react'
import { useState } from 'react' import { useState } from 'react'
import RelayUrls from './RelayUrl' import RelayUrls from './RelayUrl'

View File

@@ -1,19 +1,20 @@
import { Button } from '@renderer/components/ui/button' import { Button } from '@/components/ui/button'
import { Input } from '@renderer/components/ui/input' import { Input } from '@/components/ui/input'
import { useFetchRelayInfos } from '@renderer/hooks' import { useFetchRelayInfos } from '@/hooks'
import { isWebsocketUrl, normalizeUrl } from '@renderer/lib/url' import { isWebsocketUrl, normalizeUrl } from '@/lib/url'
import { useRelaySettings } from '@renderer/providers/RelaySettingsProvider' import { useRelaySettings } from '@/providers/RelaySettingsProvider'
import client from '@renderer/services/client.service' import client from '@/services/client.service'
import { CircleX, SearchCheck } from 'lucide-react' import { CircleX, SearchCheck } from 'lucide-react'
import { useEffect, useState } from 'react' import { useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
export default function RelayUrls({ groupName }: { groupName: string }) { export default function RelayUrls({ groupName }: { groupName: string }) {
const { t } = useTranslation() const { t } = useTranslation()
const { relayGroups, updateRelayGroupRelayUrls } = useRelaySettings() const { relayGroups, updateRelayGroupRelayUrls } = useRelaySettings()
const rawRelayUrls = relayGroups.find((group) => group.groupName === groupName)?.relayUrls ?? [] const isActive = useMemo(
const isActive = relayGroups.find((group) => group.groupName === groupName)?.isActive ?? false () => relayGroups.find((group) => group.groupName === groupName)?.isActive ?? false,
[relayGroups, groupName]
)
const [newRelayUrl, setNewRelayUrl] = useState('') const [newRelayUrl, setNewRelayUrl] = useState('')
const [newRelayUrlError, setNewRelayUrlError] = useState<string | null>(null) const [newRelayUrlError, setNewRelayUrlError] = useState<string | null>(null)
const [relays, setRelays] = useState< const [relays, setRelays] = useState<
@@ -21,7 +22,11 @@ export default function RelayUrls({ groupName }: { groupName: string }) {
url: string url: string
isConnected: boolean isConnected: boolean
}[] }[]
>(rawRelayUrls.map((url) => ({ url, isConnected: false }))) >(
relayGroups
.find((group) => group.groupName === groupName)
?.relayUrls.map((url) => ({ url, isConnected: false })) ?? []
)
useEffect(() => { useEffect(() => {
const interval = setInterval(() => { const interval = setInterval(() => {

View File

@@ -1,7 +1,7 @@
import { Button } from '@renderer/components/ui/button' import { Button } from '@/components/ui/button'
import { useFetchRelayInfos } from '@renderer/hooks' import { useFetchRelayInfos } from '@/hooks'
import { useRelaySettings } from '@renderer/providers/RelaySettingsProvider' import { useRelaySettings } from '@/providers/RelaySettingsProvider'
import client from '@renderer/services/client.service' import client from '@/services/client.service'
import { Save, SearchCheck } from 'lucide-react' import { Save, SearchCheck } from 'lucide-react'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'

View File

@@ -1,7 +1,7 @@
import { Button } from '@renderer/components/ui/button' import { Button } from '@/components/ui/button'
import { Input } from '@renderer/components/ui/input' import { Input } from '@/components/ui/input'
import { Separator } from '@renderer/components/ui/separator' import { Separator } from '@/components/ui/separator'
import { useRelaySettings } from '@renderer/providers/RelaySettingsProvider' import { useRelaySettings } from '@/providers/RelaySettingsProvider'
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import { RelaySettingsComponentProvider } from './provider' import { RelaySettingsComponentProvider } from './provider'
import RelayGroup from './RelayGroup' import RelayGroup from './RelayGroup'

View File

@@ -1,10 +1,10 @@
import RelaySettings from '@renderer/components/RelaySettings' import RelaySettings from '@/components/RelaySettings'
import { Button } from '@renderer/components/ui/button' import { Button } from '@/components/ui/button'
import { Popover, PopoverContent, PopoverTrigger } from '@renderer/components/ui/popover' import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import { ScrollArea } from '@renderer/components/ui/scroll-area' import { ScrollArea } from '@/components/ui/scroll-area'
import { toRelaySettings } from '@renderer/lib/link' import { toRelaySettings } from '@/lib/link'
import { SecondaryPageLink } from '@renderer/PageManager' import { SecondaryPageLink } from '@/PageManager'
import { useScreenSize } from '@renderer/providers/ScreenSizeProvider' import { useScreenSize } from '@/providers/ScreenSizeProvider'
import { Server } from 'lucide-react' import { Server } from 'lucide-react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'

View File

@@ -1,10 +1,10 @@
import { Separator } from '@renderer/components/ui/separator' import { Separator } from '@/components/ui/separator'
import { isReplyNoteEvent } from '@renderer/lib/event' import { isReplyNoteEvent } from '@/lib/event'
import { isReplyETag, isRootETag } from '@renderer/lib/tag' import { isReplyETag, isRootETag } from '@/lib/tag'
import { cn } from '@renderer/lib/utils' import { cn } from '@/lib/utils'
import { useNostr } from '@renderer/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import { useNoteStats } from '@renderer/providers/NoteStatsProvider' import { useNoteStats } from '@/providers/NoteStatsProvider'
import client from '@renderer/services/client.service' import client from '@/services/client.service'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { Event as NEvent, kinds } from 'nostr-tools' import { Event as NEvent, kinds } from 'nostr-tools'
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
@@ -43,7 +43,7 @@ export default function ReplyNoteList({ event, className }: { event: NEvent; cla
return () => { return () => {
client.removeEventListener('eventPublished', handleEventPublished) client.removeEventListener('eventPublished', handleEventPublished)
} }
}, []) }, [event])
useEffect(() => { useEffect(() => {
if (loading) return if (loading) return
@@ -87,7 +87,7 @@ export default function ReplyNoteList({ event, className }: { event: NEvent; cla
return () => { return () => {
promise.then((closer) => closer?.()) promise.then((closer) => closer?.())
} }
}, []) }, [event])
useEffect(() => { useEffect(() => {
updateNoteReplyCount(event.id, replies.length) updateNoteReplyCount(event.id, replies.length)
@@ -123,7 +123,7 @@ export default function ReplyNoteList({ event, className }: { event: NEvent; cla
replyMap[reply.id] = { event: reply, level: level + 1, parent } replyMap[reply.id] = { event: reply, level: level + 1, parent }
} }
setReplyMap(replyMap) setReplyMap(replyMap)
}, [replies]) }, [replies, event.id, updateNoteReplyCount])
const loadMore = async () => { const loadMore = async () => {
if (loading || !until || !timelineKey) return if (loading || !until || !timelineKey) return

View File

@@ -1,5 +1,5 @@
import { Button } from '@renderer/components/ui/button' import { Button } from '@/components/ui/button'
import { cn } from '@renderer/lib/utils' import { cn } from '@/lib/utils'
import { ChevronUp } from 'lucide-react' import { ChevronUp } from 'lucide-react'
export default function ScrollToTopButton({ export default function ScrollToTopButton({

View File

@@ -1,4 +1,4 @@
import { Button } from '@renderer/components/ui/button' import { Button } from '@/components/ui/button'
import { Search } from 'lucide-react' import { Search } from 'lucide-react'
import { useState } from 'react' import { useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'

View File

@@ -1,17 +1,11 @@
import { SecondaryPageLink } from '@renderer/PageManager' import { SecondaryPageLink } from '@/PageManager'
import { Avatar, AvatarFallback, AvatarImage } from '@renderer/components/ui/avatar' import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { import { CommandDialog, CommandInput, CommandItem, CommandList } from '@/components/ui/command'
CommandDialog, import { useSearchProfiles } from '@/hooks'
CommandInput, import { toNote, toNoteList, toProfile, toProfileList } from '@/lib/link'
CommandItem, import { generateImageByPubkey } from '@/lib/pubkey'
CommandList import { useRelaySettings } from '@/providers/RelaySettingsProvider'
} from '@renderer/components/ui/command' import { TProfile } from '@/types'
import { useSearchProfiles } from '@renderer/hooks'
import { isMacOS } from '@renderer/lib/env'
import { toNote, toNoteList, toProfile, toProfileList } from '@renderer/lib/link'
import { generateImageByPubkey } from '@renderer/lib/pubkey'
import { useRelaySettings } from '@renderer/providers/RelaySettingsProvider'
import { TProfile } from '@renderer/types'
import { Hash, Notebook, UserRound } from 'lucide-react' import { Hash, Notebook, UserRound } from 'lucide-react'
import { nip19 } from 'nostr-tools' import { nip19 } from 'nostr-tools'
import { Dispatch, useEffect, useMemo, useState } from 'react' import { Dispatch, useEffect, useMemo, useState } from 'react'
@@ -68,7 +62,7 @@ export function SearchDialog({ open, setOpen }: { open: boolean; setOpen: Dispat
)} )}
</> </>
) )
}, [input, profiles]) }, [input, profiles, setOpen])
useEffect(() => { useEffect(() => {
const handler = setTimeout(() => { const handler = setTimeout(() => {
@@ -81,11 +75,7 @@ export function SearchDialog({ open, setOpen }: { open: boolean; setOpen: Dispat
}, [input]) }, [input])
return ( return (
<CommandDialog <CommandDialog open={open} onOpenChange={setOpen} classNames={{ content: 'max-sm:top-0' }}>
open={open}
onOpenChange={setOpen}
classNames={{ content: isMacOS() ? 'max-sm:top-9' : 'max-sm:top-0' }}
>
<CommandInput value={input} onValueChange={setInput} /> <CommandInput value={input} onValueChange={setInput} />
<CommandList>{list}</CommandList> <CommandList>{list}</CommandList>
</CommandDialog> </CommandDialog>

View File

@@ -1,6 +1,5 @@
import Logo from '@renderer/assets/Logo' import Logo from '@/assets/Logo'
import { Button } from '@renderer/components/ui/button' import { Button } from '@/components/ui/button'
import { IS_ELECTRON } from '@renderer/lib/env'
import { Info } from 'lucide-react' import { Info } from 'lucide-react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import AboutInfoDialog from '../AboutInfoDialog' import AboutInfoDialog from '../AboutInfoDialog'
@@ -15,24 +14,22 @@ export default function PrimaryPageSidebar() {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<div className="w-52 h-full shrink-0 hidden xl:flex flex-col pb-8 pt-10 pl-4 justify-between relative"> <div className="w-52 h-full shrink-0 hidden xl:flex flex-col pb-8 pt-10 pl-4 justify-between relative">
<div className="draggable absolute top-0 left-0 h-11 w-full" /> <div className="absolute top-0 left-0 h-11 w-full" />
<div className="space-y-2"> <div className="space-y-2">
<div className="draggable ml-4 mb-8 w-40"> <div className="ml-4 mb-8 w-40">
<Logo /> <Logo />
</div> </div>
<PostButton variant="sidebar" /> <PostButton variant="sidebar" />
<RelaySettingsButton variant="sidebar" /> <RelaySettingsButton variant="sidebar" />
<NotificationButton variant="sidebar" /> <NotificationButton variant="sidebar" />
<SearchButton variant="sidebar" /> <SearchButton variant="sidebar" />
{IS_ELECTRON && <RefreshButton variant="sidebar" />} <RefreshButton variant="sidebar" />
{!IS_ELECTRON && ( <AboutInfoDialog>
<AboutInfoDialog> <Button variant="sidebar" size="sidebar">
<Button variant="sidebar" size="sidebar"> <Info />
<Info /> {t('About')}
{t('About')} </Button>
</Button> </AboutInfoDialog>
</AboutInfoDialog>
)}
</div> </div>
<AccountButton variant="sidebar" /> <AccountButton variant="sidebar" />
</div> </div>

View File

@@ -1,5 +1,5 @@
import { Button } from '@renderer/components/ui/button' import { Button } from '@/components/ui/button'
import { useTheme } from '@renderer/providers/ThemeProvider' import { useTheme } from '@/providers/ThemeProvider'
import { Moon, Sun, SunMoon } from 'lucide-react' import { Moon, Sun, SunMoon } from 'lucide-react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'

View File

@@ -0,0 +1,23 @@
import { cn } from '@/lib/utils'
export function Titlebar({
children,
className,
visible = true
}: {
children?: React.ReactNode
className?: string
visible?: boolean
}) {
return (
<div
className={cn(
'absolute top-0 w-full h-9 max-sm:h-11 z-50 bg-background/80 backdrop-blur-md flex items-center font-semibold gap-1 px-2 duration-700 transition-transform',
visible ? '' : '-translate-y-full',
className
)}
>
{children}
</div>
)
}

View File

@@ -1,11 +1,11 @@
import { Avatar, AvatarFallback, AvatarImage } from '@renderer/components/ui/avatar' import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { HoverCard, HoverCardContent, HoverCardTrigger } from '@renderer/components/ui/hover-card' import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card'
import { Skeleton } from '@renderer/components/ui/skeleton' import { Skeleton } from '@/components/ui/skeleton'
import { useFetchProfile } from '@renderer/hooks' import { useFetchProfile } from '@/hooks'
import { generateImageByPubkey } from '@renderer/lib/pubkey' import { generateImageByPubkey } from '@/lib/pubkey'
import { toProfile } from '@renderer/lib/link' import { toProfile } from '@/lib/link'
import { cn } from '@renderer/lib/utils' import { cn } from '@/lib/utils'
import { SecondaryPageLink } from '@renderer/PageManager' import { SecondaryPageLink } from '@/PageManager'
import ProfileCard from '../ProfileCard' import ProfileCard from '../ProfileCard'
import { useMemo } from 'react' import { useMemo } from 'react'

View File

@@ -1,8 +1,8 @@
import FollowButton from '@renderer/components/FollowButton' import FollowButton from '@/components/FollowButton'
import Nip05 from '@renderer/components/Nip05' import Nip05 from '@/components/Nip05'
import UserAvatar from '@renderer/components/UserAvatar' import UserAvatar from '@/components/UserAvatar'
import Username from '@renderer/components/Username' import Username from '@/components/Username'
import { useFetchProfile } from '@renderer/hooks' import { useFetchProfile } from '@/hooks'
export default function UserItem({ pubkey }: { pubkey: string }) { export default function UserItem({ pubkey }: { pubkey: string }) {
const { profile } = useFetchProfile(pubkey) const { profile } = useFetchProfile(pubkey)

View File

@@ -1,9 +1,9 @@
import { HoverCard, HoverCardContent, HoverCardTrigger } from '@renderer/components/ui/hover-card' import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card'
import { Skeleton } from '@renderer/components/ui/skeleton' import { Skeleton } from '@/components/ui/skeleton'
import { useFetchProfile } from '@renderer/hooks' import { useFetchProfile } from '@/hooks'
import { toProfile } from '@renderer/lib/link' import { toProfile } from '@/lib/link'
import { cn } from '@renderer/lib/utils' import { cn } from '@/lib/utils'
import { SecondaryPageLink } from '@renderer/PageManager' import { SecondaryPageLink } from '@/PageManager'
import ProfileCard from '../ProfileCard' import ProfileCard from '../ProfileCard'
export default function Username({ export default function Username({

View File

@@ -1,4 +1,4 @@
import { cn } from '@renderer/lib/utils' import { cn } from '@/lib/utils'
import NsfwOverlay from '../NsfwOverlay' import NsfwOverlay from '../NsfwOverlay'
export default function VideoPlayer({ export default function VideoPlayer({

View File

@@ -1,6 +1,6 @@
import { Image } from '@nextui-org/image' import { Image } from '@nextui-org/image'
import { useFetchWebMetadata } from '@renderer/hooks/useFetchWebMetadata' import { useFetchWebMetadata } from '@/hooks/useFetchWebMetadata'
import { cn } from '@renderer/lib/utils' import { cn } from '@/lib/utils'
import { useMemo } from 'react' import { useMemo } from 'react'
export default function WebPreview({ export default function WebPreview({

View File

@@ -1,7 +1,7 @@
import * as React from "react" import * as React from 'react'
import * as AvatarPrimitive from "@radix-ui/react-avatar" import * as AvatarPrimitive from '@radix-ui/react-avatar'
import { cn } from "@renderer/lib/utils" import { cn } from '@/lib/utils'
const Avatar = React.forwardRef< const Avatar = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>, React.ElementRef<typeof AvatarPrimitive.Root>,
@@ -9,10 +9,7 @@ const Avatar = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<AvatarPrimitive.Root <AvatarPrimitive.Root
ref={ref} ref={ref}
className={cn( className={cn('relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full', className)}
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
className
)}
{...props} {...props}
/> />
)) ))
@@ -24,7 +21,7 @@ const AvatarImage = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<AvatarPrimitive.Image <AvatarPrimitive.Image
ref={ref} ref={ref}
className={cn("aspect-square h-full w-full", className)} className={cn('aspect-square h-full w-full', className)}
{...props} {...props}
/> />
)) ))
@@ -37,7 +34,7 @@ const AvatarFallback = React.forwardRef<
<AvatarPrimitive.Fallback <AvatarPrimitive.Fallback
ref={ref} ref={ref}
className={cn( className={cn(
"flex h-full w-full items-center justify-center rounded-full bg-muted", 'flex h-full w-full items-center justify-center rounded-full bg-muted',
className className
)} )}
{...props} {...props}

View File

@@ -2,29 +2,30 @@ import * as React from 'react'
import { Slot } from '@radix-ui/react-slot' import { Slot } from '@radix-ui/react-slot'
import { cva, type VariantProps } from 'class-variance-authority' import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '@renderer/lib/utils' import { cn } from '@/lib/utils'
const buttonVariants = cva( const buttonVariants = cva(
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0', 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
{ {
variants: { variants: {
variant: { variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90', default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90',
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90', destructive: 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground', outline:
secondary: 'bg-secondary text-secondary-foreground hover:bg-muted/80', 'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
secondary: 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
'secondary-2': 'bg-secondary text-secondary-foreground hover:bg-highlight', 'secondary-2': 'bg-secondary text-secondary-foreground hover:bg-highlight',
ghost: 'text-muted-foreground hover:bg-accent hover:text-foreground', ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline', link: 'text-primary underline-offset-4 hover:underline',
titlebar: 'non-draggable hover:bg-accent hover:text-accent-foreground', titlebar: 'hover:bg-accent hover:text-accent-foreground',
sidebar: 'non-draggable hover:bg-accent hover:text-accent-foreground', sidebar: 'hover:bg-accent hover:text-accent-foreground',
'small-screen-titlebar': 'non-draggable hover:bg-accent hover:text-accent-foreground' 'small-screen-titlebar': 'hover:bg-accent hover:text-accent-foreground'
}, },
size: { size: {
default: 'h-8 rounded-lg px-3', default: 'h-9 px-4 py-2',
sm: 'h-8 rounded-lg px-2', sm: 'h-8 rounded-md px-3 text-xs',
lg: 'h-10 px-4 py-2', lg: 'h-10 rounded-md px-8',
icon: 'h-8 w-8 rounded-full', icon: 'h-9 w-9',
titlebar: 'h-7 w-7 rounded-full', titlebar: 'h-7 w-7 rounded-full',
sidebar: 'w-full flex py-2 px-4 rounded-full justify-start gap-4 text-lg font-semibold', sidebar: 'w-full flex py-2 px-4 rounded-full justify-start gap-4 text-lg font-semibold',
'small-screen-titlebar': 'h-8 w-8 rounded-full' 'small-screen-titlebar': 'h-8 w-8 rounded-full'

View File

@@ -1,6 +1,6 @@
import * as React from 'react' import * as React from 'react'
import { cn } from '@renderer/lib/utils' import { cn } from '@/lib/utils'
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>( const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => ( ({ className, ...props }, ref) => (
@@ -20,23 +20,22 @@ const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDiv
) )
CardHeader.displayName = 'CardHeader' CardHeader.displayName = 'CardHeader'
const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>( const CardTitle = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => ( ({ className, ...props }, ref) => (
<h3 <div
ref={ref} ref={ref}
className={cn('text-2xl font-semibold leading-none tracking-tight', className)} className={cn('font-semibold leading-none tracking-tight', className)}
{...props} {...props}
/> />
) )
) )
CardTitle.displayName = 'CardTitle' CardTitle.displayName = 'CardTitle'
const CardDescription = React.forwardRef< const CardDescription = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
HTMLParagraphElement, ({ className, ...props }, ref) => (
React.HTMLAttributes<HTMLParagraphElement> <div ref={ref} className={cn('text-sm text-muted-foreground', className)} {...props} />
>(({ className, ...props }, ref) => ( )
<p ref={ref} className={cn('text-sm text-muted-foreground', className)} {...props} /> )
))
CardDescription.displayName = 'CardDescription' CardDescription.displayName = 'CardDescription'
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>( const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(

Some files were not shown because too many files have changed in this diff Show More