refactor: remove electron-related code
@@ -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
|
||||
@@ -1,4 +0,0 @@
|
||||
node_modules
|
||||
dist
|
||||
out
|
||||
.gitignore
|
||||
@@ -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
@@ -1 +0,0 @@
|
||||
github: [CodyTseng]
|
||||
57
.github/workflows/release.yml
vendored
@@ -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
|
||||
dist
|
||||
out
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.log*
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
@@ -2,5 +2,4 @@ out
|
||||
dist
|
||||
pnpm-lock.yaml
|
||||
LICENSE.md
|
||||
tsconfig.json
|
||||
tsconfig.*.json
|
||||
*.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 Groups:** Easily manage and switch between relay groups
|
||||
- **Clean Interface:** Enjoy a minimalist design and intuitive interactions
|
||||
- **Cross-Platform:** Available on macOS, Windows, Linux, and web browsers
|
||||
|
||||
## Web Version
|
||||
|
||||
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.
|
||||
## Run Locally
|
||||
|
||||
```bash
|
||||
# Clone this repository
|
||||
@@ -45,15 +30,10 @@ cd jumble
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Build the app
|
||||
npm run build:mac
|
||||
# or npm run build:win
|
||||
# or npm run build:linux
|
||||
# or npm run build:web
|
||||
# Run the app
|
||||
npm run dev
|
||||
```
|
||||
|
||||
The executable file will be in the `dist` folder.
|
||||
|
||||
## Donate
|
||||
|
||||
If you like this project, you can buy me a coffee :) ⚡️ codytseng@getalby.com ⚡️
|
||||
|
||||
BIN
build/icon.icns
BIN
build/icon.ico
|
Before Width: | Height: | Size: 13 KiB |
BIN
build/icon.png
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 71 KiB |
@@ -1,17 +1,21 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "default",
|
||||
"style": "new-york",
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.js",
|
||||
"css": "src/renderer/src/assets/main.css",
|
||||
"baseColor": "slate",
|
||||
"css": "src/index.css",
|
||||
"baseColor": "zinc",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@renderer/components",
|
||||
"utils": "@renderer/lib/utils"
|
||||
}
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"iconLibrary": "lucide"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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'
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -1,23 +1,34 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="icon" href="/src/assets/favicon-light.svg" media="(prefers-color-scheme: light)" type="image/svg+xml" />
|
||||
<link rel="icon" href="/src/assets/favicon-dark.svg" media="(prefers-color-scheme: dark)" type="image/svg+xml" />
|
||||
<link
|
||||
rel="icon"
|
||||
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:type" content="website" />
|
||||
<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
|
||||
property="og:image"
|
||||
content="https://github.com/CodyTseng/jumble/blob/master/resources/og-image.png?raw=true"
|
||||
/>
|
||||
<title>Jumble</title>
|
||||
</head>
|
||||
|
||||
<title>Jumble</title>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
7777
package-lock.json
generated
118
package.json
@@ -2,7 +2,8 @@
|
||||
"name": "jumble",
|
||||
"version": "0.1.0",
|
||||
"description": "Yet another Nostr desktop client",
|
||||
"main": "./out/main/index.js",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"author": "codytseng",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
@@ -11,80 +12,63 @@
|
||||
},
|
||||
"homepage": "https://github.com/CodyTseng/jumble",
|
||||
"scripts": {
|
||||
"dev": "vite --host",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"format": "prettier --write .",
|
||||
"lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix",
|
||||
"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"
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@electron-toolkit/preload": "^3.0.1",
|
||||
"@electron-toolkit/utils": "^3.0.0",
|
||||
"@nextui-org/image": "^2.0.32",
|
||||
"@nextui-org/system": "^2.2.6",
|
||||
"@nextui-org/theme": "^2.2.11",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.2",
|
||||
"@radix-ui/react-avatar": "^1.1.1",
|
||||
"@radix-ui/react-dialog": "^1.1.2",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.2",
|
||||
"@radix-ui/react-hover-card": "^1.1.2",
|
||||
"@radix-ui/react-popover": "^1.1.2",
|
||||
"@radix-ui/react-radio-group": "^1.2.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",
|
||||
"@nextui-org/image": "^2.2.3",
|
||||
"@radix-ui/react-avatar": "^1.1.2",
|
||||
"@radix-ui/react-dialog": "^1.1.4",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.4",
|
||||
"@radix-ui/react-hover-card": "^1.1.4",
|
||||
"@radix-ui/react-popover": "^1.1.4",
|
||||
"@radix-ui/react-scroll-area": "^1.2.2",
|
||||
"@radix-ui/react-separator": "^1.1.1",
|
||||
"@radix-ui/react-slot": "^1.1.1",
|
||||
"@radix-ui/react-switch": "^1.1.2",
|
||||
"@radix-ui/react-toast": "^1.2.4",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.0.0",
|
||||
"dataloader": "^2.2.2",
|
||||
"dataloader": "^2.2.3",
|
||||
"dayjs": "^1.11.13",
|
||||
"framer-motion": "^11.11.17",
|
||||
"i18next": "^23.16.5",
|
||||
"i18next-browser-languagedetector": "^8.0.0",
|
||||
"lru-cache": "^11.0.1",
|
||||
"lucide-react": "^0.453.0",
|
||||
"nostr-tools": "^2.9.1",
|
||||
"framer-motion": "^11.15.0",
|
||||
"i18next": "^24.2.0",
|
||||
"i18next-browser-languagedetector": "^8.0.2",
|
||||
"lru-cache": "^11.0.2",
|
||||
"lucide-react": "^0.469.0",
|
||||
"nostr-tools": "^2.10.4",
|
||||
"path-to-regexp": "^8.2.0",
|
||||
"qrcode.react": "^4.1.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",
|
||||
"qrcode.react": "^4.2.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"tailwindcss": "^3.4.14",
|
||||
"typescript": "^5.5.2",
|
||||
"vite": "^5.3.1"
|
||||
"react-i18next": "^15.2.0",
|
||||
"react-resizable-panels": "^2.1.7",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: [require('tailwindcss'), require('autoprefixer')]
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {}
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 23 KiB |
@@ -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 |
@@ -1,8 +1,8 @@
|
||||
import 'yet-another-react-lightbox/styles.css'
|
||||
import './assets/main.css'
|
||||
import './index.css'
|
||||
|
||||
import { Toaster } from '@renderer/components/ui/toaster'
|
||||
import { ThemeProvider } from '@renderer/providers/ThemeProvider'
|
||||
import { Toaster } from '@/components/ui/toaster'
|
||||
import { ThemeProvider } from '@/providers/ThemeProvider'
|
||||
import { PageManager } from './PageManager'
|
||||
import NoteListPage from './pages/primary/NoteListPage'
|
||||
import { FollowListProvider } from './providers/FollowListProvider'
|
||||
@@ -1,11 +1,7 @@
|
||||
import Sidebar from '@renderer/components/Sidebar'
|
||||
import {
|
||||
ResizableHandle,
|
||||
ResizablePanel,
|
||||
ResizablePanelGroup
|
||||
} from '@renderer/components/ui/resizable'
|
||||
import { cn } from '@renderer/lib/utils'
|
||||
import HomePage from '@renderer/pages/secondary/HomePage'
|
||||
import Sidebar from '@/components/Sidebar'
|
||||
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '@/components/ui/resizable'
|
||||
import { cn } from '@/lib/utils'
|
||||
import HomePage from '@/pages/secondary/HomePage'
|
||||
import { cloneElement, createContext, useContext, useEffect, useState } from 'react'
|
||||
import { useScreenSize } from './providers/ScreenSizeProvider'
|
||||
import { routes } from './routes'
|
||||
@@ -187,7 +183,9 @@ export function SecondaryPageLink({
|
||||
<span
|
||||
className={cn('cursor-pointer', className)}
|
||||
onClick={(e) => {
|
||||
onClick && onClick(e)
|
||||
if (onClick) {
|
||||
onClick(e)
|
||||
}
|
||||
push(to)
|
||||
}}
|
||||
>
|
||||
@@ -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
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger
|
||||
} from '@renderer/components/ui/dialog'
|
||||
} from '@/components/ui/dialog'
|
||||
import Username from '../Username'
|
||||
|
||||
export default function AboutInfoDialog({ children }: { children: React.ReactNode }) {
|
||||
@@ -38,17 +38,6 @@ export default function AboutInfoDialog({ children }: { children: React.ReactNod
|
||||
GitHub
|
||||
</a>
|
||||
</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>
|
||||
If you like this project, you can buy me a coffee ☕️ <br />
|
||||
<div className="font-semibold">⚡️ codytseng@getalby.com ⚡️</div>
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Button } from '@renderer/components/ui/button'
|
||||
import { useNostr } from '@renderer/providers/NostrProvider'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { useNostr } from '@/providers/NostrProvider'
|
||||
import { LogIn } from 'lucide-react'
|
||||
|
||||
export default function LoginButton({
|
||||
@@ -1,16 +1,16 @@
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@renderer/components/ui/avatar'
|
||||
import { Button } from '@renderer/components/ui/button'
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger
|
||||
} from '@renderer/components/ui/dropdown-menu'
|
||||
import { useFetchProfile } from '@renderer/hooks'
|
||||
import { toProfile } from '@renderer/lib/link'
|
||||
import { formatPubkey, generateImageByPubkey } from '@renderer/lib/pubkey'
|
||||
import { useSecondaryPage } from '@renderer/PageManager'
|
||||
import { useNostr } from '@renderer/providers/NostrProvider'
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { useFetchProfile } from '@/hooks'
|
||||
import { toProfile } from '@/lib/link'
|
||||
import { formatPubkey, generateImageByPubkey } from '@/lib/pubkey'
|
||||
import { useSecondaryPage } from '@/PageManager'
|
||||
import { useNostr } from '@/providers/NostrProvider'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export default function ProfileButton({
|
||||
@@ -69,9 +69,7 @@ export default function ProfileButton({
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger className="non-draggable" asChild>
|
||||
{triggerComponent}
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuTrigger asChild>{triggerComponent}</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem onClick={() => push(toProfile(pubkey))}>{t('Profile')}</DropdownMenuItem>
|
||||
<DropdownMenuItem className="text-destructive focus:text-destructive" onClick={logout}>
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useNostr } from '@renderer/providers/NostrProvider'
|
||||
import { useNostr } from '@/providers/NostrProvider'
|
||||
import LoginButton from './LoginButton'
|
||||
import ProfileButton from './ProfileButton'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Button } from '@renderer/components/ui/button'
|
||||
import { useSecondaryPage } from '@renderer/PageManager'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { useSecondaryPage } from '@/PageManager'
|
||||
import { ChevronLeft } from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { isNsfwEvent } from '@renderer/lib/event'
|
||||
import { cn } from '@renderer/lib/utils'
|
||||
import { isNsfwEvent } from '@/lib/event'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Event } from 'nostr-tools'
|
||||
import { memo } from 'react'
|
||||
import {
|
||||
@@ -1,5 +1,5 @@
|
||||
import { toNoteList } from '@renderer/lib/link'
|
||||
import { SecondaryPageLink } from '@renderer/PageManager'
|
||||
import { toNoteList } from '@/lib/link'
|
||||
import { SecondaryPageLink } from '@/PageManager'
|
||||
import { TEmbeddedRenderer } from './types'
|
||||
|
||||
export function EmbeddedHashtag({ hashtag }: { hashtag: string }) {
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useFetchEvent } from '@renderer/hooks'
|
||||
import { toNoStrudelArticle, toNoStrudelNote, toNoStrudelStream } from '@renderer/lib/link'
|
||||
import { cn } from '@renderer/lib/utils'
|
||||
import { useFetchEvent } from '@/hooks'
|
||||
import { toNoStrudelArticle, toNoStrudelNote, toNoStrudelStream } from '@/lib/link'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { kinds } from 'nostr-tools'
|
||||
import ShortTextNoteCard from '../NoteCard/ShortTextNoteCard'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useSecondaryPage } from '@renderer/PageManager'
|
||||
import { toNoteList } from '@renderer/lib/link'
|
||||
import { useSecondaryPage } from '@/PageManager'
|
||||
import { toNoteList } from '@/lib/link'
|
||||
import { TEmbeddedRenderer } from './types'
|
||||
|
||||
export function EmbeddedWebsocketUrl({ url }: { url: string }) {
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Button } from '@renderer/components/ui/button'
|
||||
import { useToast } from '@renderer/hooks'
|
||||
import { useFollowList } from '@renderer/providers/FollowListProvider'
|
||||
import { useNostr } from '@renderer/providers/NostrProvider'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { useToast } from '@/hooks'
|
||||
import { useFollowList } from '@/providers/FollowListProvider'
|
||||
import { useNostr } from '@/providers/NostrProvider'
|
||||
import { Loader } from 'lucide-react'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Image } from '@nextui-org/image'
|
||||
import { ScrollArea, ScrollBar } from '@renderer/components/ui/scroll-area'
|
||||
import { cn } from '@renderer/lib/utils'
|
||||
import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useState } from 'react'
|
||||
import Lightbox from 'yet-another-react-lightbox'
|
||||
import Zoom from 'yet-another-react-lightbox/plugins/zoom'
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Button } from '@renderer/components/ui/button'
|
||||
import { Input } from '@renderer/components/ui/input'
|
||||
import { useNostr } from '@renderer/providers/NostrProvider'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { useNostr } from '@/providers/NostrProvider'
|
||||
import { Loader } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@@ -1,8 +1,7 @@
|
||||
import { Button } from '@renderer/components/ui/button'
|
||||
import { Input } from '@renderer/components/ui/input'
|
||||
import { IS_ELECTRON, isElectron } from '@renderer/lib/env'
|
||||
import { useNostr } from '@renderer/providers/NostrProvider'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { useNostr } from '@/providers/NostrProvider'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export default function PrivateKeyLogin({ onLoginSuccess }: { onLoginSuccess: () => void }) {
|
||||
@@ -10,17 +9,6 @@ export default function PrivateKeyLogin({ onLoginSuccess }: { onLoginSuccess: ()
|
||||
const { nsecLogin } = useNostr()
|
||||
const [nsec, setNsec] = useState('')
|
||||
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>) => {
|
||||
setNsec(e.target.value)
|
||||
@@ -40,15 +28,9 @@ export default function PrivateKeyLogin({ onLoginSuccess }: { onLoginSuccess: ()
|
||||
return (
|
||||
<>
|
||||
<div className="text-orange-400">
|
||||
{!IS_ELECTRON
|
||||
? t(
|
||||
'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
|
||||
})}
|
||||
{t(
|
||||
'Using private key login is insecure. It is recommended to use a browser extension for login, such as alby, nostr-keyx or nos2x.'
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Input
|
||||
@@ -1,13 +1,12 @@
|
||||
import { Button } from '@renderer/components/ui/button'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle
|
||||
} from '@renderer/components/ui/dialog'
|
||||
import { IS_ELECTRON } from '@renderer/lib/env'
|
||||
import { useNostr } from '@renderer/providers/NostrProvider'
|
||||
} from '@/components/ui/dialog'
|
||||
import { useNostr } from '@/providers/NostrProvider'
|
||||
import { ArrowLeft } from 'lucide-react'
|
||||
import { Dispatch, useState } from 'react'
|
||||
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">
|
||||
{t('Login with Browser Extension')}
|
||||
</Button>
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useFetchNip05 } from '@renderer/hooks/useFetchNip05'
|
||||
import { useFetchNip05 } from '@/hooks/useFetchNip05'
|
||||
import { BadgeAlert, BadgeCheck } from 'lucide-react'
|
||||
|
||||
export default function Nip05({ nip05, pubkey }: { nip05?: string; pubkey: string }) {
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useSecondaryPage } from '@renderer/PageManager'
|
||||
import { toNote } from '@renderer/lib/link'
|
||||
import { useSecondaryPage } from '@/PageManager'
|
||||
import { toNote } from '@/lib/link'
|
||||
import { Event } from 'nostr-tools'
|
||||
import Content from '../Content'
|
||||
import { FormattedTimestamp } from '../FormattedTimestamp'
|
||||
@@ -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 { useMemo } from 'react'
|
||||
import ShortTextNoteCard from './ShortTextNoteCard'
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useFetchEvent } from '@renderer/hooks'
|
||||
import { getParentEventId, getRootEventId } from '@renderer/lib/event'
|
||||
import { toNote } from '@renderer/lib/link'
|
||||
import { cn } from '@renderer/lib/utils'
|
||||
import { useSecondaryPage } from '@renderer/PageManager'
|
||||
import { useFetchEvent } from '@/hooks'
|
||||
import { getParentEventId, getRootEventId } from '@/lib/event'
|
||||
import { toNote } from '@/lib/link'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useSecondaryPage } from '@/PageManager'
|
||||
import { Repeat2 } from 'lucide-react'
|
||||
import { Event } from 'nostr-tools'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Event } from 'nostr-tools'
|
||||
import { kinds } from 'nostr-tools'
|
||||
import { Event, kinds } from 'nostr-tools'
|
||||
import RepostNoteCard from './RepostNoteCard'
|
||||
import ShortTextNoteCard from './ShortTextNoteCard'
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Button } from '@renderer/components/ui/button'
|
||||
import { Switch } from '@renderer/components/ui/switch'
|
||||
import { useFetchRelayInfos } from '@renderer/hooks'
|
||||
import { isReplyNoteEvent } from '@renderer/lib/event'
|
||||
import { cn } from '@renderer/lib/utils'
|
||||
import { useNostr } from '@renderer/providers/NostrProvider'
|
||||
import { useScreenSize } from '@renderer/providers/ScreenSizeProvider'
|
||||
import client from '@renderer/services/client.service'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import { useFetchRelayInfos } from '@/hooks'
|
||||
import { isReplyNoteEvent } from '@/lib/event'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useNostr } from '@/providers/NostrProvider'
|
||||
import { useScreenSize } from '@/providers/ScreenSizeProvider'
|
||||
import client from '@/services/client.service'
|
||||
import dayjs from 'dayjs'
|
||||
import { Event, Filter, kinds } from 'nostr-tools'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
@@ -34,7 +34,6 @@ export default function NoteList({
|
||||
const [hasMore, setHasMore] = useState<boolean>(true)
|
||||
const [initialized, setInitialized] = useState(false)
|
||||
const [displayReplies, setDisplayReplies] = useState(false)
|
||||
const observer = useRef<IntersectionObserver | null>(null)
|
||||
const bottomRef = useRef<HTMLDivElement | null>(null)
|
||||
const noteFilter = useMemo(() => {
|
||||
return {
|
||||
@@ -109,19 +108,21 @@ export default function NoteList({
|
||||
threshold: 1
|
||||
}
|
||||
|
||||
observer.current = new IntersectionObserver((entries) => {
|
||||
const observerInstance = new IntersectionObserver((entries) => {
|
||||
if (entries[0].isIntersecting && hasMore) {
|
||||
loadMore()
|
||||
}
|
||||
}, options)
|
||||
|
||||
if (bottomRef.current) {
|
||||
observer.current.observe(bottomRef.current)
|
||||
const currentBottomRef = bottomRef.current
|
||||
|
||||
if (currentBottomRef) {
|
||||
observerInstance.observe(currentBottomRef)
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (observer.current && bottomRef.current) {
|
||||
observer.current.unobserve(bottomRef.current)
|
||||
if (observerInstance && currentBottomRef) {
|
||||
observerInstance.unobserve(currentBottomRef)
|
||||
}
|
||||
}
|
||||
}, [initialized, hasMore, events, timelineKey])
|
||||
@@ -1,8 +1,8 @@
|
||||
import { createReactionDraftEvent } from '@renderer/lib/draft-event'
|
||||
import { cn } from '@renderer/lib/utils'
|
||||
import { useNostr } from '@renderer/providers/NostrProvider'
|
||||
import { useNoteStats } from '@renderer/providers/NoteStatsProvider'
|
||||
import client from '@renderer/services/client.service'
|
||||
import { createReactionDraftEvent } from '@/lib/draft-event'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useNostr } from '@/providers/NostrProvider'
|
||||
import { useNoteStats } from '@/providers/NoteStatsProvider'
|
||||
import client from '@/services/client.service'
|
||||
import { Heart, Loader } from 'lucide-react'
|
||||
import { Event } from 'nostr-tools'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
@@ -37,7 +37,7 @@ export default function LikeButton({
|
||||
if (hasLiked === undefined) {
|
||||
fetchNoteLikedStatus(event)
|
||||
}
|
||||
}, [])
|
||||
}, [canFetch, event])
|
||||
|
||||
const like = async (e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
@@ -4,8 +4,8 @@ import {
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle
|
||||
} from '@renderer/components/ui/dialog'
|
||||
import { ScrollArea, ScrollBar } from '@renderer/components/ui/scroll-area'
|
||||
} from '@/components/ui/dialog'
|
||||
import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area'
|
||||
import { Event } from 'nostr-tools'
|
||||
|
||||
export default function RawEventDialog({
|
||||
@@ -25,9 +25,7 @@ export default function RawEventDialog({
|
||||
<DialogDescription className="hidden" />
|
||||
</DialogHeader>
|
||||
<ScrollArea className="h-full">
|
||||
<pre className="text-sm overflow-x-auto text-muted-foreground">
|
||||
{JSON.stringify(event, null, 2)}
|
||||
</pre>
|
||||
<pre className="text-sm text-muted-foreground">{JSON.stringify(event, null, 2)}</pre>
|
||||
<ScrollBar orientation="horizontal" />
|
||||
</ScrollArea>
|
||||
</DialogContent>
|
||||
@@ -3,8 +3,8 @@ import {
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger
|
||||
} from '@renderer/components/ui/dropdown-menu'
|
||||
import { getSharableEventId } from '@renderer/lib/event'
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { getSharableEventId } from '@/lib/event'
|
||||
import { Code, Copy, Ellipsis } from 'lucide-react'
|
||||
import { Event } from 'nostr-tools'
|
||||
import { useState } from 'react'
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useNostr } from '@renderer/providers/NostrProvider'
|
||||
import { useNoteStats } from '@renderer/providers/NoteStatsProvider'
|
||||
import { useNostr } from '@/providers/NostrProvider'
|
||||
import { useNoteStats } from '@/providers/NoteStatsProvider'
|
||||
import { MessageCircle } from 'lucide-react'
|
||||
import { Event } from 'nostr-tools'
|
||||
import { useMemo, useState } from 'react'
|
||||
@@ -3,13 +3,13 @@ import {
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger
|
||||
} from '@renderer/components/ui/dropdown-menu'
|
||||
import { createRepostDraftEvent } from '@renderer/lib/draft-event'
|
||||
import { getSharableEventId } from '@renderer/lib/event'
|
||||
import { cn } from '@renderer/lib/utils'
|
||||
import { useNostr } from '@renderer/providers/NostrProvider'
|
||||
import { useNoteStats } from '@renderer/providers/NoteStatsProvider'
|
||||
import client from '@renderer/services/client.service'
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { createRepostDraftEvent } from '@/lib/draft-event'
|
||||
import { getSharableEventId } from '@/lib/event'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useNostr } from '@/providers/NostrProvider'
|
||||
import { useNoteStats } from '@/providers/NoteStatsProvider'
|
||||
import client from '@/services/client.service'
|
||||
import { Loader, PencilLine, Repeat } from 'lucide-react'
|
||||
import { Event } from 'nostr-tools'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
@@ -45,7 +45,7 @@ export default function RepostButton({
|
||||
if (hasReposted === undefined) {
|
||||
fetchNoteRepostedStatus(event)
|
||||
}
|
||||
}, [])
|
||||
}, [canFetch, event])
|
||||
|
||||
const repost = async (e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
@@ -1,4 +1,4 @@
|
||||
import { cn } from '@renderer/lib/utils'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Event } from 'nostr-tools'
|
||||
import LikeButton from './LikeButton'
|
||||
import NoteOptions from './NoteOptions'
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Button } from '@renderer/components/ui/button'
|
||||
import { toNotifications } from '@renderer/lib/link'
|
||||
import { useSecondaryPage } from '@renderer/PageManager'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { toNotifications } from '@/lib/link'
|
||||
import { useSecondaryPage } from '@/PageManager'
|
||||
import { Bell } from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useFetchEvent } from '@renderer/hooks'
|
||||
import { toNote } from '@renderer/lib/link'
|
||||
import { tagNameEquals } from '@renderer/lib/tag'
|
||||
import { useSecondaryPage } from '@renderer/PageManager'
|
||||
import { useNostr } from '@renderer/providers/NostrProvider'
|
||||
import client from '@renderer/services/client.service'
|
||||
import { useFetchEvent } from '@/hooks'
|
||||
import { toNote } from '@/lib/link'
|
||||
import { tagNameEquals } from '@/lib/tag'
|
||||
import { useSecondaryPage } from '@/PageManager'
|
||||
import { useNostr } from '@/providers/NostrProvider'
|
||||
import client from '@/services/client.service'
|
||||
import dayjs from 'dayjs'
|
||||
import { Heart, MessageCircle, Repeat } from 'lucide-react'
|
||||
import { Event, kinds, nip19, validateEvent } from 'nostr-tools'
|
||||
@@ -22,7 +22,6 @@ export default function NotificationList() {
|
||||
const [notifications, setNotifications] = useState<Event[]>([])
|
||||
const [until, setUntil] = useState<number | undefined>(dayjs().unix())
|
||||
const bottomRef = useRef<HTMLDivElement | null>(null)
|
||||
const observer = useRef<IntersectionObserver | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!pubkey) {
|
||||
@@ -74,19 +73,21 @@ export default function NotificationList() {
|
||||
threshold: 1
|
||||
}
|
||||
|
||||
observer.current = new IntersectionObserver((entries) => {
|
||||
const observerInstance = new IntersectionObserver((entries) => {
|
||||
if (entries[0].isIntersecting) {
|
||||
loadMore()
|
||||
}
|
||||
}, options)
|
||||
|
||||
if (bottomRef.current) {
|
||||
observer.current.observe(bottomRef.current)
|
||||
const currentBottomRef = bottomRef.current
|
||||
|
||||
if (currentBottomRef) {
|
||||
observerInstance.observe(currentBottomRef)
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (observer.current && bottomRef.current) {
|
||||
observer.current.unobserve(bottomRef.current)
|
||||
if (observerInstance && currentBottomRef) {
|
||||
observerInstance.unobserve(currentBottomRef)
|
||||
}
|
||||
}
|
||||
}, [until, initialized, timelineKey])
|
||||
@@ -141,7 +142,7 @@ function ReactionNotification({ notification }: { notification: Event }) {
|
||||
return eventId
|
||||
? nip19.neventEncode(author ? { id: eventId, author } : { id: eventId })
|
||||
: undefined
|
||||
}, [notification.id])
|
||||
}, [notification])
|
||||
const { event } = useFetchEvent(bech32Id)
|
||||
if (!event || !bech32Id || event.kind !== kinds.ShortTextNote) return null
|
||||
|
||||
@@ -191,7 +192,7 @@ function RepostNotification({ notification }: { notification: Event }) {
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}, [])
|
||||
}, [notification.content])
|
||||
if (!event) return null
|
||||
|
||||
return (
|
||||
@@ -1,4 +1,4 @@
|
||||
import { cn } from '@renderer/lib/utils'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useState } from 'react'
|
||||
|
||||
export default function NsfwOverlay({ className }: { className?: string }) {
|
||||
@@ -1,7 +1,7 @@
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Event } from 'nostr-tools'
|
||||
import UserAvatar from '../UserAvatar'
|
||||
import { cn } from '@renderer/lib/utils'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import UserAvatar from '../UserAvatar'
|
||||
|
||||
export default function ParentNotePreview({
|
||||
event,
|
||||
@@ -1,5 +1,5 @@
|
||||
import PostDialog from '@renderer/components/PostDialog'
|
||||
import { Button } from '@renderer/components/ui/button'
|
||||
import PostDialog from '@/components/PostDialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { PencilLine } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Button } from '@renderer/components/ui/button'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@renderer/components/ui/popover'
|
||||
import { extractMentions } from '@renderer/lib/event'
|
||||
import { useNostr } from '@renderer/providers/NostrProvider'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
||||
import { extractMentions } from '@/lib/event'
|
||||
import { useNostr } from '@/providers/NostrProvider'
|
||||
import { Event } from 'nostr-tools'
|
||||
import { useEffect, useState } from 'react'
|
||||
import UserAvatar from '../UserAvatar'
|
||||
@@ -23,7 +23,7 @@ export default function Mentions({
|
||||
extractMentions(content, parentEvent).then(({ pubkeys }) =>
|
||||
setPubkeys(pubkeys.filter((p) => p !== pubkey))
|
||||
)
|
||||
}, [content])
|
||||
}, [content, parentEvent, pubkey])
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Card } from '@renderer/components/ui/card'
|
||||
import { Card } from '@/components/ui/card'
|
||||
import dayjs from 'dayjs'
|
||||
import Content from '../Content'
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Button } from '@renderer/components/ui/button'
|
||||
import { useToast } from '@renderer/hooks/use-toast'
|
||||
import { useNostr } from '@renderer/providers/NostrProvider'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { useToast } from '@/hooks/use-toast'
|
||||
import { useNostr } from '@/providers/NostrProvider'
|
||||
import { ImageUp, LoaderCircle } from 'lucide-react'
|
||||
import { useRef, useState } from 'react'
|
||||
import { z } from 'zod'
|
||||
|
||||
export default function Uploader({
|
||||
setContent
|
||||
@@ -38,7 +39,8 @@ export default function Uploader({
|
||||
}
|
||||
|
||||
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) {
|
||||
setContent((prevContent) => `${prevContent}\n${imageUrl}`)
|
||||
} else {
|
||||
@@ -1,17 +1,17 @@
|
||||
import { Button } from '@renderer/components/ui/button'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle
|
||||
} from '@renderer/components/ui/dialog'
|
||||
import { ScrollArea } from '@renderer/components/ui/scroll-area'
|
||||
import { Textarea } from '@renderer/components/ui/textarea'
|
||||
import { useToast } from '@renderer/hooks/use-toast'
|
||||
import { createShortTextNoteDraftEvent } from '@renderer/lib/draft-event'
|
||||
import { useNostr } from '@renderer/providers/NostrProvider'
|
||||
import client from '@renderer/services/client.service'
|
||||
} from '@/components/ui/dialog'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { useToast } from '@/hooks/use-toast'
|
||||
import { createShortTextNoteDraftEvent } from '@/lib/draft-event'
|
||||
import { useNostr } from '@/providers/NostrProvider'
|
||||
import client from '@/services/client.service'
|
||||
import { LoaderCircle } from 'lucide-react'
|
||||
import { Event } from 'nostr-tools'
|
||||
import { Dispatch, useState } from 'react'
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Image } from '@nextui-org/image'
|
||||
import { generateImageByPubkey } from '@renderer/lib/pubkey'
|
||||
import { cn } from '@renderer/lib/utils'
|
||||
import { generateImageByPubkey } from '@/lib/pubkey'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
|
||||
export default function ProfileBanner({
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@renderer/components/ui/avatar'
|
||||
import { useFetchProfile } from '@renderer/hooks'
|
||||
import { generateImageByPubkey } from '@renderer/lib/pubkey'
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import { useFetchProfile } from '@/hooks'
|
||||
import { generateImageByPubkey } from '@/lib/pubkey'
|
||||
import { useMemo } from 'react'
|
||||
import FollowButton from '../FollowButton'
|
||||
import Nip05 from '../Nip05'
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Button } from '@renderer/components/ui/button'
|
||||
import { usePrimaryPage } from '@renderer/PageManager'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { usePrimaryPage } from '@/PageManager'
|
||||
import { RefreshCcw } from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Button } from '@renderer/components/ui/button'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger
|
||||
} from '@renderer/components/ui/dropdown-menu'
|
||||
import { Input } from '@renderer/components/ui/input'
|
||||
import { useRelaySettings } from '@renderer/providers/RelaySettingsProvider'
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { useRelaySettings } from '@/providers/RelaySettingsProvider'
|
||||
import { Check, ChevronDown, Circle, CircleCheck, EllipsisVertical } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
import RelayUrls from './RelayUrl'
|
||||
@@ -1,19 +1,20 @@
|
||||
import { Button } from '@renderer/components/ui/button'
|
||||
import { Input } from '@renderer/components/ui/input'
|
||||
import { useFetchRelayInfos } from '@renderer/hooks'
|
||||
import { isWebsocketUrl, normalizeUrl } from '@renderer/lib/url'
|
||||
import { useRelaySettings } from '@renderer/providers/RelaySettingsProvider'
|
||||
import client from '@renderer/services/client.service'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { useFetchRelayInfos } from '@/hooks'
|
||||
import { isWebsocketUrl, normalizeUrl } from '@/lib/url'
|
||||
import { useRelaySettings } from '@/providers/RelaySettingsProvider'
|
||||
import client from '@/services/client.service'
|
||||
import { CircleX, SearchCheck } from 'lucide-react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export default function RelayUrls({ groupName }: { groupName: string }) {
|
||||
const { t } = useTranslation()
|
||||
const { relayGroups, updateRelayGroupRelayUrls } = useRelaySettings()
|
||||
const rawRelayUrls = relayGroups.find((group) => group.groupName === groupName)?.relayUrls ?? []
|
||||
const isActive = relayGroups.find((group) => group.groupName === groupName)?.isActive ?? false
|
||||
|
||||
const isActive = useMemo(
|
||||
() => relayGroups.find((group) => group.groupName === groupName)?.isActive ?? false,
|
||||
[relayGroups, groupName]
|
||||
)
|
||||
const [newRelayUrl, setNewRelayUrl] = useState('')
|
||||
const [newRelayUrlError, setNewRelayUrlError] = useState<string | null>(null)
|
||||
const [relays, setRelays] = useState<
|
||||
@@ -21,7 +22,11 @@ export default function RelayUrls({ groupName }: { groupName: string }) {
|
||||
url: string
|
||||
isConnected: boolean
|
||||
}[]
|
||||
>(rawRelayUrls.map((url) => ({ url, isConnected: false })))
|
||||
>(
|
||||
relayGroups
|
||||
.find((group) => group.groupName === groupName)
|
||||
?.relayUrls.map((url) => ({ url, isConnected: false })) ?? []
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Button } from '@renderer/components/ui/button'
|
||||
import { useFetchRelayInfos } from '@renderer/hooks'
|
||||
import { useRelaySettings } from '@renderer/providers/RelaySettingsProvider'
|
||||
import client from '@renderer/services/client.service'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { useFetchRelayInfos } from '@/hooks'
|
||||
import { useRelaySettings } from '@/providers/RelaySettingsProvider'
|
||||
import client from '@/services/client.service'
|
||||
import { Save, SearchCheck } from 'lucide-react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Button } from '@renderer/components/ui/button'
|
||||
import { Input } from '@renderer/components/ui/input'
|
||||
import { Separator } from '@renderer/components/ui/separator'
|
||||
import { useRelaySettings } from '@renderer/providers/RelaySettingsProvider'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { useRelaySettings } from '@/providers/RelaySettingsProvider'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { RelaySettingsComponentProvider } from './provider'
|
||||
import RelayGroup from './RelayGroup'
|
||||
@@ -1,10 +1,10 @@
|
||||
import RelaySettings from '@renderer/components/RelaySettings'
|
||||
import { Button } from '@renderer/components/ui/button'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@renderer/components/ui/popover'
|
||||
import { ScrollArea } from '@renderer/components/ui/scroll-area'
|
||||
import { toRelaySettings } from '@renderer/lib/link'
|
||||
import { SecondaryPageLink } from '@renderer/PageManager'
|
||||
import { useScreenSize } from '@renderer/providers/ScreenSizeProvider'
|
||||
import RelaySettings from '@/components/RelaySettings'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { toRelaySettings } from '@/lib/link'
|
||||
import { SecondaryPageLink } from '@/PageManager'
|
||||
import { useScreenSize } from '@/providers/ScreenSizeProvider'
|
||||
import { Server } from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Separator } from '@renderer/components/ui/separator'
|
||||
import { isReplyNoteEvent } from '@renderer/lib/event'
|
||||
import { isReplyETag, isRootETag } from '@renderer/lib/tag'
|
||||
import { cn } from '@renderer/lib/utils'
|
||||
import { useNostr } from '@renderer/providers/NostrProvider'
|
||||
import { useNoteStats } from '@renderer/providers/NoteStatsProvider'
|
||||
import client from '@renderer/services/client.service'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { isReplyNoteEvent } from '@/lib/event'
|
||||
import { isReplyETag, isRootETag } from '@/lib/tag'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useNostr } from '@/providers/NostrProvider'
|
||||
import { useNoteStats } from '@/providers/NoteStatsProvider'
|
||||
import client from '@/services/client.service'
|
||||
import dayjs from 'dayjs'
|
||||
import { Event as NEvent, kinds } from 'nostr-tools'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
@@ -43,7 +43,7 @@ export default function ReplyNoteList({ event, className }: { event: NEvent; cla
|
||||
return () => {
|
||||
client.removeEventListener('eventPublished', handleEventPublished)
|
||||
}
|
||||
}, [])
|
||||
}, [event])
|
||||
|
||||
useEffect(() => {
|
||||
if (loading) return
|
||||
@@ -87,7 +87,7 @@ export default function ReplyNoteList({ event, className }: { event: NEvent; cla
|
||||
return () => {
|
||||
promise.then((closer) => closer?.())
|
||||
}
|
||||
}, [])
|
||||
}, [event])
|
||||
|
||||
useEffect(() => {
|
||||
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 }
|
||||
}
|
||||
setReplyMap(replyMap)
|
||||
}, [replies])
|
||||
}, [replies, event.id, updateNoteReplyCount])
|
||||
|
||||
const loadMore = async () => {
|
||||
if (loading || !until || !timelineKey) return
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Button } from '@renderer/components/ui/button'
|
||||
import { cn } from '@renderer/lib/utils'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { ChevronUp } from 'lucide-react'
|
||||
|
||||
export default function ScrollToTopButton({
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Button } from '@renderer/components/ui/button'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Search } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@@ -1,17 +1,11 @@
|
||||
import { SecondaryPageLink } from '@renderer/PageManager'
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@renderer/components/ui/avatar'
|
||||
import {
|
||||
CommandDialog,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList
|
||||
} from '@renderer/components/ui/command'
|
||||
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 { SecondaryPageLink } from '@/PageManager'
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import { CommandDialog, CommandInput, CommandItem, CommandList } from '@/components/ui/command'
|
||||
import { useSearchProfiles } from '@/hooks'
|
||||
import { toNote, toNoteList, toProfile, toProfileList } from '@/lib/link'
|
||||
import { generateImageByPubkey } from '@/lib/pubkey'
|
||||
import { useRelaySettings } from '@/providers/RelaySettingsProvider'
|
||||
import { TProfile } from '@/types'
|
||||
import { Hash, Notebook, UserRound } from 'lucide-react'
|
||||
import { nip19 } from 'nostr-tools'
|
||||
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(() => {
|
||||
const handler = setTimeout(() => {
|
||||
@@ -81,11 +75,7 @@ export function SearchDialog({ open, setOpen }: { open: boolean; setOpen: Dispat
|
||||
}, [input])
|
||||
|
||||
return (
|
||||
<CommandDialog
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
classNames={{ content: isMacOS() ? 'max-sm:top-9' : 'max-sm:top-0' }}
|
||||
>
|
||||
<CommandDialog open={open} onOpenChange={setOpen} classNames={{ content: 'max-sm:top-0' }}>
|
||||
<CommandInput value={input} onValueChange={setInput} />
|
||||
<CommandList>{list}</CommandList>
|
||||
</CommandDialog>
|
||||
@@ -1,6 +1,5 @@
|
||||
import Logo from '@renderer/assets/Logo'
|
||||
import { Button } from '@renderer/components/ui/button'
|
||||
import { IS_ELECTRON } from '@renderer/lib/env'
|
||||
import Logo from '@/assets/Logo'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Info } from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import AboutInfoDialog from '../AboutInfoDialog'
|
||||
@@ -15,24 +14,22 @@ export default function PrimaryPageSidebar() {
|
||||
const { t } = useTranslation()
|
||||
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="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="draggable ml-4 mb-8 w-40">
|
||||
<div className="ml-4 mb-8 w-40">
|
||||
<Logo />
|
||||
</div>
|
||||
<PostButton variant="sidebar" />
|
||||
<RelaySettingsButton variant="sidebar" />
|
||||
<NotificationButton variant="sidebar" />
|
||||
<SearchButton variant="sidebar" />
|
||||
{IS_ELECTRON && <RefreshButton variant="sidebar" />}
|
||||
{!IS_ELECTRON && (
|
||||
<AboutInfoDialog>
|
||||
<Button variant="sidebar" size="sidebar">
|
||||
<Info />
|
||||
{t('About')}
|
||||
</Button>
|
||||
</AboutInfoDialog>
|
||||
)}
|
||||
<RefreshButton variant="sidebar" />
|
||||
<AboutInfoDialog>
|
||||
<Button variant="sidebar" size="sidebar">
|
||||
<Info />
|
||||
{t('About')}
|
||||
</Button>
|
||||
</AboutInfoDialog>
|
||||
</div>
|
||||
<AccountButton variant="sidebar" />
|
||||
</div>
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Button } from '@renderer/components/ui/button'
|
||||
import { useTheme } from '@renderer/providers/ThemeProvider'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { useTheme } from '@/providers/ThemeProvider'
|
||||
import { Moon, Sun, SunMoon } from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
23
src/components/Titlebar/index.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@renderer/components/ui/avatar'
|
||||
import { HoverCard, HoverCardContent, HoverCardTrigger } from '@renderer/components/ui/hover-card'
|
||||
import { Skeleton } from '@renderer/components/ui/skeleton'
|
||||
import { useFetchProfile } from '@renderer/hooks'
|
||||
import { generateImageByPubkey } from '@renderer/lib/pubkey'
|
||||
import { toProfile } from '@renderer/lib/link'
|
||||
import { cn } from '@renderer/lib/utils'
|
||||
import { SecondaryPageLink } from '@renderer/PageManager'
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { useFetchProfile } from '@/hooks'
|
||||
import { generateImageByPubkey } from '@/lib/pubkey'
|
||||
import { toProfile } from '@/lib/link'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { SecondaryPageLink } from '@/PageManager'
|
||||
import ProfileCard from '../ProfileCard'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import FollowButton from '@renderer/components/FollowButton'
|
||||
import Nip05 from '@renderer/components/Nip05'
|
||||
import UserAvatar from '@renderer/components/UserAvatar'
|
||||
import Username from '@renderer/components/Username'
|
||||
import { useFetchProfile } from '@renderer/hooks'
|
||||
import FollowButton from '@/components/FollowButton'
|
||||
import Nip05 from '@/components/Nip05'
|
||||
import UserAvatar from '@/components/UserAvatar'
|
||||
import Username from '@/components/Username'
|
||||
import { useFetchProfile } from '@/hooks'
|
||||
|
||||
export default function UserItem({ pubkey }: { pubkey: string }) {
|
||||
const { profile } = useFetchProfile(pubkey)
|
||||
@@ -1,9 +1,9 @@
|
||||
import { HoverCard, HoverCardContent, HoverCardTrigger } from '@renderer/components/ui/hover-card'
|
||||
import { Skeleton } from '@renderer/components/ui/skeleton'
|
||||
import { useFetchProfile } from '@renderer/hooks'
|
||||
import { toProfile } from '@renderer/lib/link'
|
||||
import { cn } from '@renderer/lib/utils'
|
||||
import { SecondaryPageLink } from '@renderer/PageManager'
|
||||
import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { useFetchProfile } from '@/hooks'
|
||||
import { toProfile } from '@/lib/link'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { SecondaryPageLink } from '@/PageManager'
|
||||
import ProfileCard from '../ProfileCard'
|
||||
|
||||
export default function Username({
|
||||
@@ -1,4 +1,4 @@
|
||||
import { cn } from '@renderer/lib/utils'
|
||||
import { cn } from '@/lib/utils'
|
||||
import NsfwOverlay from '../NsfwOverlay'
|
||||
|
||||
export default function VideoPlayer({
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Image } from '@nextui-org/image'
|
||||
import { useFetchWebMetadata } from '@renderer/hooks/useFetchWebMetadata'
|
||||
import { cn } from '@renderer/lib/utils'
|
||||
import { useFetchWebMetadata } from '@/hooks/useFetchWebMetadata'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
export default function WebPreview({
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as React from "react"
|
||||
import * as AvatarPrimitive from "@radix-ui/react-avatar"
|
||||
import * as React from 'react'
|
||||
import * as AvatarPrimitive from '@radix-ui/react-avatar'
|
||||
|
||||
import { cn } from "@renderer/lib/utils"
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const Avatar = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Root>,
|
||||
@@ -9,10 +9,7 @@ const Avatar = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
|
||||
className
|
||||
)}
|
||||
className={cn('relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
@@ -24,7 +21,7 @@ const AvatarImage = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Image
|
||||
ref={ref}
|
||||
className={cn("aspect-square h-full w-full", className)}
|
||||
className={cn('aspect-square h-full w-full', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
@@ -37,7 +34,7 @@ const AvatarFallback = React.forwardRef<
|
||||
<AvatarPrimitive.Fallback
|
||||
ref={ref}
|
||||
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
|
||||
)}
|
||||
{...props}
|
||||
@@ -2,29 +2,30 @@ import * as React from 'react'
|
||||
import { Slot } from '@radix-ui/react-slot'
|
||||
import { cva, type VariantProps } from 'class-variance-authority'
|
||||
|
||||
import { cn } from '@renderer/lib/utils'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
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: {
|
||||
variant: {
|
||||
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
||||
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
|
||||
outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
|
||||
secondary: 'bg-secondary text-secondary-foreground hover:bg-muted/80',
|
||||
default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90',
|
||||
destructive: 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
|
||||
outline:
|
||||
'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',
|
||||
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',
|
||||
titlebar: 'non-draggable hover:bg-accent hover:text-accent-foreground',
|
||||
sidebar: 'non-draggable hover:bg-accent hover:text-accent-foreground',
|
||||
'small-screen-titlebar': 'non-draggable hover:bg-accent hover:text-accent-foreground'
|
||||
titlebar: 'hover:bg-accent hover:text-accent-foreground',
|
||||
sidebar: 'hover:bg-accent hover:text-accent-foreground',
|
||||
'small-screen-titlebar': 'hover:bg-accent hover:text-accent-foreground'
|
||||
},
|
||||
size: {
|
||||
default: 'h-8 rounded-lg px-3',
|
||||
sm: 'h-8 rounded-lg px-2',
|
||||
lg: 'h-10 px-4 py-2',
|
||||
icon: 'h-8 w-8 rounded-full',
|
||||
default: 'h-9 px-4 py-2',
|
||||
sm: 'h-8 rounded-md px-3 text-xs',
|
||||
lg: 'h-10 rounded-md px-8',
|
||||
icon: 'h-9 w-9',
|
||||
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',
|
||||
'small-screen-titlebar': 'h-8 w-8 rounded-full'
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from '@renderer/lib/utils'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
@@ -20,23 +20,22 @@ const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDiv
|
||||
)
|
||||
CardHeader.displayName = 'CardHeader'
|
||||
|
||||
const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
|
||||
const CardTitle = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<h3
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('text-2xl font-semibold leading-none tracking-tight', className)}
|
||||
className={cn('font-semibold leading-none tracking-tight', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
)
|
||||
CardTitle.displayName = 'CardTitle'
|
||||
|
||||
const CardDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<p ref={ref} className={cn('text-sm text-muted-foreground', className)} {...props} />
|
||||
))
|
||||
const CardDescription = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn('text-sm text-muted-foreground', className)} {...props} />
|
||||
)
|
||||
)
|
||||
CardDescription.displayName = 'CardDescription'
|
||||
|
||||
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||