Merge pull request #24 from reyamir/feat/channels
Initial support for public chat channels (NIP-28)
This commit is contained in:
@@ -15,12 +15,15 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emoji-mart/data": "^1.1.2",
|
"@emoji-mart/data": "^1.1.2",
|
||||||
"@emoji-mart/react": "^1.1.1",
|
"@emoji-mart/react": "^1.1.1",
|
||||||
|
"@radix-ui/react-alert-dialog": "^1.0.3",
|
||||||
"@radix-ui/react-collapsible": "^1.0.2",
|
"@radix-ui/react-collapsible": "^1.0.2",
|
||||||
"@radix-ui/react-dialog": "^1.0.3",
|
"@radix-ui/react-dialog": "^1.0.3",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.0.4",
|
"@radix-ui/react-dropdown-menu": "^2.0.4",
|
||||||
"@radix-ui/react-icons": "^1.3.0",
|
"@radix-ui/react-icons": "^1.3.0",
|
||||||
"@radix-ui/react-popover": "^1.0.5",
|
"@radix-ui/react-popover": "^1.0.5",
|
||||||
"@radix-ui/react-tabs": "^1.0.3",
|
"@radix-ui/react-tabs": "^1.0.3",
|
||||||
|
"@radix-ui/react-tooltip": "^1.0.5",
|
||||||
|
"@rehooks/local-storage": "^2.4.4",
|
||||||
"@supabase/supabase-js": "^2.15.0",
|
"@supabase/supabase-js": "^2.15.0",
|
||||||
"@tauri-apps/api": "^1.2.0",
|
"@tauri-apps/api": "^1.2.0",
|
||||||
"dayjs": "^1.11.7",
|
"dayjs": "^1.11.7",
|
||||||
|
|||||||
74
pnpm-lock.yaml
generated
74
pnpm-lock.yaml
generated
@@ -3,12 +3,15 @@ lockfileVersion: 5.4
|
|||||||
specifiers:
|
specifiers:
|
||||||
'@emoji-mart/data': ^1.1.2
|
'@emoji-mart/data': ^1.1.2
|
||||||
'@emoji-mart/react': ^1.1.1
|
'@emoji-mart/react': ^1.1.1
|
||||||
|
'@radix-ui/react-alert-dialog': ^1.0.3
|
||||||
'@radix-ui/react-collapsible': ^1.0.2
|
'@radix-ui/react-collapsible': ^1.0.2
|
||||||
'@radix-ui/react-dialog': ^1.0.3
|
'@radix-ui/react-dialog': ^1.0.3
|
||||||
'@radix-ui/react-dropdown-menu': ^2.0.4
|
'@radix-ui/react-dropdown-menu': ^2.0.4
|
||||||
'@radix-ui/react-icons': ^1.3.0
|
'@radix-ui/react-icons': ^1.3.0
|
||||||
'@radix-ui/react-popover': ^1.0.5
|
'@radix-ui/react-popover': ^1.0.5
|
||||||
'@radix-ui/react-tabs': ^1.0.3
|
'@radix-ui/react-tabs': ^1.0.3
|
||||||
|
'@radix-ui/react-tooltip': ^1.0.5
|
||||||
|
'@rehooks/local-storage': ^2.4.4
|
||||||
'@supabase/supabase-js': ^2.15.0
|
'@supabase/supabase-js': ^2.15.0
|
||||||
'@tailwindcss/typography': ^0.5.9
|
'@tailwindcss/typography': ^0.5.9
|
||||||
'@tauri-apps/api': ^1.2.0
|
'@tauri-apps/api': ^1.2.0
|
||||||
@@ -54,12 +57,15 @@ specifiers:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@emoji-mart/data': 1.1.2
|
'@emoji-mart/data': 1.1.2
|
||||||
'@emoji-mart/react': 1.1.1_kyrnz3vmphzqyjjk2ivrm6bcsu
|
'@emoji-mart/react': 1.1.1_kyrnz3vmphzqyjjk2ivrm6bcsu
|
||||||
|
'@radix-ui/react-alert-dialog': 1.0.3_zn3vyfk3tbnwebg5ldvieekjaq
|
||||||
'@radix-ui/react-collapsible': 1.0.2_biqbaboplfbrettd7655fr4n2y
|
'@radix-ui/react-collapsible': 1.0.2_biqbaboplfbrettd7655fr4n2y
|
||||||
'@radix-ui/react-dialog': 1.0.3_zn3vyfk3tbnwebg5ldvieekjaq
|
'@radix-ui/react-dialog': 1.0.3_zn3vyfk3tbnwebg5ldvieekjaq
|
||||||
'@radix-ui/react-dropdown-menu': 2.0.4_zn3vyfk3tbnwebg5ldvieekjaq
|
'@radix-ui/react-dropdown-menu': 2.0.4_zn3vyfk3tbnwebg5ldvieekjaq
|
||||||
'@radix-ui/react-icons': 1.3.0_react@18.2.0
|
'@radix-ui/react-icons': 1.3.0_react@18.2.0
|
||||||
'@radix-ui/react-popover': 1.0.5_zn3vyfk3tbnwebg5ldvieekjaq
|
'@radix-ui/react-popover': 1.0.5_zn3vyfk3tbnwebg5ldvieekjaq
|
||||||
'@radix-ui/react-tabs': 1.0.3_biqbaboplfbrettd7655fr4n2y
|
'@radix-ui/react-tabs': 1.0.3_biqbaboplfbrettd7655fr4n2y
|
||||||
|
'@radix-ui/react-tooltip': 1.0.5_zn3vyfk3tbnwebg5ldvieekjaq
|
||||||
|
'@rehooks/local-storage': 2.4.4_react@18.2.0
|
||||||
'@supabase/supabase-js': 2.15.0
|
'@supabase/supabase-js': 2.15.0
|
||||||
'@tauri-apps/api': 1.2.0
|
'@tauri-apps/api': 1.2.0
|
||||||
dayjs: 1.11.7
|
dayjs: 1.11.7
|
||||||
@@ -559,6 +565,26 @@ packages:
|
|||||||
'@babel/runtime': 7.21.0
|
'@babel/runtime': 7.21.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@radix-ui/react-alert-dialog/1.0.3_zn3vyfk3tbnwebg5ldvieekjaq:
|
||||||
|
resolution:
|
||||||
|
{ integrity: sha512-QXFy7+bhGi0u+paF2QbJeSCHZs4gLMJIPm6sajUamyW0fro6g1CaSGc5zmc4QmK2NlSGUrq8m+UsUqJYtzvXow== }
|
||||||
|
peerDependencies:
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.21.0
|
||||||
|
'@radix-ui/primitive': 1.0.0
|
||||||
|
'@radix-ui/react-compose-refs': 1.0.0_react@18.2.0
|
||||||
|
'@radix-ui/react-context': 1.0.0_react@18.2.0
|
||||||
|
'@radix-ui/react-dialog': 1.0.3_zn3vyfk3tbnwebg5ldvieekjaq
|
||||||
|
'@radix-ui/react-primitive': 1.0.2_biqbaboplfbrettd7655fr4n2y
|
||||||
|
'@radix-ui/react-slot': 1.0.1_react@18.2.0
|
||||||
|
react: 18.2.0
|
||||||
|
react-dom: 18.2.0_react@18.2.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@types/react'
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@radix-ui/react-arrow/1.0.2_biqbaboplfbrettd7655fr4n2y:
|
/@radix-ui/react-arrow/1.0.2_biqbaboplfbrettd7655fr4n2y:
|
||||||
resolution:
|
resolution:
|
||||||
{ integrity: sha512-fqYwhhI9IarZ0ll2cUSfKuXHlJK0qE4AfnRrPBbRwEH/4mGQn04/QFGomLi8TXWIdv9WJk//KgGm+aDxVIr1wA== }
|
{ integrity: sha512-fqYwhhI9IarZ0ll2cUSfKuXHlJK0qE4AfnRrPBbRwEH/4mGQn04/QFGomLi8TXWIdv9WJk//KgGm+aDxVIr1wA== }
|
||||||
@@ -926,6 +952,32 @@ packages:
|
|||||||
react-dom: 18.2.0_react@18.2.0
|
react-dom: 18.2.0_react@18.2.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@radix-ui/react-tooltip/1.0.5_zn3vyfk3tbnwebg5ldvieekjaq:
|
||||||
|
resolution:
|
||||||
|
{ integrity: sha512-cDKVcfzyO6PpckZekODJZDe5ZxZ2fCZlzKzTmPhe4mX9qTHRfLcKgqb0OKf22xLwDequ2tVleim+ZYx3rabD5w== }
|
||||||
|
peerDependencies:
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.21.0
|
||||||
|
'@radix-ui/primitive': 1.0.0
|
||||||
|
'@radix-ui/react-compose-refs': 1.0.0_react@18.2.0
|
||||||
|
'@radix-ui/react-context': 1.0.0_react@18.2.0
|
||||||
|
'@radix-ui/react-dismissable-layer': 1.0.3_biqbaboplfbrettd7655fr4n2y
|
||||||
|
'@radix-ui/react-id': 1.0.0_react@18.2.0
|
||||||
|
'@radix-ui/react-popper': 1.1.1_zn3vyfk3tbnwebg5ldvieekjaq
|
||||||
|
'@radix-ui/react-portal': 1.0.2_biqbaboplfbrettd7655fr4n2y
|
||||||
|
'@radix-ui/react-presence': 1.0.0_biqbaboplfbrettd7655fr4n2y
|
||||||
|
'@radix-ui/react-primitive': 1.0.2_biqbaboplfbrettd7655fr4n2y
|
||||||
|
'@radix-ui/react-slot': 1.0.1_react@18.2.0
|
||||||
|
'@radix-ui/react-use-controllable-state': 1.0.0_react@18.2.0
|
||||||
|
'@radix-ui/react-visually-hidden': 1.0.2_biqbaboplfbrettd7655fr4n2y
|
||||||
|
react: 18.2.0
|
||||||
|
react-dom: 18.2.0_react@18.2.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@types/react'
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@radix-ui/react-use-callback-ref/1.0.0_react@18.2.0:
|
/@radix-ui/react-use-callback-ref/1.0.0_react@18.2.0:
|
||||||
resolution:
|
resolution:
|
||||||
{ integrity: sha512-GZtyzoHz95Rhs6S63D2t/eqvdFCm7I+yHMLVQheKM7nBD8mbZIt+ct1jz4536MDnaOGKIxynJ8eHTkVGVVkoTg== }
|
{ integrity: sha512-GZtyzoHz95Rhs6S63D2t/eqvdFCm7I+yHMLVQheKM7nBD8mbZIt+ct1jz4536MDnaOGKIxynJ8eHTkVGVVkoTg== }
|
||||||
@@ -990,6 +1042,19 @@ packages:
|
|||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@radix-ui/react-visually-hidden/1.0.2_biqbaboplfbrettd7655fr4n2y:
|
||||||
|
resolution:
|
||||||
|
{ integrity: sha512-qirnJxtYn73HEk1rXL12/mXnu2rwsNHDID10th2JGtdK25T9wX+mxRmGt7iPSahw512GbZOc0syZX1nLQGoEOg== }
|
||||||
|
peerDependencies:
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.21.0
|
||||||
|
'@radix-ui/react-primitive': 1.0.2_biqbaboplfbrettd7655fr4n2y
|
||||||
|
react: 18.2.0
|
||||||
|
react-dom: 18.2.0_react@18.2.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@radix-ui/rect/1.0.0:
|
/@radix-ui/rect/1.0.0:
|
||||||
resolution:
|
resolution:
|
||||||
{ integrity: sha512-d0O68AYy/9oeEy1DdC07bz1/ZXX+DqCskRd3i4JzLSTXwefzaepQrKjXC7aNM8lTHjFLDO0pDgaEiQ7jEk+HVg== }
|
{ integrity: sha512-d0O68AYy/9oeEy1DdC07bz1/ZXX+DqCskRd3i4JzLSTXwefzaepQrKjXC7aNM8lTHjFLDO0pDgaEiQ7jEk+HVg== }
|
||||||
@@ -997,6 +1062,15 @@ packages:
|
|||||||
'@babel/runtime': 7.21.0
|
'@babel/runtime': 7.21.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@rehooks/local-storage/2.4.4_react@18.2.0:
|
||||||
|
resolution:
|
||||||
|
{ integrity: sha512-zE+kfOkG59n/1UTxdmbwktIosclr67Nlbf2MzUJ9mNtCSypVscNHeD1qT6JCSo5Pjj8DO893IKWNLJqKKzDL/Q== }
|
||||||
|
peerDependencies:
|
||||||
|
react: '>=16.8.0'
|
||||||
|
dependencies:
|
||||||
|
react: 18.2.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@rushstack/eslint-patch/1.2.0:
|
/@rushstack/eslint-patch/1.2.0:
|
||||||
resolution:
|
resolution:
|
||||||
{ integrity: sha512-sXo/qW2/pAcmT43VoRKOJbDOfV3cYpq3szSVfIThQXNt+E4DfKj361vaAt3c88U5tPUxzEswam7GW48PJqtKAg== }
|
{ integrity: sha512-sXo/qW2/pAcmT43VoRKOJbDOfV3cYpq3szSVfIThQXNt+E4DfKj361vaAt3c88U5tPUxzEswam7GW48PJqtKAg== }
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Channel" (
|
||||||
|
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
"eventId" TEXT NOT NULL,
|
||||||
|
"content" TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Channel_eventId_key" ON "Channel"("eventId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Channel_eventId_idx" ON "Channel"("eventId");
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Chat" (
|
||||||
|
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
"pubkey" TEXT NOT NULL,
|
||||||
|
"createdAt" INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Chat_pubkey_key" ON "Chat"("pubkey");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Chat_pubkey_idx" ON "Chat"("pubkey");
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- Added the required column `accountId` to the `Chat` table without a default value. This is not possible if the table is not empty.
|
||||||
|
- Added the required column `accountId` to the `Channel` table without a default value. This is not possible if the table is not empty.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- RedefineTables
|
||||||
|
PRAGMA foreign_keys=OFF;
|
||||||
|
CREATE TABLE "new_Chat" (
|
||||||
|
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
"pubkey" TEXT NOT NULL,
|
||||||
|
"createdAt" INTEGER NOT NULL,
|
||||||
|
"accountId" INTEGER NOT NULL,
|
||||||
|
CONSTRAINT "Chat_accountId_fkey" FOREIGN KEY ("accountId") REFERENCES "Account" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
INSERT INTO "new_Chat" ("createdAt", "id", "pubkey") SELECT "createdAt", "id", "pubkey" FROM "Chat";
|
||||||
|
DROP TABLE "Chat";
|
||||||
|
ALTER TABLE "new_Chat" RENAME TO "Chat";
|
||||||
|
CREATE UNIQUE INDEX "Chat_pubkey_key" ON "Chat"("pubkey");
|
||||||
|
CREATE INDEX "Chat_pubkey_idx" ON "Chat"("pubkey");
|
||||||
|
CREATE TABLE "new_Channel" (
|
||||||
|
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
"eventId" TEXT NOT NULL,
|
||||||
|
"content" TEXT NOT NULL,
|
||||||
|
"accountId" INTEGER NOT NULL,
|
||||||
|
CONSTRAINT "Channel_accountId_fkey" FOREIGN KEY ("accountId") REFERENCES "Account" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
INSERT INTO "new_Channel" ("content", "eventId", "id") SELECT "content", "eventId", "id" FROM "Channel";
|
||||||
|
DROP TABLE "Channel";
|
||||||
|
ALTER TABLE "new_Channel" RENAME TO "Channel";
|
||||||
|
CREATE UNIQUE INDEX "Channel_eventId_key" ON "Channel"("eventId");
|
||||||
|
CREATE INDEX "Channel_eventId_idx" ON "Channel"("eventId");
|
||||||
|
PRAGMA foreign_key_check;
|
||||||
|
PRAGMA foreign_keys=ON;
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
-- RedefineTables
|
||||||
|
PRAGMA foreign_keys=OFF;
|
||||||
|
CREATE TABLE "new_Channel" (
|
||||||
|
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
"eventId" TEXT NOT NULL,
|
||||||
|
"content" TEXT NOT NULL,
|
||||||
|
"active" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"accountId" INTEGER NOT NULL,
|
||||||
|
CONSTRAINT "Channel_accountId_fkey" FOREIGN KEY ("accountId") REFERENCES "Account" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
INSERT INTO "new_Channel" ("accountId", "content", "eventId", "id") SELECT "accountId", "content", "eventId", "id" FROM "Channel";
|
||||||
|
DROP TABLE "Channel";
|
||||||
|
ALTER TABLE "new_Channel" RENAME TO "Channel";
|
||||||
|
CREATE UNIQUE INDEX "Channel_eventId_key" ON "Channel"("eventId");
|
||||||
|
CREATE INDEX "Channel_eventId_idx" ON "Channel"("eventId");
|
||||||
|
PRAGMA foreign_key_check;
|
||||||
|
PRAGMA foreign_keys=ON;
|
||||||
@@ -21,6 +21,8 @@ model Account {
|
|||||||
plebs Pleb[]
|
plebs Pleb[]
|
||||||
messages Message[]
|
messages Message[]
|
||||||
notes Note[]
|
notes Note[]
|
||||||
|
chats Chat[]
|
||||||
|
channels Channel[]
|
||||||
|
|
||||||
@@index([pubkey])
|
@@index([pubkey])
|
||||||
}
|
}
|
||||||
@@ -66,6 +68,29 @@ model Message {
|
|||||||
@@index([pubkey, createdAt])
|
@@index([pubkey, createdAt])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model Chat {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
pubkey String @unique
|
||||||
|
createdAt Int
|
||||||
|
|
||||||
|
Account Account @relation(fields: [accountId], references: [id])
|
||||||
|
accountId Int
|
||||||
|
|
||||||
|
@@index([pubkey])
|
||||||
|
}
|
||||||
|
|
||||||
|
model Channel {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
eventId String @unique
|
||||||
|
content String
|
||||||
|
active Boolean @default(false)
|
||||||
|
|
||||||
|
Account Account @relation(fields: [accountId], references: [id])
|
||||||
|
accountId Int
|
||||||
|
|
||||||
|
@@index([eventId])
|
||||||
|
}
|
||||||
|
|
||||||
model Relay {
|
model Relay {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
url String
|
url String
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ struct CreateAccountData {
|
|||||||
#[derive(Deserialize, Type)]
|
#[derive(Deserialize, Type)]
|
||||||
struct GetPlebData {
|
struct GetPlebData {
|
||||||
account_id: i32,
|
account_id: i32,
|
||||||
|
kind: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Type)]
|
#[derive(Deserialize, Type)]
|
||||||
@@ -81,6 +82,42 @@ struct GetLatestNoteData {
|
|||||||
date: i32,
|
date: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Type)]
|
||||||
|
struct CreateChatData {
|
||||||
|
pubkey: String,
|
||||||
|
created_at: i32,
|
||||||
|
account_id: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Type)]
|
||||||
|
struct GetChatData {
|
||||||
|
account_id: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Type)]
|
||||||
|
struct CreateChannelData {
|
||||||
|
event_id: String,
|
||||||
|
content: String,
|
||||||
|
account_id: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Type)]
|
||||||
|
struct GetChannelData {
|
||||||
|
limit: i32,
|
||||||
|
offset: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Type)]
|
||||||
|
struct GetActiveChannelData {
|
||||||
|
active: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Type)]
|
||||||
|
struct UpdateChannelData {
|
||||||
|
event_id: String,
|
||||||
|
active: bool,
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
#[specta::specta]
|
#[specta::specta]
|
||||||
async fn get_accounts(db: DbState<'_>) -> Result<Vec<account::Data>, ()> {
|
async fn get_accounts(db: DbState<'_>) -> Result<Vec<account::Data>, ()> {
|
||||||
@@ -105,7 +142,10 @@ async fn create_account(db: DbState<'_>, data: CreateAccountData) -> Result<acco
|
|||||||
#[specta::specta]
|
#[specta::specta]
|
||||||
async fn get_plebs(db: DbState<'_>, data: GetPlebData) -> Result<Vec<pleb::Data>, ()> {
|
async fn get_plebs(db: DbState<'_>, data: GetPlebData) -> Result<Vec<pleb::Data>, ()> {
|
||||||
db.pleb()
|
db.pleb()
|
||||||
.find_many(vec![pleb::account_id::equals(data.account_id)])
|
.find_many(vec![
|
||||||
|
pleb::account_id::equals(data.account_id),
|
||||||
|
pleb::kind::equals(data.kind),
|
||||||
|
])
|
||||||
.exec()
|
.exec()
|
||||||
.await
|
.await
|
||||||
.map_err(|_| ())
|
.map_err(|_| ())
|
||||||
@@ -215,6 +255,92 @@ async fn count_total_notes(db: DbState<'_>) -> Result<i64, ()> {
|
|||||||
db.note().count(vec![]).exec().await.map_err(|_| ())
|
db.note().count(vec![]).exec().await.map_err(|_| ())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
|
async fn create_channel(db: DbState<'_>, data: CreateChannelData) -> Result<channel::Data, ()> {
|
||||||
|
db.channel()
|
||||||
|
.upsert(
|
||||||
|
channel::event_id::equals(data.event_id.clone()),
|
||||||
|
channel::create(
|
||||||
|
data.event_id,
|
||||||
|
data.content,
|
||||||
|
account::id::equals(data.account_id),
|
||||||
|
vec![],
|
||||||
|
),
|
||||||
|
vec![],
|
||||||
|
)
|
||||||
|
.exec()
|
||||||
|
.await
|
||||||
|
.map_err(|_| ())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
|
async fn update_channel(db: DbState<'_>, data: UpdateChannelData) -> Result<channel::Data, ()> {
|
||||||
|
db.channel()
|
||||||
|
.update(
|
||||||
|
channel::event_id::equals(data.event_id), // Unique filter
|
||||||
|
vec![channel::active::set(data.active)], // Vec of updates
|
||||||
|
)
|
||||||
|
.exec()
|
||||||
|
.await
|
||||||
|
.map_err(|_| ())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
|
async fn get_channels(db: DbState<'_>, data: GetChannelData) -> Result<Vec<channel::Data>, ()> {
|
||||||
|
db.channel()
|
||||||
|
.find_many(vec![])
|
||||||
|
.take(data.limit.into())
|
||||||
|
.skip(data.offset.into())
|
||||||
|
.exec()
|
||||||
|
.await
|
||||||
|
.map_err(|_| ())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
|
async fn get_active_channels(
|
||||||
|
db: DbState<'_>,
|
||||||
|
data: GetActiveChannelData,
|
||||||
|
) -> Result<Vec<channel::Data>, ()> {
|
||||||
|
db.channel()
|
||||||
|
.find_many(vec![channel::active::equals(data.active)])
|
||||||
|
.exec()
|
||||||
|
.await
|
||||||
|
.map_err(|_| ())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
|
async fn create_chat(db: DbState<'_>, data: CreateChatData) -> Result<chat::Data, ()> {
|
||||||
|
db.chat()
|
||||||
|
.upsert(
|
||||||
|
chat::pubkey::equals(data.pubkey.clone()),
|
||||||
|
chat::create(
|
||||||
|
data.pubkey,
|
||||||
|
data.created_at,
|
||||||
|
account::id::equals(data.account_id),
|
||||||
|
vec![],
|
||||||
|
),
|
||||||
|
vec![],
|
||||||
|
)
|
||||||
|
.exec()
|
||||||
|
.await
|
||||||
|
.map_err(|_| ())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
|
async fn get_chats(db: DbState<'_>, data: GetChatData) -> Result<Vec<chat::Data>, ()> {
|
||||||
|
db.chat()
|
||||||
|
.find_many(vec![chat::account_id::equals(data.account_id)])
|
||||||
|
.exec()
|
||||||
|
.await
|
||||||
|
.map_err(|_| ())
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
let db = PrismaClient::_builder().build().await.unwrap();
|
let db = PrismaClient::_builder().build().await.unwrap();
|
||||||
@@ -230,7 +356,13 @@ async fn main() {
|
|||||||
create_note,
|
create_note,
|
||||||
get_notes,
|
get_notes,
|
||||||
get_latest_notes,
|
get_latest_notes,
|
||||||
get_note_by_id
|
get_note_by_id,
|
||||||
|
create_channel,
|
||||||
|
update_channel,
|
||||||
|
get_channels,
|
||||||
|
get_active_channels,
|
||||||
|
create_chat,
|
||||||
|
get_chats
|
||||||
],
|
],
|
||||||
"../src/utils/bindings.ts",
|
"../src/utils/bindings.ts",
|
||||||
)
|
)
|
||||||
@@ -272,7 +404,13 @@ async fn main() {
|
|||||||
get_notes,
|
get_notes,
|
||||||
get_latest_notes,
|
get_latest_notes,
|
||||||
get_note_by_id,
|
get_note_by_id,
|
||||||
count_total_notes
|
count_total_notes,
|
||||||
|
create_channel,
|
||||||
|
update_channel,
|
||||||
|
get_channels,
|
||||||
|
get_active_channels,
|
||||||
|
create_chat,
|
||||||
|
get_chats
|
||||||
])
|
])
|
||||||
.manage(Arc::new(db))
|
.manage(Arc::new(db))
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
|
|||||||
@@ -2,8 +2,6 @@
|
|||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
@import './assets/editor.css';
|
|
||||||
|
|
||||||
/* Fixed next/image bug, source: https://nextjs.org/docs/api-reference/next/image */
|
/* Fixed next/image bug, source: https://nextjs.org/docs/api-reference/next/image */
|
||||||
@supports (font: -apple-system-body) and (-webkit-appearance: none) {
|
@supports (font: -apple-system-body) and (-webkit-appearance: none) {
|
||||||
img[loading='lazy'] {
|
img[loading='lazy'] {
|
||||||
|
|||||||
@@ -1,326 +0,0 @@
|
|||||||
.w-md-editor-bar {
|
|
||||||
position: absolute;
|
|
||||||
cursor: s-resize;
|
|
||||||
right: 4px;
|
|
||||||
bottom: 4px;
|
|
||||||
margin-top: -11px;
|
|
||||||
margin-right: 0;
|
|
||||||
width: 14px;
|
|
||||||
z-index: 3;
|
|
||||||
height: 10px;
|
|
||||||
border-radius: 0 0 3px 0;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
.w-md-editor-bar svg {
|
|
||||||
display: block;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
.w-md-editor-aree {
|
|
||||||
overflow: auto;
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
||||||
.w-md-editor-text {
|
|
||||||
min-height: 100%;
|
|
||||||
position: relative;
|
|
||||||
text-align: left;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
word-break: keep-all;
|
|
||||||
overflow-wrap: break-word;
|
|
||||||
box-sizing: border-box;
|
|
||||||
margin: 0;
|
|
||||||
-webkit-font-variant-ligatures: common-ligatures;
|
|
||||||
font-variant-ligatures: common-ligatures;
|
|
||||||
@apply p-4;
|
|
||||||
}
|
|
||||||
.w-md-editor-text-pre,
|
|
||||||
.w-md-editor-text-input,
|
|
||||||
.w-md-editor-text > .w-md-editor-text-pre {
|
|
||||||
margin: 0;
|
|
||||||
border: 0;
|
|
||||||
background: none;
|
|
||||||
box-sizing: inherit;
|
|
||||||
display: inherit;
|
|
||||||
font-family: inherit;
|
|
||||||
font-size: inherit;
|
|
||||||
font-style: inherit;
|
|
||||||
-webkit-font-variant-ligatures: inherit;
|
|
||||||
font-variant-ligatures: inherit;
|
|
||||||
font-weight: inherit;
|
|
||||||
letter-spacing: inherit;
|
|
||||||
line-height: inherit;
|
|
||||||
tab-size: inherit;
|
|
||||||
text-indent: inherit;
|
|
||||||
text-rendering: inherit;
|
|
||||||
text-transform: inherit;
|
|
||||||
white-space: inherit;
|
|
||||||
overflow-wrap: inherit;
|
|
||||||
word-break: inherit;
|
|
||||||
word-break: normal;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
.w-md-editor-text-pre > code,
|
|
||||||
.w-md-editor-text-input > code,
|
|
||||||
.w-md-editor-text > .w-md-editor-text-pre > code {
|
|
||||||
font-family: inherit;
|
|
||||||
}
|
|
||||||
.w-md-editor-text-pre {
|
|
||||||
position: relative;
|
|
||||||
margin: 0px !important;
|
|
||||||
pointer-events: none;
|
|
||||||
background-color: transparent !important;
|
|
||||||
}
|
|
||||||
.w-md-editor-text-pre > code {
|
|
||||||
padding: 0 !important;
|
|
||||||
}
|
|
||||||
.w-md-editor-text-input {
|
|
||||||
position: absolute;
|
|
||||||
top: 0px;
|
|
||||||
left: 0px;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
resize: none;
|
|
||||||
color: inherit;
|
|
||||||
overflow: hidden;
|
|
||||||
outline: 0;
|
|
||||||
padding: inherit;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
@apply placeholder:text-zinc-500;
|
|
||||||
}
|
|
||||||
.w-md-editor-text-input:empty {
|
|
||||||
-webkit-text-fill-color: inherit !important;
|
|
||||||
}
|
|
||||||
.w-md-editor-text-pre,
|
|
||||||
.w-md-editor-text-input {
|
|
||||||
word-wrap: pre;
|
|
||||||
word-break: break-word;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Hack to apply on some CSS on IE10 and IE11
|
|
||||||
*/
|
|
||||||
@media all and (-ms-high-contrast: none), (-ms-high-contrast: active) {
|
|
||||||
/**
|
|
||||||
* IE doesn't support '-webkit-text-fill-color'
|
|
||||||
* So we use 'color: transparent' to make the text transparent on IE
|
|
||||||
* Unlike other browsers, it doesn't affect caret color in IE
|
|
||||||
*/
|
|
||||||
.w-md-editor-text-input {
|
|
||||||
color: transparent !important;
|
|
||||||
}
|
|
||||||
.w-md-editor-text-input::selection {
|
|
||||||
background-color: #accef7 !important;
|
|
||||||
color: transparent !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.w-md-editor-text-pre .punctuation {
|
|
||||||
color: var(--color-prettylights-syntax-comment) !important;
|
|
||||||
}
|
|
||||||
.w-md-editor-text-pre .token.url,
|
|
||||||
.w-md-editor-text-pre .token.content {
|
|
||||||
color: var(--color-prettylights-syntax-constant) !important;
|
|
||||||
}
|
|
||||||
.w-md-editor-text-pre .token.title.important {
|
|
||||||
color: var(--color-prettylights-syntax-markup-bold);
|
|
||||||
}
|
|
||||||
.w-md-editor-text-pre .token.code-block .function {
|
|
||||||
color: var(--color-prettylights-syntax-entity);
|
|
||||||
}
|
|
||||||
.w-md-editor-text-pre .token.bold {
|
|
||||||
font-weight: unset !important;
|
|
||||||
}
|
|
||||||
.w-md-editor-text-pre .token.title {
|
|
||||||
line-height: unset !important;
|
|
||||||
font-size: unset !important;
|
|
||||||
font-weight: unset !important;
|
|
||||||
}
|
|
||||||
.w-md-editor-text-pre .token.code.keyword {
|
|
||||||
color: var(--color-prettylights-syntax-constant) !important;
|
|
||||||
}
|
|
||||||
.w-md-editor-text-pre .token.strike,
|
|
||||||
.w-md-editor-text-pre .token.strike .content {
|
|
||||||
color: var(--color-prettylights-syntax-markup-deleted-text) !important;
|
|
||||||
}
|
|
||||||
.w-md-editor-toolbar-child {
|
|
||||||
position: absolute;
|
|
||||||
border-radius: 3px;
|
|
||||||
box-shadow: 0 0 0 1px var(--color-border-default), 0 0 0 var(--color-border-default),
|
|
||||||
0 1px 1px var(--color-border-default);
|
|
||||||
background-color: var(--color-canvas-default);
|
|
||||||
z-index: 1;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.w-md-editor-toolbar-child.active {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
.w-md-editor-toolbar-child .w-md-editor-toolbar {
|
|
||||||
border-bottom: 0;
|
|
||||||
padding: 3px;
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
.w-md-editor-toolbar-child .w-md-editor-toolbar ul > li {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
.w-md-editor-toolbar-child .w-md-editor-toolbar ul > li button:not(.cta-btn) {
|
|
||||||
width: -webkit-fill-available;
|
|
||||||
height: initial;
|
|
||||||
box-sizing: border-box;
|
|
||||||
padding: 3px 4px 2px 4px;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
.w-md-editor-toolbar {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
user-select: none;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
.w-md-editor-toolbar.bottom {
|
|
||||||
border-bottom: 0px;
|
|
||||||
border-top: 1px solid var(--color-border-default);
|
|
||||||
border-radius: 0 0 3px 3px;
|
|
||||||
}
|
|
||||||
.w-md-editor-toolbar ul,
|
|
||||||
.w-md-editor-toolbar li {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
list-style: none;
|
|
||||||
line-height: initial;
|
|
||||||
}
|
|
||||||
.w-md-editor-toolbar li {
|
|
||||||
display: inline-block;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
.w-md-editor-toolbar li + li {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
.w-md-editor-toolbar li > button:not(.cta-btn) {
|
|
||||||
border: none;
|
|
||||||
height: 20px;
|
|
||||||
line-height: 14px;
|
|
||||||
background: none;
|
|
||||||
text-transform: none;
|
|
||||||
font-weight: normal;
|
|
||||||
overflow: visible;
|
|
||||||
outline: none;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.3s;
|
|
||||||
white-space: nowrap;
|
|
||||||
@apply rounded py-1 px-2 text-zinc-500;
|
|
||||||
}
|
|
||||||
.w-md-editor-toolbar li > button:not(.cta-btn):hover,
|
|
||||||
.w-md-editor-toolbar li > button:not(.cta-btn):focus {
|
|
||||||
@apply bg-zinc-700 text-zinc-100;
|
|
||||||
}
|
|
||||||
.w-md-editor-toolbar li > button:not(.cta-btn):active {
|
|
||||||
background-color: var(--color-neutral-muted);
|
|
||||||
color: var(--color-danger-fg);
|
|
||||||
}
|
|
||||||
.w-md-editor-toolbar li > button:not(.cta-btn):disabled {
|
|
||||||
color: var(--color-border-default);
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
.w-md-editor-toolbar li > button:not(.cta-btn):disabled:hover {
|
|
||||||
background-color: transparent;
|
|
||||||
color: var(--color-border-default);
|
|
||||||
}
|
|
||||||
.w-md-editor-toolbar li.active > button:not(.cta-btn) {
|
|
||||||
color: var(--color-accent-fg);
|
|
||||||
background-color: var(--color-neutral-muted);
|
|
||||||
}
|
|
||||||
.w-md-editor-toolbar-divider {
|
|
||||||
height: 14px;
|
|
||||||
width: 1px;
|
|
||||||
margin: -3px 3px 0 3px !important;
|
|
||||||
vertical-align: middle;
|
|
||||||
background-color: var(--color-border-default);
|
|
||||||
}
|
|
||||||
.w-md-editor {
|
|
||||||
text-align: left;
|
|
||||||
border-radius: 3px;
|
|
||||||
padding-bottom: 1px;
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column-reverse;
|
|
||||||
@apply gap-3;
|
|
||||||
}
|
|
||||||
.w-md-editor.w-md-editor-rtl {
|
|
||||||
direction: rtl !important;
|
|
||||||
text-align: right !important;
|
|
||||||
}
|
|
||||||
.w-md-editor.w-md-editor-rtl .w-md-editor-preview {
|
|
||||||
right: unset !important;
|
|
||||||
left: 0;
|
|
||||||
text-align: right !important;
|
|
||||||
box-shadow: inset -1px 0 0 0 var(--color-border-default);
|
|
||||||
}
|
|
||||||
.w-md-editor.w-md-editor-rtl .w-md-editor-text {
|
|
||||||
text-align: right !important;
|
|
||||||
}
|
|
||||||
.w-md-editor-toolbar {
|
|
||||||
@apply h-10 shrink-0;
|
|
||||||
}
|
|
||||||
.w-md-editor-content {
|
|
||||||
@apply relative h-full overflow-auto rounded-lg border-[0.5px] border-white/30 bg-zinc-800 shadow-inner;
|
|
||||||
}
|
|
||||||
.w-md-editor .copied {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
.w-md-editor-input {
|
|
||||||
width: 50%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
.w-md-editor-text-pre > code {
|
|
||||||
word-break: break-word !important;
|
|
||||||
white-space: pre-wrap !important;
|
|
||||||
}
|
|
||||||
.w-md-editor-preview {
|
|
||||||
width: 50%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
box-shadow: inset 1px 0 0 0 var(--color-border-default);
|
|
||||||
position: absolute;
|
|
||||||
padding: 10px 20px;
|
|
||||||
overflow: auto;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
border-radius: 0 0 5px 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
.w-md-editor-preview .anchor {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.w-md-editor-preview .contains-task-list {
|
|
||||||
list-style: none;
|
|
||||||
}
|
|
||||||
.w-md-editor-show-preview .w-md-editor-input {
|
|
||||||
width: 0%;
|
|
||||||
overflow: hidden;
|
|
||||||
background-color: var(--color-canvas-default);
|
|
||||||
}
|
|
||||||
.w-md-editor-show-preview .w-md-editor-preview {
|
|
||||||
width: 100%;
|
|
||||||
box-shadow: inset 0 0 0 0;
|
|
||||||
}
|
|
||||||
.w-md-editor-show-edit .w-md-editor-input {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.w-md-editor-show-edit .w-md-editor-preview {
|
|
||||||
width: 0%;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
.w-md-editor-fullscreen {
|
|
||||||
overflow: hidden;
|
|
||||||
position: fixed;
|
|
||||||
z-index: 99999;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
height: 100% !important;
|
|
||||||
}
|
|
||||||
.w-md-editor-fullscreen .w-md-editor-content {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
18
src/assets/icons/hide.tsx
Normal file
18
src/assets/icons/hide.tsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
export default function HideIcon({ className }: { className: string }) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
strokeWidth={1.5}
|
||||||
|
stroke="currentColor"
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M3.98 8.223A10.477 10.477 0 001.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.45 10.45 0 0112 4.5c4.756 0 8.773 3.162 10.065 7.498a10.523 10.523 0 01-4.293 5.774M6.228 6.228L3 3m3.228 3.228l3.65 3.65m7.894 7.894L21 21m-3.228-3.228l-3.65-3.65m0 0a3 3 0 10-4.243-4.243m4.242 4.242L9.88 9.88"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
18
src/assets/icons/mute.tsx
Normal file
18
src/assets/icons/mute.tsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
export default function MuteIcon({ className }: { className: string }) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
strokeWidth={1.5}
|
||||||
|
stroke="currentColor"
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
18
src/assets/icons/reply.tsx
Normal file
18
src/assets/icons/reply.tsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
export default function ReplyIcon({ className }: { className: string }) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
strokeWidth={1.5}
|
||||||
|
stroke="currentColor"
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M12 20.25c4.97 0 9-3.694 9-8.25s-4.03-8.25-9-8.25S3 7.444 3 12c0 2.104.859 4.023 2.273 5.48.432.447.74 1.04.586 1.641a4.483 4.483 0 01-.923 1.785A5.969 5.969 0 006 21c1.282 0 2.47-.402 3.445-1.087.81.22 1.668.337 2.555.337z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ const AppActions = dynamic(() => import('@components/appHeader/actions'), {
|
|||||||
ssr: false,
|
ssr: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const NoteConnector = dynamic(() => import('@components/note/connector'), {
|
const EventCollector = dynamic(() => import('@components/eventCollector'), {
|
||||||
ssr: false,
|
ssr: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@ export default function AppHeader() {
|
|||||||
<div data-tauri-drag-region className="flex h-full w-full items-center justify-between">
|
<div data-tauri-drag-region className="flex h-full w-full items-center justify-between">
|
||||||
<div className="flex h-full items-center divide-x divide-zinc-900 px-4 pt-px"></div>
|
<div className="flex h-full items-center divide-x divide-zinc-900 px-4 pt-px"></div>
|
||||||
<div>
|
<div>
|
||||||
<NoteConnector />
|
<EventCollector />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
59
src/components/channels/browseChannelItem.tsx
Normal file
59
src/components/channels/browseChannelItem.tsx
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { ImageWithFallback } from '@components/imageWithFallback';
|
||||||
|
|
||||||
|
import { DEFAULT_AVATAR } from '@stores/constants';
|
||||||
|
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
|
export const BrowseChannelItem = ({ data }: { data: any }) => {
|
||||||
|
const router = useRouter();
|
||||||
|
const channel = JSON.parse(data.content);
|
||||||
|
|
||||||
|
const openChannel = useCallback(
|
||||||
|
(id: string) => {
|
||||||
|
router.push({
|
||||||
|
pathname: '/channels/[id]',
|
||||||
|
query: { id: id },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[router]
|
||||||
|
);
|
||||||
|
|
||||||
|
const joinChannel = useCallback(
|
||||||
|
async (id: string) => {
|
||||||
|
const { updateChannel } = await import('@utils/bindings');
|
||||||
|
updateChannel({ event_id: id, active: true })
|
||||||
|
.then(() => openChannel(id))
|
||||||
|
.catch(console.error);
|
||||||
|
},
|
||||||
|
[openChannel]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
onClick={() => openChannel(data.eventId)}
|
||||||
|
className="group relative flex items-center gap-2 border-b border-zinc-800 px-3 py-2.5 hover:bg-black/20"
|
||||||
|
>
|
||||||
|
<div className="relative h-11 w-11 shrink overflow-hidden rounded-md border border-white/10">
|
||||||
|
<ImageWithFallback
|
||||||
|
src={channel.picture || DEFAULT_AVATAR}
|
||||||
|
alt={data.id}
|
||||||
|
fill={true}
|
||||||
|
className="rounded-md object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex w-full flex-1 flex-col items-start text-start">
|
||||||
|
<span className="truncate font-medium leading-tight text-zinc-200">{channel.name}</span>
|
||||||
|
<span className="text-sm leading-tight text-zinc-400">{channel.about}</span>
|
||||||
|
</div>
|
||||||
|
<div className="absolute right-2 top-1/2 hidden -translate-y-1/2 transform group-hover:inline-flex">
|
||||||
|
<button
|
||||||
|
onClick={() => joinChannel(data.eventId)}
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
Join
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
41
src/components/channels/channelList.tsx
Normal file
41
src/components/channels/channelList.tsx
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { ChannelListItem } from '@components/channels/channelListItem';
|
||||||
|
import { CreateChannelModal } from '@components/channels/createChannelModal';
|
||||||
|
|
||||||
|
import { GlobeIcon } from '@radix-ui/react-icons';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
export default function ChannelList() {
|
||||||
|
const [list, setList] = useState([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchChannels = async () => {
|
||||||
|
const { getActiveChannels } = await import('@utils/bindings');
|
||||||
|
return await getActiveChannels({ active: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchChannels()
|
||||||
|
.then((res) => setList(res))
|
||||||
|
.catch(console.error);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-px">
|
||||||
|
<Link
|
||||||
|
href="/channels"
|
||||||
|
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">
|
||||||
|
<GlobeIcon className="h-3 w-3 text-zinc-500" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h5 className="text-sm font-medium text-zinc-500 group-hover:text-zinc-400">Browse channels</h5>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
{list.map((item) => (
|
||||||
|
<ChannelListItem key={item.id} data={item} />
|
||||||
|
))}
|
||||||
|
<CreateChannelModal />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
36
src/components/channels/channelListItem.tsx
Normal file
36
src/components/channels/channelListItem.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { ImageWithFallback } from '@components/imageWithFallback';
|
||||||
|
|
||||||
|
import { DEFAULT_AVATAR } from '@stores/constants';
|
||||||
|
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
|
||||||
|
export const ChannelListItem = ({ data }: { data: any }) => {
|
||||||
|
const router = useRouter();
|
||||||
|
const channel = JSON.parse(data.content);
|
||||||
|
|
||||||
|
const openChannel = (id: string) => {
|
||||||
|
router.push({
|
||||||
|
pathname: '/channels/[id]',
|
||||||
|
query: { id: id },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
onClick={() => openChannel(data.eventId)}
|
||||||
|
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-0 overflow-hidden rounded">
|
||||||
|
<ImageWithFallback
|
||||||
|
src={channel?.picture || DEFAULT_AVATAR}
|
||||||
|
alt={data.eventId}
|
||||||
|
fill={true}
|
||||||
|
className="rounded object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h5 className="truncate text-sm font-medium text-zinc-400">{channel.name}</h5>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
137
src/components/channels/createChannelModal.tsx
Normal file
137
src/components/channels/createChannelModal.tsx
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
import { RelayContext } from '@components/relaysProvider';
|
||||||
|
|
||||||
|
import { dateToUnix } from '@utils/getDate';
|
||||||
|
|
||||||
|
import * as Dialog from '@radix-ui/react-dialog';
|
||||||
|
import { Cross1Icon, PlusIcon } from '@radix-ui/react-icons';
|
||||||
|
import useLocalStorage from '@rehooks/local-storage';
|
||||||
|
import { getEventHash, signEvent } from 'nostr-tools';
|
||||||
|
import { useCallback, useContext, useState } from 'react';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
|
||||||
|
export const CreateChannelModal = () => {
|
||||||
|
const [pool, relays]: any = useContext(RelayContext);
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
const [activeAccount]: any = useLocalStorage('activeAccount', {});
|
||||||
|
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
handleSubmit,
|
||||||
|
reset,
|
||||||
|
formState: { isDirty, isValid },
|
||||||
|
} = useForm();
|
||||||
|
|
||||||
|
const insertChannelToDB = useCallback(async (id, data, account) => {
|
||||||
|
const { createChannel } = await import('@utils/bindings');
|
||||||
|
return await createChannel({ event_id: id, content: data, account_id: account });
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onSubmit = (data) => {
|
||||||
|
const event: any = {
|
||||||
|
content: JSON.stringify(data),
|
||||||
|
created_at: dateToUnix(),
|
||||||
|
kind: 40,
|
||||||
|
pubkey: activeAccount.pubkey,
|
||||||
|
tags: [],
|
||||||
|
};
|
||||||
|
event.id = getEventHash(event);
|
||||||
|
event.sig = signEvent(event, activeAccount.privkey);
|
||||||
|
|
||||||
|
// publish channel
|
||||||
|
pool.publish(event, relays);
|
||||||
|
// save to database
|
||||||
|
insertChannelToDB(event.id, data, activeAccount.id);
|
||||||
|
// close modal
|
||||||
|
setOpen(false);
|
||||||
|
// reset form
|
||||||
|
reset();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog.Root open={open} onOpenChange={setOpen}>
|
||||||
|
<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 channel</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-min w-full max-w-xl flex-col rounded-lg shadow-modal">
|
||||||
|
<div className="sticky left-0 top-0 flex h-12 w-full shrink-0 items-center justify-between rounded-t-lg bg-zinc-950 px-3">
|
||||||
|
<div className="flex w-full items-center justify-between">
|
||||||
|
<h5 className="font-medium leading-none text-zinc-500"># Create channel</h5>
|
||||||
|
<Dialog.Close asChild>
|
||||||
|
<button
|
||||||
|
autoFocus={false}
|
||||||
|
className="inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-900"
|
||||||
|
>
|
||||||
|
<Cross1Icon className="h-3 w-3 text-zinc-300" />
|
||||||
|
</button>
|
||||||
|
</Dialog.Close>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex h-full w-full flex-col overflow-y-auto rounded-b-lg bg-zinc-950 px-3 pb-3">
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
|
className="flex h-full w-full flex-col gap-4 rounded-lg border border-white/20 bg-zinc-900 p-4"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<label className="text-xs font-semibold uppercase tracking-wider text-zinc-300">
|
||||||
|
Channel name *
|
||||||
|
</label>
|
||||||
|
<div className="relative 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">
|
||||||
|
<input
|
||||||
|
type={'text'}
|
||||||
|
{...register('name', { required: true })}
|
||||||
|
spellCheck={false}
|
||||||
|
className="relative h-10 w-full rounded-lg border border-black/5 px-3 py-2 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>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<label className="text-xs font-semibold uppercase tracking-wider text-zinc-300">Picture</label>
|
||||||
|
<div className="relative 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">
|
||||||
|
<input
|
||||||
|
type={'text'}
|
||||||
|
{...register('picture')}
|
||||||
|
spellCheck={false}
|
||||||
|
className="relative h-10 w-full rounded-lg border border-black/5 px-3 py-2 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>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<label className="text-xs font-semibold uppercase tracking-wider text-zinc-300">About</label>
|
||||||
|
<div className="relative h-20 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">
|
||||||
|
<textarea
|
||||||
|
{...register('about')}
|
||||||
|
spellCheck={false}
|
||||||
|
className="relative h-20 w-full resize-none rounded-lg border border-black/5 px-3 py-2 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>
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={!isDirty || !isValid}
|
||||||
|
className="h-11 w-full transform rounded-lg bg-fuchsia-500 font-medium text-white active:translate-y-1 disabled:cursor-not-allowed disabled:opacity-30"
|
||||||
|
>
|
||||||
|
Create
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog.Content>
|
||||||
|
</Dialog.Portal>
|
||||||
|
</Dialog.Root>
|
||||||
|
);
|
||||||
|
};
|
||||||
84
src/components/channels/messages/hideMessageButton.tsx
Normal file
84
src/components/channels/messages/hideMessageButton.tsx
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import { RelayContext } from '@components/relaysProvider';
|
||||||
|
|
||||||
|
import { dateToUnix } from '@utils/getDate';
|
||||||
|
|
||||||
|
import HideIcon from '@assets/icons/hide';
|
||||||
|
|
||||||
|
import * as AlertDialog from '@radix-ui/react-alert-dialog';
|
||||||
|
import * as Tooltip from '@radix-ui/react-tooltip';
|
||||||
|
import useLocalStorage from '@rehooks/local-storage';
|
||||||
|
import { getEventHash, signEvent } from 'nostr-tools';
|
||||||
|
import { useCallback, useContext } from 'react';
|
||||||
|
|
||||||
|
export const HideMessageButton = ({ id }: { id: string }) => {
|
||||||
|
const [pool, relays]: any = useContext(RelayContext);
|
||||||
|
const [activeAccount]: any = useLocalStorage('activeAccount', {});
|
||||||
|
|
||||||
|
const hideMessage = useCallback(() => {
|
||||||
|
const event: any = {
|
||||||
|
content: '',
|
||||||
|
created_at: dateToUnix(),
|
||||||
|
kind: 43,
|
||||||
|
pubkey: activeAccount.pubkey,
|
||||||
|
tags: [['e', id]],
|
||||||
|
};
|
||||||
|
event.id = getEventHash(event);
|
||||||
|
event.sig = signEvent(event, activeAccount.privkey);
|
||||||
|
|
||||||
|
// publish note
|
||||||
|
pool.publish(event, relays);
|
||||||
|
}, [id, activeAccount.privkey, activeAccount.pubkey, pool, relays]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AlertDialog.Root>
|
||||||
|
<Tooltip.Provider>
|
||||||
|
<Tooltip.Root>
|
||||||
|
<AlertDialog.Trigger asChild>
|
||||||
|
<Tooltip.Trigger asChild>
|
||||||
|
<button className="inline-flex h-6 w-6 items-center justify-center rounded hover:bg-zinc-800">
|
||||||
|
<HideIcon className="h-4 w-4 text-zinc-400" />
|
||||||
|
</button>
|
||||||
|
</Tooltip.Trigger>
|
||||||
|
</AlertDialog.Trigger>
|
||||||
|
<Tooltip.Portal>
|
||||||
|
<Tooltip.Content
|
||||||
|
className="select-none rounded-md bg-zinc-800 px-4 py-2 text-sm leading-none text-zinc-100 shadow-[hsl(206_22%_7%_/_35%)_0px_10px_38px_-10px,_hsl(206_22%_7%_/_20%)_0px_10px_20px_-15px] will-change-[transform,opacity] data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade"
|
||||||
|
sideOffset={4}
|
||||||
|
>
|
||||||
|
Hide this message
|
||||||
|
<Tooltip.Arrow className="fill-zinc-800" />
|
||||||
|
</Tooltip.Content>
|
||||||
|
</Tooltip.Portal>
|
||||||
|
</Tooltip.Root>
|
||||||
|
</Tooltip.Provider>
|
||||||
|
<AlertDialog.Portal>
|
||||||
|
<AlertDialog.Overlay className="fixed inset-0 z-50 bg-black bg-opacity-30 backdrop-blur-sm data-[state=open]:animate-overlayShow" />
|
||||||
|
<AlertDialog.Content className="fixed left-[50%] top-[50%] z-50 max-h-[85vh] w-[90vw] max-w-[500px] translate-x-[-50%] translate-y-[-50%] rounded-md bg-zinc-900 p-6 shadow-[hsl(206_22%_7%_/_35%)_0px_10px_38px_-10px,_hsl(206_22%_7%_/_20%)_0px_10px_20px_-15px] ring-1 ring-zinc-800 focus:outline-none data-[state=open]:animate-contentShow">
|
||||||
|
<AlertDialog.Title className="m-0 font-medium text-zinc-100">Are you absolutely sure?</AlertDialog.Title>
|
||||||
|
<AlertDialog.Description className="mb-5 mt-4 text-zinc-400">
|
||||||
|
This action cannot be undone. This will permanently hide this message and you will never see this again
|
||||||
|
</AlertDialog.Description>
|
||||||
|
<div className="flex justify-end gap-4">
|
||||||
|
<AlertDialog.Cancel asChild>
|
||||||
|
<button
|
||||||
|
autoFocus={false}
|
||||||
|
className="inline-flex h-9 items-center justify-center rounded px-4 font-medium leading-none text-zinc-200 outline-none hover:bg-zinc-900 focus:shadow-[0_0_0_2px]"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</AlertDialog.Cancel>
|
||||||
|
<AlertDialog.Action asChild>
|
||||||
|
<button
|
||||||
|
autoFocus={false}
|
||||||
|
onClick={() => hideMessage()}
|
||||||
|
className="inline-flex h-9 items-center justify-center rounded bg-red-500 px-4 font-medium leading-none text-white outline-none hover:bg-red-600 focus:shadow-[0_0_0_2px] focus:shadow-red-700"
|
||||||
|
>
|
||||||
|
Yes, hide this message
|
||||||
|
</button>
|
||||||
|
</AlertDialog.Action>
|
||||||
|
</div>
|
||||||
|
</AlertDialog.Content>
|
||||||
|
</AlertDialog.Portal>
|
||||||
|
</AlertDialog.Root>
|
||||||
|
);
|
||||||
|
};
|
||||||
39
src/components/channels/messages/index.tsx
Normal file
39
src/components/channels/messages/index.tsx
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import ChannelMessageItem from '@components/channels/messages/item';
|
||||||
|
|
||||||
|
import { useCallback, useRef } from 'react';
|
||||||
|
import { Virtuoso } from 'react-virtuoso';
|
||||||
|
|
||||||
|
export const ChannelMessages = ({ data }: { data: any }) => {
|
||||||
|
const virtuosoRef = useRef(null);
|
||||||
|
|
||||||
|
const itemContent: any = useCallback(
|
||||||
|
(index: string | number) => {
|
||||||
|
return <ChannelMessageItem data={data[index]} />;
|
||||||
|
},
|
||||||
|
[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>
|
||||||
|
);
|
||||||
|
};
|
||||||
32
src/components/channels/messages/item.tsx
Normal file
32
src/components/channels/messages/item.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { HideMessageButton } from '@components/channels/messages/hideMessageButton';
|
||||||
|
import { MuteButton } from '@components/channels/messages/muteButton';
|
||||||
|
import { ReplyButton } from '@components/channels/messages/replyButton';
|
||||||
|
import { MessageUser } from '@components/chats/messageUser';
|
||||||
|
|
||||||
|
import { memo } from 'react';
|
||||||
|
|
||||||
|
const ChannelMessageItem = ({ data }: { data: any }) => {
|
||||||
|
return (
|
||||||
|
<div className="group relative 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">
|
||||||
|
{data.content}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="absolute -top-4 right-4 z-10 hidden group-hover:inline-flex">
|
||||||
|
<div className="inline-flex h-7 items-center justify-center gap-1 rounded bg-zinc-900 px-0.5 shadow-md shadow-black/20 ring-1 ring-zinc-800">
|
||||||
|
<ReplyButton id={data.id} pubkey={data.pubkey} content={data.content} />
|
||||||
|
<HideMessageButton id={data.id} />
|
||||||
|
<MuteButton pubkey={data.pubkey} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(ChannelMessageItem);
|
||||||
85
src/components/channels/messages/muteButton.tsx
Normal file
85
src/components/channels/messages/muteButton.tsx
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import { RelayContext } from '@components/relaysProvider';
|
||||||
|
|
||||||
|
import { dateToUnix } from '@utils/getDate';
|
||||||
|
|
||||||
|
import MuteIcon from '@assets/icons/mute';
|
||||||
|
|
||||||
|
import * as AlertDialog from '@radix-ui/react-alert-dialog';
|
||||||
|
import * as Tooltip from '@radix-ui/react-tooltip';
|
||||||
|
import useLocalStorage from '@rehooks/local-storage';
|
||||||
|
import { getEventHash, signEvent } from 'nostr-tools';
|
||||||
|
import { useCallback, useContext } from 'react';
|
||||||
|
|
||||||
|
export const MuteButton = ({ pubkey }: { pubkey: string }) => {
|
||||||
|
const [pool, relays]: any = useContext(RelayContext);
|
||||||
|
const [activeAccount]: any = useLocalStorage('activeAccount', {});
|
||||||
|
|
||||||
|
const muteUser = useCallback(() => {
|
||||||
|
const event: any = {
|
||||||
|
content: '',
|
||||||
|
created_at: dateToUnix(),
|
||||||
|
kind: 44,
|
||||||
|
pubkey: activeAccount.pubkey,
|
||||||
|
tags: [['p', pubkey]],
|
||||||
|
};
|
||||||
|
event.id = getEventHash(event);
|
||||||
|
event.sig = signEvent(event, activeAccount.privkey);
|
||||||
|
|
||||||
|
// publish note
|
||||||
|
pool.publish(event, relays);
|
||||||
|
}, [pubkey, activeAccount.privkey, activeAccount.pubkey, pool, relays]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AlertDialog.Root>
|
||||||
|
<Tooltip.Provider>
|
||||||
|
<Tooltip.Root>
|
||||||
|
<AlertDialog.Trigger asChild>
|
||||||
|
<Tooltip.Trigger asChild>
|
||||||
|
<button className="inline-flex h-6 w-6 items-center justify-center rounded hover:bg-zinc-800">
|
||||||
|
<MuteIcon className="h-4 w-4 text-zinc-400" />
|
||||||
|
</button>
|
||||||
|
</Tooltip.Trigger>
|
||||||
|
</AlertDialog.Trigger>
|
||||||
|
<Tooltip.Portal>
|
||||||
|
<Tooltip.Content
|
||||||
|
className="select-none rounded-md bg-zinc-800 px-4 py-2 text-sm leading-none text-zinc-100 shadow-[hsl(206_22%_7%_/_35%)_0px_10px_38px_-10px,_hsl(206_22%_7%_/_20%)_0px_10px_20px_-15px] will-change-[transform,opacity] data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade"
|
||||||
|
sideOffset={4}
|
||||||
|
>
|
||||||
|
Mute user
|
||||||
|
<Tooltip.Arrow className="fill-zinc-800" />
|
||||||
|
</Tooltip.Content>
|
||||||
|
</Tooltip.Portal>
|
||||||
|
</Tooltip.Root>
|
||||||
|
</Tooltip.Provider>
|
||||||
|
<AlertDialog.Portal>
|
||||||
|
<AlertDialog.Overlay className="fixed inset-0 z-50 bg-black bg-opacity-30 backdrop-blur-sm data-[state=open]:animate-overlayShow" />
|
||||||
|
<AlertDialog.Content className="fixed left-[50%] top-[50%] z-50 max-h-[85vh] w-[90vw] max-w-[500px] translate-x-[-50%] translate-y-[-50%] rounded-md bg-zinc-900 p-6 shadow-[hsl(206_22%_7%_/_35%)_0px_10px_38px_-10px,_hsl(206_22%_7%_/_20%)_0px_10px_20px_-15px] ring-1 ring-zinc-800 focus:outline-none data-[state=open]:animate-contentShow">
|
||||||
|
<AlertDialog.Title className="m-0 font-medium text-zinc-100">Are you absolutely sure?</AlertDialog.Title>
|
||||||
|
<AlertDialog.Description className="mb-5 mt-4 text-zinc-400">
|
||||||
|
This action cannot be undone. This will permanently mute this user and you will never receive message from
|
||||||
|
this user
|
||||||
|
</AlertDialog.Description>
|
||||||
|
<div className="flex justify-end gap-4">
|
||||||
|
<AlertDialog.Cancel asChild>
|
||||||
|
<button
|
||||||
|
autoFocus={false}
|
||||||
|
className="inline-flex h-9 items-center justify-center rounded px-4 font-medium leading-none text-zinc-200 outline-none hover:bg-zinc-900 focus:shadow-[0_0_0_2px]"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</AlertDialog.Cancel>
|
||||||
|
<AlertDialog.Action asChild>
|
||||||
|
<button
|
||||||
|
autoFocus={false}
|
||||||
|
onClick={() => muteUser()}
|
||||||
|
className="inline-flex h-9 items-center justify-center rounded bg-red-500 px-4 font-medium leading-none text-white outline-none hover:bg-red-600 focus:shadow-[0_0_0_2px] focus:shadow-red-700"
|
||||||
|
>
|
||||||
|
Yes, mute this user
|
||||||
|
</button>
|
||||||
|
</AlertDialog.Action>
|
||||||
|
</div>
|
||||||
|
</AlertDialog.Content>
|
||||||
|
</AlertDialog.Portal>
|
||||||
|
</AlertDialog.Root>
|
||||||
|
);
|
||||||
|
};
|
||||||
38
src/components/channels/messages/replyButton.tsx
Normal file
38
src/components/channels/messages/replyButton.tsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { channelReplyAtom } from '@stores/channel';
|
||||||
|
|
||||||
|
import ReplyIcon from '@assets/icons/reply';
|
||||||
|
|
||||||
|
import * as Tooltip from '@radix-ui/react-tooltip';
|
||||||
|
import { useSetAtom } from 'jotai';
|
||||||
|
|
||||||
|
export const ReplyButton = ({ id, pubkey, content }: { id: string; pubkey: string; content: string }) => {
|
||||||
|
const setChannelReplyAtom = useSetAtom(channelReplyAtom);
|
||||||
|
|
||||||
|
const createReply = () => {
|
||||||
|
setChannelReplyAtom({ id: id, pubkey: pubkey, content: content });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip.Provider>
|
||||||
|
<Tooltip.Root>
|
||||||
|
<Tooltip.Trigger asChild>
|
||||||
|
<button
|
||||||
|
onClick={() => createReply()}
|
||||||
|
className="inline-flex h-6 w-6 items-center justify-center rounded hover:bg-zinc-800"
|
||||||
|
>
|
||||||
|
<ReplyIcon className="h-4 w-4 text-zinc-400" />
|
||||||
|
</button>
|
||||||
|
</Tooltip.Trigger>
|
||||||
|
<Tooltip.Portal>
|
||||||
|
<Tooltip.Content
|
||||||
|
className="select-none rounded-md bg-zinc-800 px-4 py-2 text-sm leading-none text-zinc-100 shadow-[hsl(206_22%_7%_/_35%)_0px_10px_38px_-10px,_hsl(206_22%_7%_/_20%)_0px_10px_20px_-15px] will-change-[transform,opacity] data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade"
|
||||||
|
sideOffset={4}
|
||||||
|
>
|
||||||
|
Reply
|
||||||
|
<Tooltip.Arrow className="fill-zinc-800" />
|
||||||
|
</Tooltip.Content>
|
||||||
|
</Tooltip.Portal>
|
||||||
|
</Tooltip.Root>
|
||||||
|
</Tooltip.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,23 +1,19 @@
|
|||||||
import { ChatListItem } from '@components/chats/chatListItem';
|
import { ChatListItem } from '@components/chats/chatListItem';
|
||||||
import { ChatModal } from '@components/chats/chatModal';
|
import { ChatModal } from '@components/chats/chatModal';
|
||||||
import { ImageWithFallback } from '@components/imageWithFallback';
|
import { ImageWithFallback } from '@components/imageWithFallback';
|
||||||
import { RelayContext } from '@components/relaysProvider';
|
|
||||||
|
|
||||||
import { activeAccountAtom } from '@stores/account';
|
|
||||||
import { DEFAULT_AVATAR } from '@stores/constants';
|
import { DEFAULT_AVATAR } from '@stores/constants';
|
||||||
|
|
||||||
import { useAtomValue } from 'jotai';
|
import useLocalStorage from '@rehooks/local-storage';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { useContext, useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
export default function ChatList() {
|
export default function ChatList() {
|
||||||
const [pool, relays]: any = useContext(RelayContext);
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const activeAccount: any = useAtomValue(activeAccountAtom);
|
const [list, setList] = useState([]);
|
||||||
const accountProfile = JSON.parse(activeAccount.metadata);
|
const [activeAccount]: any = useLocalStorage('activeAccount', {});
|
||||||
|
const profile = activeAccount.metadata ? JSON.parse(activeAccount.metadata) : null;
|
||||||
const [list, setList] = useState(new Set());
|
|
||||||
|
|
||||||
const openSelfChat = () => {
|
const openSelfChat = () => {
|
||||||
router.push({
|
router.push({
|
||||||
@@ -27,26 +23,15 @@ export default function ChatList() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unsubscribe = pool.subscribe(
|
const fetchChats = async () => {
|
||||||
[
|
const { getChats } = await import('@utils/bindings');
|
||||||
{
|
return await getChats({ account_id: activeAccount.id });
|
||||||
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]);
|
|
||||||
|
fetchChats()
|
||||||
|
.then((res) => setList(res))
|
||||||
|
.catch(console.error);
|
||||||
|
}, [activeAccount.id]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-px">
|
<div className="flex flex-col gap-px">
|
||||||
@@ -56,7 +41,7 @@ export default function ChatList() {
|
|||||||
>
|
>
|
||||||
<div className="relative h-5 w-5 shrink overflow-hidden rounded bg-white">
|
<div className="relative h-5 w-5 shrink overflow-hidden rounded bg-white">
|
||||||
<ImageWithFallback
|
<ImageWithFallback
|
||||||
src={accountProfile.picture || DEFAULT_AVATAR}
|
src={profile?.picture || DEFAULT_AVATAR}
|
||||||
alt={activeAccount.pubkey}
|
alt={activeAccount.pubkey}
|
||||||
fill={true}
|
fill={true}
|
||||||
className="rounded object-cover"
|
className="rounded object-cover"
|
||||||
@@ -64,12 +49,12 @@ export default function ChatList() {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h5 className="text-sm font-medium text-zinc-400">
|
<h5 className="text-sm font-medium text-zinc-400">
|
||||||
{accountProfile.display_name || accountProfile.name} <span className="text-zinc-500">(you)</span>
|
{profile?.display_name || profile?.name} <span className="text-zinc-500">(you)</span>
|
||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{[...list].map((item: string, index) => (
|
{list.map((item) => (
|
||||||
<ChatListItem key={index} pubkey={item} />
|
<ChatListItem key={item.id} pubkey={item.pubkey} />
|
||||||
))}
|
))}
|
||||||
<ChatModal />
|
<ChatModal />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,19 +1,17 @@
|
|||||||
import { ChatModalUser } from '@components/chats/chatModalUser';
|
import { ChatModalUser } from '@components/chats/chatModalUser';
|
||||||
|
|
||||||
import { activeAccountAtom } from '@stores/account';
|
|
||||||
|
|
||||||
import * as Dialog from '@radix-ui/react-dialog';
|
import * as Dialog from '@radix-ui/react-dialog';
|
||||||
import { Cross1Icon, PlusIcon } from '@radix-ui/react-icons';
|
import { Cross1Icon, PlusIcon } from '@radix-ui/react-icons';
|
||||||
import { useAtomValue } from 'jotai';
|
import useLocalStorage from '@rehooks/local-storage';
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
export const ChatModal = () => {
|
export const ChatModal = () => {
|
||||||
const [plebs, setPlebs] = useState([]);
|
const [plebs, setPlebs] = useState([]);
|
||||||
const activeAccount: any = useAtomValue(activeAccountAtom);
|
const [activeAccount]: any = useLocalStorage('activeAccount', {});
|
||||||
|
|
||||||
const fetchPlebsByAccount = useCallback(async (id) => {
|
const fetchPlebsByAccount = useCallback(async (id) => {
|
||||||
const { getPlebs } = await import('@utils/bindings');
|
const { getPlebs } = await import('@utils/bindings');
|
||||||
return await getPlebs({ account_id: id });
|
return await getPlebs({ account_id: id, kind: 0 });
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -42,7 +40,10 @@ export const ChatModal = () => {
|
|||||||
<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="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">
|
<div className="flex items-center gap-2">
|
||||||
<Dialog.Close asChild>
|
<Dialog.Close asChild>
|
||||||
<button className="inline-flex h-5 w-5 items-center justify-center rounded bg-zinc-900">
|
<button
|
||||||
|
autoFocus={false}
|
||||||
|
className="inline-flex h-5 w-5 items-center justify-center rounded bg-zinc-900"
|
||||||
|
>
|
||||||
<Cross1Icon className="h-3 w-3 text-zinc-300" />
|
<Cross1Icon className="h-3 w-3 text-zinc-300" />
|
||||||
</button>
|
</button>
|
||||||
</Dialog.Close>
|
</Dialog.Close>
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
import MessageListItem from '@components/chats/messageListItem';
|
import MessageListItem from '@components/chats/messageListItem';
|
||||||
|
|
||||||
|
import useLocalStorage from '@rehooks/local-storage';
|
||||||
import { useCallback, useRef } from 'react';
|
import { useCallback, useRef } from 'react';
|
||||||
import { Virtuoso } from 'react-virtuoso';
|
import { Virtuoso } from 'react-virtuoso';
|
||||||
|
|
||||||
export const MessageList = ({ data }: { data: any }) => {
|
export const MessageList = ({ data }: { data: any }) => {
|
||||||
|
const [activeAccount]: any = useLocalStorage('activeAccount', {});
|
||||||
const virtuosoRef = useRef(null);
|
const virtuosoRef = useRef(null);
|
||||||
|
|
||||||
const itemContent: any = useCallback(
|
const itemContent: any = useCallback(
|
||||||
(index: string | number) => {
|
(index: string | number) => {
|
||||||
const activeAccount = JSON.parse(localStorage.getItem('activeAccount'));
|
|
||||||
return (
|
return (
|
||||||
<MessageListItem
|
<MessageListItem
|
||||||
data={data[index]}
|
data={data[index]}
|
||||||
@@ -17,7 +18,7 @@ export const MessageList = ({ data }: { data: any }) => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[data]
|
[activeAccount.privkey, activeAccount.pubkey, data]
|
||||||
);
|
);
|
||||||
|
|
||||||
const computeItemKey = useCallback(
|
const computeItemKey = useCallback(
|
||||||
|
|||||||
@@ -1,31 +1,32 @@
|
|||||||
import { RelayContext } from '@components/relaysProvider';
|
import { RelayContext } from '@components/relaysProvider';
|
||||||
|
|
||||||
import { lastLoginAtom } from '@stores/account';
|
|
||||||
import { hasNewerNoteAtom } from '@stores/note';
|
import { hasNewerNoteAtom } from '@stores/note';
|
||||||
|
|
||||||
import { dateToUnix } from '@utils/getDate';
|
import { dateToUnix } from '@utils/getDate';
|
||||||
import { getParentID, pubkeyArray } from '@utils/transform';
|
import { getParentID, pubkeyArray } from '@utils/transform';
|
||||||
|
|
||||||
|
import useLocalStorage, { writeStorage } from '@rehooks/local-storage';
|
||||||
import { TauriEvent } from '@tauri-apps/api/event';
|
import { TauriEvent } from '@tauri-apps/api/event';
|
||||||
import { appWindow, getCurrent } from '@tauri-apps/api/window';
|
import { appWindow, getCurrent } from '@tauri-apps/api/window';
|
||||||
import { useSetAtom } from 'jotai';
|
import { useSetAtom } from 'jotai';
|
||||||
import { useCallback, useContext, useEffect, useRef, useState } from 'react';
|
import { useCallback, useContext, useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
export default function NoteConnector() {
|
export default function EventCollector() {
|
||||||
const [pool, relays]: any = useContext(RelayContext);
|
const [pool, relays]: any = useContext(RelayContext);
|
||||||
|
|
||||||
const setLastLoginAtom = useSetAtom(lastLoginAtom);
|
const [isOnline] = useState(true);
|
||||||
const setHasNewerNote = useSetAtom(hasNewerNoteAtom);
|
const setHasNewerNote = useSetAtom(hasNewerNoteAtom);
|
||||||
|
|
||||||
const [isOnline] = useState(true);
|
const [activeAccount]: any = useLocalStorage('activeAccount', {});
|
||||||
|
const [follows] = useLocalStorage('activeAccountFollows', []);
|
||||||
|
|
||||||
const now = useRef(new Date());
|
const now = useRef(new Date());
|
||||||
const unsubscribe = useRef(null);
|
const unsubscribe = useRef(null);
|
||||||
|
|
||||||
const subscribe = useCallback(async () => {
|
const subscribe = useCallback(async () => {
|
||||||
const { createNote } = await import('@utils/bindings');
|
const { createNote } = await import('@utils/bindings');
|
||||||
const activeAccount = JSON.parse(localStorage.getItem('activeAccount'));
|
const { createChat } = await import('@utils/bindings');
|
||||||
const follows = JSON.parse(localStorage.getItem('activeAccountFollows'));
|
const { createChannel } = await import('@utils/bindings');
|
||||||
|
|
||||||
unsubscribe.current = pool.subscribe(
|
unsubscribe.current = pool.subscribe(
|
||||||
[
|
[
|
||||||
@@ -34,9 +35,19 @@ export default function NoteConnector() {
|
|||||||
authors: pubkeyArray(follows),
|
authors: pubkeyArray(follows),
|
||||||
since: dateToUnix(now.current),
|
since: dateToUnix(now.current),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
kinds: [4],
|
||||||
|
'#p': [activeAccount.pubkey],
|
||||||
|
since: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kinds: [40],
|
||||||
|
since: 0,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
relays,
|
relays,
|
||||||
(event) => {
|
(event) => {
|
||||||
|
if (event.kind === 1) {
|
||||||
const parentID = getParentID(event.tags, event.id);
|
const parentID = getParentID(event.tags, event.id);
|
||||||
// insert event to local database
|
// insert event to local database
|
||||||
createNote({
|
createNote({
|
||||||
@@ -55,22 +66,30 @@ export default function NoteConnector() {
|
|||||||
setHasNewerNote(true)
|
setHasNewerNote(true)
|
||||||
)
|
)
|
||||||
.catch(console.error);
|
.catch(console.error);
|
||||||
},
|
} else if (event.kind === 4) {
|
||||||
10000
|
if (event.pubkey !== activeAccount.pubkey) {
|
||||||
|
createChat({ pubkey: event.pubkey, created_at: event.created_at, account_id: activeAccount.id });
|
||||||
|
}
|
||||||
|
} else if (event.kind === 40) {
|
||||||
|
createChannel({ event_id: event.id, content: event.content, account_id: activeAccount.id });
|
||||||
|
} else {
|
||||||
|
console.error;
|
||||||
|
}
|
||||||
|
}
|
||||||
);
|
);
|
||||||
}, [pool, relays, setHasNewerNote]);
|
}, [activeAccount.id, activeAccount.pubkey, follows, pool, relays, setHasNewerNote]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
subscribe();
|
subscribe();
|
||||||
getCurrent().listen(TauriEvent.WINDOW_CLOSE_REQUESTED, () => {
|
getCurrent().listen(TauriEvent.WINDOW_CLOSE_REQUESTED, () => {
|
||||||
setLastLoginAtom(now.current);
|
writeStorage('lastLogin', now.current);
|
||||||
appWindow.close();
|
appWindow.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
unsubscribe.current;
|
unsubscribe.current;
|
||||||
};
|
};
|
||||||
}, [setHasNewerNote, setLastLoginAtom, subscribe]);
|
}, [setHasNewerNote, subscribe]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="inline-flex items-center gap-1 rounded-md px-1.5 py-1 hover:bg-zinc-900">
|
<div className="inline-flex items-center gap-1 rounded-md px-1.5 py-1 hover:bg-zinc-900">
|
||||||
@@ -6,6 +6,7 @@ import { noteContentAtom } from '@stores/note';
|
|||||||
|
|
||||||
import { dateToUnix } from '@utils/getDate';
|
import { dateToUnix } from '@utils/getDate';
|
||||||
|
|
||||||
|
import useLocalStorage from '@rehooks/local-storage';
|
||||||
import { useAtom } from 'jotai';
|
import { useAtom } from 'jotai';
|
||||||
import { useResetAtom } from 'jotai/utils';
|
import { useResetAtom } from 'jotai/utils';
|
||||||
import { getEventHash, signEvent } from 'nostr-tools';
|
import { getEventHash, signEvent } from 'nostr-tools';
|
||||||
@@ -17,9 +18,9 @@ export default function FormBase() {
|
|||||||
const [value, setValue] = useAtom(noteContentAtom);
|
const [value, setValue] = useAtom(noteContentAtom);
|
||||||
const resetValue = useResetAtom(noteContentAtom);
|
const resetValue = useResetAtom(noteContentAtom);
|
||||||
|
|
||||||
const submitEvent = () => {
|
const [activeAccount]: any = useLocalStorage('activeAccount', {});
|
||||||
const activeAccount = JSON.parse(localStorage.getItem('activeAccount'));
|
|
||||||
|
|
||||||
|
const submitEvent = () => {
|
||||||
const event: any = {
|
const event: any = {
|
||||||
content: value,
|
content: value,
|
||||||
created_at: dateToUnix(),
|
created_at: dateToUnix(),
|
||||||
|
|||||||
130
src/components/form/channelMessage.tsx
Normal file
130
src/components/form/channelMessage.tsx
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
import ImagePicker from '@components/form/imagePicker';
|
||||||
|
import { RelayContext } from '@components/relaysProvider';
|
||||||
|
import { UserMini } from '@components/user/mini';
|
||||||
|
|
||||||
|
import { channelReplyAtom } from '@stores/channel';
|
||||||
|
|
||||||
|
import { dateToUnix } from '@utils/getDate';
|
||||||
|
|
||||||
|
import { Cross1Icon } from '@radix-ui/react-icons';
|
||||||
|
import useLocalStorage from '@rehooks/local-storage';
|
||||||
|
import { useAtomValue } from 'jotai';
|
||||||
|
import { useResetAtom } from 'jotai/utils';
|
||||||
|
import { getEventHash, signEvent } from 'nostr-tools';
|
||||||
|
import { useCallback, useContext, useState } from 'react';
|
||||||
|
|
||||||
|
export default function FormChannelMessage({ eventId }: { eventId: string | string[] }) {
|
||||||
|
const [pool, relays]: any = useContext(RelayContext);
|
||||||
|
|
||||||
|
const [value, setValue] = useState('');
|
||||||
|
const [activeAccount]: any = useLocalStorage('activeAccount', {});
|
||||||
|
|
||||||
|
const channelReply = useAtomValue(channelReplyAtom);
|
||||||
|
const resetChannelReply = useResetAtom(channelReplyAtom);
|
||||||
|
|
||||||
|
const submitEvent = useCallback(() => {
|
||||||
|
let tags;
|
||||||
|
|
||||||
|
if (channelReply.id !== null) {
|
||||||
|
tags = [
|
||||||
|
['e', eventId, '', 'root'],
|
||||||
|
['e', channelReply.id, '', 'reply'],
|
||||||
|
['p', channelReply.pubkey, ''],
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
tags = [['e', eventId, '', 'root']];
|
||||||
|
}
|
||||||
|
|
||||||
|
const event: any = {
|
||||||
|
content: value,
|
||||||
|
created_at: dateToUnix(),
|
||||||
|
kind: 42,
|
||||||
|
pubkey: activeAccount.pubkey,
|
||||||
|
tags: tags,
|
||||||
|
};
|
||||||
|
event.id = getEventHash(event);
|
||||||
|
event.sig = signEvent(event, activeAccount.privkey);
|
||||||
|
|
||||||
|
// publish note
|
||||||
|
pool.publish(event, relays);
|
||||||
|
// reset state
|
||||||
|
setValue('');
|
||||||
|
// reset channel reply
|
||||||
|
resetChannelReply();
|
||||||
|
}, [
|
||||||
|
value,
|
||||||
|
channelReply.id,
|
||||||
|
channelReply.pubkey,
|
||||||
|
activeAccount.pubkey,
|
||||||
|
activeAccount.privkey,
|
||||||
|
eventId,
|
||||||
|
resetChannelReply,
|
||||||
|
pool,
|
||||||
|
relays,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const handleEnterPress = (e) => {
|
||||||
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
submitEvent();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopReply = () => {
|
||||||
|
resetChannelReply();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`relative ${
|
||||||
|
channelReply.id ? 'h-36' : '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`}
|
||||||
|
>
|
||||||
|
{channelReply.id && (
|
||||||
|
<div className="absolute left-0 top-0 z-10 h-14 w-full p-[2px]">
|
||||||
|
<div className="flex h-full w-full items-center justify-between rounded-t-md border-b border-zinc-700/70 bg-zinc-900 px-3">
|
||||||
|
<div className="flex w-full flex-col">
|
||||||
|
<UserMini pubkey={channelReply.pubkey} />
|
||||||
|
<div className="-mt-3.5 pl-[32px]">
|
||||||
|
<div className="text-xs text-zinc-200">{channelReply.content}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => stopReply()}
|
||||||
|
className="inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-800"
|
||||||
|
>
|
||||||
|
<Cross1Icon className="h-3 w-3 text-zinc-100" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<textarea
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => setValue(e.target.value)}
|
||||||
|
onKeyDown={handleEnterPress}
|
||||||
|
spellCheck={false}
|
||||||
|
placeholder="Message"
|
||||||
|
className={`relative ${
|
||||||
|
channelReply.id ? 'h-36 pt-16' : 'h-24 pt-3'
|
||||||
|
} w-full resize-none rounded-lg border border-black/5 px-3.5 pb-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 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -3,12 +3,15 @@ import { RelayContext } from '@components/relaysProvider';
|
|||||||
|
|
||||||
import { dateToUnix } from '@utils/getDate';
|
import { dateToUnix } from '@utils/getDate';
|
||||||
|
|
||||||
|
import useLocalStorage from '@rehooks/local-storage';
|
||||||
import { getEventHash, nip04, signEvent } from 'nostr-tools';
|
import { getEventHash, nip04, signEvent } from 'nostr-tools';
|
||||||
import { useCallback, useContext, useState } from 'react';
|
import { useCallback, useContext, useState } from 'react';
|
||||||
|
|
||||||
export default function FormChat({ receiverPubkey }: { receiverPubkey: string }) {
|
export default function FormChat({ receiverPubkey }: { receiverPubkey: string }) {
|
||||||
const [pool, relays]: any = useContext(RelayContext);
|
const [pool, relays]: any = useContext(RelayContext);
|
||||||
|
|
||||||
const [value, setValue] = useState('');
|
const [value, setValue] = useState('');
|
||||||
|
const [activeAccount]: any = useLocalStorage('activeAccount', {});
|
||||||
|
|
||||||
const encryptMessage = useCallback(
|
const encryptMessage = useCallback(
|
||||||
async (privkey: string) => {
|
async (privkey: string) => {
|
||||||
@@ -18,7 +21,6 @@ export default function FormChat({ receiverPubkey }: { receiverPubkey: string })
|
|||||||
);
|
);
|
||||||
|
|
||||||
const submitEvent = useCallback(() => {
|
const submitEvent = useCallback(() => {
|
||||||
const activeAccount = JSON.parse(localStorage.getItem('activeAccount'));
|
|
||||||
encryptMessage(activeAccount.privkey)
|
encryptMessage(activeAccount.privkey)
|
||||||
.then((encryptedContent) => {
|
.then((encryptedContent) => {
|
||||||
const event: any = {
|
const event: any = {
|
||||||
@@ -36,7 +38,7 @@ export default function FormChat({ receiverPubkey }: { receiverPubkey: string })
|
|||||||
setValue('');
|
setValue('');
|
||||||
})
|
})
|
||||||
.catch(console.error);
|
.catch(console.error);
|
||||||
}, [encryptMessage, receiverPubkey, pool, relays]);
|
}, [encryptMessage, activeAccount.privkey, activeAccount.pubkey, receiverPubkey, pool, relays]);
|
||||||
|
|
||||||
const handleEnterPress = (e) => {
|
const handleEnterPress = (e) => {
|
||||||
if (e.key === 'Enter' && !e.shiftKey) {
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
|
|||||||
@@ -1,22 +1,19 @@
|
|||||||
import { ImageWithFallback } from '@components/imageWithFallback';
|
import { ImageWithFallback } from '@components/imageWithFallback';
|
||||||
import { RelayContext } from '@components/relaysProvider';
|
import { RelayContext } from '@components/relaysProvider';
|
||||||
|
|
||||||
import { activeAccountAtom } from '@stores/account';
|
|
||||||
|
|
||||||
import { dateToUnix } from '@utils/getDate';
|
import { dateToUnix } from '@utils/getDate';
|
||||||
|
|
||||||
import destr from 'destr';
|
import useLocalStorage from '@rehooks/local-storage';
|
||||||
import { useAtomValue } from 'jotai';
|
|
||||||
import { getEventHash, signEvent } from 'nostr-tools';
|
import { getEventHash, signEvent } from 'nostr-tools';
|
||||||
import { useContext, useState } from 'react';
|
import { useContext, useState } from 'react';
|
||||||
|
|
||||||
export default function FormComment({ eventID }: { eventID: any }) {
|
export default function FormComment({ eventID }: { eventID: any }) {
|
||||||
const [pool, relays]: any = useContext(RelayContext);
|
const [pool, relays]: any = useContext(RelayContext);
|
||||||
|
|
||||||
const activeAccount: any = useAtomValue(activeAccountAtom);
|
const [activeAccount]: any = useLocalStorage('activeAccount', {});
|
||||||
const [value, setValue] = useState('');
|
const [value, setValue] = useState('');
|
||||||
|
|
||||||
const profile = destr(activeAccount.metadata);
|
const profile = JSON.parse(activeAccount.metadata);
|
||||||
|
|
||||||
const submitEvent = () => {
|
const submitEvent = () => {
|
||||||
const event: any = {
|
const event: any = {
|
||||||
|
|||||||
@@ -29,20 +29,19 @@ export const ActiveAccount = memo(function ActiveAccount({ user }: { user: any }
|
|||||||
const insertFollowsToStorage = useCallback(
|
const insertFollowsToStorage = useCallback(
|
||||||
async (tags) => {
|
async (tags) => {
|
||||||
const { createPleb } = await import('@utils/bindings');
|
const { createPleb } = await import('@utils/bindings');
|
||||||
const activeAccount = JSON.parse(localStorage.getItem('activeAccount'));
|
|
||||||
|
|
||||||
for (const tag of tags) {
|
for (const tag of tags) {
|
||||||
const metadata: any = await fetchMetadata(tag[1], pool, relays);
|
const metadata: any = await fetchMetadata(tag[1]);
|
||||||
createPleb({
|
createPleb({
|
||||||
pleb_id: tag[1] + '-lume' + activeAccount.id.toString(),
|
pleb_id: tag[1] + '-lume' + user.id.toString(),
|
||||||
pubkey: tag[1],
|
pubkey: tag[1],
|
||||||
kind: 0,
|
kind: 0,
|
||||||
metadata: metadata.content,
|
metadata: metadata.content,
|
||||||
account_id: activeAccount.id,
|
account_id: user.id,
|
||||||
}).catch(console.error);
|
}).catch(console.error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[pool, relays]
|
[user.id]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -56,7 +55,7 @@ export const ActiveAccount = memo(function ActiveAccount({ user }: { user: any }
|
|||||||
relays,
|
relays,
|
||||||
(event: any) => {
|
(event: any) => {
|
||||||
if (event.tags.length > 0) {
|
if (event.tags.length > 0) {
|
||||||
insertFollowsToStorage(event.tags);
|
//insertFollowsToStorage(event.tags);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
20000,
|
20000,
|
||||||
|
|||||||
@@ -6,21 +6,24 @@ import { APP_VERSION } from '@stores/constants';
|
|||||||
import LumeSymbol from '@assets/icons/Lume';
|
import LumeSymbol from '@assets/icons/Lume';
|
||||||
|
|
||||||
import { PlusIcon } from '@radix-ui/react-icons';
|
import { PlusIcon } from '@radix-ui/react-icons';
|
||||||
|
import useLocalStorage from '@rehooks/local-storage';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
export default function MultiAccounts() {
|
export default function MultiAccounts() {
|
||||||
const [users, setUsers] = useState([]);
|
const [users, setUsers] = useState([]);
|
||||||
|
const [activeAccount]: any = useLocalStorage('activeAccount', {});
|
||||||
|
|
||||||
const renderAccount = useCallback((user: { pubkey: string }) => {
|
const renderAccount = useCallback(
|
||||||
const activeAccount = JSON.parse(localStorage.getItem('activeAccount'));
|
(user: { pubkey: string }) => {
|
||||||
|
|
||||||
if (user.pubkey === activeAccount.pubkey) {
|
if (user.pubkey === activeAccount.pubkey) {
|
||||||
return <ActiveAccount key={user.pubkey} user={user} />;
|
return <ActiveAccount key={user.pubkey} user={user} />;
|
||||||
} else {
|
} else {
|
||||||
return <InactiveAccount key={user.pubkey} user={user} />;
|
return <InactiveAccount key={user.pubkey} user={user} />;
|
||||||
}
|
}
|
||||||
}, []);
|
},
|
||||||
|
[activeAccount.pubkey]
|
||||||
|
);
|
||||||
|
|
||||||
const fetchAccounts = useCallback(async () => {
|
const fetchAccounts = useCallback(async () => {
|
||||||
const { getAccounts } = await import('@utils/bindings');
|
const { getAccounts } = await import('@utils/bindings');
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import ChannelList from '@components/channels/channelList';
|
||||||
|
|
||||||
import * as Collapsible from '@radix-ui/react-collapsible';
|
import * as Collapsible from '@radix-ui/react-collapsible';
|
||||||
import { TriangleUpIcon } from '@radix-ui/react-icons';
|
import { TriangleUpIcon } from '@radix-ui/react-icons';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
@@ -18,7 +20,9 @@ export default function Channels() {
|
|||||||
</div>
|
</div>
|
||||||
<h3 className="text-[11px] font-bold uppercase tracking-widest text-zinc-600">Channels</h3>
|
<h3 className="text-[11px] font-bold uppercase tracking-widest text-zinc-600">Channels</h3>
|
||||||
</Collapsible.Trigger>
|
</Collapsible.Trigger>
|
||||||
<Collapsible.Content></Collapsible.Content>
|
<Collapsible.Content>
|
||||||
|
<ChannelList />
|
||||||
|
</Collapsible.Content>
|
||||||
</div>
|
</div>
|
||||||
</Collapsible.Root>
|
</Collapsible.Root>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,16 +2,13 @@ import { ImageWithFallback } from '@components/imageWithFallback';
|
|||||||
import { RelayContext } from '@components/relaysProvider';
|
import { RelayContext } from '@components/relaysProvider';
|
||||||
import { UserExtend } from '@components/user/extend';
|
import { UserExtend } from '@components/user/extend';
|
||||||
|
|
||||||
import { activeAccountAtom } from '@stores/account';
|
|
||||||
|
|
||||||
import { dateToUnix } from '@utils/getDate';
|
import { dateToUnix } from '@utils/getDate';
|
||||||
|
|
||||||
import CommentIcon from '@assets/icons/comment';
|
import CommentIcon from '@assets/icons/comment';
|
||||||
|
|
||||||
import * as Dialog from '@radix-ui/react-dialog';
|
import * as Dialog from '@radix-ui/react-dialog';
|
||||||
import { SizeIcon } from '@radix-ui/react-icons';
|
import { SizeIcon } from '@radix-ui/react-icons';
|
||||||
import destr from 'destr';
|
import useLocalStorage from '@rehooks/local-storage';
|
||||||
import { useAtomValue } from 'jotai';
|
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { getEventHash, signEvent } from 'nostr-tools';
|
import { getEventHash, signEvent } from 'nostr-tools';
|
||||||
import { memo, useContext, useState } from 'react';
|
import { memo, useContext, useState } from 'react';
|
||||||
@@ -26,7 +23,7 @@ export const NoteComment = memo(function NoteComment({
|
|||||||
count: number;
|
count: number;
|
||||||
eventID: string;
|
eventID: string;
|
||||||
eventPubkey: string;
|
eventPubkey: string;
|
||||||
eventTime: string;
|
eventTime: number;
|
||||||
eventContent: any;
|
eventContent: any;
|
||||||
}) {
|
}) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -35,8 +32,8 @@ export const NoteComment = memo(function NoteComment({
|
|||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [value, setValue] = useState('');
|
const [value, setValue] = useState('');
|
||||||
|
|
||||||
const activeAccount: any = useAtomValue(activeAccountAtom);
|
const [activeAccount]: any = useLocalStorage('activeAccount', {});
|
||||||
const profile = destr(activeAccount.metadata);
|
const profile = activeAccount.metadata ? JSON.parse(activeAccount.metadata) : null;
|
||||||
|
|
||||||
const openThread = () => {
|
const openThread = () => {
|
||||||
router.push(`/newsfeed/${eventID}`);
|
router.push(`/newsfeed/${eventID}`);
|
||||||
@@ -68,8 +65,8 @@ export const NoteComment = memo(function NoteComment({
|
|||||||
</button>
|
</button>
|
||||||
</Dialog.Trigger>
|
</Dialog.Trigger>
|
||||||
<Dialog.Portal>
|
<Dialog.Portal>
|
||||||
<Dialog.Overlay className="fixed inset-0 bg-black bg-opacity-30 backdrop-blur-sm data-[state=open]:animate-overlayShow" />
|
<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 overflow-y-auto">
|
<Dialog.Content className="fixed inset-0 z-50 overflow-y-auto">
|
||||||
<div className="flex min-h-full items-center justify-center">
|
<div className="flex min-h-full items-center justify-center">
|
||||||
<div className="relative w-full max-w-2xl rounded-lg bg-zinc-900 p-4 text-zinc-100 ring-1 ring-zinc-800">
|
<div className="relative w-full max-w-2xl rounded-lg bg-zinc-900 p-4 text-zinc-100 ring-1 ring-zinc-800">
|
||||||
{/* root note */}
|
{/* root note */}
|
||||||
@@ -90,7 +87,7 @@ export const NoteComment = memo(function NoteComment({
|
|||||||
<div>
|
<div>
|
||||||
<div className="relative h-11 w-11 shrink-0 overflow-hidden rounded-md border border-white/10">
|
<div className="relative h-11 w-11 shrink-0 overflow-hidden rounded-md border border-white/10">
|
||||||
<ImageWithFallback
|
<ImageWithFallback
|
||||||
src={profile.picture}
|
src={profile?.picture}
|
||||||
alt="user's avatar"
|
alt="user's avatar"
|
||||||
fill={true}
|
fill={true}
|
||||||
className="rounded-md object-cover"
|
className="rounded-md object-cover"
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
import { RelayContext } from '@components/relaysProvider';
|
import { RelayContext } from '@components/relaysProvider';
|
||||||
|
|
||||||
import { activeAccountAtom } from '@stores/account';
|
|
||||||
|
|
||||||
import { dateToUnix } from '@utils/getDate';
|
import { dateToUnix } from '@utils/getDate';
|
||||||
|
|
||||||
import LikeIcon from '@assets/icons/like';
|
import LikeIcon from '@assets/icons/like';
|
||||||
import LikedIcon from '@assets/icons/liked';
|
import LikedIcon from '@assets/icons/liked';
|
||||||
|
|
||||||
import { useAtomValue } from 'jotai';
|
import useLocalStorage from '@rehooks/local-storage';
|
||||||
import { getEventHash, signEvent } from 'nostr-tools';
|
import { getEventHash, signEvent } from 'nostr-tools';
|
||||||
import { memo, useContext, useEffect, useState } from 'react';
|
import { memo, useContext, useEffect, useState } from 'react';
|
||||||
|
|
||||||
@@ -22,8 +20,7 @@ export const NoteReaction = memo(function NoteReaction({
|
|||||||
}) {
|
}) {
|
||||||
const [pool, relays]: any = useContext(RelayContext);
|
const [pool, relays]: any = useContext(RelayContext);
|
||||||
|
|
||||||
const activeAccount: any = useAtomValue(activeAccountAtom);
|
const [activeAccount]: any = useLocalStorage('activeAccount', {});
|
||||||
|
|
||||||
const [isReact, setIsReact] = useState(false);
|
const [isReact, setIsReact] = useState(false);
|
||||||
const [like, setLike] = useState(0);
|
const [like, setLike] = useState(0);
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export default function NoteMetadata({
|
|||||||
{
|
{
|
||||||
'#e': [eventID],
|
'#e': [eventID],
|
||||||
since: parseInt(eventTime),
|
since: parseInt(eventTime),
|
||||||
kinds: [1, 7],
|
kinds: [7],
|
||||||
limit: 50,
|
limit: 50,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -48,11 +48,7 @@ export default function NoteMetadata({
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
1000,
|
1000
|
||||||
undefined,
|
|
||||||
{
|
|
||||||
unsubscribeOnEose: true,
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { UserMention } from '@components/user/mention';
|
|||||||
|
|
||||||
import { getParentID } from '@utils/transform';
|
import { getParentID } from '@utils/transform';
|
||||||
|
|
||||||
|
import useLocalStorage from '@rehooks/local-storage';
|
||||||
import destr from 'destr';
|
import destr from 'destr';
|
||||||
import { memo, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
import { memo, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import reactStringReplace from 'react-string-replace';
|
import reactStringReplace from 'react-string-replace';
|
||||||
@@ -15,12 +16,13 @@ import reactStringReplace from 'react-string-replace';
|
|||||||
export const NoteParent = memo(function NoteParent({ id }: { id: string }) {
|
export const NoteParent = memo(function NoteParent({ id }: { id: string }) {
|
||||||
const [pool, relays]: any = useContext(RelayContext);
|
const [pool, relays]: any = useContext(RelayContext);
|
||||||
|
|
||||||
|
const [activeAccount]: any = useLocalStorage('activeAccount', {});
|
||||||
const [event, setEvent] = useState(null);
|
const [event, setEvent] = useState(null);
|
||||||
|
|
||||||
const unsubscribe = useRef(null);
|
const unsubscribe = useRef(null);
|
||||||
|
|
||||||
const fetchEvent = useCallback(async () => {
|
const fetchEvent = useCallback(async () => {
|
||||||
const { createNote } = await import('@utils/bindings');
|
const { createNote } = await import('@utils/bindings');
|
||||||
const activeAccount = JSON.parse(localStorage.getItem('activeAccount'));
|
|
||||||
|
|
||||||
unsubscribe.current = pool.subscribe(
|
unsubscribe.current = pool.subscribe(
|
||||||
[
|
[
|
||||||
@@ -54,7 +56,7 @@ export const NoteParent = memo(function NoteParent({ id }: { id: string }) {
|
|||||||
unsubscribeOnEose: true,
|
unsubscribeOnEose: true,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}, [id, pool, relays]);
|
}, [activeAccount.id, id, pool, relays]);
|
||||||
|
|
||||||
const checkNoteExist = useCallback(async () => {
|
const checkNoteExist = useCallback(async () => {
|
||||||
const { getNoteById } = await import('@utils/bindings');
|
const { getNoteById } = await import('@utils/bindings');
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { UserMention } from '@components/user/mention';
|
|||||||
|
|
||||||
import { getParentID } from '@utils/transform';
|
import { getParentID } from '@utils/transform';
|
||||||
|
|
||||||
|
import useLocalStorage from '@rehooks/local-storage';
|
||||||
import destr from 'destr';
|
import destr from 'destr';
|
||||||
import { memo, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
import { memo, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import reactStringReplace from 'react-string-replace';
|
import reactStringReplace from 'react-string-replace';
|
||||||
@@ -11,12 +12,13 @@ import reactStringReplace from 'react-string-replace';
|
|||||||
export const NoteRepost = memo(function NoteRepost({ id }: { id: string }) {
|
export const NoteRepost = memo(function NoteRepost({ id }: { id: string }) {
|
||||||
const [pool, relays]: any = useContext(RelayContext);
|
const [pool, relays]: any = useContext(RelayContext);
|
||||||
|
|
||||||
|
const [activeAccount]: any = useLocalStorage('activeAccount', {});
|
||||||
const [event, setEvent] = useState(null);
|
const [event, setEvent] = useState(null);
|
||||||
|
|
||||||
const unsubscribe = useRef(null);
|
const unsubscribe = useRef(null);
|
||||||
|
|
||||||
const fetchEvent = useCallback(async () => {
|
const fetchEvent = useCallback(async () => {
|
||||||
const { createNote } = await import('@utils/bindings');
|
const { createNote } = await import('@utils/bindings');
|
||||||
const activeAccount = JSON.parse(localStorage.getItem('activeAccount'));
|
|
||||||
|
|
||||||
unsubscribe.current = pool.subscribe(
|
unsubscribe.current = pool.subscribe(
|
||||||
[
|
[
|
||||||
@@ -50,7 +52,7 @@ export const NoteRepost = memo(function NoteRepost({ id }: { id: string }) {
|
|||||||
unsubscribeOnEose: true,
|
unsubscribeOnEose: true,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}, [id, pool, relays]);
|
}, [activeAccount.id, id, pool, relays]);
|
||||||
|
|
||||||
const checkNoteExist = useCallback(async () => {
|
const checkNoteExist = useCallback(async () => {
|
||||||
const { getNoteById } = await import('@utils/bindings');
|
const { getNoteById } = await import('@utils/bindings');
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export const RelayContext = createContext({});
|
|||||||
const relays = [
|
const relays = [
|
||||||
'wss://relay.damus.io',
|
'wss://relay.damus.io',
|
||||||
'wss://nostr-pub.wellorder.net',
|
'wss://nostr-pub.wellorder.net',
|
||||||
'wss://nostr.bongbong.com',
|
//'wss://nostr.bongbong.com',
|
||||||
'wss://nostr.zebedee.cloud',
|
'wss://nostr.zebedee.cloud',
|
||||||
'wss://nostr.fmt.wiz.biz',
|
'wss://nostr.fmt.wiz.biz',
|
||||||
'wss://relay.snort.social',
|
'wss://relay.snort.social',
|
||||||
|
|||||||
26
src/components/user/mini.tsx
Normal file
26
src/components/user/mini.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { ImageWithFallback } from '@components/imageWithFallback';
|
||||||
|
|
||||||
|
import { DEFAULT_AVATAR } from '@stores/constants';
|
||||||
|
|
||||||
|
import { useMetadata } from '@utils/metadata';
|
||||||
|
import { truncate } from '@utils/truncate';
|
||||||
|
|
||||||
|
export const UserMini = ({ pubkey }: { pubkey: string }) => {
|
||||||
|
const profile = useMetadata(pubkey);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="group flex items-start gap-1">
|
||||||
|
<div className="relative h-7 w-7 shrink overflow-hidden rounded border border-white/10">
|
||||||
|
<ImageWithFallback
|
||||||
|
src={profile?.picture || DEFAULT_AVATAR}
|
||||||
|
alt={pubkey}
|
||||||
|
fill={true}
|
||||||
|
className="rounded object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className="text-xs font-medium leading-none text-zinc-500">
|
||||||
|
Replying to {profile?.name || truncate(pubkey, 16, ' .... ')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
import ActiveLink from '@components/activeLink';
|
|
||||||
import AccountColumn from '@components/columns/account';
|
|
||||||
|
|
||||||
import { useLocalStorage } from '@rehooks/local-storage';
|
|
||||||
|
|
||||||
export default function UserLayout({ children }: { children: React.ReactNode }) {
|
|
||||||
const [currentUser]: any = useLocalStorage('current-user');
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex h-full w-full flex-row">
|
|
||||||
<div className="relative h-full w-[70px] shrink-0 border-r border-zinc-900">
|
|
||||||
<div data-tauri-drag-region className="absolute top-0 left-0 h-12 w-full" />
|
|
||||||
<AccountColumn />
|
|
||||||
</div>
|
|
||||||
<div className="grid grow grid-cols-4">
|
|
||||||
<div className="col-span-1">
|
|
||||||
<div className="flex h-full flex-col flex-wrap justify-between overflow-hidden px-2 pt-3 pb-4">
|
|
||||||
{/* main */}
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
{/* menu */}
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<div className="flex items-center justify-between px-2">
|
|
||||||
<h3 className="text-sm font-bold text-zinc-400">Menu</h3>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-1 text-zinc-500">
|
|
||||||
<ActiveLink
|
|
||||||
href={`/profile/${currentUser.id}`}
|
|
||||||
activeClassName="ring-1 ring-white/10 dark:bg-zinc-900 dark:text-white"
|
|
||||||
className="flex h-10 items-center gap-1 rounded-lg px-2.5 text-sm font-medium hover:bg-zinc-900"
|
|
||||||
>
|
|
||||||
<span>Personal Page</span>
|
|
||||||
</ActiveLink>
|
|
||||||
<ActiveLink
|
|
||||||
href={`/profile/update?pubkey=${currentUser.id}`}
|
|
||||||
activeClassName="ring-1 ring-white/10 dark:bg-zinc-900 dark:text-white"
|
|
||||||
className="flex h-10 items-center gap-1 rounded-lg px-2.5 text-sm font-medium hover:bg-zinc-900"
|
|
||||||
>
|
|
||||||
<span>Update Profile</span>
|
|
||||||
</ActiveLink>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="col-span-3 m-3 ml-0 overflow-hidden rounded-lg border border-zinc-800 bg-zinc-900 shadow-input shadow-black/20">
|
|
||||||
<div className="h-full w-full rounded-lg">{children}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
101
src/pages/channels/[id].tsx
Normal file
101
src/pages/channels/[id].tsx
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import BaseLayout from '@layouts/base';
|
||||||
|
import WithSidebarLayout from '@layouts/withSidebar';
|
||||||
|
|
||||||
|
import { ChannelMessages } from '@components/channels/messages/index';
|
||||||
|
import FormChannelMessage from '@components/form/channelMessage';
|
||||||
|
import { RelayContext } from '@components/relaysProvider';
|
||||||
|
|
||||||
|
import { channelReplyAtom } from '@stores/channel';
|
||||||
|
|
||||||
|
import useLocalStorage from '@rehooks/local-storage';
|
||||||
|
import { useResetAtom } from 'jotai/utils';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
import {
|
||||||
|
JSXElementConstructor,
|
||||||
|
ReactElement,
|
||||||
|
ReactFragment,
|
||||||
|
ReactPortal,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
const [pool, relays]: any = useContext(RelayContext);
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const id: string | string[] = router.query.id || null;
|
||||||
|
|
||||||
|
const [messages, setMessages] = useState([]);
|
||||||
|
const [activeAccount]: any = useLocalStorage('activeAccount', {});
|
||||||
|
const resetChannelReply = useResetAtom(channelReplyAtom);
|
||||||
|
|
||||||
|
const muted = useRef(new Set());
|
||||||
|
const hided = useRef(new Set());
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// reset channel reply
|
||||||
|
resetChannelReply();
|
||||||
|
// subscribe event
|
||||||
|
const unsubscribe = pool.subscribe(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
authors: [activeAccount.pubkey],
|
||||||
|
kinds: [43, 44],
|
||||||
|
since: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'#e': [id],
|
||||||
|
kinds: [42],
|
||||||
|
since: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
relays,
|
||||||
|
(event: any) => {
|
||||||
|
if (event.kind === 44) {
|
||||||
|
muted.current = muted.current.add(event.tags[0][1]);
|
||||||
|
} else if (event.kind === 43) {
|
||||||
|
hided.current = hided.current.add(event.tags[0][1]);
|
||||||
|
} else {
|
||||||
|
if (muted.current.has(event.pubkey)) {
|
||||||
|
console.log('muted');
|
||||||
|
} else if (hided.current.has(event.id)) {
|
||||||
|
console.log('hided');
|
||||||
|
} else {
|
||||||
|
setMessages((messages) => [event, ...messages]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsubscribe;
|
||||||
|
};
|
||||||
|
}, [id, pool, relays, activeAccount.pubkey, resetChannelReply]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-full w-full flex-col justify-between">
|
||||||
|
<ChannelMessages data={messages.sort((a, b) => a.created_at - b.created_at)} />
|
||||||
|
<div className="shrink-0 p-3">
|
||||||
|
<FormChannelMessage eventId={id} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Page.getLayout = function getLayout(
|
||||||
|
page:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| ReactElement<unknown, string | JSXElementConstructor<unknown>>
|
||||||
|
| ReactFragment
|
||||||
|
| ReactPortal
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<BaseLayout>
|
||||||
|
<WithSidebarLayout>{page}</WithSidebarLayout>
|
||||||
|
</BaseLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
45
src/pages/channels/index.tsx
Normal file
45
src/pages/channels/index.tsx
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import BaseLayout from '@layouts/base';
|
||||||
|
import WithSidebarLayout from '@layouts/withSidebar';
|
||||||
|
|
||||||
|
import { BrowseChannelItem } from '@components/channels/browseChannelItem';
|
||||||
|
|
||||||
|
import { JSXElementConstructor, ReactElement, ReactFragment, ReactPortal, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
const [list, setList] = useState([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchChannels = async () => {
|
||||||
|
const { getChannels } = await import('@utils/bindings');
|
||||||
|
return await getChannels({ limit: 100, offset: 0 });
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchChannels()
|
||||||
|
.then((res) => setList(res))
|
||||||
|
.catch(console.error);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-full w-full overflow-y-auto">
|
||||||
|
{list.map((channel) => (
|
||||||
|
<BrowseChannelItem key={channel.id} data={channel} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Page.getLayout = function getLayout(
|
||||||
|
page:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| ReactElement<unknown, string | JSXElementConstructor<unknown>>
|
||||||
|
| ReactFragment
|
||||||
|
| ReactPortal
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<BaseLayout>
|
||||||
|
<WithSidebarLayout>{page}</WithSidebarLayout>
|
||||||
|
</BaseLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -5,9 +5,7 @@ import { MessageList } from '@components/chats/messageList';
|
|||||||
import FormChat from '@components/form/chat';
|
import FormChat from '@components/form/chat';
|
||||||
import { RelayContext } from '@components/relaysProvider';
|
import { RelayContext } from '@components/relaysProvider';
|
||||||
|
|
||||||
import { activeAccountAtom } from '@stores/account';
|
import useLocalStorage from '@rehooks/local-storage';
|
||||||
|
|
||||||
import { useAtomValue } from 'jotai';
|
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import {
|
import {
|
||||||
JSXElementConstructor,
|
JSXElementConstructor,
|
||||||
@@ -25,7 +23,7 @@ export default function Page() {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const pubkey: any = router.query.pubkey || null;
|
const pubkey: any = router.query.pubkey || null;
|
||||||
|
|
||||||
const activeAccount: any = useAtomValue(activeAccountAtom);
|
const [activeAccount]: any = useLocalStorage('activeAccount', {});
|
||||||
const [messages, setMessages] = useState([]);
|
const [messages, setMessages] = useState([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -1,17 +1,13 @@
|
|||||||
import BaseLayout from '@layouts/base';
|
import BaseLayout from '@layouts/base';
|
||||||
|
|
||||||
import { activeAccountAtom, activeAccountFollowsAtom } from '@stores/account';
|
|
||||||
|
|
||||||
import LumeSymbol from '@assets/icons/Lume';
|
import LumeSymbol from '@assets/icons/Lume';
|
||||||
|
|
||||||
import { useSetAtom } from 'jotai';
|
import { writeStorage } from '@rehooks/local-storage';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { JSXElementConstructor, ReactElement, ReactFragment, ReactPortal, useCallback, useEffect } from 'react';
|
import { JSXElementConstructor, ReactElement, ReactFragment, ReactPortal, useCallback, useEffect } from 'react';
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const setActiveAccount = useSetAtom(activeAccountAtom);
|
|
||||||
const setActiveAccountFollows = useSetAtom(activeAccountFollowsAtom);
|
|
||||||
|
|
||||||
const fetchActiveAccount = useCallback(async () => {
|
const fetchActiveAccount = useCallback(async () => {
|
||||||
const { getAccounts } = await import('@utils/bindings');
|
const { getAccounts } = await import('@utils/bindings');
|
||||||
@@ -20,7 +16,7 @@ export default function Page() {
|
|||||||
|
|
||||||
const fetchFollowsByAccount = useCallback(async (id) => {
|
const fetchFollowsByAccount = useCallback(async (id) => {
|
||||||
const { getPlebs } = await import('@utils/bindings');
|
const { getPlebs } = await import('@utils/bindings');
|
||||||
return await getPlebs({ account_id: id });
|
return await getPlebs({ account_id: id, kind: 0 });
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -29,10 +25,10 @@ export default function Page() {
|
|||||||
if (res.length > 0) {
|
if (res.length > 0) {
|
||||||
// fetch follows
|
// fetch follows
|
||||||
fetchFollowsByAccount(res[0].id).then((follows) => {
|
fetchFollowsByAccount(res[0].id).then((follows) => {
|
||||||
setActiveAccountFollows(follows);
|
writeStorage('activeAccountFollows', follows);
|
||||||
});
|
});
|
||||||
// update local storage
|
// update local storage
|
||||||
setActiveAccount(res[0]);
|
writeStorage('activeAccount', res[0]);
|
||||||
// redirect
|
// redirect
|
||||||
router.replace('/init');
|
router.replace('/init');
|
||||||
} else {
|
} else {
|
||||||
@@ -40,7 +36,7 @@ export default function Page() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(console.error);
|
.catch(console.error);
|
||||||
}, [fetchActiveAccount, setActiveAccount, fetchFollowsByAccount, setActiveAccountFollows, router]);
|
}, [fetchActiveAccount, fetchFollowsByAccount, router]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative h-full overflow-hidden">
|
<div className="relative h-full overflow-hidden">
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { getParentID, pubkeyArray } from '@utils/transform';
|
|||||||
|
|
||||||
import LumeSymbol from '@assets/icons/Lume';
|
import LumeSymbol from '@assets/icons/Lume';
|
||||||
|
|
||||||
|
import { useLocalStorage } from '@rehooks/local-storage';
|
||||||
import { invoke } from '@tauri-apps/api/tauri';
|
import { invoke } from '@tauri-apps/api/tauri';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import {
|
import {
|
||||||
@@ -30,11 +31,13 @@ export default function Page() {
|
|||||||
|
|
||||||
const [eose, setEose] = useState(false);
|
const [eose, setEose] = useState(false);
|
||||||
|
|
||||||
|
const [lastLogin] = useLocalStorage('lastLogin', '');
|
||||||
|
const [activeAccount]: any = useLocalStorage('activeAccount', {});
|
||||||
|
const [follows] = useLocalStorage('activeAccountFollows', []);
|
||||||
|
|
||||||
const fetchData = useCallback(
|
const fetchData = useCallback(
|
||||||
async (since: Date) => {
|
async (since: Date) => {
|
||||||
const { createNote } = await import('@utils/bindings');
|
const { createNote } = await import('@utils/bindings');
|
||||||
const activeAccount = JSON.parse(localStorage.getItem('activeAccount'));
|
|
||||||
const follows = JSON.parse(localStorage.getItem('activeAccountFollows'));
|
|
||||||
|
|
||||||
unsubscribe.current = pool.subscribe(
|
unsubscribe.current = pool.subscribe(
|
||||||
[
|
[
|
||||||
@@ -67,20 +70,19 @@ export default function Page() {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[pool, relays]
|
[activeAccount.id, follows, pool, relays]
|
||||||
);
|
);
|
||||||
|
|
||||||
const isNoteExist = useCallback(async () => {
|
const isNoteExist = useCallback(async () => {
|
||||||
invoke('count_total_notes').then((res: number) => {
|
invoke('count_total_notes').then((res: number) => {
|
||||||
if (res > 0) {
|
if (res > 0) {
|
||||||
const lastLogin = JSON.parse(localStorage.getItem('lastLogin'));
|
|
||||||
const parseDate = new Date(lastLogin);
|
const parseDate = new Date(lastLogin);
|
||||||
fetchData(parseDate);
|
fetchData(parseDate);
|
||||||
} else {
|
} else {
|
||||||
fetchData(hoursAgo(24, now.current));
|
fetchData(hoursAgo(24, now.current));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, [fetchData]);
|
}, [fetchData, lastLogin]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (eose === false) {
|
if (eose === false) {
|
||||||
|
|||||||
@@ -1,53 +1,10 @@
|
|||||||
import BaseLayout from '@layouts/base';
|
import BaseLayout from '@layouts/base';
|
||||||
import WithSidebarLayout from '@layouts/withSidebar';
|
import WithSidebarLayout from '@layouts/withSidebar';
|
||||||
|
|
||||||
import FormComment from '@components/form/comment';
|
import { JSXElementConstructor, ReactElement, ReactFragment, ReactPortal } from 'react';
|
||||||
import { NoteComment } from '@components/note/comment';
|
|
||||||
import { NoteExtend } from '@components/note/extend';
|
|
||||||
import { RelayContext } from '@components/relaysProvider';
|
|
||||||
|
|
||||||
import { useRouter } from 'next/router';
|
|
||||||
import {
|
|
||||||
JSXElementConstructor,
|
|
||||||
ReactElement,
|
|
||||||
ReactFragment,
|
|
||||||
ReactPortal,
|
|
||||||
useContext,
|
|
||||||
useEffect,
|
|
||||||
useState,
|
|
||||||
} from 'react';
|
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
const [pool, relays]: any = useContext(RelayContext);
|
return <div className="scrollbar-hide flex h-full flex-col gap-2 overflow-y-auto py-3"></div>;
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const id = router.query.id || null;
|
|
||||||
|
|
||||||
const [rootEvent, setRootEvent] = useState(null);
|
|
||||||
const [comments, setComments] = useState([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
/*getNoteByID(id)
|
|
||||||
.then((res) => {
|
|
||||||
setRootEvent(res);
|
|
||||||
getAllCommentNotes(id).then((res: any) => setComments(res));
|
|
||||||
})
|
|
||||||
.catch(console.error);*/
|
|
||||||
}, [id, pool, relays]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="scrollbar-hide flex h-full flex-col gap-2 overflow-y-auto py-3">
|
|
||||||
<div className="flex h-min min-h-min w-full select-text flex-col px-3">
|
|
||||||
{rootEvent && <NoteExtend event={rootEvent} />}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<FormComment eventID={id} />
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col">
|
|
||||||
{comments.length > 0 && comments.map((comment) => <NoteComment key={comment.id} event={comment} />)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Page.getLayout = function getLayout(
|
Page.getLayout = function getLayout(
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ export default function Page() {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
for (const follow of follows) {
|
for (const follow of follows) {
|
||||||
const metadata: any = await fetchMetadata(follow, pool, relays);
|
const metadata: any = await fetchMetadata(follow);
|
||||||
createPleb({
|
createPleb({
|
||||||
pleb_id: follow + '-lume' + id,
|
pleb_id: follow + '-lume' + id,
|
||||||
pubkey: follow,
|
pubkey: follow,
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export default function Page() {
|
|||||||
const { createPleb } = await import('@utils/bindings');
|
const { createPleb } = await import('@utils/bindings');
|
||||||
if (profile?.id !== null) {
|
if (profile?.id !== null) {
|
||||||
for (const tag of tags) {
|
for (const tag of tags) {
|
||||||
const metadata: any = await fetchMetadata(tag[1], pool, relays);
|
const metadata: any = await fetchMetadata(tag[1]);
|
||||||
createPleb({
|
createPleb({
|
||||||
pleb_id: tag[1] + '-lume' + profile.id.toString(),
|
pleb_id: tag[1] + '-lume' + profile.id.toString(),
|
||||||
pubkey: tag[1],
|
pubkey: tag[1],
|
||||||
@@ -60,7 +60,7 @@ export default function Page() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[pool, profile.id, relays]
|
[profile.id]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
import { atomWithStorage, createJSONStorage } from 'jotai/utils';
|
|
||||||
|
|
||||||
const createMyJsonStorage = () => {
|
|
||||||
const storage = createJSONStorage(() => localStorage);
|
|
||||||
const getItem = (key) => {
|
|
||||||
const value = storage.getItem(key);
|
|
||||||
return value;
|
|
||||||
};
|
|
||||||
return { ...storage, getItem };
|
|
||||||
};
|
|
||||||
|
|
||||||
export const activeAccountAtom = atomWithStorage('activeAccount', {}, createMyJsonStorage());
|
|
||||||
export const activeAccountFollowsAtom = atomWithStorage('activeAccountFollows', [], createMyJsonStorage());
|
|
||||||
export const lastLoginAtom = atomWithStorage('lastLogin', [], createMyJsonStorage());
|
|
||||||
4
src/stores/channel.tsx
Normal file
4
src/stores/channel.tsx
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import { atomWithReset } from 'jotai/utils';
|
||||||
|
|
||||||
|
// channel reply id
|
||||||
|
export const channelReplyAtom = atomWithReset({ id: null, pubkey: null, content: null });
|
||||||
@@ -44,20 +44,30 @@ export function getNoteById(data: GetNoteByIdData) {
|
|||||||
return invoke<Note | null>('get_note_by_id', { data });
|
return invoke<Note | null>('get_note_by_id', { data });
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CreateNoteData = {
|
export function createChannel(data: CreateChannelData) {
|
||||||
event_id: string;
|
return invoke<Channel>('create_channel', { data });
|
||||||
pubkey: string;
|
}
|
||||||
kind: number;
|
|
||||||
tags: string;
|
export function updateChannel(data: UpdateChannelData) {
|
||||||
content: string;
|
return invoke<Channel>('update_channel', { data });
|
||||||
parent_id: string;
|
}
|
||||||
parent_comment_id: string;
|
|
||||||
created_at: number;
|
export function getChannels(data: GetChannelData) {
|
||||||
account_id: number;
|
return invoke<Channel[]>('get_channels', { data });
|
||||||
};
|
}
|
||||||
export type CreatePlebData = { pleb_id: string; pubkey: string; kind: number; metadata: string; account_id: number };
|
|
||||||
export type GetNoteByIdData = { event_id: string };
|
export function getActiveChannels(data: GetActiveChannelData) {
|
||||||
export type Pleb = { id: number; plebId: string; pubkey: string; kind: number; metadata: string; accountId: number };
|
return invoke<Channel[]>('get_active_channels', { data });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createChat(data: CreateChatData) {
|
||||||
|
return invoke<Chat>('create_chat', { data });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getChats(data: GetChatData) {
|
||||||
|
return invoke<Chat[]>('get_chats', { data });
|
||||||
|
}
|
||||||
|
|
||||||
export type Note = {
|
export type Note = {
|
||||||
id: number;
|
id: number;
|
||||||
eventId: string;
|
eventId: string;
|
||||||
@@ -70,9 +80,31 @@ export type Note = {
|
|||||||
createdAt: number;
|
createdAt: number;
|
||||||
accountId: number;
|
accountId: number;
|
||||||
};
|
};
|
||||||
|
export type CreateChannelData = { event_id: string; content: string; account_id: number };
|
||||||
|
export type CreatePlebData = { pleb_id: string; pubkey: string; kind: number; metadata: string; account_id: number };
|
||||||
|
export type Chat = { id: number; pubkey: string; createdAt: number; accountId: number };
|
||||||
export type Account = { id: number; pubkey: string; privkey: string; active: boolean; metadata: string };
|
export type Account = { id: number; pubkey: string; privkey: string; active: boolean; metadata: string };
|
||||||
export type GetPlebPubkeyData = { pubkey: string };
|
export type GetChannelData = { limit: number; offset: number };
|
||||||
export type GetPlebData = { account_id: number };
|
|
||||||
export type CreateAccountData = { pubkey: string; privkey: string; metadata: string };
|
|
||||||
export type GetLatestNoteData = { date: number };
|
export type GetLatestNoteData = { date: number };
|
||||||
|
export type GetPlebData = { account_id: number; kind: number };
|
||||||
|
export type CreateAccountData = { pubkey: string; privkey: string; metadata: string };
|
||||||
|
export type GetPlebPubkeyData = { pubkey: string };
|
||||||
|
export type Channel = { id: number; eventId: string; content: string; active: boolean; accountId: number };
|
||||||
|
export type GetChatData = { account_id: number };
|
||||||
|
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 UpdateChannelData = { event_id: string; active: boolean };
|
||||||
|
export type Pleb = { id: number; plebId: string; pubkey: string; kind: number; metadata: string; accountId: number };
|
||||||
|
export type CreateChatData = { pubkey: string; created_at: number; account_id: number };
|
||||||
export type GetNoteData = { date: number; limit: number; offset: number };
|
export type GetNoteData = { date: number; limit: number; offset: number };
|
||||||
|
export type GetActiveChannelData = { active: boolean };
|
||||||
|
export type GetNoteByIdData = { event_id: string };
|
||||||
|
|||||||
@@ -1,35 +1,52 @@
|
|||||||
import { RelayContext } from '@components/relaysProvider';
|
import { fetch } from '@tauri-apps/api/http';
|
||||||
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { Author } from 'nostr-relaypool';
|
export const fetchMetadata = async (pubkey: string) => {
|
||||||
import { useCallback, useContext, useEffect, useState } from 'react';
|
const result = await fetch(`https://rbr.bio/${pubkey}/metadata.json`, {
|
||||||
|
method: 'GET',
|
||||||
export const fetchMetadata = (pubkey: string, pool: any, relays: any) => {
|
timeout: 5,
|
||||||
const author = new Author(pool, relays, pubkey);
|
});
|
||||||
return new Promise((resolve) => author.metaData(resolve, 0));
|
return await result.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useMetadata = (pubkey) => {
|
export const useMetadata = (pubkey) => {
|
||||||
const [pool, relays]: any = useContext(RelayContext);
|
|
||||||
const [profile, setProfile] = useState(null);
|
const [profile, setProfile] = useState(null);
|
||||||
|
|
||||||
|
/*
|
||||||
|
const insertPlebToDB = useCallback(async (account, pubkey, metadata) => {
|
||||||
|
const { createPleb } = await import('@utils/bindings');
|
||||||
|
return await createPleb({
|
||||||
|
pleb_id: pubkey + '-lume' + account.toString(),
|
||||||
|
pubkey: pubkey,
|
||||||
|
kind: 1,
|
||||||
|
metadata: metadata,
|
||||||
|
account_id: account,
|
||||||
|
}).catch(console.error);
|
||||||
|
}, []);
|
||||||
|
*/
|
||||||
|
|
||||||
const getCachedMetadata = useCallback(async () => {
|
const getCachedMetadata = useCallback(async () => {
|
||||||
const { getPlebByPubkey } = await import('@utils/bindings');
|
const { getPlebByPubkey } = await import('@utils/bindings');
|
||||||
getPlebByPubkey({ pubkey: pubkey })
|
getPlebByPubkey({ pubkey: pubkey })
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (res) {
|
if (res) {
|
||||||
const metadata = JSON.parse(res.metadata);
|
const metadata = JSON.parse(res.metadata);
|
||||||
|
// update state
|
||||||
setProfile(metadata);
|
setProfile(metadata);
|
||||||
} else {
|
} else {
|
||||||
fetchMetadata(pubkey, pool, relays).then((res: any) => {
|
fetchMetadata(pubkey).then((res: any) => {
|
||||||
if (res.content) {
|
if (res.content) {
|
||||||
const metadata = JSON.parse(res.content);
|
const metadata = JSON.parse(res.content);
|
||||||
|
// update state
|
||||||
setProfile(metadata);
|
setProfile(metadata);
|
||||||
|
// save to database
|
||||||
|
// insertPlebToDB(activeAccount.id, pubkey, metadata);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(console.error);
|
.catch(console.error);
|
||||||
}, [pool, relays, pubkey]);
|
}, [pubkey]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getCachedMetadata().catch(console.error);
|
getCachedMetadata().catch(console.error);
|
||||||
|
|||||||
Reference in New Issue
Block a user