mirror of
https://github.com/luminous-devs/lume.git
synced 2024-09-18 11:13:30 +00:00
Merge pull request #23 from reyamir/feat/chats
Implemented e2e encrypted direct message and new data model
This commit is contained in:
commit
6089cdb034
2
.gitignore
vendored
2
.gitignore
vendored
@ -15,6 +15,8 @@ out
|
||||
.next
|
||||
.vscode
|
||||
pnpm-lock.yaml
|
||||
*.db
|
||||
*.db-journal
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
|
@ -93,6 +93,12 @@ Install dependencies
|
||||
pnpm install
|
||||
```
|
||||
|
||||
Generate prisma database
|
||||
|
||||
```
|
||||
pnpm init-db
|
||||
```
|
||||
|
||||
Run development window
|
||||
|
||||
```
|
||||
|
23
package.json
23
package.json
@ -6,6 +6,7 @@
|
||||
"dev": "next dev -p 1420",
|
||||
"build": "next build && next export -o dist",
|
||||
"tauri": "tauri",
|
||||
"init-db": "cd src-tauri/ && cargo prisma generate",
|
||||
"prepare": "husky install"
|
||||
},
|
||||
"lint-staged": {
|
||||
@ -20,24 +21,22 @@
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
"@radix-ui/react-popover": "^1.0.5",
|
||||
"@radix-ui/react-tabs": "^1.0.3",
|
||||
"@supabase/supabase-js": "^2.13.0",
|
||||
"@supabase/supabase-js": "^2.15.0",
|
||||
"@tauri-apps/api": "^1.2.0",
|
||||
"dayjs": "^1.11.7",
|
||||
"destr": "^1.2.2",
|
||||
"emoji-mart": "^5.5.2",
|
||||
"framer-motion": "^9.1.7",
|
||||
"jotai": "^2.0.3",
|
||||
"jotai-cache": "^0.3.0",
|
||||
"next": "^13.2.4",
|
||||
"next": "^13.3.0",
|
||||
"nostr-relaypool": "^0.5.18",
|
||||
"nostr-tools": "^1.8.1",
|
||||
"nostr-tools": "^1.8.2",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hook-form": "^7.43.9",
|
||||
"react-player": "^2.12.0",
|
||||
"react-string-replace": "^1.1.0",
|
||||
"react-virtuoso": "^4.1.1",
|
||||
"tauri-plugin-sql-api": "github:tauri-apps/tauri-plugin-sql",
|
||||
"react-virtuoso": "^4.2.0",
|
||||
"unique-names-generator": "^4.7.1",
|
||||
"ws": "^8.13.0"
|
||||
},
|
||||
@ -46,14 +45,14 @@
|
||||
"@tauri-apps/cli": "^1.2.3",
|
||||
"@trivago/prettier-plugin-sort-imports": "^4.1.1",
|
||||
"@types/node": "^18.15.11",
|
||||
"@types/react": "^18.0.31",
|
||||
"@types/react": "^18.0.33",
|
||||
"@types/react-dom": "^18.0.11",
|
||||
"@typescript-eslint/eslint-plugin": "^5.57.0",
|
||||
"@typescript-eslint/parser": "^5.57.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.57.1",
|
||||
"@typescript-eslint/parser": "^5.57.1",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"csstype": "^3.1.1",
|
||||
"csstype": "^3.1.2",
|
||||
"eslint": "^8.37.0",
|
||||
"eslint-config-next": "^13.2.4",
|
||||
"eslint-config-next": "^13.3.0",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-plugin-react": "^7.32.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
@ -61,7 +60,7 @@
|
||||
"lint-staged": "^13.2.0",
|
||||
"postcss": "^8.4.21",
|
||||
"prettier": "^2.8.7",
|
||||
"prettier-plugin-tailwindcss": "^0.2.6",
|
||||
"prettier-plugin-tailwindcss": "^0.2.7",
|
||||
"prop-types": "^15.8.1",
|
||||
"tailwindcss": "^3.3.1",
|
||||
"typescript": "^4.9.5"
|
||||
|
473
pnpm-lock.yaml
473
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
2
src-tauri/.cargo/config.toml
Normal file
2
src-tauri/.cargo/config.toml
Normal file
@ -0,0 +1,2 @@
|
||||
[alias]
|
||||
prisma = "run --bin prisma --"
|
2
src-tauri/.gitignore
vendored
2
src-tauri/.gitignore
vendored
@ -2,3 +2,5 @@
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
|
||||
# prisma
|
||||
src/db.rs
|
||||
|
2407
src-tauri/Cargo.lock
generated
2407
src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -17,11 +17,11 @@ tauri-build = { version = "1.2", features = [] }
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
tauri = { version = "1.2", features = ["clipboard-read-text", "clipboard-write-text", "http-request", "os-all", "shell-open", "system-tray", "window-close", "window-start-dragging"] }
|
||||
|
||||
[dependencies.tauri-plugin-sql]
|
||||
git = "https://github.com/tauri-apps/plugins-workspace"
|
||||
branch = "dev"
|
||||
features = ["sqlite"]
|
||||
prisma-client-rust = { git = "https://github.com/Brendonovich/prisma-client-rust", tag = "0.6.7", default-features = false, features = ["sqlite", "migrations", "mocking", "specta"] }
|
||||
prisma-client-rust-cli = { git = "https://github.com/Brendonovich/prisma-client-rust", tag = "0.6.7", default-features = false, features = ["sqlite", "migrations", "mocking", "specta"] }
|
||||
specta = "1.0.0"
|
||||
tauri-specta = { version = "1.0.0", features = ["typescript"] }
|
||||
tokio = { version = "1.26.0", features = ["macros"] }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
objc = "0.2.7"
|
||||
|
@ -1,104 +0,0 @@
|
||||
-- Add migration script here
|
||||
-- create relays
|
||||
CREATE TABLE
|
||||
relays (
|
||||
id INTEGER PRIMARY KEY,
|
||||
relay_url TEXT NOT NULL,
|
||||
relay_status INTEGER NOT NULL DEFAULT 1,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- add default relays
|
||||
-- relay status:
|
||||
-- 0: off
|
||||
-- 1: on
|
||||
INSERT INTO
|
||||
relays (relay_url, relay_status)
|
||||
VALUES
|
||||
("wss://relay.damus.io", "1"),
|
||||
("wss://eden.nostr.land", "0"),
|
||||
("wss://nostr-pub.wellorder.net", "1"),
|
||||
("wss://nostr.bongbong.com", "1"),
|
||||
("wss://nostr.zebedee.cloud", "1"),
|
||||
("wss://nostr.fmt.wiz.biz", "1"),
|
||||
("wss://nostr.walletofsatoshi.com", "0"),
|
||||
("wss://relay.snort.social", "1"),
|
||||
("wss://offchain.pub", "1"),
|
||||
("wss://brb.io", "0"),
|
||||
("wss://relay.current.fyi", "1"),
|
||||
("wss://nostr.relayer.se", "0"),
|
||||
("wss://nostr.bitcoiner.social", "1"),
|
||||
("wss://relay.nostr.info", "1"),
|
||||
("wss://relay.zeh.app", "0"),
|
||||
("wss://nostr-01.dorafactory.org", "1"),
|
||||
("wss://nostr.zhongwen.world", "1"),
|
||||
("wss://nostro.cc", "1"),
|
||||
("wss://relay.nostr.net.in", "1"),
|
||||
("wss://nos.lol", "1");
|
||||
|
||||
-- create accounts
|
||||
-- is_active (part of multi-account feature):
|
||||
-- 0: false
|
||||
-- 1: true
|
||||
CREATE TABLE
|
||||
accounts (
|
||||
id TEXT PRIMARY KEY,
|
||||
privkey TEXT NOT NULL,
|
||||
npub TEXT NOT NULL,
|
||||
nsec TEXT NOT NULL,
|
||||
is_active INTEGER NOT NULL DEFAULT 0,
|
||||
metadata TEXT
|
||||
);
|
||||
|
||||
-- create follows
|
||||
-- kind (part of multi-newsfeed feature):
|
||||
-- 0: direct
|
||||
-- 1: follow of follow
|
||||
CREATE TABLE
|
||||
follows (
|
||||
id INTEGER PRIMARY KEY,
|
||||
pubkey TEXT NOT NULL,
|
||||
account TEXT NOT NULL,
|
||||
kind INTEGER NOT NULL DEFAULT 0,
|
||||
metadata TEXT
|
||||
);
|
||||
|
||||
-- create index for pubkey in follows
|
||||
CREATE UNIQUE INDEX index_pubkey_on_follows ON follows (pubkey);
|
||||
|
||||
-- create cache profiles
|
||||
CREATE TABLE
|
||||
cache_profiles (
|
||||
id TEXT PRIMARY KEY,
|
||||
metadata TEXT,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- create cache notes
|
||||
CREATE TABLE
|
||||
cache_notes (
|
||||
id TEXT PRIMARY KEY,
|
||||
pubkey TEXT NOT NULL,
|
||||
created_at TEXT,
|
||||
kind INTEGER NOT NULL DEFAULT 1,
|
||||
tags TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
parent_id TEXT,
|
||||
parent_comment_id TEXT
|
||||
);
|
||||
|
||||
-- create settings
|
||||
CREATE TABLE
|
||||
settings (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
setting_key TEXT NOT NULL,
|
||||
setting_value TEXT NOT NULL
|
||||
);
|
||||
|
||||
-- add default setting
|
||||
INSERT INTO
|
||||
settings (setting_key, setting_value)
|
||||
VALUES
|
||||
("last_login", "0");
|
0
src-tauri/prisma/migrations/.keep
Normal file
0
src-tauri/prisma/migrations/.keep
Normal file
@ -0,0 +1,73 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "Account" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"pubkey" TEXT NOT NULL,
|
||||
"privkey" TEXT NOT NULL,
|
||||
"active" BOOLEAN NOT NULL DEFAULT false,
|
||||
"metadata" TEXT NOT NULL
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Follow" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"pubkey" TEXT NOT NULL,
|
||||
"kind" INTEGER NOT NULL,
|
||||
"metadata" TEXT NOT NULL,
|
||||
"accountId" INTEGER NOT NULL,
|
||||
CONSTRAINT "Follow_accountId_fkey" FOREIGN KEY ("accountId") REFERENCES "Account" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Note" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"eventId" TEXT NOT NULL,
|
||||
"pubkey" TEXT NOT NULL,
|
||||
"kind" INTEGER NOT NULL,
|
||||
"tags" TEXT NOT NULL,
|
||||
"content" TEXT NOT NULL,
|
||||
"parent_id" TEXT NOT NULL,
|
||||
"parent_comment_id" TEXT NOT NULL,
|
||||
"createdAt" INTEGER NOT NULL,
|
||||
"accountId" INTEGER NOT NULL,
|
||||
CONSTRAINT "Note_accountId_fkey" FOREIGN KEY ("accountId") REFERENCES "Account" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Message" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"pubkey" TEXT NOT NULL,
|
||||
"content" TEXT NOT NULL,
|
||||
"tags" TEXT NOT NULL,
|
||||
"createdAt" INTEGER NOT NULL,
|
||||
"accountId" INTEGER NOT NULL,
|
||||
CONSTRAINT "Message_accountId_fkey" FOREIGN KEY ("accountId") REFERENCES "Account" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Relay" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"url" TEXT NOT NULL,
|
||||
"active" BOOLEAN NOT NULL DEFAULT true
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Setting" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"key" TEXT NOT NULL,
|
||||
"value" TEXT NOT NULL
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Account_privkey_key" ON "Account"("privkey");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Account_pubkey_idx" ON "Account"("pubkey");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Note_eventId_key" ON "Note"("eventId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Note_eventId_idx" ON "Note"("eventId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Message_pubkey_idx" ON "Message"("pubkey");
|
@ -0,0 +1,11 @@
|
||||
-- DropIndex
|
||||
DROP INDEX "Message_pubkey_idx";
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "Note_eventId_idx";
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Message_pubkey_createdAt_idx" ON "Message"("pubkey", "createdAt");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Note_eventId_createdAt_idx" ON "Note"("eventId", "createdAt");
|
@ -0,0 +1,30 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the `Follow` table. If the table is not empty, all the data it contains will be lost.
|
||||
- A unique constraint covering the columns `[pubkey]` on the table `Account` will be added. If there are existing duplicate values, this will fail.
|
||||
|
||||
*/
|
||||
-- DropTable
|
||||
PRAGMA foreign_keys=off;
|
||||
DROP TABLE "Follow";
|
||||
PRAGMA foreign_keys=on;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Pleb" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"pubkey" TEXT NOT NULL,
|
||||
"kind" INTEGER NOT NULL,
|
||||
"metadata" TEXT NOT NULL,
|
||||
"accountId" INTEGER NOT NULL,
|
||||
CONSTRAINT "Pleb_accountId_fkey" FOREIGN KEY ("accountId") REFERENCES "Account" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Pleb_pubkey_key" ON "Pleb"("pubkey");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Pleb_pubkey_idx" ON "Pleb"("pubkey");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Account_pubkey_key" ON "Account"("pubkey");
|
@ -0,0 +1,5 @@
|
||||
-- DropIndex
|
||||
DROP INDEX "Pleb_pubkey_idx";
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "Pleb_pubkey_key";
|
@ -0,0 +1,23 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- Added the required column `plebId` to the `Pleb` table without a default value. This is not possible if the table is not empty.
|
||||
|
||||
*/
|
||||
-- RedefineTables
|
||||
PRAGMA foreign_keys=OFF;
|
||||
CREATE TABLE "new_Pleb" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"plebId" TEXT NOT NULL,
|
||||
"pubkey" TEXT NOT NULL,
|
||||
"kind" INTEGER NOT NULL,
|
||||
"metadata" TEXT NOT NULL,
|
||||
"accountId" INTEGER NOT NULL,
|
||||
CONSTRAINT "Pleb_accountId_fkey" FOREIGN KEY ("accountId") REFERENCES "Account" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
);
|
||||
INSERT INTO "new_Pleb" ("accountId", "id", "kind", "metadata", "pubkey") SELECT "accountId", "id", "kind", "metadata", "pubkey" FROM "Pleb";
|
||||
DROP TABLE "Pleb";
|
||||
ALTER TABLE "new_Pleb" RENAME TO "Pleb";
|
||||
CREATE UNIQUE INDEX "Pleb_plebId_key" ON "Pleb"("plebId");
|
||||
PRAGMA foreign_key_check;
|
||||
PRAGMA foreign_keys=ON;
|
3
src-tauri/prisma/migrations/migration_lock.toml
Normal file
3
src-tauri/prisma/migrations/migration_lock.toml
Normal file
@ -0,0 +1,3 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (i.e. Git)
|
||||
provider = "sqlite"
|
79
src-tauri/prisma/schema.prisma
Normal file
79
src-tauri/prisma/schema.prisma
Normal file
@ -0,0 +1,79 @@
|
||||
datasource db {
|
||||
provider = "sqlite"
|
||||
url = "file:../../lume.db"
|
||||
}
|
||||
|
||||
generator client {
|
||||
provider = "cargo prisma"
|
||||
// The location to generate the client. Is relative to the position of the schema
|
||||
output = "../src/db.rs"
|
||||
module_path = "db"
|
||||
}
|
||||
|
||||
model Account {
|
||||
id Int @id @default(autoincrement())
|
||||
pubkey String @unique
|
||||
privkey String @unique
|
||||
active Boolean @default(false)
|
||||
metadata String
|
||||
|
||||
// related
|
||||
plebs Pleb[]
|
||||
messages Message[]
|
||||
notes Note[]
|
||||
|
||||
@@index([pubkey])
|
||||
}
|
||||
|
||||
model Pleb {
|
||||
id Int @id @default(autoincrement())
|
||||
plebId String @unique
|
||||
pubkey String
|
||||
kind Int
|
||||
metadata String
|
||||
|
||||
Account Account @relation(fields: [accountId], references: [id])
|
||||
accountId Int
|
||||
}
|
||||
|
||||
model Note {
|
||||
id Int @id @default(autoincrement())
|
||||
eventId String @unique
|
||||
pubkey String
|
||||
kind Int
|
||||
tags String
|
||||
content String
|
||||
parent_id String
|
||||
parent_comment_id String
|
||||
createdAt Int
|
||||
|
||||
Account Account @relation(fields: [accountId], references: [id])
|
||||
accountId Int
|
||||
|
||||
@@index([eventId, createdAt])
|
||||
}
|
||||
|
||||
model Message {
|
||||
id Int @id @default(autoincrement())
|
||||
pubkey String
|
||||
content String
|
||||
tags String
|
||||
createdAt Int
|
||||
|
||||
Account Account @relation(fields: [accountId], references: [id])
|
||||
accountId Int
|
||||
|
||||
@@index([pubkey, createdAt])
|
||||
}
|
||||
|
||||
model Relay {
|
||||
id Int @id @default(autoincrement())
|
||||
url String
|
||||
active Boolean @default(true)
|
||||
}
|
||||
|
||||
model Setting {
|
||||
id Int @id @default(autoincrement())
|
||||
key String
|
||||
value String
|
||||
}
|
3
src-tauri/src/bin/prisma.rs
Normal file
3
src-tauri/src/bin/prisma.rs
Normal file
@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
prisma_client_rust_cli::run();
|
||||
}
|
@ -7,15 +7,238 @@
|
||||
#[macro_use]
|
||||
extern crate objc;
|
||||
|
||||
use prisma_client_rust::Direction;
|
||||
use tauri::{Manager, WindowEvent};
|
||||
use tauri_plugin_sql::{Migration, MigrationKind};
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
use window_ext::WindowExt;
|
||||
#[cfg(target_os = "macos")]
|
||||
mod window_ext;
|
||||
|
||||
fn main() {
|
||||
mod db;
|
||||
|
||||
use db::*;
|
||||
use serde::Deserialize;
|
||||
use specta::{collect_types, Type};
|
||||
use std::{sync::Arc, vec};
|
||||
use tauri::State;
|
||||
use tauri_specta::ts;
|
||||
|
||||
type DbState<'a> = State<'a, Arc<PrismaClient>>;
|
||||
|
||||
#[derive(Deserialize, Type)]
|
||||
struct CreateAccountData {
|
||||
pubkey: String,
|
||||
privkey: String,
|
||||
metadata: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Type)]
|
||||
struct GetPlebData {
|
||||
account_id: i32,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Type)]
|
||||
struct GetPlebPubkeyData {
|
||||
pubkey: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Type)]
|
||||
struct CreatePlebData {
|
||||
pleb_id: String,
|
||||
pubkey: String,
|
||||
kind: i32,
|
||||
metadata: String,
|
||||
account_id: i32,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Type)]
|
||||
struct CreateNoteData {
|
||||
event_id: String,
|
||||
pubkey: String,
|
||||
kind: i32,
|
||||
tags: String,
|
||||
content: String,
|
||||
parent_id: String,
|
||||
parent_comment_id: String,
|
||||
created_at: i32,
|
||||
account_id: i32,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Type)]
|
||||
struct GetNoteByIdData {
|
||||
event_id: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Type)]
|
||||
struct GetNoteData {
|
||||
date: i32,
|
||||
limit: i32,
|
||||
offset: i32,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Type)]
|
||||
struct GetLatestNoteData {
|
||||
date: i32,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
async fn get_accounts(db: DbState<'_>) -> Result<Vec<account::Data>, ()> {
|
||||
db.account()
|
||||
.find_many(vec![account::active::equals(false)])
|
||||
.exec()
|
||||
.await
|
||||
.map_err(|_| ())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
async fn create_account(db: DbState<'_>, data: CreateAccountData) -> Result<account::Data, ()> {
|
||||
db.account()
|
||||
.create(data.pubkey, data.privkey, data.metadata, vec![])
|
||||
.exec()
|
||||
.await
|
||||
.map_err(|_| ())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
async fn get_plebs(db: DbState<'_>, data: GetPlebData) -> Result<Vec<pleb::Data>, ()> {
|
||||
db.pleb()
|
||||
.find_many(vec![pleb::account_id::equals(data.account_id)])
|
||||
.exec()
|
||||
.await
|
||||
.map_err(|_| ())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
async fn get_pleb_by_pubkey(
|
||||
db: DbState<'_>,
|
||||
data: GetPlebPubkeyData,
|
||||
) -> Result<Option<pleb::Data>, ()> {
|
||||
db.pleb()
|
||||
.find_first(vec![pleb::pubkey::equals(data.pubkey)])
|
||||
.exec()
|
||||
.await
|
||||
.map_err(|_| ())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
async fn create_pleb(db: DbState<'_>, data: CreatePlebData) -> Result<pleb::Data, ()> {
|
||||
let pleb_id = data.pleb_id.clone();
|
||||
let metadata = data.metadata.clone();
|
||||
|
||||
db.pleb()
|
||||
.upsert(
|
||||
pleb::pleb_id::equals(pleb_id),
|
||||
pleb::create(
|
||||
data.pleb_id,
|
||||
data.pubkey,
|
||||
data.kind,
|
||||
data.metadata,
|
||||
account::id::equals(data.account_id),
|
||||
vec![],
|
||||
),
|
||||
vec![pleb::metadata::set(metadata)],
|
||||
)
|
||||
.exec()
|
||||
.await
|
||||
.map_err(|_| ())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
async fn create_note(db: DbState<'_>, data: CreateNoteData) -> Result<note::Data, ()> {
|
||||
let event_id = data.event_id.clone();
|
||||
let content = data.content.clone();
|
||||
|
||||
db.note()
|
||||
.upsert(
|
||||
note::event_id::equals(event_id),
|
||||
note::create(
|
||||
data.event_id,
|
||||
data.pubkey,
|
||||
data.kind,
|
||||
data.tags,
|
||||
data.content,
|
||||
data.parent_id,
|
||||
data.parent_comment_id,
|
||||
data.created_at,
|
||||
account::id::equals(data.account_id),
|
||||
vec![],
|
||||
),
|
||||
vec![note::content::set(content)],
|
||||
)
|
||||
.exec()
|
||||
.await
|
||||
.map_err(|_| ())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
async fn get_notes(db: DbState<'_>, data: GetNoteData) -> Result<Vec<note::Data>, ()> {
|
||||
db.note()
|
||||
.find_many(vec![note::created_at::lte(data.date)])
|
||||
.order_by(note::created_at::order(Direction::Desc))
|
||||
.take(data.limit.into())
|
||||
.skip(data.offset.into())
|
||||
.exec()
|
||||
.await
|
||||
.map_err(|_| ())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
async fn get_latest_notes(db: DbState<'_>, data: GetLatestNoteData) -> Result<Vec<note::Data>, ()> {
|
||||
db.note()
|
||||
.find_many(vec![note::created_at::gt(data.date)])
|
||||
.order_by(note::created_at::order(Direction::Desc))
|
||||
.exec()
|
||||
.await
|
||||
.map_err(|_| ())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
async fn get_note_by_id(db: DbState<'_>, data: GetNoteByIdData) -> Result<Option<note::Data>, ()> {
|
||||
db.note()
|
||||
.find_unique(note::event_id::equals(data.event_id))
|
||||
.exec()
|
||||
.await
|
||||
.map_err(|_| ())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn count_total_notes(db: DbState<'_>) -> Result<i64, ()> {
|
||||
db.note().count(vec![]).exec().await.map_err(|_| ())
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let db = PrismaClient::_builder().build().await.unwrap();
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
ts::export(
|
||||
collect_types![
|
||||
get_accounts,
|
||||
create_account,
|
||||
get_plebs,
|
||||
get_pleb_by_pubkey,
|
||||
create_pleb,
|
||||
create_note,
|
||||
get_notes,
|
||||
get_latest_notes,
|
||||
get_note_by_id
|
||||
],
|
||||
"../src/utils/bindings.ts",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
db._db_push().await.unwrap();
|
||||
|
||||
tauri::Builder::default()
|
||||
.setup(|app| {
|
||||
let main_window = app.get_window("main").unwrap();
|
||||
@ -25,23 +248,11 @@ fn main() {
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.plugin(
|
||||
tauri_plugin_sql::Builder::default()
|
||||
.add_migrations(
|
||||
"sqlite:lume.db",
|
||||
vec![Migration {
|
||||
version: 1,
|
||||
description: "create default tables",
|
||||
sql: include_str!("../migrations/20230226004139_create_tables.sql"),
|
||||
kind: MigrationKind::Up,
|
||||
}],
|
||||
)
|
||||
.build(),
|
||||
)
|
||||
.on_window_event(|e| {
|
||||
#[cfg(target_os = "macos")]
|
||||
let apply_offset = || {
|
||||
let win = e.window();
|
||||
// keep inset for traffic lights when window resize (macos)
|
||||
win.position_traffic_lights(8.0, 20.0);
|
||||
};
|
||||
#[cfg(target_os = "macos")]
|
||||
@ -51,6 +262,19 @@ fn main() {
|
||||
_ => {}
|
||||
}
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
get_accounts,
|
||||
create_account,
|
||||
get_plebs,
|
||||
get_pleb_by_pubkey,
|
||||
create_pleb,
|
||||
create_note,
|
||||
get_notes,
|
||||
get_latest_notes,
|
||||
get_note_by_id,
|
||||
count_total_notes
|
||||
])
|
||||
.manage(Arc::new(db))
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
|
77
src/components/chats/chatList.tsx
Normal file
77
src/components/chats/chatList.tsx
Normal file
@ -0,0 +1,77 @@
|
||||
import { ChatListItem } from '@components/chats/chatListItem';
|
||||
import { ChatModal } from '@components/chats/chatModal';
|
||||
import { ImageWithFallback } from '@components/imageWithFallback';
|
||||
import { RelayContext } from '@components/relaysProvider';
|
||||
|
||||
import { activeAccountAtom } from '@stores/account';
|
||||
import { DEFAULT_AVATAR } from '@stores/constants';
|
||||
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useContext, useEffect, useState } from 'react';
|
||||
|
||||
export default function ChatList() {
|
||||
const [pool, relays]: any = useContext(RelayContext);
|
||||
const router = useRouter();
|
||||
|
||||
const activeAccount: any = useAtomValue(activeAccountAtom);
|
||||
const accountProfile = JSON.parse(activeAccount.metadata);
|
||||
|
||||
const [list, setList] = useState(new Set());
|
||||
|
||||
const openSelfChat = () => {
|
||||
router.push({
|
||||
pathname: '/chats/[pubkey]',
|
||||
query: { pubkey: activeAccount.pubkey },
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = pool.subscribe(
|
||||
[
|
||||
{
|
||||
kinds: [4],
|
||||
'#p': [activeAccount.pubkey],
|
||||
since: 0,
|
||||
},
|
||||
],
|
||||
relays,
|
||||
(event: any) => {
|
||||
if (event.pubkey !== activeAccount.pubkey) {
|
||||
setList((list) => new Set(list).add(event.pubkey));
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
unsubscribe;
|
||||
};
|
||||
}, [pool, relays, activeAccount.pubkey]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-px">
|
||||
<div
|
||||
onClick={() => openSelfChat()}
|
||||
className="inline-flex items-center gap-2 rounded-md px-2.5 py-1.5 hover:bg-zinc-900"
|
||||
>
|
||||
<div className="relative h-5 w-5 shrink overflow-hidden rounded bg-white">
|
||||
<ImageWithFallback
|
||||
src={accountProfile.picture || DEFAULT_AVATAR}
|
||||
alt={activeAccount.pubkey}
|
||||
fill={true}
|
||||
className="rounded object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h5 className="text-sm font-medium text-zinc-400">
|
||||
{accountProfile.display_name || accountProfile.name} <span className="text-zinc-500">(you)</span>
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
{[...list].map((item: string, index) => (
|
||||
<ChatListItem key={index} pubkey={item} />
|
||||
))}
|
||||
<ChatModal />
|
||||
</div>
|
||||
);
|
||||
}
|
41
src/components/chats/chatListItem.tsx
Normal file
41
src/components/chats/chatListItem.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
import { ImageWithFallback } from '@components/imageWithFallback';
|
||||
|
||||
import { DEFAULT_AVATAR } from '@stores/constants';
|
||||
|
||||
import { useMetadata } from '@utils/metadata';
|
||||
import { truncate } from '@utils/truncate';
|
||||
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
export const ChatListItem = ({ pubkey }: { pubkey: string }) => {
|
||||
const router = useRouter();
|
||||
const profile = useMetadata(pubkey);
|
||||
|
||||
const openChat = () => {
|
||||
router.push({
|
||||
pathname: '/chats/[pubkey]',
|
||||
query: { pubkey: pubkey },
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={() => openChat()}
|
||||
className="inline-flex items-center gap-2 rounded-md px-2.5 py-1.5 hover:bg-zinc-900"
|
||||
>
|
||||
<div className="relative h-5 w-5 shrink overflow-hidden rounded">
|
||||
<ImageWithFallback
|
||||
src={profile?.picture || DEFAULT_AVATAR}
|
||||
alt={pubkey}
|
||||
fill={true}
|
||||
className="rounded object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h5 className="text-sm font-medium text-zinc-400">
|
||||
{profile?.display_name || profile?.name || truncate(pubkey, 16, ' .... ')}
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
63
src/components/chats/chatModal.tsx
Normal file
63
src/components/chats/chatModal.tsx
Normal file
@ -0,0 +1,63 @@
|
||||
import { ChatModalUser } from '@components/chats/chatModalUser';
|
||||
|
||||
import { activeAccountAtom } from '@stores/account';
|
||||
|
||||
import * as Dialog from '@radix-ui/react-dialog';
|
||||
import { Cross1Icon, PlusIcon } from '@radix-ui/react-icons';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
export const ChatModal = () => {
|
||||
const [plebs, setPlebs] = useState([]);
|
||||
const activeAccount: any = useAtomValue(activeAccountAtom);
|
||||
|
||||
const fetchPlebsByAccount = useCallback(async (id) => {
|
||||
const { getPlebs } = await import('@utils/bindings');
|
||||
return await getPlebs({ account_id: id });
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchPlebsByAccount(activeAccount.id)
|
||||
.then((res) => setPlebs(res))
|
||||
.catch(console.error);
|
||||
}, [activeAccount.id, fetchPlebsByAccount]);
|
||||
|
||||
return (
|
||||
<Dialog.Root>
|
||||
<Dialog.Trigger asChild>
|
||||
<div className="group inline-flex items-center gap-2 rounded-md px-2.5 py-1.5 hover:bg-zinc-950">
|
||||
<div className="inline-flex h-5 w-5 shrink items-center justify-center rounded bg-zinc-900">
|
||||
<PlusIcon className="h-3 w-3 text-zinc-500" />
|
||||
</div>
|
||||
<div>
|
||||
<h5 className="text-sm font-medium text-zinc-500 group-hover:text-zinc-400">Add a new chat</h5>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Trigger>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay className="fixed inset-0 z-50 bg-black bg-opacity-30 backdrop-blur-sm data-[state=open]:animate-overlayShow" />
|
||||
<Dialog.Content className="fixed inset-0 z-50 overflow-y-auto">
|
||||
<div className="flex min-h-full items-center justify-center">
|
||||
<div className="relative flex h-[500px] w-full max-w-2xl flex-col rounded-lg bg-zinc-900 text-zinc-100 ring-1 ring-zinc-800">
|
||||
<div className="sticky left-0 top-0 flex h-12 w-full shrink-0 items-center justify-between rounded-t-lg border-b border-zinc-800 bg-zinc-950 px-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Dialog.Close asChild>
|
||||
<button className="inline-flex h-5 w-5 items-center justify-center rounded bg-zinc-900">
|
||||
<Cross1Icon className="h-3 w-3 text-zinc-300" />
|
||||
</button>
|
||||
</Dialog.Close>
|
||||
<h5 className="font-semibold leading-none text-zinc-500">New chat</h5>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col overflow-y-auto">
|
||||
{plebs.map((pleb) => (
|
||||
<ChatModalUser key={pleb.id} data={pleb} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
);
|
||||
};
|
48
src/components/chats/chatModalUser.tsx
Normal file
48
src/components/chats/chatModalUser.tsx
Normal file
@ -0,0 +1,48 @@
|
||||
import { ImageWithFallback } from '@components/imageWithFallback';
|
||||
|
||||
import { DEFAULT_AVATAR } from '@stores/constants';
|
||||
|
||||
import { truncate } from '@utils/truncate';
|
||||
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
export const ChatModalUser = ({ data }: { data: any }) => {
|
||||
const router = useRouter();
|
||||
const profile = JSON.parse(data.metadata);
|
||||
|
||||
const openNewChat = () => {
|
||||
router.push({
|
||||
pathname: '/chats/[pubkey]',
|
||||
query: { pubkey: data.pubkey },
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="group flex items-center justify-between px-3 py-2 hover:bg-zinc-800">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative h-10 w-10 shrink overflow-hidden rounded-md">
|
||||
<ImageWithFallback
|
||||
src={profile?.picture || DEFAULT_AVATAR}
|
||||
alt={data.pubkey}
|
||||
fill={true}
|
||||
className="rounded-md object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-full flex-1 flex-col items-start text-start">
|
||||
<span className="truncate text-sm font-semibold leading-tight text-zinc-200">
|
||||
{profile?.display_name || profile?.name}
|
||||
</span>
|
||||
<span className="text-sm leading-tight text-zinc-400">{truncate(data.pubkey, 16, ' .... ')}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
onClick={() => openNewChat()}
|
||||
className="hidden h-8 items-center justify-center rounded-md bg-fuchsia-500 px-3 text-sm font-medium shadow-button hover:bg-fuchsia-600 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 group-hover:inline-flex"
|
||||
>
|
||||
Send message
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
46
src/components/chats/messageList.tsx
Normal file
46
src/components/chats/messageList.tsx
Normal file
@ -0,0 +1,46 @@
|
||||
import MessageListItem from '@components/chats/messageListItem';
|
||||
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { Virtuoso } from 'react-virtuoso';
|
||||
|
||||
export const MessageList = ({ data }: { data: any }) => {
|
||||
const virtuosoRef = useRef(null);
|
||||
|
||||
const itemContent: any = useCallback(
|
||||
(index: string | number) => {
|
||||
const activeAccount = JSON.parse(localStorage.getItem('activeAccount'));
|
||||
return (
|
||||
<MessageListItem
|
||||
data={data[index]}
|
||||
activeAccountPubkey={activeAccount.pubkey}
|
||||
activeAccountPrivkey={activeAccount.privkey}
|
||||
/>
|
||||
);
|
||||
},
|
||||
[data]
|
||||
);
|
||||
|
||||
const computeItemKey = useCallback(
|
||||
(index: string | number) => {
|
||||
return data[index].id;
|
||||
},
|
||||
[data]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="h-full w-full">
|
||||
<Virtuoso
|
||||
ref={virtuosoRef}
|
||||
data={data}
|
||||
itemContent={itemContent}
|
||||
computeItemKey={computeItemKey}
|
||||
initialTopMostItemIndex={data.length - 1}
|
||||
alignToBottom={true}
|
||||
followOutput={true}
|
||||
overscan={50}
|
||||
increaseViewportBy={{ top: 200, bottom: 200 }}
|
||||
className="scrollbar-hide h-full w-full overflow-y-auto"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
51
src/components/chats/messageListItem.tsx
Normal file
51
src/components/chats/messageListItem.tsx
Normal file
@ -0,0 +1,51 @@
|
||||
import { MessageUser } from '@components/chats/messageUser';
|
||||
|
||||
import { nip04 } from 'nostr-tools';
|
||||
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
const MessageListItem = ({
|
||||
data,
|
||||
activeAccountPubkey,
|
||||
activeAccountPrivkey,
|
||||
}: {
|
||||
data: any;
|
||||
activeAccountPubkey: string;
|
||||
activeAccountPrivkey: string;
|
||||
}) => {
|
||||
const [content, setContent] = useState('');
|
||||
|
||||
const sender = useMemo(() => {
|
||||
const pTag = data.tags.find(([k, v]) => k === 'p' && v && v !== '')[1];
|
||||
if (pTag === activeAccountPubkey) {
|
||||
return data.pubkey;
|
||||
} else {
|
||||
return pTag;
|
||||
}
|
||||
}, [data.pubkey, data.tags, activeAccountPubkey]);
|
||||
|
||||
const decryptContent = useCallback(async () => {
|
||||
const result = await nip04.decrypt(activeAccountPrivkey, sender, data.content);
|
||||
setContent(result);
|
||||
}, [data.content, activeAccountPrivkey, sender]);
|
||||
|
||||
useEffect(() => {
|
||||
decryptContent().catch(console.error);
|
||||
}, [decryptContent]);
|
||||
|
||||
return (
|
||||
<div className="flex h-min min-h-min w-full select-text flex-col px-5 py-2 hover:bg-black/20">
|
||||
<div className="flex flex-col">
|
||||
<MessageUser pubkey={data.pubkey} time={data.created_at} />
|
||||
<div className="-mt-[17px] pl-[48px]">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="prose prose-zinc max-w-none break-words text-sm leading-tight dark:prose-invert prose-p:m-0 prose-p:text-sm prose-p:leading-tight prose-a:font-normal prose-a:text-fuchsia-500 prose-a:no-underline prose-img:m-0 prose-video:m-0">
|
||||
{content}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(MessageListItem);
|
37
src/components/chats/messageUser.tsx
Normal file
37
src/components/chats/messageUser.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
import { ImageWithFallback } from '@components/imageWithFallback';
|
||||
|
||||
import { DEFAULT_AVATAR } from '@stores/constants';
|
||||
|
||||
import { useMetadata } from '@utils/metadata';
|
||||
import { truncate } from '@utils/truncate';
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
export const MessageUser = ({ pubkey, time }: { pubkey: string; time: number }) => {
|
||||
const profile = useMetadata(pubkey);
|
||||
|
||||
return (
|
||||
<div className="group flex items-start gap-3">
|
||||
<div className="relative h-9 w-9 shrink overflow-hidden rounded-md">
|
||||
<ImageWithFallback
|
||||
src={profile?.picture || DEFAULT_AVATAR}
|
||||
alt={pubkey}
|
||||
fill={true}
|
||||
className="rounded-md object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-full flex-1 items-start justify-between">
|
||||
<div className="flex items-baseline gap-2 text-sm">
|
||||
<span className="font-semibold leading-none text-zinc-200 group-hover:underline">
|
||||
{profile?.display_name || profile?.name || truncate(pubkey, 16, ' .... ')}
|
||||
</span>
|
||||
<span className="leading-none text-zinc-500">·</span>
|
||||
<span className="leading-none text-zinc-500">{dayjs().to(dayjs.unix(time))}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,47 +0,0 @@
|
||||
import AccountList from '@components/columns/account/list';
|
||||
|
||||
import LumeSymbol from '@assets/icons/Lume';
|
||||
|
||||
import { PlusIcon } from '@radix-ui/react-icons';
|
||||
import { getVersion } from '@tauri-apps/api/app';
|
||||
import Link from 'next/link';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
export default function AccountColumn() {
|
||||
const [version, setVersion] = useState(null);
|
||||
|
||||
const getAppVersion = useCallback(async () => {
|
||||
const appVersion = await getVersion();
|
||||
setVersion(appVersion);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
getAppVersion().catch(console.error);
|
||||
}, [getAppVersion]);
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center justify-between px-2 pb-4 pt-4">
|
||||
<div className="flex flex-col gap-4">
|
||||
<Link
|
||||
href="/explore"
|
||||
className="group relative flex h-11 w-11 shrink cursor-pointer items-center justify-center rounded-md bg-zinc-900 hover:bg-zinc-800"
|
||||
>
|
||||
<LumeSymbol className="h-6 w-auto text-zinc-400 group-hover:text-zinc-200" />
|
||||
</Link>
|
||||
<AccountList />
|
||||
<Link
|
||||
href="/onboarding"
|
||||
className="group relative flex h-11 w-11 shrink cursor-pointer items-center justify-center rounded-md border-2 border-dashed border-zinc-600 hover:border-zinc-400"
|
||||
>
|
||||
<PlusIcon className="h-4 w-4 text-zinc-400 group-hover:text-zinc-200" />
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex flex-col gap-0.5 text-center">
|
||||
<span className="animate-moveBg from-fuchsia-300 via-orange-100 to-amber-300 text-sm font-black uppercase leading-tight text-zinc-600 hover:bg-gradient-to-r hover:bg-clip-text hover:text-transparent">
|
||||
Lume
|
||||
</span>
|
||||
<span className="text-xs font-medium text-zinc-700">v{version}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
import { ActiveAccount } from '@components/columns/account/active';
|
||||
import { InactiveAccount } from '@components/columns/account/inactive';
|
||||
|
||||
import { activeAccountAtom } from '@stores/account';
|
||||
|
||||
import { getAccounts } from '@utils/storage';
|
||||
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
export default function AccountList() {
|
||||
const activeAccount: any = useAtomValue(activeAccountAtom);
|
||||
const [users, setUsers] = useState([]);
|
||||
|
||||
const renderAccount = useCallback(
|
||||
(user: { id: string }) => {
|
||||
if (user.id === activeAccount.id) {
|
||||
return <ActiveAccount key={user.id} user={user} />;
|
||||
} else {
|
||||
return <InactiveAccount key={user.id} user={user} />;
|
||||
}
|
||||
},
|
||||
[activeAccount.id]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchAccount = async () => {
|
||||
const result: any = await getAccounts();
|
||||
setUsers(result);
|
||||
};
|
||||
|
||||
fetchAccount().catch(console.error);
|
||||
}, []);
|
||||
|
||||
return <>{users.map((user) => renderAccount(user))}</>;
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
import * as Collapsible from '@radix-ui/react-collapsible';
|
||||
import { TriangleUpIcon } from '@radix-ui/react-icons';
|
||||
import { useState } from 'react';
|
||||
|
||||
export default function Chats() {
|
||||
const [open, setOpen] = useState(true);
|
||||
|
||||
return (
|
||||
<Collapsible.Root open={open} onOpenChange={setOpen} className="h-full shrink-0">
|
||||
<div className="flex h-full flex-col gap-1 px-2 pb-8">
|
||||
<Collapsible.Trigger className="flex cursor-pointer items-center gap-2 px-2 py-1">
|
||||
<div
|
||||
className={`inline-flex h-6 w-6 transform items-center justify-center transition-transform duration-150 ease-in-out ${
|
||||
open ? 'rotate-180' : ''
|
||||
}`}
|
||||
>
|
||||
<TriangleUpIcon className="h-4 w-4 text-zinc-500" />
|
||||
</div>
|
||||
<h3 className="bg-gradient-to-r from-red-300 via-pink-100 to-blue-300 bg-clip-text text-xs font-bold uppercase tracking-wide text-transparent">
|
||||
Chats
|
||||
</h3>
|
||||
</Collapsible.Trigger>
|
||||
<Collapsible.Content className="h-full"></Collapsible.Content>
|
||||
</div>
|
||||
</Collapsible.Root>
|
||||
);
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
import Chats from '@components/columns/navigator/chats';
|
||||
import Newsfeed from '@components/columns/navigator/newsfeed';
|
||||
|
||||
export default function NavigatorColumn() {
|
||||
return (
|
||||
<div className="relative flex h-full flex-col gap-1 overflow-hidden pt-4">
|
||||
{/* Newsfeed */}
|
||||
<Newsfeed />
|
||||
{/* Chats */}
|
||||
<Chats />
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,50 +0,0 @@
|
||||
import ActiveLink from '@components/activeLink';
|
||||
|
||||
import * as Collapsible from '@radix-ui/react-collapsible';
|
||||
import { TriangleUpIcon } from '@radix-ui/react-icons';
|
||||
import { useState } from 'react';
|
||||
|
||||
export default function Newsfeed() {
|
||||
const [open, setOpen] = useState(true);
|
||||
|
||||
return (
|
||||
<Collapsible.Root open={open} onOpenChange={setOpen}>
|
||||
<div className="flex flex-col gap-1 px-2">
|
||||
<Collapsible.Trigger className="flex cursor-pointer items-center gap-2 px-2 py-1">
|
||||
<div
|
||||
className={`inline-flex h-6 w-6 transform items-center justify-center transition-transform duration-150 ease-in-out ${
|
||||
open ? 'rotate-180' : ''
|
||||
}`}
|
||||
>
|
||||
<TriangleUpIcon className="h-4 w-4 text-zinc-500" />
|
||||
</div>
|
||||
<h3 className="bg-gradient-to-r from-fuchsia-300 via-orange-100 to-amber-300 bg-clip-text text-xs font-bold uppercase tracking-wide text-transparent">
|
||||
Newsfeed
|
||||
</h3>
|
||||
</Collapsible.Trigger>
|
||||
<Collapsible.Content className="flex flex-col gap-1 text-zinc-400">
|
||||
<ActiveLink
|
||||
href={`/newsfeed/following`}
|
||||
activeClassName="ring-1 ring-white/10 dark:bg-zinc-900 dark:text-white hover:dark:bg-zinc-800"
|
||||
className="flex h-8 items-center gap-2.5 rounded-md px-2.5 text-sm font-medium hover:bg-zinc-900"
|
||||
>
|
||||
<div className="inline-flex h-5 w-5 items-center justify-center">
|
||||
<span className="h-4 w-3 rounded-sm bg-gradient-to-br from-fuchsia-500 via-purple-300 to-pink-300"></span>
|
||||
</div>
|
||||
<span>Following</span>
|
||||
</ActiveLink>
|
||||
<ActiveLink
|
||||
href={`/newsfeed/circle`}
|
||||
activeClassName="ring-1 ring-white/10 dark:bg-zinc-900 dark:text-white hover:dark:bg-zinc-800"
|
||||
className="flex h-8 items-center gap-2.5 rounded-md px-2.5 text-sm font-medium hover:bg-zinc-900"
|
||||
>
|
||||
<div className="inline-flex h-5 w-5 items-center justify-center">
|
||||
<span className="h-4 w-3 rounded-sm bg-gradient-to-br from-amber-500 via-orange-200 to-yellow-300"></span>
|
||||
</div>
|
||||
<span>Circle</span>
|
||||
</ActiveLink>
|
||||
</Collapsible.Content>
|
||||
</div>
|
||||
</Collapsible.Root>
|
||||
);
|
||||
}
|
@ -2,12 +2,11 @@ import EmojiPicker from '@components/form/emojiPicker';
|
||||
import ImagePicker from '@components/form/imagePicker';
|
||||
import { RelayContext } from '@components/relaysProvider';
|
||||
|
||||
import { activeAccountAtom } from '@stores/account';
|
||||
import { noteContentAtom } from '@stores/note';
|
||||
|
||||
import { dateToUnix } from '@utils/getDate';
|
||||
|
||||
import { useAtom, useAtomValue } from 'jotai';
|
||||
import { useAtom } from 'jotai';
|
||||
import { useResetAtom } from 'jotai/utils';
|
||||
import { getEventHash, signEvent } from 'nostr-tools';
|
||||
import { useContext } from 'react';
|
||||
@ -15,23 +14,21 @@ import { useContext } from 'react';
|
||||
export default function FormBase() {
|
||||
const [pool, relays]: any = useContext(RelayContext);
|
||||
|
||||
const activeAccount: any = useAtomValue(activeAccountAtom);
|
||||
const [value, setValue] = useAtom(noteContentAtom);
|
||||
const resetValue = useResetAtom(noteContentAtom);
|
||||
|
||||
const pubkey = activeAccount.id;
|
||||
const privkey = activeAccount.privkey;
|
||||
|
||||
const submitEvent = () => {
|
||||
const activeAccount = JSON.parse(localStorage.getItem('activeAccount'));
|
||||
|
||||
const event: any = {
|
||||
content: value,
|
||||
created_at: dateToUnix(),
|
||||
kind: 1,
|
||||
pubkey: pubkey,
|
||||
pubkey: activeAccount.pubkey,
|
||||
tags: [],
|
||||
};
|
||||
event.id = getEventHash(event);
|
||||
event.sig = signEvent(event, privkey);
|
||||
event.sig = signEvent(event, activeAccount.privkey);
|
||||
|
||||
// publish note
|
||||
pool.publish(event, relays);
|
||||
|
79
src/components/form/chat.tsx
Normal file
79
src/components/form/chat.tsx
Normal file
@ -0,0 +1,79 @@
|
||||
import ImagePicker from '@components/form/imagePicker';
|
||||
import { RelayContext } from '@components/relaysProvider';
|
||||
|
||||
import { dateToUnix } from '@utils/getDate';
|
||||
|
||||
import { getEventHash, nip04, signEvent } from 'nostr-tools';
|
||||
import { useCallback, useContext, useState } from 'react';
|
||||
|
||||
export default function FormChat({ receiverPubkey }: { receiverPubkey: string }) {
|
||||
const [pool, relays]: any = useContext(RelayContext);
|
||||
const [value, setValue] = useState('');
|
||||
|
||||
const encryptMessage = useCallback(
|
||||
async (privkey: string) => {
|
||||
return await nip04.encrypt(privkey, receiverPubkey, value);
|
||||
},
|
||||
[receiverPubkey, value]
|
||||
);
|
||||
|
||||
const submitEvent = useCallback(() => {
|
||||
const activeAccount = JSON.parse(localStorage.getItem('activeAccount'));
|
||||
encryptMessage(activeAccount.privkey)
|
||||
.then((encryptedContent) => {
|
||||
const event: any = {
|
||||
content: encryptedContent,
|
||||
created_at: dateToUnix(),
|
||||
kind: 4,
|
||||
pubkey: activeAccount.pubkey,
|
||||
tags: [['p', receiverPubkey]],
|
||||
};
|
||||
event.id = getEventHash(event);
|
||||
event.sig = signEvent(event, activeAccount.privkey);
|
||||
// publish note
|
||||
pool.publish(event, relays);
|
||||
// reset state
|
||||
setValue('');
|
||||
})
|
||||
.catch(console.error);
|
||||
}, [encryptMessage, receiverPubkey, pool, relays]);
|
||||
|
||||
const handleEnterPress = (e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
submitEvent();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative h-24 w-full shrink-0 overflow-hidden before:pointer-events-none before:absolute before:-inset-1 before:rounded-[11px] before:border before:border-fuchsia-500 before:opacity-0 before:ring-2 before:ring-fuchsia-500/20 before:transition after:pointer-events-none after:absolute after:inset-px after:rounded-[7px] after:shadow-highlight after:shadow-white/5 after:transition focus-within:before:opacity-100 focus-within:after:shadow-fuchsia-500/100 dark:focus-within:after:shadow-fuchsia-500/20">
|
||||
<div>
|
||||
<textarea
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
onKeyDown={handleEnterPress}
|
||||
spellCheck={false}
|
||||
placeholder="Message"
|
||||
className="relative h-24 w-full resize-none rounded-lg border border-black/5 px-3.5 py-3 text-sm shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-200 dark:shadow-black/10 dark:placeholder:text-zinc-500"
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute bottom-2 w-full px-2">
|
||||
<div className="flex w-full items-center justify-between bg-zinc-800">
|
||||
<div className="flex items-center gap-2 divide-x divide-zinc-700">
|
||||
<ImagePicker />
|
||||
<div className="flex items-center gap-2 pl-2"></div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => submitEvent()}
|
||||
disabled={value.length === 0 ? true : false}
|
||||
className="inline-flex h-8 w-16 items-center justify-center rounded-md bg-fuchsia-500 px-4 text-sm font-medium shadow-button hover:bg-fuchsia-600 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
Send
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -23,7 +23,7 @@ export default function FormComment({ eventID }: { eventID: any }) {
|
||||
content: value,
|
||||
created_at: dateToUnix(),
|
||||
kind: 1,
|
||||
pubkey: activeAccount.id,
|
||||
pubkey: activeAccount.pubkey,
|
||||
tags: [['e', eventID]],
|
||||
};
|
||||
event.id = getEventHash(event);
|
||||
@ -42,7 +42,7 @@ export default function FormComment({ eventID }: { eventID: any }) {
|
||||
<div className="relative h-11 w-11 shrink-0 overflow-hidden rounded-md border border-white/10">
|
||||
<ImageWithFallback
|
||||
src={profile?.picture}
|
||||
alt={activeAccount.id}
|
||||
alt={activeAccount.pubkey}
|
||||
fill={true}
|
||||
className="rounded-md object-cover"
|
||||
/>
|
||||
|
@ -1,50 +1,65 @@
|
||||
import { RelayContext } from '@components/relaysProvider';
|
||||
|
||||
import { dateToUnix } from '@utils/getDate';
|
||||
import { createFollows } from '@utils/storage';
|
||||
import { tagsToArray } from '@utils/transform';
|
||||
import { DEFAULT_AVATAR } from '@stores/constants';
|
||||
|
||||
import { fetchMetadata } from '@utils/metadata';
|
||||
|
||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
|
||||
import { AvatarIcon, ExitIcon, GearIcon } from '@radix-ui/react-icons';
|
||||
import { writeText } from '@tauri-apps/api/clipboard';
|
||||
import destr from 'destr';
|
||||
import Image from 'next/image';
|
||||
import { useRouter } from 'next/router';
|
||||
import { nip19 } from 'nostr-tools';
|
||||
import { memo, useContext, useEffect, useRef } from 'react';
|
||||
import { memo, useCallback, useContext, useEffect } from 'react';
|
||||
|
||||
export const ActiveAccount = memo(function ActiveAccount({ user }: { user: any }) {
|
||||
const [pool, relays]: any = useContext(RelayContext);
|
||||
|
||||
const router = useRouter();
|
||||
const userData = destr(user.metadata);
|
||||
|
||||
const now = useRef(new Date());
|
||||
const userData = JSON.parse(user.metadata);
|
||||
|
||||
const openProfilePage = () => {
|
||||
router.push(`/users/${user.id}`);
|
||||
router.push(`/users/${user.pubkey}`);
|
||||
};
|
||||
|
||||
const copyPublicKey = async () => {
|
||||
await writeText(nip19.npubEncode(user.id));
|
||||
await writeText(nip19.npubEncode(user.pubkey));
|
||||
};
|
||||
|
||||
const insertFollowsToStorage = useCallback(
|
||||
async (tags) => {
|
||||
const { createPleb } = await import('@utils/bindings');
|
||||
const activeAccount = JSON.parse(localStorage.getItem('activeAccount'));
|
||||
|
||||
for (const tag of tags) {
|
||||
const metadata: any = await fetchMetadata(tag[1], pool, relays);
|
||||
createPleb({
|
||||
pleb_id: tag[1] + '-lume' + activeAccount.id.toString(),
|
||||
pubkey: tag[1],
|
||||
kind: 0,
|
||||
metadata: metadata.content,
|
||||
account_id: activeAccount.id,
|
||||
}).catch(console.error);
|
||||
}
|
||||
},
|
||||
[pool, relays]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = pool.subscribe(
|
||||
[
|
||||
{
|
||||
kinds: [3],
|
||||
authors: [user.id],
|
||||
since: dateToUnix(now.current),
|
||||
authors: [user.pubkey],
|
||||
},
|
||||
],
|
||||
relays,
|
||||
(event: any) => {
|
||||
if (event.tags.length > 0) {
|
||||
createFollows(tagsToArray(event.tags), user.id, 0);
|
||||
insertFollowsToStorage(event.tags);
|
||||
}
|
||||
},
|
||||
undefined,
|
||||
20000,
|
||||
undefined,
|
||||
{
|
||||
unsubscribeOnEose: true,
|
||||
@ -54,19 +69,17 @@ export const ActiveAccount = memo(function ActiveAccount({ user }: { user: any }
|
||||
return () => {
|
||||
unsubscribe;
|
||||
};
|
||||
}, [pool, relays, user.id]);
|
||||
}, [insertFollowsToStorage, pool, relays, user.pubkey]);
|
||||
|
||||
return (
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger asChild>
|
||||
<button className="relative h-11 w-11 rounded-md">
|
||||
<button className="relative h-11 w-11 rounded-lg">
|
||||
<Image
|
||||
src={userData.picture}
|
||||
src={userData.picture || DEFAULT_AVATAR}
|
||||
alt="user's avatar"
|
||||
fill={true}
|
||||
className="rounded-md object-cover"
|
||||
placeholder="blur"
|
||||
blurDataURL="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkqAcAAIUAgUW0RjgAAAAASUVORK5CYII="
|
||||
className="rounded-lg object-cover"
|
||||
priority
|
||||
/>
|
||||
</button>
|
@ -1,17 +1,24 @@
|
||||
import destr from 'destr';
|
||||
import { DEFAULT_AVATAR } from '@stores/constants';
|
||||
|
||||
import Image from 'next/image';
|
||||
import { memo } from 'react';
|
||||
|
||||
export const InactiveAccount = memo(function InactiveAccount({ user }: { user: any }) {
|
||||
const userData = destr(user.metadata);
|
||||
const userData = JSON.parse(user.metadata);
|
||||
|
||||
const setCurrentUser = () => {
|
||||
console.log('clicked');
|
||||
};
|
||||
|
||||
return (
|
||||
<button onClick={() => setCurrentUser()} className="relative h-11 w-11 shrink rounded-md">
|
||||
<Image src={userData.picture} alt="user's avatar" fill={true} className="rounded-md object-cover" />
|
||||
<button onClick={() => setCurrentUser()} className="relative h-11 w-11 shrink rounded-lg">
|
||||
<Image
|
||||
src={userData.picture || DEFAULT_AVATAR}
|
||||
alt="user's avatar"
|
||||
fill={true}
|
||||
className="rounded-lg object-cover"
|
||||
priority
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
});
|
61
src/components/multiAccounts/index.tsx
Normal file
61
src/components/multiAccounts/index.tsx
Normal file
@ -0,0 +1,61 @@
|
||||
import { ActiveAccount } from '@components/multiAccounts/activeAccount';
|
||||
import { InactiveAccount } from '@components/multiAccounts/inactiveAccount';
|
||||
|
||||
import { APP_VERSION } from '@stores/constants';
|
||||
|
||||
import LumeSymbol from '@assets/icons/Lume';
|
||||
|
||||
import { PlusIcon } from '@radix-ui/react-icons';
|
||||
import Link from 'next/link';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
export default function MultiAccounts() {
|
||||
const [users, setUsers] = useState([]);
|
||||
|
||||
const renderAccount = useCallback((user: { pubkey: string }) => {
|
||||
const activeAccount = JSON.parse(localStorage.getItem('activeAccount'));
|
||||
|
||||
if (user.pubkey === activeAccount.pubkey) {
|
||||
return <ActiveAccount key={user.pubkey} user={user} />;
|
||||
} else {
|
||||
return <InactiveAccount key={user.pubkey} user={user} />;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const fetchAccounts = useCallback(async () => {
|
||||
const { getAccounts } = await import('@utils/bindings');
|
||||
const accounts = await getAccounts();
|
||||
// update state
|
||||
setUsers(accounts);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchAccounts().catch(console.error);
|
||||
}, [fetchAccounts]);
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center justify-between px-2 pb-4 pt-3">
|
||||
<div className="flex flex-col gap-4">
|
||||
<Link
|
||||
href="/explore"
|
||||
className="group relative flex h-11 w-11 shrink cursor-pointer items-center justify-center rounded-lg bg-zinc-900 hover:bg-zinc-800"
|
||||
>
|
||||
<LumeSymbol className="h-6 w-auto text-zinc-400 group-hover:text-zinc-200" />
|
||||
</Link>
|
||||
<div>{users.map((user) => renderAccount(user))}</div>
|
||||
<Link
|
||||
href="/onboarding"
|
||||
className="group relative flex h-11 w-11 shrink cursor-pointer items-center justify-center rounded-lg border-2 border-dashed border-zinc-600 hover:border-zinc-400"
|
||||
>
|
||||
<PlusIcon className="h-4 w-4 text-zinc-400 group-hover:text-zinc-200" />
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex flex-col gap-0.5 text-center">
|
||||
<span className="animate-moveBg from-fuchsia-300 via-orange-100 to-amber-300 text-sm font-black uppercase leading-tight text-zinc-600 hover:bg-gradient-to-r hover:bg-clip-text hover:text-transparent">
|
||||
Lume
|
||||
</span>
|
||||
<span className="text-xs font-medium text-zinc-700">v{APP_VERSION}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
25
src/components/navigation/channels.tsx
Normal file
25
src/components/navigation/channels.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
import * as Collapsible from '@radix-ui/react-collapsible';
|
||||
import { TriangleUpIcon } from '@radix-ui/react-icons';
|
||||
import { useState } from 'react';
|
||||
|
||||
export default function Channels() {
|
||||
const [open, setOpen] = useState(true);
|
||||
|
||||
return (
|
||||
<Collapsible.Root open={open} onOpenChange={setOpen}>
|
||||
<div className="flex flex-col px-2">
|
||||
<Collapsible.Trigger className="flex cursor-pointer items-center gap-1 px-1 py-1">
|
||||
<div
|
||||
className={`inline-flex h-5 w-5 transform items-center justify-center transition-transform duration-150 ease-in-out ${
|
||||
open ? 'rotate-180' : ''
|
||||
}`}
|
||||
>
|
||||
<TriangleUpIcon className="h-4 w-4 text-zinc-700" />
|
||||
</div>
|
||||
<h3 className="text-[11px] font-bold uppercase tracking-widest text-zinc-600">Channels</h3>
|
||||
</Collapsible.Trigger>
|
||||
<Collapsible.Content></Collapsible.Content>
|
||||
</div>
|
||||
</Collapsible.Root>
|
||||
);
|
||||
}
|
29
src/components/navigation/chats.tsx
Normal file
29
src/components/navigation/chats.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
import ChatList from '@components/chats/chatList';
|
||||
|
||||
import * as Collapsible from '@radix-ui/react-collapsible';
|
||||
import { TriangleUpIcon } from '@radix-ui/react-icons';
|
||||
import { useState } from 'react';
|
||||
|
||||
export default function Chats() {
|
||||
const [open, setOpen] = useState(true);
|
||||
|
||||
return (
|
||||
<Collapsible.Root open={open} onOpenChange={setOpen}>
|
||||
<div className="flex flex-col px-2">
|
||||
<Collapsible.Trigger className="flex cursor-pointer items-center gap-1 px-1 py-1">
|
||||
<div
|
||||
className={`inline-flex h-5 w-5 transform items-center justify-center transition-transform duration-150 ease-in-out ${
|
||||
open ? 'rotate-180' : ''
|
||||
}`}
|
||||
>
|
||||
<TriangleUpIcon className="h-4 w-4 text-zinc-700" />
|
||||
</div>
|
||||
<h3 className="text-[11px] font-bold uppercase tracking-widest text-zinc-600">Chats</h3>
|
||||
</Collapsible.Trigger>
|
||||
<Collapsible.Content>
|
||||
<ChatList />
|
||||
</Collapsible.Content>
|
||||
</div>
|
||||
</Collapsible.Root>
|
||||
);
|
||||
}
|
16
src/components/navigation/index.tsx
Normal file
16
src/components/navigation/index.tsx
Normal file
@ -0,0 +1,16 @@
|
||||
import Channels from '@components/navigation/channels';
|
||||
import Chats from '@components/navigation/chats';
|
||||
import Newsfeed from '@components/navigation/newsfeed';
|
||||
|
||||
export default function Navigation() {
|
||||
return (
|
||||
<div className="relative flex h-full flex-col gap-1 overflow-hidden pt-3">
|
||||
{/* Newsfeed */}
|
||||
<Newsfeed />
|
||||
{/* Channels */}
|
||||
<Channels />
|
||||
{/* Chats */}
|
||||
<Chats />
|
||||
</div>
|
||||
);
|
||||
}
|
42
src/components/navigation/newsfeed.tsx
Normal file
42
src/components/navigation/newsfeed.tsx
Normal file
@ -0,0 +1,42 @@
|
||||
import ActiveLink from '@components/activeLink';
|
||||
|
||||
import * as Collapsible from '@radix-ui/react-collapsible';
|
||||
import { TriangleUpIcon } from '@radix-ui/react-icons';
|
||||
import { useState } from 'react';
|
||||
|
||||
export default function Newsfeed() {
|
||||
const [open, setOpen] = useState(true);
|
||||
|
||||
return (
|
||||
<Collapsible.Root open={open} onOpenChange={setOpen}>
|
||||
<div className="flex flex-col px-2">
|
||||
<Collapsible.Trigger className="flex cursor-pointer items-center gap-1 px-1 py-1">
|
||||
<div
|
||||
className={`inline-flex h-5 w-5 transform items-center justify-center transition-transform duration-150 ease-in-out ${
|
||||
open ? 'rotate-180' : ''
|
||||
}`}
|
||||
>
|
||||
<TriangleUpIcon className="h-4 w-4 text-zinc-700" />
|
||||
</div>
|
||||
<h3 className="text-[11px] font-bold uppercase tracking-widest text-zinc-600">Newsfeed</h3>
|
||||
</Collapsible.Trigger>
|
||||
<Collapsible.Content className="flex flex-col text-zinc-400">
|
||||
<ActiveLink
|
||||
href={`/newsfeed/following`}
|
||||
activeClassName="dark:bg-zinc-900 dark:text-zinc-100 hover:dark:bg-zinc-800"
|
||||
className="flex h-8 items-center gap-2.5 rounded-md px-2.5 text-sm font-medium hover:text-zinc-200"
|
||||
>
|
||||
<span>Following</span>
|
||||
</ActiveLink>
|
||||
<ActiveLink
|
||||
href={`/newsfeed/circle`}
|
||||
activeClassName="dark:bg-zinc-900 dark:text-zinc-100 hover:dark:bg-zinc-800"
|
||||
className="flex h-8 items-center gap-2.5 rounded-md px-2.5 text-sm font-medium hover:text-zinc-200"
|
||||
>
|
||||
<span>Circle</span>
|
||||
</ActiveLink>
|
||||
</Collapsible.Content>
|
||||
</div>
|
||||
</Collapsible.Root>
|
||||
);
|
||||
}
|
@ -63,13 +63,18 @@ export const NoteBase = memo(function NoteBase({ event }: { event: any }) {
|
||||
|
||||
const getParent = useMemo(() => {
|
||||
if (event.parent_id) {
|
||||
if (event.parent_id !== event.id && !event.content.includes('#[0]')) {
|
||||
if (event.parent_id !== event.eventId && !event.content.includes('#[0]')) {
|
||||
return <NoteParent id={event.parent_id} />;
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}, [event.content, event.id, event.parent_id]);
|
||||
}, [event.content, event.eventId, event.parent_id]);
|
||||
|
||||
const openUserPage = (e) => {
|
||||
e.stopPropagation();
|
||||
router.push(`/users/${event.pubkey}`);
|
||||
};
|
||||
|
||||
const openThread = (e) => {
|
||||
const selection = window.getSelection();
|
||||
@ -87,7 +92,9 @@ export const NoteBase = memo(function NoteBase({ event }: { event: any }) {
|
||||
>
|
||||
<>{getParent}</>
|
||||
<div className="relative z-10 flex flex-col">
|
||||
<UserExtend pubkey={event.pubkey} time={event.created_at} />
|
||||
<div onClick={(e) => openUserPage(e)}>
|
||||
<UserExtend pubkey={event.pubkey} time={event.createdAt || event.created_at} />
|
||||
</div>
|
||||
<div className="-mt-5 pl-[52px]">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="prose prose-zinc max-w-none break-words text-[15px] leading-tight dark:prose-invert prose-p:m-0 prose-p:text-[15px] prose-p:leading-tight prose-a:font-normal prose-a:text-fuchsia-500 prose-a:no-underline prose-img:m-0 prose-video:m-0">
|
||||
@ -97,10 +104,10 @@ export const NoteBase = memo(function NoteBase({ event }: { event: any }) {
|
||||
</div>
|
||||
<div onClick={(e) => e.stopPropagation()} className="mt-5 pl-[52px]">
|
||||
<NoteMetadata
|
||||
eventID={event.id}
|
||||
eventID={event.eventId}
|
||||
eventPubkey={event.pubkey}
|
||||
eventContent={event.content}
|
||||
eventTime={event.created_at}
|
||||
eventTime={event.createdAt || event.created_at}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -60,7 +60,7 @@ export const NoteComment = memo(function NoteComment({ event }: { event: any })
|
||||
return (
|
||||
<div className="relative z-10 flex h-min min-h-min w-full select-text flex-col border-b border-zinc-800 px-3 py-5 hover:bg-black/20">
|
||||
<div className="relative z-10 flex flex-col">
|
||||
<UserExtend pubkey={event.pubkey} time={event.created_at} />
|
||||
<UserExtend pubkey={event.pubkey} time={event.createdAt || event.created_at} />
|
||||
<div className="-mt-5 pl-[52px]">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="prose prose-zinc max-w-none break-words text-[15px] leading-tight dark:prose-invert prose-p:m-0 prose-p:text-[15px] prose-p:leading-tight prose-a:font-normal prose-a:text-fuchsia-500 prose-a:no-underline prose-img:m-0 prose-video:m-0">
|
||||
@ -70,10 +70,10 @@ export const NoteComment = memo(function NoteComment({ event }: { event: any })
|
||||
</div>
|
||||
<div onClick={(e) => e.stopPropagation()} className="mt-5 pl-[52px]">
|
||||
<NoteMetadata
|
||||
eventID={event.id}
|
||||
eventID={event.eventId}
|
||||
eventPubkey={event.pubkey}
|
||||
eventContent={event.content}
|
||||
eventTime={event.created_at}
|
||||
eventTime={event.createdAt || event.created_at}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,69 +1,90 @@
|
||||
import { RelayContext } from '@components/relaysProvider';
|
||||
|
||||
import { activeAccountAtom } from '@stores/account';
|
||||
import { lastLoginAtom } from '@stores/account';
|
||||
import { hasNewerNoteAtom } from '@stores/note';
|
||||
|
||||
import { dateToUnix } from '@utils/getDate';
|
||||
import { createCacheNote, getAllFollowsByID, updateLastLoginTime } from '@utils/storage';
|
||||
import { pubkeyArray } from '@utils/transform';
|
||||
import { getParentID, pubkeyArray } from '@utils/transform';
|
||||
|
||||
import { TauriEvent } from '@tauri-apps/api/event';
|
||||
import { appWindow, getCurrent } from '@tauri-apps/api/window';
|
||||
import { useAtomValue, useSetAtom } from 'jotai';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { useCallback, useContext, useEffect, useRef, useState } from 'react';
|
||||
|
||||
export default function NoteConnector() {
|
||||
const [pool, relays]: any = useContext(RelayContext);
|
||||
|
||||
const setLastLoginAtom = useSetAtom(lastLoginAtom);
|
||||
const setHasNewerNote = useSetAtom(hasNewerNoteAtom);
|
||||
const activeAccount: any = useAtomValue(activeAccountAtom);
|
||||
|
||||
const [isOnline] = useState(true);
|
||||
const now = useRef(new Date());
|
||||
|
||||
const subscribe = useCallback(() => {
|
||||
getAllFollowsByID(activeAccount.id).then((follows) => {
|
||||
pool.subscribe(
|
||||
[
|
||||
{
|
||||
kinds: [1],
|
||||
authors: pubkeyArray(follows),
|
||||
since: dateToUnix(now.current),
|
||||
},
|
||||
],
|
||||
relays,
|
||||
(event: any) => {
|
||||
// insert event to local database
|
||||
createCacheNote(event);
|
||||
setHasNewerNote(true);
|
||||
}
|
||||
);
|
||||
});
|
||||
}, [activeAccount.id, pool, relays, setHasNewerNote]);
|
||||
const now = useRef(new Date());
|
||||
const unsubscribe = useRef(null);
|
||||
|
||||
const subscribe = useCallback(async () => {
|
||||
const { createNote } = await import('@utils/bindings');
|
||||
const activeAccount = JSON.parse(localStorage.getItem('activeAccount'));
|
||||
const follows = JSON.parse(localStorage.getItem('activeAccountFollows'));
|
||||
|
||||
unsubscribe.current = pool.subscribe(
|
||||
[
|
||||
{
|
||||
kinds: [1],
|
||||
authors: pubkeyArray(follows),
|
||||
since: dateToUnix(now.current),
|
||||
},
|
||||
],
|
||||
relays,
|
||||
(event) => {
|
||||
const parentID = getParentID(event.tags, event.id);
|
||||
// insert event to local database
|
||||
createNote({
|
||||
event_id: event.id,
|
||||
pubkey: event.pubkey,
|
||||
kind: event.kind,
|
||||
tags: JSON.stringify(event.tags),
|
||||
content: event.content,
|
||||
parent_id: parentID,
|
||||
parent_comment_id: '',
|
||||
created_at: event.created_at,
|
||||
account_id: activeAccount.id,
|
||||
})
|
||||
.then(() =>
|
||||
// notify user reload to get newer note
|
||||
setHasNewerNote(true)
|
||||
)
|
||||
.catch(console.error);
|
||||
},
|
||||
10000
|
||||
);
|
||||
}, [pool, relays, setHasNewerNote]);
|
||||
|
||||
useEffect(() => {
|
||||
subscribe();
|
||||
getCurrent().listen(TauriEvent.WINDOW_CLOSE_REQUESTED, () => {
|
||||
updateLastLoginTime(now.current);
|
||||
setLastLoginAtom(now.current);
|
||||
appWindow.close();
|
||||
});
|
||||
}, [activeAccount.id, pool, relays, setHasNewerNote, subscribe]);
|
||||
|
||||
return () => {
|
||||
unsubscribe.current;
|
||||
};
|
||||
}, [setHasNewerNote, setLastLoginAtom, subscribe]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="inline-flex items-center gap-1 rounded-md px-1.5 py-1 hover:bg-zinc-900">
|
||||
<span className="relative flex h-1.5 w-1.5">
|
||||
<span
|
||||
className={`absolute inline-flex h-full w-full animate-ping rounded-full opacity-75 ${
|
||||
isOnline ? 'bg-green-400' : 'bg-red-400'
|
||||
}`}
|
||||
></span>
|
||||
<span
|
||||
className={`relative inline-flex h-1.5 w-1.5 rounded-full ${isOnline ? 'bg-green-400' : 'bg-amber-400'}`}
|
||||
></span>
|
||||
</span>
|
||||
<p className="text-xs font-medium text-zinc-500">{isOnline ? 'Online' : 'Offline'}</p>
|
||||
</div>
|
||||
</>
|
||||
<div className="inline-flex items-center gap-1 rounded-md px-1.5 py-1 hover:bg-zinc-900">
|
||||
<span className="relative flex h-1.5 w-1.5">
|
||||
<span
|
||||
className={`absolute inline-flex h-full w-full animate-ping rounded-full opacity-75 ${
|
||||
isOnline ? 'bg-green-400' : 'bg-red-400'
|
||||
}`}
|
||||
></span>
|
||||
<span
|
||||
className={`relative inline-flex h-1.5 w-1.5 rounded-full ${isOnline ? 'bg-green-400' : 'bg-amber-400'}`}
|
||||
></span>
|
||||
</span>
|
||||
<p className="text-xs font-medium text-zinc-500">{isOnline ? 'Online' : 'Offline'}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -60,7 +60,7 @@ export const NoteExtend = memo(function NoteExtend({ event }: { event: any }) {
|
||||
return (
|
||||
<div className="relative z-10 flex h-min min-h-min w-full select-text flex-col">
|
||||
<div className="relative z-10 flex flex-col">
|
||||
<UserLarge pubkey={event.pubkey} time={event.created_at} />
|
||||
<UserLarge pubkey={event.pubkey} time={event.createdAt || event.created_at} />
|
||||
<div className="mt-2">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="prose prose-zinc max-w-none break-words text-[15px] leading-tight dark:prose-invert prose-p:m-0 prose-p:text-[15px] prose-p:leading-tight prose-a:font-normal prose-a:text-fuchsia-500 prose-a:no-underline prose-img:m-0 prose-video:m-0">
|
||||
@ -70,10 +70,10 @@ export const NoteExtend = memo(function NoteExtend({ event }: { event: any }) {
|
||||
</div>
|
||||
<div className="mt-5 flex items-center border-b border-t border-zinc-800 py-2">
|
||||
<NoteMetadata
|
||||
eventID={event.id}
|
||||
eventID={event.eventId}
|
||||
eventPubkey={event.pubkey}
|
||||
eventContent={event.content}
|
||||
eventTime={event.created_at}
|
||||
eventTime={event.createdAt || event.created_at}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -3,7 +3,6 @@ import { RelayContext } from '@components/relaysProvider';
|
||||
import { UserExtend } from '@components/user/extend';
|
||||
|
||||
import { activeAccountAtom } from '@stores/account';
|
||||
import { relaysAtom } from '@stores/relays';
|
||||
|
||||
import { dateToUnix } from '@utils/getDate';
|
||||
|
||||
@ -33,11 +32,10 @@ export const NoteComment = memo(function NoteComment({
|
||||
const router = useRouter();
|
||||
const [pool, relays]: any = useContext(RelayContext);
|
||||
|
||||
const activeAccount: any = useAtomValue(activeAccountAtom);
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const [value, setValue] = useState('');
|
||||
|
||||
const activeAccount: any = useAtomValue(activeAccountAtom);
|
||||
const profile = destr(activeAccount.metadata);
|
||||
|
||||
const openThread = () => {
|
||||
@ -49,7 +47,7 @@ export const NoteComment = memo(function NoteComment({
|
||||
content: value,
|
||||
created_at: dateToUnix(),
|
||||
kind: 1,
|
||||
pubkey: activeAccount.id,
|
||||
pubkey: activeAccount.pubkey,
|
||||
tags: [['e', eventID]],
|
||||
};
|
||||
event.id = getEventHash(event);
|
||||
|
@ -38,7 +38,7 @@ export const NoteReaction = memo(function NoteReaction({
|
||||
['p', eventPubkey],
|
||||
],
|
||||
created_at: dateToUnix(),
|
||||
pubkey: activeAccount.id,
|
||||
pubkey: activeAccount.pubkey,
|
||||
};
|
||||
event.id = getEventHash(event);
|
||||
event.sig = signEvent(event, activeAccount.privkey);
|
||||
|
@ -2,8 +2,6 @@ import { NoteComment } from '@components/note/meta/comment';
|
||||
import { NoteReaction } from '@components/note/meta/reaction';
|
||||
import { RelayContext } from '@components/relaysProvider';
|
||||
|
||||
import { createCacheCommentNote } from '@utils/storage';
|
||||
|
||||
import { useContext, useEffect, useState } from 'react';
|
||||
|
||||
export default function NoteMetadata({
|
||||
@ -39,7 +37,7 @@ export default function NoteMetadata({
|
||||
// update state
|
||||
setComments((comments) => (comments += 1));
|
||||
// save comment to database
|
||||
createCacheCommentNote(event, eventID);
|
||||
// createCacheCommentNote(event, eventID);
|
||||
break;
|
||||
case 7:
|
||||
if (event.content === '🤙' || event.content === '+') {
|
||||
|
@ -6,7 +6,7 @@ import { RelayContext } from '@components/relaysProvider';
|
||||
import { UserExtend } from '@components/user/extend';
|
||||
import { UserMention } from '@components/user/mention';
|
||||
|
||||
import { createCacheNote, getNoteByID } from '@utils/storage';
|
||||
import { getParentID } from '@utils/transform';
|
||||
|
||||
import destr from 'destr';
|
||||
import { memo, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
||||
@ -18,7 +18,10 @@ export const NoteParent = memo(function NoteParent({ id }: { id: string }) {
|
||||
const [event, setEvent] = useState(null);
|
||||
const unsubscribe = useRef(null);
|
||||
|
||||
const fetchEvent = useCallback(() => {
|
||||
const fetchEvent = useCallback(async () => {
|
||||
const { createNote } = await import('@utils/bindings');
|
||||
const activeAccount = JSON.parse(localStorage.getItem('activeAccount'));
|
||||
|
||||
unsubscribe.current = pool.subscribe(
|
||||
[
|
||||
{
|
||||
@ -31,7 +34,19 @@ export const NoteParent = memo(function NoteParent({ id }: { id: string }) {
|
||||
// update state
|
||||
setEvent(event);
|
||||
// insert to database
|
||||
createCacheNote(event);
|
||||
const parentID = getParentID(event.tags, event.id);
|
||||
// insert event to local database
|
||||
createNote({
|
||||
event_id: event.id,
|
||||
pubkey: event.pubkey,
|
||||
kind: event.kind,
|
||||
tags: JSON.stringify(event.tags),
|
||||
content: event.content,
|
||||
parent_id: parentID,
|
||||
parent_comment_id: '',
|
||||
created_at: event.created_at,
|
||||
account_id: activeAccount.id,
|
||||
}).catch(console.error);
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
@ -41,19 +56,26 @@ export const NoteParent = memo(function NoteParent({ id }: { id: string }) {
|
||||
);
|
||||
}, [id, pool, relays]);
|
||||
|
||||
const checkNoteExist = useCallback(async () => {
|
||||
const { getNoteById } = await import('@utils/bindings');
|
||||
getNoteById({ event_id: id })
|
||||
.then((res) => {
|
||||
if (res) {
|
||||
setEvent(res);
|
||||
} else {
|
||||
fetchEvent();
|
||||
}
|
||||
})
|
||||
.catch(console.error);
|
||||
}, [fetchEvent, id]);
|
||||
|
||||
useEffect(() => {
|
||||
getNoteByID(id).then((res) => {
|
||||
if (res) {
|
||||
setEvent(res);
|
||||
} else {
|
||||
fetchEvent();
|
||||
}
|
||||
});
|
||||
checkNoteExist();
|
||||
|
||||
return () => {
|
||||
unsubscribe.current;
|
||||
};
|
||||
}, [fetchEvent, id]);
|
||||
}, [checkNoteExist]);
|
||||
|
||||
const content = useMemo(() => {
|
||||
let parsedContent = event ? event.content : null;
|
||||
@ -110,7 +132,7 @@ export const NoteParent = memo(function NoteParent({ id }: { id: string }) {
|
||||
<div className="relative pb-5">
|
||||
<div className="absolute left-[21px] top-0 h-full w-0.5 bg-gradient-to-t from-zinc-800 to-zinc-600"></div>
|
||||
<div className="relative z-10 flex flex-col">
|
||||
<UserExtend pubkey={event.pubkey} time={event.created_at} />
|
||||
<UserExtend pubkey={event.pubkey} time={event.createdAt || event.created_at} />
|
||||
<div className="-mt-5 pl-[52px]">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="prose prose-zinc max-w-none break-words text-[15px] leading-tight dark:prose-invert prose-p:m-0 prose-p:text-[15px] prose-p:leading-tight prose-a:font-normal prose-a:text-fuchsia-500 prose-a:no-underline prose-img:m-0 prose-video:m-0">
|
||||
@ -120,10 +142,10 @@ export const NoteParent = memo(function NoteParent({ id }: { id: string }) {
|
||||
</div>
|
||||
<div onClick={(e) => e.stopPropagation()} className="mt-5 pl-[52px]">
|
||||
<NoteMetadata
|
||||
eventID={event.id}
|
||||
eventID={event.eventId}
|
||||
eventPubkey={event.pubkey}
|
||||
eventContent={event.content}
|
||||
eventTime={event.created_at}
|
||||
eventTime={event.createdAt || event.created_at}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -2,7 +2,7 @@ import { RelayContext } from '@components/relaysProvider';
|
||||
import { UserExtend } from '@components/user/extend';
|
||||
import { UserMention } from '@components/user/mention';
|
||||
|
||||
import { createCacheNote, getNoteByID } from '@utils/storage';
|
||||
import { getParentID } from '@utils/transform';
|
||||
|
||||
import destr from 'destr';
|
||||
import { memo, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
||||
@ -14,7 +14,10 @@ export const NoteRepost = memo(function NoteRepost({ id }: { id: string }) {
|
||||
const [event, setEvent] = useState(null);
|
||||
const unsubscribe = useRef(null);
|
||||
|
||||
const fetchEvent = useCallback(() => {
|
||||
const fetchEvent = useCallback(async () => {
|
||||
const { createNote } = await import('@utils/bindings');
|
||||
const activeAccount = JSON.parse(localStorage.getItem('activeAccount'));
|
||||
|
||||
unsubscribe.current = pool.subscribe(
|
||||
[
|
||||
{
|
||||
@ -27,7 +30,19 @@ export const NoteRepost = memo(function NoteRepost({ id }: { id: string }) {
|
||||
// update state
|
||||
setEvent(event);
|
||||
// insert to database
|
||||
createCacheNote(event);
|
||||
const parentID = getParentID(event.tags, event.id);
|
||||
// insert event to local database
|
||||
createNote({
|
||||
event_id: event.id,
|
||||
pubkey: event.pubkey,
|
||||
kind: event.kind,
|
||||
tags: JSON.stringify(event.tags),
|
||||
content: event.content,
|
||||
parent_id: parentID,
|
||||
parent_comment_id: '',
|
||||
created_at: event.created_at,
|
||||
account_id: activeAccount.id,
|
||||
}).catch(console.error);
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
@ -37,19 +52,26 @@ export const NoteRepost = memo(function NoteRepost({ id }: { id: string }) {
|
||||
);
|
||||
}, [id, pool, relays]);
|
||||
|
||||
const checkNoteExist = useCallback(async () => {
|
||||
const { getNoteById } = await import('@utils/bindings');
|
||||
getNoteById({ event_id: id })
|
||||
.then((res) => {
|
||||
if (res) {
|
||||
setEvent(res);
|
||||
} else {
|
||||
fetchEvent();
|
||||
}
|
||||
})
|
||||
.catch(console.error);
|
||||
}, [fetchEvent, id]);
|
||||
|
||||
useEffect(() => {
|
||||
getNoteByID(id).then((res) => {
|
||||
if (res) {
|
||||
setEvent(res);
|
||||
} else {
|
||||
fetchEvent();
|
||||
}
|
||||
});
|
||||
checkNoteExist();
|
||||
|
||||
return () => {
|
||||
unsubscribe.current;
|
||||
};
|
||||
}, [fetchEvent, id]);
|
||||
}, [checkNoteExist]);
|
||||
|
||||
const content = useMemo(() => {
|
||||
let parsedContent = event ? event.content : null;
|
||||
@ -89,7 +111,7 @@ export const NoteRepost = memo(function NoteRepost({ id }: { id: string }) {
|
||||
return (
|
||||
<div className="relative mb-2 mt-3 rounded-lg border border-zinc-700 bg-zinc-800 p-2 py-3">
|
||||
<div className="relative z-10 flex flex-col">
|
||||
<UserExtend pubkey={event.pubkey} time={event.created_at} />
|
||||
<UserExtend pubkey={event.pubkey} time={event.createdAt || event.created_at} />
|
||||
<div className="-mt-5 pl-[52px]">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="prose prose-zinc max-w-none break-words text-[15px] leading-tight dark:prose-invert prose-p:m-0 prose-p:text-[15px] prose-p:leading-tight prose-a:font-normal prose-a:text-fuchsia-500 prose-a:no-underline prose-img:m-0 prose-video:m-0">
|
||||
|
@ -2,32 +2,13 @@ import { ImageWithFallback } from '@components/imageWithFallback';
|
||||
|
||||
import { DEFAULT_AVATAR } from '@stores/constants';
|
||||
|
||||
import { createCacheProfile } from '@utils/storage';
|
||||
import { useMetadata } from '@utils/metadata';
|
||||
import { truncate } from '@utils/truncate';
|
||||
|
||||
import { fetch } from '@tauri-apps/api/http';
|
||||
import destr from 'destr';
|
||||
import { memo, useCallback, useEffect, useState } from 'react';
|
||||
import { memo } from 'react';
|
||||
|
||||
export const UserBase = memo(function UserBase({ pubkey }: { pubkey: string }) {
|
||||
const [profile, setProfile] = useState(null);
|
||||
|
||||
const fetchProfile = useCallback(async (id: string) => {
|
||||
const res = await fetch(`https://rbr.bio/${id}/metadata.json`, {
|
||||
method: 'GET',
|
||||
timeout: 30,
|
||||
});
|
||||
return res.data;
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchProfile(pubkey)
|
||||
.then((res: any) => {
|
||||
setProfile(destr(res.content));
|
||||
createCacheProfile(res.pubkey, res.content);
|
||||
})
|
||||
.catch(console.error);
|
||||
}, [fetchProfile, pubkey]);
|
||||
const profile = useMetadata(pubkey);
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
|
@ -2,68 +2,32 @@ import { ImageWithFallback } from '@components/imageWithFallback';
|
||||
|
||||
import { DEFAULT_AVATAR } from '@stores/constants';
|
||||
|
||||
import { createCacheProfile, getCacheProfile } from '@utils/storage';
|
||||
import { useMetadata } from '@utils/metadata';
|
||||
import { truncate } from '@utils/truncate';
|
||||
|
||||
import { DotsHorizontalIcon } from '@radix-ui/react-icons';
|
||||
import { fetch } from '@tauri-apps/api/http';
|
||||
import dayjs from 'dayjs';
|
||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
import destr from 'destr';
|
||||
import { useRouter } from 'next/router';
|
||||
import { memo, useCallback, useEffect, useState } from 'react';
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
export const UserExtend = memo(function UserExtend({ pubkey, time }: { pubkey: string; time: any }) {
|
||||
const router = useRouter();
|
||||
const [profile, setProfile] = useState(null);
|
||||
|
||||
const openUserPage = (e) => {
|
||||
e.stopPropagation();
|
||||
router.push(`/users/${pubkey}`);
|
||||
};
|
||||
|
||||
const fetchProfile = useCallback(async (id: string) => {
|
||||
const res = await fetch(`https://rbr.bio/${id}/metadata.json`, {
|
||||
method: 'GET',
|
||||
timeout: 30,
|
||||
});
|
||||
return res.data;
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
getCacheProfile(pubkey).then((res) => {
|
||||
if (res) {
|
||||
setProfile(destr(res.metadata));
|
||||
} else {
|
||||
fetchProfile(pubkey)
|
||||
.then((res: any) => {
|
||||
setProfile(destr(res.content));
|
||||
createCacheProfile(pubkey, res.content);
|
||||
})
|
||||
.catch(console.error);
|
||||
}
|
||||
});
|
||||
}, [fetchProfile, pubkey]);
|
||||
export const UserExtend = ({ pubkey, time }: { pubkey: string; time: number }) => {
|
||||
const profile = useMetadata(pubkey);
|
||||
|
||||
return (
|
||||
<div className="group flex items-start gap-2">
|
||||
<div
|
||||
onClick={(e) => openUserPage(e)}
|
||||
className="relative h-11 w-11 shrink overflow-hidden rounded-md bg-zinc-900 ring-fuchsia-500 ring-offset-1 ring-offset-zinc-900 group-hover:ring-1"
|
||||
>
|
||||
<div className="relative h-11 w-11 shrink overflow-hidden rounded-md bg-white">
|
||||
<ImageWithFallback
|
||||
src={profile?.picture || DEFAULT_AVATAR}
|
||||
alt={pubkey}
|
||||
fill={true}
|
||||
className="rounded-md border border-white/10 object-cover"
|
||||
className="rounded-md object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-full flex-1 items-start justify-between">
|
||||
<div className="flex w-full justify-between">
|
||||
<div className="flex items-baseline gap-2 text-sm">
|
||||
<span onClick={(e) => openUserPage(e)} className="font-bold leading-tight group-hover:underline">
|
||||
<span className="font-bold leading-tight group-hover:underline">
|
||||
{profile?.display_name || profile?.name || truncate(pubkey, 16, ' .... ')}
|
||||
</span>
|
||||
<span className="leading-tight text-zinc-500">·</span>
|
||||
@ -78,4 +42,4 @@ export const UserExtend = memo(function UserExtend({ pubkey, time }: { pubkey: s
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
@ -2,32 +2,11 @@ import { ImageWithFallback } from '@components/imageWithFallback';
|
||||
|
||||
import { DEFAULT_AVATAR } from '@stores/constants';
|
||||
|
||||
import { createCacheProfile } from '@utils/storage';
|
||||
import { useMetadata } from '@utils/metadata';
|
||||
import { truncate } from '@utils/truncate';
|
||||
|
||||
import { fetch } from '@tauri-apps/api/http';
|
||||
import destr from 'destr';
|
||||
import { memo, useCallback, useEffect, useState } from 'react';
|
||||
|
||||
export const UserFollow = memo(function UserFollow({ pubkey }: { pubkey: string }) {
|
||||
const [profile, setProfile] = useState(null);
|
||||
|
||||
const fetchProfile = useCallback(async (id: string) => {
|
||||
const res = await fetch(`https://rbr.bio/${id}/metadata.json`, {
|
||||
method: 'GET',
|
||||
timeout: 30,
|
||||
});
|
||||
return res.data;
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchProfile(pubkey)
|
||||
.then((res: any) => {
|
||||
setProfile(destr(res.content));
|
||||
createCacheProfile(res.pubkey, res.content);
|
||||
})
|
||||
.catch(console.error);
|
||||
}, [fetchProfile, pubkey]);
|
||||
export const UserFollow = ({ pubkey }: { pubkey: string }) => {
|
||||
const profile = useMetadata(pubkey);
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
@ -47,4 +26,4 @@ export const UserFollow = memo(function UserFollow({ pubkey }: { pubkey: string
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
@ -2,47 +2,21 @@ import { ImageWithFallback } from '@components/imageWithFallback';
|
||||
|
||||
import { DEFAULT_AVATAR } from '@stores/constants';
|
||||
|
||||
import { createCacheProfile, getCacheProfile } from '@utils/storage';
|
||||
import { useMetadata } from '@utils/metadata';
|
||||
import { truncate } from '@utils/truncate';
|
||||
|
||||
import { DotsHorizontalIcon } from '@radix-ui/react-icons';
|
||||
import { fetch } from '@tauri-apps/api/http';
|
||||
import dayjs from 'dayjs';
|
||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
import destr from 'destr';
|
||||
import { memo, useCallback, useEffect, useState } from 'react';
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
export const UserLarge = memo(function UserLarge({ pubkey, time }: { pubkey: string; time: any }) {
|
||||
const [profile, setProfile] = useState(null);
|
||||
|
||||
const fetchProfile = useCallback(async (id: string) => {
|
||||
const res = await fetch(`https://rbr.bio/${id}/metadata.json`, {
|
||||
method: 'GET',
|
||||
timeout: 30,
|
||||
});
|
||||
return res.data;
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
getCacheProfile(pubkey).then((res) => {
|
||||
if (res) {
|
||||
setProfile(destr(res.metadata));
|
||||
} else {
|
||||
fetchProfile(pubkey)
|
||||
.then((res: any) => {
|
||||
setProfile(destr(res.content));
|
||||
createCacheProfile(pubkey, res.content);
|
||||
})
|
||||
.catch(console.error);
|
||||
}
|
||||
});
|
||||
}, [fetchProfile, pubkey]);
|
||||
export const UserLarge = ({ pubkey, time }: { pubkey: string; time: number }) => {
|
||||
const profile = useMetadata(pubkey);
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative h-11 w-11 shrink overflow-hidden rounded-md bg-zinc-900">
|
||||
<div className="relative h-11 w-11 shrink overflow-hidden rounded-md bg-white">
|
||||
<ImageWithFallback
|
||||
src={profile?.picture || DEFAULT_AVATAR}
|
||||
alt={pubkey}
|
||||
@ -69,4 +43,4 @@ export const UserLarge = memo(function UserLarge({ pubkey, time }: { pubkey: str
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
@ -1,35 +1,11 @@
|
||||
import { createCacheProfile, getCacheProfile } from '@utils/storage';
|
||||
import { useMetadata } from '@utils/metadata';
|
||||
import { truncate } from '@utils/truncate';
|
||||
|
||||
import { fetch } from '@tauri-apps/api/http';
|
||||
import destr from 'destr';
|
||||
import { memo, useCallback, useEffect, useState } from 'react';
|
||||
|
||||
export const UserMention = memo(function UserMention({ pubkey }: { pubkey: string }) {
|
||||
const [profile, setProfile] = useState(null);
|
||||
|
||||
const fetchProfile = useCallback(async (id: string) => {
|
||||
const res = await fetch(`https://rbr.bio/${id}/metadata.json`, {
|
||||
method: 'GET',
|
||||
timeout: 30,
|
||||
});
|
||||
return res.data;
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
getCacheProfile(pubkey).then((res) => {
|
||||
if (res) {
|
||||
setProfile(destr(res.metadata));
|
||||
} else {
|
||||
fetchProfile(pubkey)
|
||||
.then((res: any) => {
|
||||
setProfile(destr(res.content));
|
||||
createCacheProfile(pubkey, res.content);
|
||||
})
|
||||
.catch(console.error);
|
||||
}
|
||||
});
|
||||
}, [fetchProfile, pubkey]);
|
||||
|
||||
return <span className="cursor-pointer text-fuchsia-500">@{profile?.name || truncate(pubkey, 16, ' .... ')}</span>;
|
||||
});
|
||||
export const UserMention = ({ pubkey }: { pubkey: string }) => {
|
||||
const profile = useMetadata(pubkey);
|
||||
return (
|
||||
<span className="cursor-pointer text-fuchsia-500">
|
||||
@{profile?.name || profile?.username || truncate(pubkey, 16, ' .... ')}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
@ -1,44 +0,0 @@
|
||||
import { ImageWithFallback } from '@components/imageWithFallback';
|
||||
|
||||
import { DEFAULT_AVATAR } from '@stores/constants';
|
||||
|
||||
import { getCacheProfile } from '@utils/storage';
|
||||
import { truncate } from '@utils/truncate';
|
||||
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
export const UserMini = ({ pubkey }: { pubkey: string }) => {
|
||||
const [profile, setProfile] = useState(null);
|
||||
|
||||
const fetchCacheProfile = useCallback(async (id: string) => {
|
||||
const res = await getCacheProfile(id);
|
||||
const data = JSON.parse(res.metadata);
|
||||
setProfile(data);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchCacheProfile(pubkey).catch(console.error);
|
||||
}, [fetchCacheProfile, pubkey]);
|
||||
|
||||
if (profile) {
|
||||
return (
|
||||
<div className="flex cursor-pointer items-center gap-2.5 rounded-md px-2.5 py-1.5 text-sm font-medium hover:bg-zinc-900">
|
||||
<div className="relative h-5 w-5 shrink-0 overflow-hidden rounded">
|
||||
<ImageWithFallback
|
||||
src={profile?.picture || DEFAULT_AVATAR}
|
||||
alt={pubkey}
|
||||
fill={true}
|
||||
className="rounded object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div className="inline-flex w-full flex-1 flex-col overflow-hidden">
|
||||
<p className="truncate leading-tight text-zinc-300">
|
||||
{profile?.display_name || profile?.name || truncate(pubkey, 16, ' .... ')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return <></>;
|
||||
}
|
||||
};
|
@ -1,6 +1,6 @@
|
||||
import AppHeader from '@components/appHeader';
|
||||
import AccountColumn from '@components/columns/account';
|
||||
import NavigatorColumn from '@components/columns/navigator';
|
||||
import MultiAccounts from '@components/multiAccounts';
|
||||
import Navigation from '@components/navigation';
|
||||
|
||||
export default function WithSidebarLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
@ -13,11 +13,11 @@ export default function WithSidebarLayout({ children }: { children: React.ReactN
|
||||
</div>
|
||||
<div className="relative flex min-h-0 w-full flex-1">
|
||||
<div className="relative w-[68px] shrink-0 border-r border-zinc-900">
|
||||
<AccountColumn />
|
||||
<MultiAccounts />
|
||||
</div>
|
||||
<div className="grid w-full grid-cols-4 xl:grid-cols-5">
|
||||
<div className="scrollbar-hide col-span-1 overflow-y-auto overflow-x-hidden border-r border-zinc-900">
|
||||
<NavigatorColumn />
|
||||
<Navigation />
|
||||
</div>
|
||||
<div className="col-span-3 m-3 overflow-hidden rounded-lg border border-zinc-800 bg-zinc-900 shadow-input shadow-black/20 xl:col-span-2 xl:mr-1.5">
|
||||
<div className="h-full w-full rounded-lg">{children}</div>
|
||||
|
@ -2,6 +2,7 @@ import RelayProvider from '@components/relaysProvider';
|
||||
|
||||
import type { NextPage } from 'next';
|
||||
import type { AppProps } from 'next/app';
|
||||
import { useRouter } from 'next/router';
|
||||
import { ReactElement, ReactNode } from 'react';
|
||||
|
||||
import '../App.css';
|
||||
@ -16,8 +17,9 @@ type AppPropsWithLayout = AppProps & {
|
||||
};
|
||||
|
||||
export default function MyApp({ Component, pageProps }: AppPropsWithLayout) {
|
||||
const router = useRouter();
|
||||
// Use the layout defined at the page level, if available
|
||||
const getLayout = Component.getLayout ?? ((page) => page);
|
||||
|
||||
return <RelayProvider>{getLayout(<Component {...pageProps} />)}</RelayProvider>;
|
||||
return <RelayProvider>{getLayout(<Component key={router.asPath} {...pageProps} />)}</RelayProvider>;
|
||||
}
|
||||
|
80
src/pages/chats/[pubkey].tsx
Normal file
80
src/pages/chats/[pubkey].tsx
Normal file
@ -0,0 +1,80 @@
|
||||
import BaseLayout from '@layouts/base';
|
||||
import WithSidebarLayout from '@layouts/withSidebar';
|
||||
|
||||
import { MessageList } from '@components/chats/messageList';
|
||||
import FormChat from '@components/form/chat';
|
||||
import { RelayContext } from '@components/relaysProvider';
|
||||
|
||||
import { activeAccountAtom } from '@stores/account';
|
||||
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { useRouter } from 'next/router';
|
||||
import {
|
||||
JSXElementConstructor,
|
||||
ReactElement,
|
||||
ReactFragment,
|
||||
ReactPortal,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
export default function Page() {
|
||||
const [pool, relays]: any = useContext(RelayContext);
|
||||
|
||||
const router = useRouter();
|
||||
const pubkey: any = router.query.pubkey || null;
|
||||
|
||||
const activeAccount: any = useAtomValue(activeAccountAtom);
|
||||
const [messages, setMessages] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = pool.subscribe(
|
||||
[
|
||||
{
|
||||
kinds: [4],
|
||||
authors: [pubkey],
|
||||
'#p': [activeAccount.pubkey],
|
||||
},
|
||||
{
|
||||
kinds: [4],
|
||||
authors: [activeAccount.pubkey],
|
||||
'#p': [pubkey],
|
||||
},
|
||||
],
|
||||
relays,
|
||||
(event: any) => {
|
||||
setMessages((messages) => [event, ...messages]);
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
unsubscribe;
|
||||
};
|
||||
}, [pool, relays, pubkey, activeAccount.pubkey]);
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col justify-between">
|
||||
<MessageList data={messages.sort((a, b) => a.created_at - b.created_at)} />
|
||||
<div className="shrink-0 p-3">
|
||||
<FormChat receiverPubkey={pubkey} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Page.getLayout = function getLayout(
|
||||
page:
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| ReactElement<unknown, string | JSXElementConstructor<unknown>>
|
||||
| ReactFragment
|
||||
| ReactPortal
|
||||
) {
|
||||
return (
|
||||
<BaseLayout>
|
||||
<WithSidebarLayout>{page}</WithSidebarLayout>
|
||||
</BaseLayout>
|
||||
);
|
||||
};
|
@ -1,25 +1,38 @@
|
||||
import BaseLayout from '@layouts/base';
|
||||
|
||||
import { activeAccountAtom } from '@stores/account';
|
||||
|
||||
import { getActiveAccount } from '@utils/storage';
|
||||
import { activeAccountAtom, activeAccountFollowsAtom } from '@stores/account';
|
||||
|
||||
import LumeSymbol from '@assets/icons/Lume';
|
||||
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { useRouter } from 'next/router';
|
||||
import { JSXElementConstructor, ReactElement, ReactFragment, ReactPortal, useEffect } from 'react';
|
||||
import { JSXElementConstructor, ReactElement, ReactFragment, ReactPortal, useCallback, useEffect } from 'react';
|
||||
|
||||
export default function Page() {
|
||||
const router = useRouter();
|
||||
const setActiveAccount = useSetAtom(activeAccountAtom);
|
||||
const setActiveAccountFollows = useSetAtom(activeAccountFollowsAtom);
|
||||
|
||||
const fetchActiveAccount = useCallback(async () => {
|
||||
const { getAccounts } = await import('@utils/bindings');
|
||||
return await getAccounts();
|
||||
}, []);
|
||||
|
||||
const fetchFollowsByAccount = useCallback(async (id) => {
|
||||
const { getPlebs } = await import('@utils/bindings');
|
||||
return await getPlebs({ account_id: id });
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
getActiveAccount()
|
||||
fetchActiveAccount()
|
||||
.then((res: any) => {
|
||||
if (res) {
|
||||
if (res.length > 0) {
|
||||
// fetch follows
|
||||
fetchFollowsByAccount(res[0].id).then((follows) => {
|
||||
setActiveAccountFollows(follows);
|
||||
});
|
||||
// update local storage
|
||||
setActiveAccount(res);
|
||||
setActiveAccount(res[0]);
|
||||
// redirect
|
||||
router.replace('/init');
|
||||
} else {
|
||||
@ -27,7 +40,7 @@ export default function Page() {
|
||||
}
|
||||
})
|
||||
.catch(console.error);
|
||||
}, [router, setActiveAccount]);
|
||||
}, [fetchActiveAccount, setActiveAccount, fetchFollowsByAccount, setActiveAccountFollows, router]);
|
||||
|
||||
return (
|
||||
<div className="relative h-full overflow-hidden">
|
||||
|
@ -2,16 +2,12 @@ import BaseLayout from '@layouts/base';
|
||||
|
||||
import { RelayContext } from '@components/relaysProvider';
|
||||
|
||||
import { activeAccountAtom } from '@stores/account';
|
||||
import { relaysAtom } from '@stores/relays';
|
||||
|
||||
import { dateToUnix, hoursAgo } from '@utils/getDate';
|
||||
import { countTotalNotes, createCacheNote, getAllFollowsByID, getLastLoginTime } from '@utils/storage';
|
||||
import { pubkeyArray } from '@utils/transform';
|
||||
import { getParentID, pubkeyArray } from '@utils/transform';
|
||||
|
||||
import LumeSymbol from '@assets/icons/Lume';
|
||||
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { invoke } from '@tauri-apps/api/tauri';
|
||||
import { useRouter } from 'next/router';
|
||||
import {
|
||||
JSXElementConstructor,
|
||||
@ -29,65 +25,74 @@ export default function Page() {
|
||||
const router = useRouter();
|
||||
const [pool, relays]: any = useContext(RelayContext);
|
||||
|
||||
const activeAccount: any = useAtomValue(activeAccountAtom);
|
||||
const [done, setDone] = useState(false);
|
||||
|
||||
const now = useRef(new Date());
|
||||
const unsubscribe = useRef(null);
|
||||
const timer = useRef(null);
|
||||
|
||||
const [eose, setEose] = useState(false);
|
||||
|
||||
const fetchData = useCallback(
|
||||
(since) => {
|
||||
getAllFollowsByID(activeAccount.id).then((follows) => {
|
||||
unsubscribe.current = pool.subscribe(
|
||||
[
|
||||
{
|
||||
kinds: [1],
|
||||
authors: pubkeyArray(follows),
|
||||
since: dateToUnix(since),
|
||||
until: dateToUnix(now.current),
|
||||
},
|
||||
],
|
||||
relays,
|
||||
(event) => {
|
||||
// insert event to local database
|
||||
createCacheNote(event);
|
||||
},
|
||||
undefined,
|
||||
() => {
|
||||
// wait for 8 seconds
|
||||
timer.current = setTimeout(() => setDone(true), 8000);
|
||||
},
|
||||
async (since: Date) => {
|
||||
const { createNote } = await import('@utils/bindings');
|
||||
const activeAccount = JSON.parse(localStorage.getItem('activeAccount'));
|
||||
const follows = JSON.parse(localStorage.getItem('activeAccountFollows'));
|
||||
|
||||
unsubscribe.current = pool.subscribe(
|
||||
[
|
||||
{
|
||||
unsubscribeOnEose: true,
|
||||
}
|
||||
);
|
||||
});
|
||||
kinds: [1],
|
||||
authors: pubkeyArray(follows),
|
||||
since: dateToUnix(since),
|
||||
until: dateToUnix(now.current),
|
||||
},
|
||||
],
|
||||
relays,
|
||||
(event) => {
|
||||
const parentID = getParentID(event.tags, event.id);
|
||||
// insert event to local database
|
||||
createNote({
|
||||
event_id: event.id,
|
||||
pubkey: event.pubkey,
|
||||
kind: event.kind,
|
||||
tags: JSON.stringify(event.tags),
|
||||
content: event.content,
|
||||
parent_id: parentID,
|
||||
parent_comment_id: '',
|
||||
created_at: event.created_at,
|
||||
account_id: activeAccount.id,
|
||||
}).catch(console.error);
|
||||
},
|
||||
undefined,
|
||||
() => {
|
||||
setEose(true);
|
||||
}
|
||||
);
|
||||
},
|
||||
[activeAccount.id, pool, relays]
|
||||
[pool, relays]
|
||||
);
|
||||
|
||||
const isNoteExist = useCallback(async () => {
|
||||
invoke('count_total_notes').then((res: number) => {
|
||||
if (res > 0) {
|
||||
const lastLogin = JSON.parse(localStorage.getItem('lastLogin'));
|
||||
const parseDate = new Date(lastLogin);
|
||||
fetchData(parseDate);
|
||||
} else {
|
||||
fetchData(hoursAgo(24, now.current));
|
||||
}
|
||||
});
|
||||
}, [fetchData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!done) {
|
||||
countTotalNotes().then((count) => {
|
||||
if (count.total === 0) {
|
||||
fetchData(hoursAgo(24, now.current));
|
||||
} else {
|
||||
getLastLoginTime().then((time) => {
|
||||
const parseDate = new Date(time.setting_value);
|
||||
fetchData(parseDate);
|
||||
});
|
||||
}
|
||||
});
|
||||
if (eose === false) {
|
||||
isNoteExist();
|
||||
} else {
|
||||
router.replace('/newsfeed/following');
|
||||
}
|
||||
|
||||
return () => {
|
||||
unsubscribe.current;
|
||||
clearTimeout(timer.current);
|
||||
};
|
||||
}, [activeAccount.id, done, pool, relays, router, fetchData]);
|
||||
}, [router, eose, isNoteExist]);
|
||||
|
||||
return (
|
||||
<div className="relative h-full overflow-hidden">
|
||||
|
@ -6,8 +6,6 @@ import { NoteComment } from '@components/note/comment';
|
||||
import { NoteExtend } from '@components/note/extend';
|
||||
import { RelayContext } from '@components/relaysProvider';
|
||||
|
||||
import { getAllCommentNotes, getNoteByID } from '@utils/storage';
|
||||
|
||||
import { useRouter } from 'next/router';
|
||||
import {
|
||||
JSXElementConstructor,
|
||||
@ -29,12 +27,12 @@ export default function Page() {
|
||||
const [comments, setComments] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
getNoteByID(id)
|
||||
/*getNoteByID(id)
|
||||
.then((res) => {
|
||||
setRootEvent(res);
|
||||
getAllCommentNotes(id).then((res: any) => setComments(res));
|
||||
})
|
||||
.catch(console.error);
|
||||
.catch(console.error);*/
|
||||
}, [id, pool, relays]);
|
||||
|
||||
return (
|
||||
|
@ -8,7 +8,7 @@ import { Placeholder } from '@components/note/placeholder';
|
||||
import { hasNewerNoteAtom } from '@stores/note';
|
||||
|
||||
import { dateToUnix } from '@utils/getDate';
|
||||
import { getLatestNotes, getNotes } from '@utils/storage';
|
||||
import { filterDuplicateParentID } from '@utils/transform';
|
||||
|
||||
import { ArrowUpIcon } from '@radix-ui/react-icons';
|
||||
import { useAtom } from 'jotai';
|
||||
@ -42,29 +42,43 @@ export default function Page() {
|
||||
|
||||
const computeItemKey = useCallback(
|
||||
(index: string | number) => {
|
||||
return data[index].id;
|
||||
return data[index].eventId;
|
||||
},
|
||||
[data]
|
||||
);
|
||||
|
||||
const initialData = useCallback(async () => {
|
||||
const result: any = await getNotes(dateToUnix(now.current), limit.current, offset.current);
|
||||
const { getNotes } = await import('@utils/bindings');
|
||||
const result: any = await getNotes({
|
||||
date: dateToUnix(now.current),
|
||||
limit: limit.current,
|
||||
offset: offset.current,
|
||||
});
|
||||
setData((data) => [...data, ...result]);
|
||||
}, []);
|
||||
|
||||
const loadMore = useCallback(async () => {
|
||||
const { getNotes } = await import('@utils/bindings');
|
||||
offset.current += limit.current;
|
||||
// next query
|
||||
const result: any = await getNotes(dateToUnix(now.current), limit.current, offset.current);
|
||||
const result: any = await getNotes({
|
||||
date: dateToUnix(now.current),
|
||||
limit: limit.current,
|
||||
offset: offset.current,
|
||||
});
|
||||
setData((data) => [...data, ...result]);
|
||||
}, []);
|
||||
|
||||
const loadLatest = useCallback(async () => {
|
||||
offset.current += limit.current;
|
||||
const { getLatestNotes } = await import('@utils/bindings');
|
||||
// next query
|
||||
const result: any = await getLatestNotes(dateToUnix(now.current));
|
||||
const result: any = await getLatestNotes({ date: dateToUnix(now.current) });
|
||||
// update data
|
||||
setData((data) => [...result, ...data]);
|
||||
if (result.length > 0) {
|
||||
setData((data) => [...data, ...result]);
|
||||
} else {
|
||||
setData((data) => [...data, result]);
|
||||
}
|
||||
// hide newer trigger
|
||||
setHasNewerNote(false);
|
||||
// scroll to top
|
||||
@ -90,7 +104,7 @@ export default function Page() {
|
||||
)}
|
||||
<Virtuoso
|
||||
ref={virtuosoRef}
|
||||
data={data}
|
||||
data={filterDuplicateParentID(data)}
|
||||
itemContent={itemContent}
|
||||
computeItemKey={computeItemKey}
|
||||
components={COMPONENTS}
|
||||
|
@ -2,13 +2,20 @@ import BaseLayout from '@layouts/base';
|
||||
|
||||
import { RelayContext } from '@components/relaysProvider';
|
||||
|
||||
import { createAccount } from '@utils/storage';
|
||||
|
||||
import { ArrowLeftIcon, EyeClosedIcon, EyeOpenIcon } from '@radix-ui/react-icons';
|
||||
import Image from 'next/image';
|
||||
import { useRouter } from 'next/router';
|
||||
import { generatePrivateKey, getEventHash, getPublicKey, nip19, signEvent } from 'nostr-tools';
|
||||
import { JSXElementConstructor, ReactElement, ReactFragment, ReactPortal, useContext, useMemo, useState } from 'react';
|
||||
import {
|
||||
JSXElementConstructor,
|
||||
ReactElement,
|
||||
ReactFragment,
|
||||
ReactPortal,
|
||||
useCallback,
|
||||
useContext,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { Config, names, uniqueNamesGenerator } from 'unique-names-generator';
|
||||
|
||||
const config: Config = {
|
||||
@ -34,28 +41,16 @@ export default function Page() {
|
||||
};
|
||||
|
||||
// auto-generated profile metadata
|
||||
const metadata = useMemo(
|
||||
const metadata: any = useMemo(
|
||||
() => ({
|
||||
display_name: name,
|
||||
name: name,
|
||||
username: name.toLowerCase(),
|
||||
picture: 'https://void.cat/d/KmypFh2fBdYCEvyJrPiN89',
|
||||
picture: 'https://void.cat/d/KmypFh2fBdYCEvyJrPiN89.webp',
|
||||
}),
|
||||
[name]
|
||||
);
|
||||
|
||||
// build profile
|
||||
const data = useMemo(
|
||||
() => ({
|
||||
pubkey: pubKey,
|
||||
privkey: privKey,
|
||||
npub: npub,
|
||||
nsec: nsec,
|
||||
metadata: metadata,
|
||||
}),
|
||||
[metadata, npub, nsec, privKey, pubKey]
|
||||
);
|
||||
|
||||
// toggle privatek key
|
||||
const showPrivateKey = () => {
|
||||
if (type === 'password') {
|
||||
@ -66,7 +61,8 @@ export default function Page() {
|
||||
};
|
||||
|
||||
// create account and broadcast to all relays
|
||||
const submit = () => {
|
||||
const submit = useCallback(async () => {
|
||||
const { createAccount } = await import('@utils/bindings');
|
||||
setLoading(true);
|
||||
|
||||
// build event
|
||||
@ -81,16 +77,16 @@ export default function Page() {
|
||||
event.sig = signEvent(event, privKey);
|
||||
|
||||
// insert to database then broadcast
|
||||
createAccount(data)
|
||||
.then(() => {
|
||||
createAccount({ pubkey: pubKey, privkey: privKey, metadata: metadata })
|
||||
.then((res) => {
|
||||
pool.publish(event, relays);
|
||||
router.push({
|
||||
pathname: '/onboarding/create/step-2',
|
||||
query: { id: pubKey, privkey: privKey },
|
||||
query: { id: res.id, pubkey: res.pubkey, privkey: res.privkey },
|
||||
});
|
||||
})
|
||||
.catch(console.error);
|
||||
};
|
||||
}, [pool, pubKey, privKey, metadata, relays, router]);
|
||||
|
||||
return (
|
||||
<div className="grid h-full w-full grid-rows-5">
|
||||
|
@ -3,7 +3,8 @@ import BaseLayout from '@layouts/base';
|
||||
import { RelayContext } from '@components/relaysProvider';
|
||||
import { UserBase } from '@components/user/base';
|
||||
|
||||
import { createFollows } from '@utils/storage';
|
||||
import { fetchMetadata } from '@utils/metadata';
|
||||
import { followsTag } from '@utils/transform';
|
||||
|
||||
import { CheckCircledIcon } from '@radix-ui/react-icons';
|
||||
import { createClient } from '@supabase/supabase-js';
|
||||
@ -15,6 +16,7 @@ import {
|
||||
ReactElement,
|
||||
ReactFragment,
|
||||
ReactPortal,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
@ -64,7 +66,7 @@ export default function Page() {
|
||||
const [pool, relays]: any = useContext(RelayContext);
|
||||
|
||||
const router = useRouter();
|
||||
const { id, privkey }: any = router.query || '';
|
||||
const { id, pubkey, privkey }: any = router.query || '';
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [list, setList]: any = useState(initialList);
|
||||
@ -76,41 +78,36 @@ export default function Page() {
|
||||
setFollows(arr);
|
||||
};
|
||||
|
||||
// build event tags
|
||||
const tags = () => {
|
||||
const arr = [];
|
||||
// push item to tags
|
||||
follows.forEach((item) => {
|
||||
arr.push(['p', item]);
|
||||
});
|
||||
return arr;
|
||||
};
|
||||
|
||||
// save follows to database then broadcast
|
||||
const submit = () => {
|
||||
const submit = useCallback(async () => {
|
||||
const { createPleb } = await import('@utils/bindings');
|
||||
setLoading(true);
|
||||
|
||||
for (const follow of follows) {
|
||||
const metadata: any = await fetchMetadata(follow, pool, relays);
|
||||
createPleb({
|
||||
pleb_id: follow + '-lume' + id,
|
||||
pubkey: follow,
|
||||
kind: 0,
|
||||
metadata: metadata.content,
|
||||
account_id: parseInt(id),
|
||||
}).catch(console.error);
|
||||
}
|
||||
|
||||
// build event
|
||||
const event: any = {
|
||||
content: '',
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
kind: 3,
|
||||
pubkey: id,
|
||||
tags: tags(),
|
||||
pubkey: pubkey,
|
||||
tags: followsTag(follows),
|
||||
};
|
||||
event.id = getEventHash(event);
|
||||
event.sig = signEvent(event, privkey);
|
||||
|
||||
createFollows(follows, id, 0)
|
||||
.then((res) => {
|
||||
if (res === 'ok') {
|
||||
// publish to relays
|
||||
pool.publish(event, relays);
|
||||
router.replace('/');
|
||||
}
|
||||
})
|
||||
.catch(console.error);
|
||||
};
|
||||
pool.publish(event, relays);
|
||||
router.replace('/');
|
||||
}, [follows, id, pool, pubkey, privkey, relays, router]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
|
@ -2,21 +2,23 @@ import BaseLayout from '@layouts/base';
|
||||
|
||||
import { RelayContext } from '@components/relaysProvider';
|
||||
|
||||
import { createAccount, createFollows } from '@utils/storage';
|
||||
import { tagsToArray } from '@utils/transform';
|
||||
import { DEFAULT_AVATAR } from '@stores/constants';
|
||||
|
||||
import { fetchMetadata } from '@utils/metadata';
|
||||
import { truncate } from '@utils/truncate';
|
||||
|
||||
import destr from 'destr';
|
||||
import Image from 'next/image';
|
||||
import { useRouter } from 'next/router';
|
||||
import { getPublicKey, nip19 } from 'nostr-tools';
|
||||
import { getPublicKey } from 'nostr-tools';
|
||||
import {
|
||||
JSXElementConstructor,
|
||||
ReactElement,
|
||||
ReactFragment,
|
||||
ReactPortal,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
@ -27,9 +29,40 @@ export default function Page() {
|
||||
const privkey: any = router.query.privkey || null;
|
||||
const pubkey = privkey ? getPublicKey(privkey) : null;
|
||||
|
||||
const [profile, setProfile] = useState(null);
|
||||
const [profile, setProfile] = useState({ id: null, metadata: null });
|
||||
const [done, setDone] = useState(false);
|
||||
|
||||
const insertAccountToStorage = useCallback(async (pubkey, privkey, metadata) => {
|
||||
const { createAccount } = await import('@utils/bindings');
|
||||
createAccount({ pubkey: pubkey, privkey: privkey, metadata: metadata })
|
||||
.then((res) =>
|
||||
setProfile({
|
||||
id: res.id,
|
||||
metadata: JSON.parse(res.metadata),
|
||||
})
|
||||
)
|
||||
.catch(console.error);
|
||||
}, []);
|
||||
|
||||
const insertFollowsToStorage = useCallback(
|
||||
async (tags) => {
|
||||
const { createPleb } = await import('@utils/bindings');
|
||||
if (profile?.id !== null) {
|
||||
for (const tag of tags) {
|
||||
const metadata: any = await fetchMetadata(tag[1], pool, relays);
|
||||
createPleb({
|
||||
pleb_id: tag[1] + '-lume' + profile.id.toString(),
|
||||
pubkey: tag[1],
|
||||
kind: 0,
|
||||
metadata: metadata.content,
|
||||
account_id: profile.id,
|
||||
}).catch(console.error);
|
||||
}
|
||||
}
|
||||
},
|
||||
[pool, profile.id, relays]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = pool.subscribe(
|
||||
[
|
||||
@ -42,34 +75,23 @@ export default function Page() {
|
||||
relays,
|
||||
(event: any) => {
|
||||
if (event.kind === 0) {
|
||||
const data = {
|
||||
pubkey: pubkey,
|
||||
privkey: privkey,
|
||||
npub: nip19.npubEncode(pubkey),
|
||||
nsec: nip19.nsecEncode(privkey),
|
||||
metadata: event.content,
|
||||
};
|
||||
setProfile(destr(event.content));
|
||||
createAccount(data);
|
||||
insertAccountToStorage(pubkey, privkey, event.content);
|
||||
} else {
|
||||
if (event.tags.length > 0) {
|
||||
createFollows(tagsToArray(event.tags), pubkey, 0);
|
||||
insertFollowsToStorage(event.tags);
|
||||
}
|
||||
}
|
||||
},
|
||||
undefined,
|
||||
() => {
|
||||
setDone(true);
|
||||
},
|
||||
{
|
||||
unsubscribeOnEose: true,
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
unsubscribe;
|
||||
};
|
||||
}, [pool, privkey, pubkey, relays]);
|
||||
}, [insertAccountToStorage, insertFollowsToStorage, pool, relays, privkey, pubkey]);
|
||||
|
||||
// submit then redirect to home
|
||||
const submit = () => {
|
||||
@ -91,13 +113,20 @@ export default function Page() {
|
||||
<div className="w-full rounded-lg bg-zinc-900 p-4 shadow-input ring-1 ring-zinc-800">
|
||||
<div className="flex space-x-4">
|
||||
<div className="relative h-10 w-10 rounded-full">
|
||||
<Image className="inline-block rounded-full" src={profile?.picture} alt="" fill={true} />
|
||||
<Image
|
||||
className="inline-block rounded-full"
|
||||
src={profile.metadata?.picture || DEFAULT_AVATAR}
|
||||
alt=""
|
||||
fill={true}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 space-y-4 py-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="font-semibold">{profile?.display_name || profile?.name}</p>
|
||||
<p className="font-semibold">{profile.metadata?.display_name || profile.metadata?.name}</p>
|
||||
<span className="leading-tight text-zinc-500">·</span>
|
||||
<p className="text-zinc-500">@{profile?.username || (pubkey && truncate(pubkey, 16, ' .... '))}</p>
|
||||
<p className="text-zinc-500">
|
||||
@{profile.metadata?.username || (pubkey && truncate(pubkey, 16, ' .... '))}
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
|
@ -10,3 +10,5 @@ const createMyJsonStorage = () => {
|
||||
};
|
||||
|
||||
export const activeAccountAtom = atomWithStorage('activeAccount', {}, createMyJsonStorage());
|
||||
export const activeAccountFollowsAtom = atomWithStorage('activeAccountFollows', [], createMyJsonStorage());
|
||||
export const lastLoginAtom = atomWithStorage('lastLogin', [], createMyJsonStorage());
|
||||
|
@ -1 +1,2 @@
|
||||
export const APP_VERSION = '0.2.1';
|
||||
export const DEFAULT_AVATAR = 'https://void.cat/d/KmypFh2fBdYCEvyJrPiN89.webp';
|
||||
|
@ -1,9 +0,0 @@
|
||||
import { isSSR } from '@utils/ssr';
|
||||
import { getAllRelays } from '@utils/storage';
|
||||
|
||||
import { atomWithCache } from 'jotai-cache';
|
||||
|
||||
export const relaysAtom = atomWithCache(async () => {
|
||||
const response = isSSR ? [] : await getAllRelays();
|
||||
return response;
|
||||
});
|
78
src/utils/bindings.ts
Normal file
78
src/utils/bindings.ts
Normal file
@ -0,0 +1,78 @@
|
||||
// This file was generated by [tauri-specta](https://github.com/oscartbeaumont/tauri-specta). Do not edit this file manually.
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
__TAURI_INVOKE__<T>(cmd: string, args?: Record<string, unknown>): Promise<T>;
|
||||
}
|
||||
}
|
||||
|
||||
const invoke = window.__TAURI_INVOKE__;
|
||||
|
||||
export function getAccounts() {
|
||||
return invoke<Account[]>('get_accounts');
|
||||
}
|
||||
|
||||
export function createAccount(data: CreateAccountData) {
|
||||
return invoke<Account>('create_account', { data });
|
||||
}
|
||||
|
||||
export function getPlebs(data: GetPlebData) {
|
||||
return invoke<Pleb[]>('get_plebs', { data });
|
||||
}
|
||||
|
||||
export function getPlebByPubkey(data: GetPlebPubkeyData) {
|
||||
return invoke<Pleb | null>('get_pleb_by_pubkey', { data });
|
||||
}
|
||||
|
||||
export function createPleb(data: CreatePlebData) {
|
||||
return invoke<Pleb>('create_pleb', { data });
|
||||
}
|
||||
|
||||
export function createNote(data: CreateNoteData) {
|
||||
return invoke<Note>('create_note', { data });
|
||||
}
|
||||
|
||||
export function getNotes(data: GetNoteData) {
|
||||
return invoke<Note[]>('get_notes', { data });
|
||||
}
|
||||
|
||||
export function getLatestNotes(data: GetLatestNoteData) {
|
||||
return invoke<Note[]>('get_latest_notes', { data });
|
||||
}
|
||||
|
||||
export function getNoteById(data: GetNoteByIdData) {
|
||||
return invoke<Note | null>('get_note_by_id', { data });
|
||||
}
|
||||
|
||||
export type CreateNoteData = {
|
||||
event_id: string;
|
||||
pubkey: string;
|
||||
kind: number;
|
||||
tags: string;
|
||||
content: string;
|
||||
parent_id: string;
|
||||
parent_comment_id: string;
|
||||
created_at: number;
|
||||
account_id: number;
|
||||
};
|
||||
export type CreatePlebData = { pleb_id: string; pubkey: string; kind: number; metadata: string; account_id: number };
|
||||
export type GetNoteByIdData = { event_id: string };
|
||||
export type Pleb = { id: number; plebId: string; pubkey: string; kind: number; metadata: string; accountId: number };
|
||||
export type Note = {
|
||||
id: number;
|
||||
eventId: string;
|
||||
pubkey: string;
|
||||
kind: number;
|
||||
tags: string;
|
||||
content: string;
|
||||
parent_id: string;
|
||||
parent_comment_id: string;
|
||||
createdAt: number;
|
||||
accountId: number;
|
||||
};
|
||||
export type Account = { id: number; pubkey: string; privkey: string; active: boolean; metadata: string };
|
||||
export type GetPlebPubkeyData = { pubkey: string };
|
||||
export type GetPlebData = { account_id: number };
|
||||
export type CreateAccountData = { pubkey: string; privkey: string; metadata: string };
|
||||
export type GetLatestNoteData = { date: number };
|
||||
export type GetNoteData = { date: number; limit: number; offset: number };
|
39
src/utils/metadata.tsx
Normal file
39
src/utils/metadata.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
import { RelayContext } from '@components/relaysProvider';
|
||||
|
||||
import { Author } from 'nostr-relaypool';
|
||||
import { useCallback, useContext, useEffect, useState } from 'react';
|
||||
|
||||
export const fetchMetadata = (pubkey: string, pool: any, relays: any) => {
|
||||
const author = new Author(pool, relays, pubkey);
|
||||
return new Promise((resolve) => author.metaData(resolve, 0));
|
||||
};
|
||||
|
||||
export const useMetadata = (pubkey) => {
|
||||
const [pool, relays]: any = useContext(RelayContext);
|
||||
const [profile, setProfile] = useState(null);
|
||||
|
||||
const getCachedMetadata = useCallback(async () => {
|
||||
const { getPlebByPubkey } = await import('@utils/bindings');
|
||||
getPlebByPubkey({ pubkey: pubkey })
|
||||
.then((res) => {
|
||||
if (res) {
|
||||
const metadata = JSON.parse(res.metadata);
|
||||
setProfile(metadata);
|
||||
} else {
|
||||
fetchMetadata(pubkey, pool, relays).then((res: any) => {
|
||||
if (res.content) {
|
||||
const metadata = JSON.parse(res.content);
|
||||
setProfile(metadata);
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(console.error);
|
||||
}, [pool, relays, pubkey]);
|
||||
|
||||
useEffect(() => {
|
||||
getCachedMetadata().catch(console.error);
|
||||
}, [getCachedMetadata]);
|
||||
|
||||
return profile;
|
||||
};
|
@ -1,175 +0,0 @@
|
||||
import { getParentID } from '@utils/transform';
|
||||
|
||||
import Database from 'tauri-plugin-sql-api';
|
||||
|
||||
let db: null | Database = null;
|
||||
|
||||
// connect database (sqlite)
|
||||
// path: tauri::api::path::BaseDirectory::App
|
||||
export async function connect(): Promise<Database> {
|
||||
if (db) {
|
||||
return db;
|
||||
}
|
||||
db = await Database.load('sqlite:lume.db');
|
||||
return db;
|
||||
}
|
||||
|
||||
// get all relays
|
||||
export async function getAllRelays() {
|
||||
const db = await connect();
|
||||
const result: any = await db.select('SELECT relay_url FROM relays WHERE relay_status = "1";');
|
||||
return result.reduce((relays, { relay_url }) => {
|
||||
relays.push(relay_url);
|
||||
return relays;
|
||||
}, []);
|
||||
}
|
||||
|
||||
// get active account
|
||||
export async function getActiveAccount() {
|
||||
const db = await connect();
|
||||
const result = await db.select(`SELECT * FROM accounts LIMIT 1;`);
|
||||
return result[0];
|
||||
}
|
||||
|
||||
// get all accounts
|
||||
export async function getAccounts() {
|
||||
const db = await connect();
|
||||
return await db.select(`SELECT * FROM accounts`);
|
||||
}
|
||||
|
||||
// get all follows by account id
|
||||
export async function getAllFollowsByID(id) {
|
||||
const db = await connect();
|
||||
return await db.select(`SELECT pubkey FROM follows WHERE account = "${id}";`);
|
||||
}
|
||||
|
||||
// create account
|
||||
export async function createAccount(data) {
|
||||
const db = await connect();
|
||||
return await db.execute(
|
||||
'INSERT OR IGNORE INTO accounts (id, privkey, npub, nsec, metadata) VALUES (?, ?, ?, ?, ?);',
|
||||
[data.pubkey, data.privkey, data.npub, data.nsec, data.metadata]
|
||||
);
|
||||
}
|
||||
|
||||
// create follow
|
||||
export async function createFollow(pubkey, account, kind) {
|
||||
const db = await connect();
|
||||
return await db.execute('INSERT OR IGNORE INTO follows (pubkey, account, kind) VALUES (?, ?, ?);', [
|
||||
pubkey,
|
||||
account,
|
||||
kind || 0,
|
||||
]);
|
||||
}
|
||||
|
||||
// create follow
|
||||
export async function createFollows(data, account, kind) {
|
||||
const db = await connect();
|
||||
data.forEach(async (item) => {
|
||||
await db.execute('INSERT OR IGNORE INTO follows (pubkey, account, kind) VALUES (?, ?, ?);', [
|
||||
item,
|
||||
account,
|
||||
kind || 0,
|
||||
]);
|
||||
});
|
||||
return 'ok';
|
||||
}
|
||||
|
||||
// create cache profile
|
||||
export async function createCacheProfile(id, metadata) {
|
||||
const db = await connect();
|
||||
return await db.execute('INSERT OR IGNORE INTO cache_profiles (id, metadata) VALUES (?, ?);', [id, metadata]);
|
||||
}
|
||||
|
||||
// get cache profile
|
||||
export async function getCacheProfile(id) {
|
||||
const db = await connect();
|
||||
const result = await db.select(`SELECT metadata FROM cache_profiles WHERE id = "${id}"`);
|
||||
return result[0];
|
||||
}
|
||||
|
||||
// get all notes
|
||||
export async function getNotes(time, limit, offset) {
|
||||
const db = await connect();
|
||||
return await db.select(
|
||||
`SELECT * FROM cache_notes WHERE created_at <= "${time}" GROUP BY parent_id ORDER BY created_at DESC LIMIT "${limit}" OFFSET "${offset}"`
|
||||
);
|
||||
}
|
||||
|
||||
// get all latest notes
|
||||
export async function getLatestNotes(time) {
|
||||
const db = await connect();
|
||||
return await db.select(
|
||||
`SELECT * FROM cache_notes WHERE created_at > "${time}" GROUP BY parent_id ORDER BY created_at DESC`
|
||||
);
|
||||
}
|
||||
|
||||
// get note by id
|
||||
export async function getNoteByID(id) {
|
||||
const db = await connect();
|
||||
const result = await db.select(`SELECT * FROM cache_notes WHERE id = "${id}"`);
|
||||
return result[0];
|
||||
}
|
||||
|
||||
// create cache note
|
||||
export async function createCacheNote(data) {
|
||||
const db = await connect();
|
||||
return await db.execute(
|
||||
'INSERT OR IGNORE INTO cache_notes (id, pubkey, created_at, kind, content, tags, parent_id) VALUES (?, ?, ?, ?, ?, ?, ?);',
|
||||
[
|
||||
data.id,
|
||||
data.pubkey,
|
||||
data.created_at,
|
||||
data.kind,
|
||||
data.content,
|
||||
JSON.stringify(data.tags),
|
||||
getParentID(data.tags, data.id),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
// get all comment notes
|
||||
export async function getAllCommentNotes(eid) {
|
||||
const db = await connect();
|
||||
return await db.select(
|
||||
`SELECT * FROM cache_notes WHERE parent_comment_id = "${eid}" ORDER BY created_at DESC LIMIT 500`
|
||||
);
|
||||
}
|
||||
|
||||
// create cache comment note
|
||||
export async function createCacheCommentNote(data, eid) {
|
||||
const db = await connect();
|
||||
return await db.execute(
|
||||
'INSERT OR IGNORE INTO cache_notes (id, pubkey, created_at, kind, content, tags, parent_id, parent_comment_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?);',
|
||||
[
|
||||
data.id,
|
||||
data.pubkey,
|
||||
data.created_at,
|
||||
data.kind,
|
||||
data.content,
|
||||
JSON.stringify(data.tags),
|
||||
getParentID(data.tags, data.id),
|
||||
eid,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
// create cache comment note
|
||||
export async function countTotalNotes() {
|
||||
const db = await connect();
|
||||
const result = await db.select('SELECT COUNT(*) AS "total" FROM cache_notes;');
|
||||
return result[0];
|
||||
}
|
||||
|
||||
// get last login time
|
||||
export async function getLastLoginTime() {
|
||||
const db = await connect();
|
||||
const result = await db.select('SELECT setting_value FROM settings WHERE setting_key = "last_login"');
|
||||
return result[0];
|
||||
}
|
||||
|
||||
// update last login time
|
||||
export async function updateLastLoginTime(time) {
|
||||
const db = await connect();
|
||||
return await db.execute(`UPDATE settings SET setting_value = "${time}" WHERE setting_key = "last_login"`);
|
||||
}
|
@ -9,6 +9,15 @@ export const tagsToArray = (arr) => {
|
||||
return newarr;
|
||||
};
|
||||
|
||||
export const followsTag = (arr) => {
|
||||
const newarr = [];
|
||||
// push item to tags
|
||||
arr.forEach((item) => {
|
||||
newarr.push(['p', item]);
|
||||
});
|
||||
return newarr;
|
||||
};
|
||||
|
||||
export const pubkeyArray = (arr) => {
|
||||
const newarr = [];
|
||||
// push item to newarr
|
||||
@ -36,3 +45,11 @@ export const getParentID = (arr, fallback) => {
|
||||
|
||||
return parentID;
|
||||
};
|
||||
|
||||
export const filterDuplicateParentID = (arr) => {
|
||||
const filteredArray = arr.filter(
|
||||
(item, index) => index === arr.findIndex((other) => item.parent_id === other.parent_id)
|
||||
);
|
||||
|
||||
return filteredArray;
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user