mirror of
https://github.com/luminous-devs/lume.git
synced 2024-09-19 11:43:30 +00:00
Merge pull request #24 from reyamir/feat/channels
Initial support for public chat channels (NIP-28)
This commit is contained in:
commit
1f73be3a06
@ -15,12 +15,15 @@
|
||||
"dependencies": {
|
||||
"@emoji-mart/data": "^1.1.2",
|
||||
"@emoji-mart/react": "^1.1.1",
|
||||
"@radix-ui/react-alert-dialog": "^1.0.3",
|
||||
"@radix-ui/react-collapsible": "^1.0.2",
|
||||
"@radix-ui/react-dialog": "^1.0.3",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.4",
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
"@radix-ui/react-popover": "^1.0.5",
|
||||
"@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",
|
||||
"@tauri-apps/api": "^1.2.0",
|
||||
"dayjs": "^1.11.7",
|
||||
|
@ -3,12 +3,15 @@ lockfileVersion: 5.4
|
||||
specifiers:
|
||||
'@emoji-mart/data': ^1.1.2
|
||||
'@emoji-mart/react': ^1.1.1
|
||||
'@radix-ui/react-alert-dialog': ^1.0.3
|
||||
'@radix-ui/react-collapsible': ^1.0.2
|
||||
'@radix-ui/react-dialog': ^1.0.3
|
||||
'@radix-ui/react-dropdown-menu': ^2.0.4
|
||||
'@radix-ui/react-icons': ^1.3.0
|
||||
'@radix-ui/react-popover': ^1.0.5
|
||||
'@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
|
||||
'@tailwindcss/typography': ^0.5.9
|
||||
'@tauri-apps/api': ^1.2.0
|
||||
@ -54,12 +57,15 @@ specifiers:
|
||||
dependencies:
|
||||
'@emoji-mart/data': 1.1.2
|
||||
'@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-dialog': 1.0.3_zn3vyfk3tbnwebg5ldvieekjaq
|
||||
'@radix-ui/react-dropdown-menu': 2.0.4_zn3vyfk3tbnwebg5ldvieekjaq
|
||||
'@radix-ui/react-icons': 1.3.0_react@18.2.0
|
||||
'@radix-ui/react-popover': 1.0.5_zn3vyfk3tbnwebg5ldvieekjaq
|
||||
'@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
|
||||
'@tauri-apps/api': 1.2.0
|
||||
dayjs: 1.11.7
|
||||
@ -559,6 +565,26 @@ packages:
|
||||
'@babel/runtime': 7.21.0
|
||||
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:
|
||||
resolution:
|
||||
{ integrity: sha512-fqYwhhI9IarZ0ll2cUSfKuXHlJK0qE4AfnRrPBbRwEH/4mGQn04/QFGomLi8TXWIdv9WJk//KgGm+aDxVIr1wA== }
|
||||
@ -926,6 +952,32 @@ packages:
|
||||
react-dom: 18.2.0_react@18.2.0
|
||||
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:
|
||||
resolution:
|
||||
{ integrity: sha512-GZtyzoHz95Rhs6S63D2t/eqvdFCm7I+yHMLVQheKM7nBD8mbZIt+ct1jz4536MDnaOGKIxynJ8eHTkVGVVkoTg== }
|
||||
@ -990,6 +1042,19 @@ packages:
|
||||
react: 18.2.0
|
||||
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:
|
||||
resolution:
|
||||
{ integrity: sha512-d0O68AYy/9oeEy1DdC07bz1/ZXX+DqCskRd3i4JzLSTXwefzaepQrKjXC7aNM8lTHjFLDO0pDgaEiQ7jEk+HVg== }
|
||||
@ -997,6 +1062,15 @@ packages:
|
||||
'@babel/runtime': 7.21.0
|
||||
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:
|
||||
resolution:
|
||||
{ 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[]
|
||||
messages Message[]
|
||||
notes Note[]
|
||||
chats Chat[]
|
||||
channels Channel[]
|
||||
|
||||
@@index([pubkey])
|
||||
}
|
||||
@ -66,6 +68,29 @@ model Message {
|
||||
@@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 {
|
||||
id Int @id @default(autoincrement())
|
||||
url String
|
||||
|
@ -35,6 +35,7 @@ struct CreateAccountData {
|
||||
#[derive(Deserialize, Type)]
|
||||
struct GetPlebData {
|
||||
account_id: i32,
|
||||
kind: i32,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Type)]
|
||||
@ -81,6 +82,42 @@ struct GetLatestNoteData {
|
||||
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]
|
||||
#[specta::specta]
|
||||
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]
|
||||
async fn get_plebs(db: DbState<'_>, data: GetPlebData) -> Result<Vec<pleb::Data>, ()> {
|
||||
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()
|
||||
.await
|
||||
.map_err(|_| ())
|
||||
@ -215,6 +255,92 @@ async fn count_total_notes(db: DbState<'_>) -> Result<i64, ()> {
|
||||
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]
|
||||
async fn main() {
|
||||
let db = PrismaClient::_builder().build().await.unwrap();
|
||||
@ -230,7 +356,13 @@ async fn main() {
|
||||
create_note,
|
||||
get_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",
|
||||
)
|
||||
@ -272,7 +404,13 @@ async fn main() {
|
||||
get_notes,
|
||||
get_latest_notes,
|
||||
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))
|
||||
.run(tauri::generate_context!())
|
||||
|
@ -2,8 +2,6 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@import './assets/editor.css';
|
||||
|
||||
/* Fixed next/image bug, source: https://nextjs.org/docs/api-reference/next/image */
|
||||
@supports (font: -apple-system-body) and (-webkit-appearance: none) {
|
||||
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,
|
||||
});
|
||||
|
||||
const NoteConnector = dynamic(() => import('@components/note/connector'), {
|
||||
const EventCollector = dynamic(() => import('@components/eventCollector'), {
|
||||
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 className="flex h-full items-center divide-x divide-zinc-900 px-4 pt-px"></div>
|
||||
<div>
|
||||
<NoteConnector />
|
||||
<EventCollector />
|
||||
</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 { ChatModal } from '@components/chats/chatModal';
|
||||
import { ImageWithFallback } from '@components/imageWithFallback';
|
||||
import { RelayContext } from '@components/relaysProvider';
|
||||
|
||||
import { activeAccountAtom } from '@stores/account';
|
||||
import { DEFAULT_AVATAR } from '@stores/constants';
|
||||
|
||||
import { useAtomValue } from 'jotai';
|
||||
import useLocalStorage from '@rehooks/local-storage';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useContext, useEffect, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export default function ChatList() {
|
||||
const [pool, relays]: any = useContext(RelayContext);
|
||||
const router = useRouter();
|
||||
|
||||
const activeAccount: any = useAtomValue(activeAccountAtom);
|
||||
const accountProfile = JSON.parse(activeAccount.metadata);
|
||||
|
||||
const [list, setList] = useState(new Set());
|
||||
const [list, setList] = useState([]);
|
||||
const [activeAccount]: any = useLocalStorage('activeAccount', {});
|
||||
const profile = activeAccount.metadata ? JSON.parse(activeAccount.metadata) : null;
|
||||
|
||||
const openSelfChat = () => {
|
||||
router.push({
|
||||
@ -27,26 +23,15 @@ export default function ChatList() {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = pool.subscribe(
|
||||
[
|
||||
{
|
||||
kinds: [4],
|
||||
'#p': [activeAccount.pubkey],
|
||||
since: 0,
|
||||
},
|
||||
],
|
||||
relays,
|
||||
(event: any) => {
|
||||
if (event.pubkey !== activeAccount.pubkey) {
|
||||
setList((list) => new Set(list).add(event.pubkey));
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
unsubscribe;
|
||||
const fetchChats = async () => {
|
||||
const { getChats } = await import('@utils/bindings');
|
||||
return await getChats({ account_id: activeAccount.id });
|
||||
};
|
||||
}, [pool, relays, activeAccount.pubkey]);
|
||||
|
||||
fetchChats()
|
||||
.then((res) => setList(res))
|
||||
.catch(console.error);
|
||||
}, [activeAccount.id]);
|
||||
|
||||
return (
|
||||
<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">
|
||||
<ImageWithFallback
|
||||
src={accountProfile.picture || DEFAULT_AVATAR}
|
||||
src={profile?.picture || DEFAULT_AVATAR}
|
||||
alt={activeAccount.pubkey}
|
||||
fill={true}
|
||||
className="rounded object-cover"
|
||||
@ -64,12 +49,12 @@ export default function ChatList() {
|
||||
</div>
|
||||
<div>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
{[...list].map((item: string, index) => (
|
||||
<ChatListItem key={index} pubkey={item} />
|
||||
{list.map((item) => (
|
||||
<ChatListItem key={item.id} pubkey={item.pubkey} />
|
||||
))}
|
||||
<ChatModal />
|
||||
</div>
|
||||
|
@ -1,19 +1,17 @@
|
||||
import { ChatModalUser } from '@components/chats/chatModalUser';
|
||||
|
||||
import { activeAccountAtom } from '@stores/account';
|
||||
|
||||
import * as Dialog from '@radix-ui/react-dialog';
|
||||
import { Cross1Icon, PlusIcon } from '@radix-ui/react-icons';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import useLocalStorage from '@rehooks/local-storage';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
export const ChatModal = () => {
|
||||
const [plebs, setPlebs] = useState([]);
|
||||
const activeAccount: any = useAtomValue(activeAccountAtom);
|
||||
const [activeAccount]: any = useLocalStorage('activeAccount', {});
|
||||
|
||||
const fetchPlebsByAccount = useCallback(async (id) => {
|
||||
const { getPlebs } = await import('@utils/bindings');
|
||||
return await getPlebs({ account_id: id });
|
||||
return await getPlebs({ account_id: id, kind: 0 });
|
||||
}, []);
|
||||
|
||||
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="flex items-center gap-2">
|
||||
<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" />
|
||||
</button>
|
||||
</Dialog.Close>
|
||||
|
@ -1,14 +1,15 @@
|
||||
import MessageListItem from '@components/chats/messageListItem';
|
||||
|
||||
import useLocalStorage from '@rehooks/local-storage';
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { Virtuoso } from 'react-virtuoso';
|
||||
|
||||
export const MessageList = ({ data }: { data: any }) => {
|
||||
const [activeAccount]: any = useLocalStorage('activeAccount', {});
|
||||
const virtuosoRef = useRef(null);
|
||||
|
||||
const itemContent: any = useCallback(
|
||||
(index: string | number) => {
|
||||
const activeAccount = JSON.parse(localStorage.getItem('activeAccount'));
|
||||
return (
|
||||
<MessageListItem
|
||||
data={data[index]}
|
||||
@ -17,7 +18,7 @@ export const MessageList = ({ data }: { data: any }) => {
|
||||
/>
|
||||
);
|
||||
},
|
||||
[data]
|
||||
[activeAccount.privkey, activeAccount.pubkey, data]
|
||||
);
|
||||
|
||||
const computeItemKey = useCallback(
|
||||
|
@ -1,31 +1,32 @@
|
||||
import { RelayContext } from '@components/relaysProvider';
|
||||
|
||||
import { lastLoginAtom } from '@stores/account';
|
||||
import { hasNewerNoteAtom } from '@stores/note';
|
||||
|
||||
import { dateToUnix } from '@utils/getDate';
|
||||
import { getParentID, pubkeyArray } from '@utils/transform';
|
||||
|
||||
import useLocalStorage, { writeStorage } from '@rehooks/local-storage';
|
||||
import { TauriEvent } from '@tauri-apps/api/event';
|
||||
import { appWindow, getCurrent } from '@tauri-apps/api/window';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { useCallback, useContext, useEffect, useRef, useState } from 'react';
|
||||
|
||||
export default function NoteConnector() {
|
||||
export default function EventCollector() {
|
||||
const [pool, relays]: any = useContext(RelayContext);
|
||||
|
||||
const setLastLoginAtom = useSetAtom(lastLoginAtom);
|
||||
const [isOnline] = useState(true);
|
||||
const setHasNewerNote = useSetAtom(hasNewerNoteAtom);
|
||||
|
||||
const [isOnline] = useState(true);
|
||||
const [activeAccount]: any = useLocalStorage('activeAccount', {});
|
||||
const [follows] = useLocalStorage('activeAccountFollows', []);
|
||||
|
||||
const now = useRef(new Date());
|
||||
const unsubscribe = useRef(null);
|
||||
|
||||
const subscribe = useCallback(async () => {
|
||||
const { createNote } = await import('@utils/bindings');
|
||||
const activeAccount = JSON.parse(localStorage.getItem('activeAccount'));
|
||||
const follows = JSON.parse(localStorage.getItem('activeAccountFollows'));
|
||||
const { createChat } = await import('@utils/bindings');
|
||||
const { createChannel } = await import('@utils/bindings');
|
||||
|
||||
unsubscribe.current = pool.subscribe(
|
||||
[
|
||||
@ -34,43 +35,61 @@ export default function NoteConnector() {
|
||||
authors: pubkeyArray(follows),
|
||||
since: dateToUnix(now.current),
|
||||
},
|
||||
{
|
||||
kinds: [4],
|
||||
'#p': [activeAccount.pubkey],
|
||||
since: 0,
|
||||
},
|
||||
{
|
||||
kinds: [40],
|
||||
since: 0,
|
||||
},
|
||||
],
|
||||
relays,
|
||||
(event) => {
|
||||
const parentID = getParentID(event.tags, event.id);
|
||||
// insert event to local database
|
||||
createNote({
|
||||
event_id: event.id,
|
||||
pubkey: event.pubkey,
|
||||
kind: event.kind,
|
||||
tags: JSON.stringify(event.tags),
|
||||
content: event.content,
|
||||
parent_id: parentID,
|
||||
parent_comment_id: '',
|
||||
created_at: event.created_at,
|
||||
account_id: activeAccount.id,
|
||||
})
|
||||
.then(() =>
|
||||
// notify user reload to get newer note
|
||||
setHasNewerNote(true)
|
||||
)
|
||||
.catch(console.error);
|
||||
},
|
||||
10000
|
||||
if (event.kind === 1) {
|
||||
const parentID = getParentID(event.tags, event.id);
|
||||
// insert event to local database
|
||||
createNote({
|
||||
event_id: event.id,
|
||||
pubkey: event.pubkey,
|
||||
kind: event.kind,
|
||||
tags: JSON.stringify(event.tags),
|
||||
content: event.content,
|
||||
parent_id: parentID,
|
||||
parent_comment_id: '',
|
||||
created_at: event.created_at,
|
||||
account_id: activeAccount.id,
|
||||
})
|
||||
.then(() =>
|
||||
// notify user reload to get newer note
|
||||
setHasNewerNote(true)
|
||||
)
|
||||
.catch(console.error);
|
||||
} else if (event.kind === 4) {
|
||||
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(() => {
|
||||
subscribe();
|
||||
getCurrent().listen(TauriEvent.WINDOW_CLOSE_REQUESTED, () => {
|
||||
setLastLoginAtom(now.current);
|
||||
writeStorage('lastLogin', now.current);
|
||||
appWindow.close();
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsubscribe.current;
|
||||
};
|
||||
}, [setHasNewerNote, setLastLoginAtom, subscribe]);
|
||||
}, [setHasNewerNote, subscribe]);
|
||||
|
||||
return (
|
||||
<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 useLocalStorage from '@rehooks/local-storage';
|
||||
import { useAtom } from 'jotai';
|
||||
import { useResetAtom } from 'jotai/utils';
|
||||
import { getEventHash, signEvent } from 'nostr-tools';
|
||||
@ -17,9 +18,9 @@ export default function FormBase() {
|
||||
const [value, setValue] = useAtom(noteContentAtom);
|
||||
const resetValue = useResetAtom(noteContentAtom);
|
||||
|
||||
const submitEvent = () => {
|
||||
const activeAccount = JSON.parse(localStorage.getItem('activeAccount'));
|
||||
const [activeAccount]: any = useLocalStorage('activeAccount', {});
|
||||
|
||||
const submitEvent = () => {
|
||||
const event: any = {
|
||||
content: value,
|
||||
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 useLocalStorage from '@rehooks/local-storage';
|
||||
import { getEventHash, nip04, signEvent } from 'nostr-tools';
|
||||
import { useCallback, useContext, useState } from 'react';
|
||||
|
||||
export default function FormChat({ receiverPubkey }: { receiverPubkey: string }) {
|
||||
const [pool, relays]: any = useContext(RelayContext);
|
||||
|
||||
const [value, setValue] = useState('');
|
||||
const [activeAccount]: any = useLocalStorage('activeAccount', {});
|
||||
|
||||
const encryptMessage = useCallback(
|
||||
async (privkey: string) => {
|
||||
@ -18,7 +21,6 @@ export default function FormChat({ receiverPubkey }: { receiverPubkey: string })
|
||||
);
|
||||
|
||||
const submitEvent = useCallback(() => {
|
||||
const activeAccount = JSON.parse(localStorage.getItem('activeAccount'));
|
||||
encryptMessage(activeAccount.privkey)
|
||||
.then((encryptedContent) => {
|
||||
const event: any = {
|
||||
@ -36,7 +38,7 @@ export default function FormChat({ receiverPubkey }: { receiverPubkey: string })
|
||||
setValue('');
|
||||
})
|
||||
.catch(console.error);
|
||||
}, [encryptMessage, receiverPubkey, pool, relays]);
|
||||
}, [encryptMessage, activeAccount.privkey, activeAccount.pubkey, receiverPubkey, pool, relays]);
|
||||
|
||||
const handleEnterPress = (e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
|
@ -1,22 +1,19 @@
|
||||
import { ImageWithFallback } from '@components/imageWithFallback';
|
||||
import { RelayContext } from '@components/relaysProvider';
|
||||
|
||||
import { activeAccountAtom } from '@stores/account';
|
||||
|
||||
import { dateToUnix } from '@utils/getDate';
|
||||
|
||||
import destr from 'destr';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import useLocalStorage from '@rehooks/local-storage';
|
||||
import { getEventHash, signEvent } from 'nostr-tools';
|
||||
import { useContext, useState } from 'react';
|
||||
|
||||
export default function FormComment({ eventID }: { eventID: any }) {
|
||||
const [pool, relays]: any = useContext(RelayContext);
|
||||
|
||||
const activeAccount: any = useAtomValue(activeAccountAtom);
|
||||
const [activeAccount]: any = useLocalStorage('activeAccount', {});
|
||||
const [value, setValue] = useState('');
|
||||
|
||||
const profile = destr(activeAccount.metadata);
|
||||
const profile = JSON.parse(activeAccount.metadata);
|
||||
|
||||
const submitEvent = () => {
|
||||
const event: any = {
|
||||
|
@ -29,20 +29,19 @@ export const ActiveAccount = memo(function ActiveAccount({ user }: { user: any }
|
||||
const insertFollowsToStorage = useCallback(
|
||||
async (tags) => {
|
||||
const { createPleb } = await import('@utils/bindings');
|
||||
const activeAccount = JSON.parse(localStorage.getItem('activeAccount'));
|
||||
|
||||
for (const tag of tags) {
|
||||
const metadata: any = await fetchMetadata(tag[1], pool, relays);
|
||||
const metadata: any = await fetchMetadata(tag[1]);
|
||||
createPleb({
|
||||
pleb_id: tag[1] + '-lume' + activeAccount.id.toString(),
|
||||
pleb_id: tag[1] + '-lume' + user.id.toString(),
|
||||
pubkey: tag[1],
|
||||
kind: 0,
|
||||
metadata: metadata.content,
|
||||
account_id: activeAccount.id,
|
||||
account_id: user.id,
|
||||
}).catch(console.error);
|
||||
}
|
||||
},
|
||||
[pool, relays]
|
||||
[user.id]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@ -56,7 +55,7 @@ export const ActiveAccount = memo(function ActiveAccount({ user }: { user: any }
|
||||
relays,
|
||||
(event: any) => {
|
||||
if (event.tags.length > 0) {
|
||||
insertFollowsToStorage(event.tags);
|
||||
//insertFollowsToStorage(event.tags);
|
||||
}
|
||||
},
|
||||
20000,
|
||||
|
@ -6,21 +6,24 @@ import { APP_VERSION } from '@stores/constants';
|
||||
import LumeSymbol from '@assets/icons/Lume';
|
||||
|
||||
import { PlusIcon } from '@radix-ui/react-icons';
|
||||
import useLocalStorage from '@rehooks/local-storage';
|
||||
import Link from 'next/link';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
export default function MultiAccounts() {
|
||||
const [users, setUsers] = useState([]);
|
||||
const [activeAccount]: any = useLocalStorage('activeAccount', {});
|
||||
|
||||
const renderAccount = useCallback((user: { pubkey: string }) => {
|
||||
const activeAccount = JSON.parse(localStorage.getItem('activeAccount'));
|
||||
|
||||
if (user.pubkey === activeAccount.pubkey) {
|
||||
return <ActiveAccount key={user.pubkey} user={user} />;
|
||||
} else {
|
||||
return <InactiveAccount key={user.pubkey} user={user} />;
|
||||
}
|
||||
}, []);
|
||||
const renderAccount = useCallback(
|
||||
(user: { pubkey: string }) => {
|
||||
if (user.pubkey === activeAccount.pubkey) {
|
||||
return <ActiveAccount key={user.pubkey} user={user} />;
|
||||
} else {
|
||||
return <InactiveAccount key={user.pubkey} user={user} />;
|
||||
}
|
||||
},
|
||||
[activeAccount.pubkey]
|
||||
);
|
||||
|
||||
const fetchAccounts = useCallback(async () => {
|
||||
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 { TriangleUpIcon } from '@radix-ui/react-icons';
|
||||
import { useState } from 'react';
|
||||
@ -18,7 +20,9 @@ export default function Channels() {
|
||||
</div>
|
||||
<h3 className="text-[11px] font-bold uppercase tracking-widest text-zinc-600">Channels</h3>
|
||||
</Collapsible.Trigger>
|
||||
<Collapsible.Content></Collapsible.Content>
|
||||
<Collapsible.Content>
|
||||
<ChannelList />
|
||||
</Collapsible.Content>
|
||||
</div>
|
||||
</Collapsible.Root>
|
||||
);
|
||||
|
@ -2,16 +2,13 @@ import { ImageWithFallback } from '@components/imageWithFallback';
|
||||
import { RelayContext } from '@components/relaysProvider';
|
||||
import { UserExtend } from '@components/user/extend';
|
||||
|
||||
import { activeAccountAtom } from '@stores/account';
|
||||
|
||||
import { dateToUnix } from '@utils/getDate';
|
||||
|
||||
import CommentIcon from '@assets/icons/comment';
|
||||
|
||||
import * as Dialog from '@radix-ui/react-dialog';
|
||||
import { SizeIcon } from '@radix-ui/react-icons';
|
||||
import destr from 'destr';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import useLocalStorage from '@rehooks/local-storage';
|
||||
import { useRouter } from 'next/router';
|
||||
import { getEventHash, signEvent } from 'nostr-tools';
|
||||
import { memo, useContext, useState } from 'react';
|
||||
@ -26,7 +23,7 @@ export const NoteComment = memo(function NoteComment({
|
||||
count: number;
|
||||
eventID: string;
|
||||
eventPubkey: string;
|
||||
eventTime: string;
|
||||
eventTime: number;
|
||||
eventContent: any;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
@ -35,8 +32,8 @@ export const NoteComment = memo(function NoteComment({
|
||||
const [open, setOpen] = useState(false);
|
||||
const [value, setValue] = useState('');
|
||||
|
||||
const activeAccount: any = useAtomValue(activeAccountAtom);
|
||||
const profile = destr(activeAccount.metadata);
|
||||
const [activeAccount]: any = useLocalStorage('activeAccount', {});
|
||||
const profile = activeAccount.metadata ? JSON.parse(activeAccount.metadata) : null;
|
||||
|
||||
const openThread = () => {
|
||||
router.push(`/newsfeed/${eventID}`);
|
||||
@ -68,8 +65,8 @@ export const NoteComment = memo(function NoteComment({
|
||||
</button>
|
||||
</Dialog.Trigger>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay className="fixed inset-0 bg-black bg-opacity-30 backdrop-blur-sm data-[state=open]:animate-overlayShow" />
|
||||
<Dialog.Content className="fixed inset-0 overflow-y-auto">
|
||||
<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 w-full max-w-2xl rounded-lg bg-zinc-900 p-4 text-zinc-100 ring-1 ring-zinc-800">
|
||||
{/* root note */}
|
||||
@ -90,7 +87,7 @@ export const NoteComment = memo(function NoteComment({
|
||||
<div>
|
||||
<div className="relative h-11 w-11 shrink-0 overflow-hidden rounded-md border border-white/10">
|
||||
<ImageWithFallback
|
||||
src={profile.picture}
|
||||
src={profile?.picture}
|
||||
alt="user's avatar"
|
||||
fill={true}
|
||||
className="rounded-md object-cover"
|
||||
|
@ -1,13 +1,11 @@
|
||||
import { RelayContext } from '@components/relaysProvider';
|
||||
|
||||
import { activeAccountAtom } from '@stores/account';
|
||||
|
||||
import { dateToUnix } from '@utils/getDate';
|
||||
|
||||
import LikeIcon from '@assets/icons/like';
|
||||
import LikedIcon from '@assets/icons/liked';
|
||||
|
||||
import { useAtomValue } from 'jotai';
|
||||
import useLocalStorage from '@rehooks/local-storage';
|
||||
import { getEventHash, signEvent } from 'nostr-tools';
|
||||
import { memo, useContext, useEffect, useState } from 'react';
|
||||
|
||||
@ -22,8 +20,7 @@ export const NoteReaction = memo(function NoteReaction({
|
||||
}) {
|
||||
const [pool, relays]: any = useContext(RelayContext);
|
||||
|
||||
const activeAccount: any = useAtomValue(activeAccountAtom);
|
||||
|
||||
const [activeAccount]: any = useLocalStorage('activeAccount', {});
|
||||
const [isReact, setIsReact] = useState(false);
|
||||
const [like, setLike] = useState(0);
|
||||
|
||||
|
@ -26,7 +26,7 @@ export default function NoteMetadata({
|
||||
{
|
||||
'#e': [eventID],
|
||||
since: parseInt(eventTime),
|
||||
kinds: [1, 7],
|
||||
kinds: [7],
|
||||
limit: 50,
|
||||
},
|
||||
],
|
||||
@ -48,11 +48,7 @@ export default function NoteMetadata({
|
||||
break;
|
||||
}
|
||||
},
|
||||
1000,
|
||||
undefined,
|
||||
{
|
||||
unsubscribeOnEose: true,
|
||||
}
|
||||
1000
|
||||
);
|
||||
|
||||
return () => {
|
||||
|
@ -8,6 +8,7 @@ import { UserMention } from '@components/user/mention';
|
||||
|
||||
import { getParentID } from '@utils/transform';
|
||||
|
||||
import useLocalStorage from '@rehooks/local-storage';
|
||||
import destr from 'destr';
|
||||
import { memo, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
||||
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 }) {
|
||||
const [pool, relays]: any = useContext(RelayContext);
|
||||
|
||||
const [activeAccount]: any = useLocalStorage('activeAccount', {});
|
||||
const [event, setEvent] = useState(null);
|
||||
|
||||
const unsubscribe = useRef(null);
|
||||
|
||||
const fetchEvent = useCallback(async () => {
|
||||
const { createNote } = await import('@utils/bindings');
|
||||
const activeAccount = JSON.parse(localStorage.getItem('activeAccount'));
|
||||
|
||||
unsubscribe.current = pool.subscribe(
|
||||
[
|
||||
@ -54,7 +56,7 @@ export const NoteParent = memo(function NoteParent({ id }: { id: string }) {
|
||||
unsubscribeOnEose: true,
|
||||
}
|
||||
);
|
||||
}, [id, pool, relays]);
|
||||
}, [activeAccount.id, id, pool, relays]);
|
||||
|
||||
const checkNoteExist = useCallback(async () => {
|
||||
const { getNoteById } = await import('@utils/bindings');
|
||||
|
@ -4,6 +4,7 @@ import { UserMention } from '@components/user/mention';
|
||||
|
||||
import { getParentID } from '@utils/transform';
|
||||
|
||||
import useLocalStorage from '@rehooks/local-storage';
|
||||
import destr from 'destr';
|
||||
import { memo, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
||||
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 }) {
|
||||
const [pool, relays]: any = useContext(RelayContext);
|
||||
|
||||
const [activeAccount]: any = useLocalStorage('activeAccount', {});
|
||||
const [event, setEvent] = useState(null);
|
||||
|
||||
const unsubscribe = useRef(null);
|
||||
|
||||
const fetchEvent = useCallback(async () => {
|
||||
const { createNote } = await import('@utils/bindings');
|
||||
const activeAccount = JSON.parse(localStorage.getItem('activeAccount'));
|
||||
|
||||
unsubscribe.current = pool.subscribe(
|
||||
[
|
||||
@ -50,7 +52,7 @@ export const NoteRepost = memo(function NoteRepost({ id }: { id: string }) {
|
||||
unsubscribeOnEose: true,
|
||||
}
|
||||
);
|
||||
}, [id, pool, relays]);
|
||||
}, [activeAccount.id, id, pool, relays]);
|
||||
|
||||
const checkNoteExist = useCallback(async () => {
|
||||
const { getNoteById } = await import('@utils/bindings');
|
||||
|
@ -6,7 +6,7 @@ export const RelayContext = createContext({});
|
||||
const relays = [
|
||||
'wss://relay.damus.io',
|
||||
'wss://nostr-pub.wellorder.net',
|
||||
'wss://nostr.bongbong.com',
|
||||
//'wss://nostr.bongbong.com',
|
||||
'wss://nostr.zebedee.cloud',
|
||||
'wss://nostr.fmt.wiz.biz',
|
||||
'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 { RelayContext } from '@components/relaysProvider';
|
||||
|
||||
import { activeAccountAtom } from '@stores/account';
|
||||
|
||||
import { useAtomValue } from 'jotai';
|
||||
import useLocalStorage from '@rehooks/local-storage';
|
||||
import { useRouter } from 'next/router';
|
||||
import {
|
||||
JSXElementConstructor,
|
||||
@ -25,7 +23,7 @@ export default function Page() {
|
||||
const router = useRouter();
|
||||
const pubkey: any = router.query.pubkey || null;
|
||||
|
||||
const activeAccount: any = useAtomValue(activeAccountAtom);
|
||||
const [activeAccount]: any = useLocalStorage('activeAccount', {});
|
||||
const [messages, setMessages] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -1,17 +1,13 @@
|
||||
import BaseLayout from '@layouts/base';
|
||||
|
||||
import { activeAccountAtom, activeAccountFollowsAtom } from '@stores/account';
|
||||
|
||||
import LumeSymbol from '@assets/icons/Lume';
|
||||
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { writeStorage } from '@rehooks/local-storage';
|
||||
import { useRouter } from 'next/router';
|
||||
import { JSXElementConstructor, ReactElement, ReactFragment, ReactPortal, useCallback, useEffect } from 'react';
|
||||
|
||||
export default function Page() {
|
||||
const router = useRouter();
|
||||
const setActiveAccount = useSetAtom(activeAccountAtom);
|
||||
const setActiveAccountFollows = useSetAtom(activeAccountFollowsAtom);
|
||||
|
||||
const fetchActiveAccount = useCallback(async () => {
|
||||
const { getAccounts } = await import('@utils/bindings');
|
||||
@ -20,7 +16,7 @@ export default function Page() {
|
||||
|
||||
const fetchFollowsByAccount = useCallback(async (id) => {
|
||||
const { getPlebs } = await import('@utils/bindings');
|
||||
return await getPlebs({ account_id: id });
|
||||
return await getPlebs({ account_id: id, kind: 0 });
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
@ -29,10 +25,10 @@ export default function Page() {
|
||||
if (res.length > 0) {
|
||||
// fetch follows
|
||||
fetchFollowsByAccount(res[0].id).then((follows) => {
|
||||
setActiveAccountFollows(follows);
|
||||
writeStorage('activeAccountFollows', follows);
|
||||
});
|
||||
// update local storage
|
||||
setActiveAccount(res[0]);
|
||||
writeStorage('activeAccount', res[0]);
|
||||
// redirect
|
||||
router.replace('/init');
|
||||
} else {
|
||||
@ -40,7 +36,7 @@ export default function Page() {
|
||||
}
|
||||
})
|
||||
.catch(console.error);
|
||||
}, [fetchActiveAccount, setActiveAccount, fetchFollowsByAccount, setActiveAccountFollows, router]);
|
||||
}, [fetchActiveAccount, fetchFollowsByAccount, router]);
|
||||
|
||||
return (
|
||||
<div className="relative h-full overflow-hidden">
|
||||
|
@ -7,6 +7,7 @@ import { getParentID, pubkeyArray } from '@utils/transform';
|
||||
|
||||
import LumeSymbol from '@assets/icons/Lume';
|
||||
|
||||
import { useLocalStorage } from '@rehooks/local-storage';
|
||||
import { invoke } from '@tauri-apps/api/tauri';
|
||||
import { useRouter } from 'next/router';
|
||||
import {
|
||||
@ -30,11 +31,13 @@ export default function Page() {
|
||||
|
||||
const [eose, setEose] = useState(false);
|
||||
|
||||
const [lastLogin] = useLocalStorage('lastLogin', '');
|
||||
const [activeAccount]: any = useLocalStorage('activeAccount', {});
|
||||
const [follows] = useLocalStorage('activeAccountFollows', []);
|
||||
|
||||
const fetchData = useCallback(
|
||||
async (since: Date) => {
|
||||
const { createNote } = await import('@utils/bindings');
|
||||
const activeAccount = JSON.parse(localStorage.getItem('activeAccount'));
|
||||
const follows = JSON.parse(localStorage.getItem('activeAccountFollows'));
|
||||
|
||||
unsubscribe.current = pool.subscribe(
|
||||
[
|
||||
@ -67,20 +70,19 @@ export default function Page() {
|
||||
}
|
||||
);
|
||||
},
|
||||
[pool, relays]
|
||||
[activeAccount.id, follows, pool, relays]
|
||||
);
|
||||
|
||||
const isNoteExist = useCallback(async () => {
|
||||
invoke('count_total_notes').then((res: number) => {
|
||||
if (res > 0) {
|
||||
const lastLogin = JSON.parse(localStorage.getItem('lastLogin'));
|
||||
const parseDate = new Date(lastLogin);
|
||||
fetchData(parseDate);
|
||||
} else {
|
||||
fetchData(hoursAgo(24, now.current));
|
||||
}
|
||||
});
|
||||
}, [fetchData]);
|
||||
}, [fetchData, lastLogin]);
|
||||
|
||||
useEffect(() => {
|
||||
if (eose === false) {
|
||||
|
@ -1,53 +1,10 @@
|
||||
import BaseLayout from '@layouts/base';
|
||||
import WithSidebarLayout from '@layouts/withSidebar';
|
||||
|
||||
import FormComment from '@components/form/comment';
|
||||
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';
|
||||
import { JSXElementConstructor, ReactElement, ReactFragment, ReactPortal } from 'react';
|
||||
|
||||
export default function Page() {
|
||||
const [pool, relays]: any = useContext(RelayContext);
|
||||
|
||||
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>
|
||||
);
|
||||
return <div className="scrollbar-hide flex h-full flex-col gap-2 overflow-y-auto py-3"></div>;
|
||||
}
|
||||
|
||||
Page.getLayout = function getLayout(
|
||||
|
@ -84,7 +84,7 @@ export default function Page() {
|
||||
setLoading(true);
|
||||
|
||||
for (const follow of follows) {
|
||||
const metadata: any = await fetchMetadata(follow, pool, relays);
|
||||
const metadata: any = await fetchMetadata(follow);
|
||||
createPleb({
|
||||
pleb_id: follow + '-lume' + id,
|
||||
pubkey: follow,
|
||||
|
@ -49,7 +49,7 @@ export default function Page() {
|
||||
const { createPleb } = await import('@utils/bindings');
|
||||
if (profile?.id !== null) {
|
||||
for (const tag of tags) {
|
||||
const metadata: any = await fetchMetadata(tag[1], pool, relays);
|
||||
const metadata: any = await fetchMetadata(tag[1]);
|
||||
createPleb({
|
||||
pleb_id: tag[1] + '-lume' + profile.id.toString(),
|
||||
pubkey: tag[1],
|
||||
@ -60,7 +60,7 @@ export default function Page() {
|
||||
}
|
||||
}
|
||||
},
|
||||
[pool, profile.id, relays]
|
||||
[profile.id]
|
||||
);
|
||||
|
||||
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 });
|
||||
}
|
||||
|
||||
export type CreateNoteData = {
|
||||
event_id: string;
|
||||
pubkey: string;
|
||||
kind: number;
|
||||
tags: string;
|
||||
content: string;
|
||||
parent_id: string;
|
||||
parent_comment_id: string;
|
||||
created_at: number;
|
||||
account_id: number;
|
||||
};
|
||||
export type CreatePlebData = { pleb_id: string; pubkey: string; kind: number; metadata: string; account_id: number };
|
||||
export type GetNoteByIdData = { event_id: string };
|
||||
export type Pleb = { id: number; plebId: string; pubkey: string; kind: number; metadata: string; accountId: number };
|
||||
export function createChannel(data: CreateChannelData) {
|
||||
return invoke<Channel>('create_channel', { data });
|
||||
}
|
||||
|
||||
export function updateChannel(data: UpdateChannelData) {
|
||||
return invoke<Channel>('update_channel', { data });
|
||||
}
|
||||
|
||||
export function getChannels(data: GetChannelData) {
|
||||
return invoke<Channel[]>('get_channels', { data });
|
||||
}
|
||||
|
||||
export function getActiveChannels(data: GetActiveChannelData) {
|
||||
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 = {
|
||||
id: number;
|
||||
eventId: string;
|
||||
@ -70,9 +80,31 @@ export type Note = {
|
||||
createdAt: 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 GetPlebPubkeyData = { pubkey: string };
|
||||
export type GetPlebData = { account_id: number };
|
||||
export type CreateAccountData = { pubkey: string; privkey: string; metadata: string };
|
||||
export type GetChannelData = { limit: number; offset: 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 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';
|
||||
import { useCallback, useContext, useEffect, useState } from 'react';
|
||||
|
||||
export const fetchMetadata = (pubkey: string, pool: any, relays: any) => {
|
||||
const author = new Author(pool, relays, pubkey);
|
||||
return new Promise((resolve) => author.metaData(resolve, 0));
|
||||
export const fetchMetadata = async (pubkey: string) => {
|
||||
const result = await fetch(`https://rbr.bio/${pubkey}/metadata.json`, {
|
||||
method: 'GET',
|
||||
timeout: 5,
|
||||
});
|
||||
return await result.data;
|
||||
};
|
||||
|
||||
export const useMetadata = (pubkey) => {
|
||||
const [pool, relays]: any = useContext(RelayContext);
|
||||
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 { getPlebByPubkey } = await import('@utils/bindings');
|
||||
getPlebByPubkey({ pubkey: pubkey })
|
||||
.then((res) => {
|
||||
if (res) {
|
||||
const metadata = JSON.parse(res.metadata);
|
||||
// update state
|
||||
setProfile(metadata);
|
||||
} else {
|
||||
fetchMetadata(pubkey, pool, relays).then((res: any) => {
|
||||
fetchMetadata(pubkey).then((res: any) => {
|
||||
if (res.content) {
|
||||
const metadata = JSON.parse(res.content);
|
||||
// update state
|
||||
setProfile(metadata);
|
||||
// save to database
|
||||
// insertPlebToDB(activeAccount.id, pubkey, metadata);
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(console.error);
|
||||
}, [pool, relays, pubkey]);
|
||||
}, [pubkey]);
|
||||
|
||||
useEffect(() => {
|
||||
getCachedMetadata().catch(console.error);
|
||||
|
Loading…
Reference in New Issue
Block a user