Browse Source

refactor: remove electron-related code

imwald
codytseng 1 year ago
parent
commit
2b1e6fe8f5
  1. 9
      .editorconfig
  2. 4
      .eslintignore
  3. 14
      .eslintrc.cjs
  4. 1
      .github/FUNDING.yml
  5. 57
      .github/workflows/release.yml
  6. 23
      .gitignore
  7. 3
      .prettierignore
  8. 3
      .vscode/extensions.json
  9. 39
      .vscode/launch.json
  10. 11
      .vscode/settings.json
  11. 21
      LICENSE
  12. 26
      README.md
  13. BIN
      build/icon.icns
  14. BIN
      build/icon.ico
  15. BIN
      build/icon.png
  16. BIN
      build/icon@2x.png
  17. 16
      components.json
  18. 34
      electron-builder.yml
  19. 21
      electron.vite.config.ts
  20. 30
      eslint.config.js
  21. 27
      index.html
  22. 9307
      package-lock.json
  23. 112
      package.json
  24. 7
      postcss.config.js
  25. 0
      public/favicon-dark.svg
  26. 0
      public/favicon-light.svg
  27. BIN
      resources/icon.png
  28. 1
      resources/icon.svg
  29. 6
      src/App.tsx
  30. 16
      src/PageManager.tsx
  31. 0
      src/assets/Logo.tsx
  32. 59
      src/common/types.ts
  33. 13
      src/components/AboutInfoDialog/index.tsx
  34. 4
      src/components/AccountButton/LoginButton.tsx
  35. 20
      src/components/AccountButton/ProfileButton.tsx
  36. 2
      src/components/AccountButton/index.tsx
  37. 4
      src/components/BackButton/index.tsx
  38. 4
      src/components/Content/index.tsx
  39. 4
      src/components/Embedded/EmbeddedHashtag.tsx
  40. 0
      src/components/Embedded/EmbeddedMention.tsx
  41. 0
      src/components/Embedded/EmbeddedNormalUrl.tsx
  42. 6
      src/components/Embedded/EmbeddedNote.tsx
  43. 4
      src/components/Embedded/EmbeddedWebsocketUrl.tsx
  44. 0
      src/components/Embedded/index.tsx
  45. 0
      src/components/Embedded/types.tsx
  46. 8
      src/components/FollowButton/index.tsx
  47. 0
      src/components/FormattedTimestamp/index.tsx
  48. 4
      src/components/ImageGallery/index.tsx
  49. 6
      src/components/LoginDialog/BunkerLogin.tsx
  50. 30
      src/components/LoginDialog/NsecLogin.tsx
  51. 9
      src/components/LoginDialog/index.tsx
  52. 2
      src/components/Nip05/index.tsx
  53. 4
      src/components/Note/index.tsx
  54. 2
      src/components/NoteCard/RepostNoteCard.tsx
  55. 10
      src/components/NoteCard/ShortTextNoteCard.tsx
  56. 3
      src/components/NoteCard/index.tsx
  57. 29
      src/components/NoteList/index.tsx
  58. 12
      src/components/NoteStats/LikeButton.tsx
  59. 8
      src/components/NoteStats/NoteOptions/RawEventDialog.tsx
  60. 4
      src/components/NoteStats/NoteOptions/index.tsx
  61. 4
      src/components/NoteStats/ReplyButton.tsx
  62. 16
      src/components/NoteStats/RepostButton.tsx
  63. 2
      src/components/NoteStats/index.tsx
  64. 0
      src/components/NoteStats/utils.ts
  65. 6
      src/components/NotificationButton/index.tsx
  66. 29
      src/components/NotificationList/index.tsx
  67. 2
      src/components/NsfwOverlay/index.tsx
  68. 4
      src/components/ParentNotePreview/index.tsx
  69. 4
      src/components/PostButton/index.tsx
  70. 10
      src/components/PostDialog/Metions.tsx
  71. 2
      src/components/PostDialog/Preview.tsx
  72. 10
      src/components/PostDialog/Uploader.tsx
  73. 16
      src/components/PostDialog/index.tsx
  74. 0
      src/components/ProfileAbout/index.tsx
  75. 4
      src/components/ProfileBanner/index.tsx
  76. 6
      src/components/ProfileCard/index.tsx
  77. 4
      src/components/RefreshButton/index.tsx
  78. 8
      src/components/RelaySettings/RelayGroup.tsx
  79. 27
      src/components/RelaySettings/RelayUrl.tsx
  80. 8
      src/components/RelaySettings/TemporaryRelayGroup.tsx
  81. 8
      src/components/RelaySettings/index.tsx
  82. 0
      src/components/RelaySettings/provider.tsx
  83. 0
      src/components/RelaySettings/types.ts
  84. 14
      src/components/RelaySettingsButton/index.tsx
  85. 0
      src/components/ReplyNote/index.tsx
  86. 20
      src/components/ReplyNoteList/index.tsx
  87. 4
      src/components/ScrollToTopButton/index.tsx
  88. 2
      src/components/SearchButton/index.tsx
  89. 30
      src/components/SearchDialog/index.tsx
  90. 13
      src/components/Sidebar/index.tsx
  91. 4
      src/components/ThemeToggle/index.tsx
  92. 23
      src/components/Titlebar/index.tsx
  93. 16
      src/components/UserAvatar/index.tsx
  94. 10
      src/components/UserItem/index.tsx
  95. 12
      src/components/Username/index.tsx
  96. 2
      src/components/VideoPlayer/index.tsx
  97. 4
      src/components/WebPreview/index.tsx
  98. 15
      src/components/ui/avatar.tsx
  99. 29
      src/components/ui/button.tsx
  100. 19
      src/components/ui/card.tsx
  101. Some files were not shown because too many files have changed in this diff Show More

9
.editorconfig

@ -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

4
.eslintignore

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

14
.eslintrc.cjs

@ -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

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

57
.github/workflows/release.yml

@ -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

@ -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?

3
.prettierignore

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

3
.vscode/extensions.json vendored

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

39
.vscode/launch.json vendored

@ -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

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

21
LICENSE

@ -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.

26
README.md

@ -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 ⚡

BIN
build/icon.icns

Binary file not shown.

BIN
build/icon.ico

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

BIN
build/icon.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

BIN
build/icon@2x.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

16
components.json

@ -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"
} }

34
electron-builder.yml

@ -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

21
electron.vite.config.ts

@ -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

@ -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'
}
}
)

27
src/renderer/index.html → index.html

@ -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"
/> />
</head>
<title>Jumble</title> <title>Jumble</title>
</head>
<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>

9307
package-lock.json generated

File diff suppressed because it is too large Load Diff

112
package.json

@ -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": "^18.3.1",
"react-resizable-panels": "^2.1.5", "react-dom": "^18.3.1",
"react-i18next": "^15.2.0",
"react-resizable-panels": "^2.1.7",
"react-string-replace": "^1.1.1", "react-string-replace": "^1.1.1",
"tailwind-merge": "^2.5.4", "tailwind-merge": "^2.5.5",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"yet-another-react-lightbox": "^3.21.6" "yet-another-react-lightbox": "^3.21.7",
"zod": "^3.24.1"
}, },
"devDependencies": { "devDependencies": {
"@electron-toolkit/eslint-config-prettier": "^2.0.0", "@eslint/js": "^9.17.0",
"@electron-toolkit/eslint-config-ts": "^2.0.0", "@types/node": "^22.10.2",
"@electron-toolkit/tsconfig": "^1.0.1", "@types/react": "^18.3.17",
"@types/node": "^20.14.8", "@types/react-dom": "^18.3.5",
"@types/react": "^18.3.3", "@vitejs/plugin-react": "^4.3.4",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react": "^4.3.1",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"electron": "^31.0.2", "eslint": "^9.17.0",
"electron-builder": "^24.13.3", "eslint-plugin-react-hooks": "^5.0.0",
"electron-vite": "^2.3.0", "eslint-plugin-react-refresh": "^0.4.16",
"eslint": "^8.57.0", "globals": "^15.13.0",
"eslint-plugin-react": "^7.34.3", "postcss": "^8.4.49",
"prettier": "^3.3.2", "prettier": "3.4.2",
"react": "^18.3.1", "tailwindcss": "^3.4.17",
"react-dom": "^18.3.1", "typescript": "~5.6.2",
"tailwindcss": "^3.4.14", "typescript-eslint": "^8.18.1",
"typescript": "^5.5.2", "vite": "^6.0.3"
"vite": "^5.3.1"
} }
} }

7
postcss.config.js

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

0
src/renderer/src/assets/favicon-dark.svg → public/favicon-dark.svg

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

0
src/renderer/src/assets/favicon-light.svg → public/favicon-light.svg

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
resources/icon.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

1
resources/icon.svg

@ -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

6
src/renderer/src/App.tsx → src/App.tsx

@ -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'

16
src/renderer/src/PageManager.tsx → src/PageManager.tsx

@ -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)
}} }}
> >

0
src/renderer/src/assets/Logo.tsx → src/assets/Logo.tsx

59
src/common/types.ts

@ -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
}

13
src/renderer/src/components/AboutInfoDialog/index.tsx → src/components/AboutInfoDialog/index.tsx

@ -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>

4
src/renderer/src/components/AccountButton/LoginButton.tsx → src/components/AccountButton/LoginButton.tsx

@ -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({

20
src/renderer/src/components/AccountButton/ProfileButton.tsx → src/components/AccountButton/ProfileButton.tsx

@ -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}>

2
src/renderer/src/components/AccountButton/index.tsx → src/components/AccountButton/index.tsx

@ -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'

4
src/renderer/src/components/BackButton/index.tsx → src/components/BackButton/index.tsx

@ -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'

4
src/renderer/src/components/Content/index.tsx → src/components/Content/index.tsx

@ -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 {

4
src/renderer/src/components/Embedded/EmbeddedHashtag.tsx → src/components/Embedded/EmbeddedHashtag.tsx

@ -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 }) {

0
src/renderer/src/components/Embedded/EmbeddedMention.tsx → src/components/Embedded/EmbeddedMention.tsx

0
src/renderer/src/components/Embedded/EmbeddedNormalUrl.tsx → src/components/Embedded/EmbeddedNormalUrl.tsx

6
src/renderer/src/components/Embedded/EmbeddedNote.tsx → src/components/Embedded/EmbeddedNote.tsx

@ -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'

4
src/renderer/src/components/Embedded/EmbeddedWebsocketUrl.tsx → src/components/Embedded/EmbeddedWebsocketUrl.tsx

@ -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 }) {

0
src/renderer/src/components/Embedded/index.tsx → src/components/Embedded/index.tsx

0
src/renderer/src/components/Embedded/types.tsx → src/components/Embedded/types.tsx

8
src/renderer/src/components/FollowButton/index.tsx → src/components/FollowButton/index.tsx

@ -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'

0
src/renderer/src/components/FormattedTimestamp/index.tsx → src/components/FormattedTimestamp/index.tsx

4
src/renderer/src/components/ImageGallery/index.tsx → src/components/ImageGallery/index.tsx

@ -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'

6
src/renderer/src/components/LoginDialog/BunkerLogin.tsx → src/components/LoginDialog/BunkerLogin.tsx

@ -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'

30
src/renderer/src/components/LoginDialog/NsecLogin.tsx → src/components/LoginDialog/NsecLogin.tsx

@ -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

9
src/renderer/src/components/LoginDialog/index.tsx → src/components/LoginDialog/index.tsx

@ -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>

2
src/renderer/src/components/Nip05/index.tsx → src/components/Nip05/index.tsx

@ -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 }) {

4
src/renderer/src/components/Note/index.tsx → src/components/Note/index.tsx

@ -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'

2
src/renderer/src/components/NoteCard/RepostNoteCard.tsx → src/components/NoteCard/RepostNoteCard.tsx

@ -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'

10
src/renderer/src/components/NoteCard/ShortTextNoteCard.tsx → src/components/NoteCard/ShortTextNoteCard.tsx

@ -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'

3
src/renderer/src/components/NoteCard/index.tsx → src/components/NoteCard/index.tsx

@ -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'

29
src/renderer/src/components/NoteList/index.tsx → src/components/NoteList/index.tsx

@ -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])

12
src/renderer/src/components/NoteStats/LikeButton.tsx → src/components/NoteStats/LikeButton.tsx

@ -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()

8
src/renderer/src/components/NoteStats/NoteOptions/RawEventDialog.tsx → src/components/NoteStats/NoteOptions/RawEventDialog.tsx

@ -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>

4
src/renderer/src/components/NoteStats/NoteOptions/index.tsx → src/components/NoteStats/NoteOptions/index.tsx

@ -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'

4
src/renderer/src/components/NoteStats/ReplyButton.tsx → src/components/NoteStats/ReplyButton.tsx

@ -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'

16
src/renderer/src/components/NoteStats/RepostButton.tsx → src/components/NoteStats/RepostButton.tsx

@ -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()

2
src/renderer/src/components/NoteStats/index.tsx → src/components/NoteStats/index.tsx

@ -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'

0
src/renderer/src/components/NoteStats/utils.ts → src/components/NoteStats/utils.ts

6
src/renderer/src/components/NotificationButton/index.tsx → src/components/NotificationButton/index.tsx

@ -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'

29
src/renderer/src/components/NotificationList/index.tsx → src/components/NotificationList/index.tsx

@ -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 (

2
src/renderer/src/components/NsfwOverlay/index.tsx → src/components/NsfwOverlay/index.tsx

@ -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 }) {

4
src/renderer/src/components/ParentNotePreview/index.tsx → src/components/ParentNotePreview/index.tsx

@ -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,

4
src/renderer/src/components/PostButton/index.tsx → src/components/PostButton/index.tsx

@ -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'

10
src/renderer/src/components/PostDialog/Metions.tsx → src/components/PostDialog/Metions.tsx

@ -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>

2
src/renderer/src/components/PostDialog/Preview.tsx → src/components/PostDialog/Preview.tsx

@ -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'

10
src/renderer/src/components/PostDialog/Uploader.tsx → src/components/PostDialog/Uploader.tsx

@ -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 {

16
src/renderer/src/components/PostDialog/index.tsx → src/components/PostDialog/index.tsx

@ -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'

0
src/renderer/src/components/ProfileAbout/index.tsx → src/components/ProfileAbout/index.tsx

4
src/renderer/src/components/ProfileBanner/index.tsx → src/components/ProfileBanner/index.tsx

@ -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({

6
src/renderer/src/components/ProfileCard/index.tsx → src/components/ProfileCard/index.tsx

@ -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'

4
src/renderer/src/components/RefreshButton/index.tsx → src/components/RefreshButton/index.tsx

@ -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'

8
src/renderer/src/components/RelaySettings/RelayGroup.tsx → src/components/RelaySettings/RelayGroup.tsx

@ -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'

27
src/renderer/src/components/RelaySettings/RelayUrl.tsx → src/components/RelaySettings/RelayUrl.tsx

@ -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(() => {

8
src/renderer/src/components/RelaySettings/TemporaryRelayGroup.tsx → src/components/RelaySettings/TemporaryRelayGroup.tsx

@ -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'

8
src/renderer/src/components/RelaySettings/index.tsx → src/components/RelaySettings/index.tsx

@ -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'

0
src/renderer/src/components/RelaySettings/provider.tsx → src/components/RelaySettings/provider.tsx

0
src/renderer/src/components/RelaySettings/types.ts → src/components/RelaySettings/types.ts

14
src/renderer/src/components/RelaySettingsButton/index.tsx → src/components/RelaySettingsButton/index.tsx

@ -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'

0
src/renderer/src/components/ReplyNote/index.tsx → src/components/ReplyNote/index.tsx

20
src/renderer/src/components/ReplyNoteList/index.tsx → src/components/ReplyNoteList/index.tsx

@ -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

4
src/renderer/src/components/ScrollToTopButton/index.tsx → src/components/ScrollToTopButton/index.tsx

@ -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({

2
src/renderer/src/components/SearchButton/index.tsx → src/components/SearchButton/index.tsx

@ -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'

30
src/renderer/src/components/SearchDialog/index.tsx → src/components/SearchDialog/index.tsx

@ -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>

13
src/renderer/src/components/Sidebar/index.tsx → src/components/Sidebar/index.tsx

@ -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>

4
src/renderer/src/components/ThemeToggle/index.tsx → src/components/ThemeToggle/index.tsx

@ -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'

23
src/components/Titlebar/index.tsx

@ -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>
)
}

16
src/renderer/src/components/UserAvatar/index.tsx → src/components/UserAvatar/index.tsx

@ -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'

10
src/renderer/src/components/UserItem/index.tsx → src/components/UserItem/index.tsx

@ -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)

12
src/renderer/src/components/Username/index.tsx → src/components/Username/index.tsx

@ -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({

2
src/renderer/src/components/VideoPlayer/index.tsx → src/components/VideoPlayer/index.tsx

@ -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({

4
src/renderer/src/components/WebPreview/index.tsx → src/components/WebPreview/index.tsx

@ -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({

15
src/renderer/src/components/ui/avatar.tsx → src/components/ui/avatar.tsx

@ -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}

29
src/renderer/src/components/ui/button.tsx → src/components/ui/button.tsx

@ -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'

19
src/renderer/src/components/ui/card.tsx → src/components/ui/card.tsx

@ -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

Loading…
Cancel
Save