diff --git a/package.json b/package.json index 5ff1829c..e9e96f80 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e899822a..d534a344 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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== } diff --git a/src-tauri/prisma/migrations/20230410024403_add_channel/migration.sql b/src-tauri/prisma/migrations/20230410024403_add_channel/migration.sql new file mode 100644 index 00000000..e39630ca --- /dev/null +++ b/src-tauri/prisma/migrations/20230410024403_add_channel/migration.sql @@ -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"); diff --git a/src-tauri/prisma/migrations/20230410071123_add_chat/migration.sql b/src-tauri/prisma/migrations/20230410071123_add_chat/migration.sql new file mode 100644 index 00000000..83fd87fb --- /dev/null +++ b/src-tauri/prisma/migrations/20230410071123_add_chat/migration.sql @@ -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"); diff --git a/src-tauri/prisma/migrations/20230410071606_add_account_related_to_chat_and_channel/migration.sql b/src-tauri/prisma/migrations/20230410071606_add_account_related_to_chat_and_channel/migration.sql new file mode 100644 index 00000000..cf191a1b --- /dev/null +++ b/src-tauri/prisma/migrations/20230410071606_add_account_related_to_chat_and_channel/migration.sql @@ -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; diff --git a/src-tauri/prisma/migrations/20230410091415_add_active_to_channel/migration.sql b/src-tauri/prisma/migrations/20230410091415_add_active_to_channel/migration.sql new file mode 100644 index 00000000..2f4baa64 --- /dev/null +++ b/src-tauri/prisma/migrations/20230410091415_add_active_to_channel/migration.sql @@ -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; diff --git a/src-tauri/prisma/schema.prisma b/src-tauri/prisma/schema.prisma index 780eca94..197198ac 100644 --- a/src-tauri/prisma/schema.prisma +++ b/src-tauri/prisma/schema.prisma @@ -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 diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index fe5caa84..261ef5f6 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -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, ()> { @@ -105,7 +142,10 @@ async fn create_account(db: DbState<'_>, data: CreateAccountData) -> Result, data: GetPlebData) -> Result, ()> { 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 { db.note().count(vec![]).exec().await.map_err(|_| ()) } +#[tauri::command] +#[specta::specta] +async fn create_channel(db: DbState<'_>, data: CreateChannelData) -> Result { + 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 { + 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, ()> { + 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, ()> { + 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 { + 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, ()> { + 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!()) diff --git a/src/App.css b/src/App.css index cd22b419..6112a3d5 100644 --- a/src/App.css +++ b/src/App.css @@ -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'] { diff --git a/src/assets/editor.css b/src/assets/editor.css deleted file mode 100644 index 1557df9f..00000000 --- a/src/assets/editor.css +++ /dev/null @@ -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%; -} diff --git a/src/assets/icons/hide.tsx b/src/assets/icons/hide.tsx new file mode 100644 index 00000000..edbcf916 --- /dev/null +++ b/src/assets/icons/hide.tsx @@ -0,0 +1,18 @@ +export default function HideIcon({ className }: { className: string }) { + return ( + + + + ); +} diff --git a/src/assets/icons/mute.tsx b/src/assets/icons/mute.tsx new file mode 100644 index 00000000..f1af1c0a --- /dev/null +++ b/src/assets/icons/mute.tsx @@ -0,0 +1,18 @@ +export default function MuteIcon({ className }: { className: string }) { + return ( + + + + ); +} diff --git a/src/assets/icons/reply.tsx b/src/assets/icons/reply.tsx new file mode 100644 index 00000000..a645e93a --- /dev/null +++ b/src/assets/icons/reply.tsx @@ -0,0 +1,18 @@ +export default function ReplyIcon({ className }: { className: string }) { + return ( + + + + ); +} diff --git a/src/components/appHeader/index.tsx b/src/components/appHeader/index.tsx index b4e3df47..1a8c0314 100644 --- a/src/components/appHeader/index.tsx +++ b/src/components/appHeader/index.tsx @@ -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() {
- +
diff --git a/src/components/channels/browseChannelItem.tsx b/src/components/channels/browseChannelItem.tsx new file mode 100644 index 00000000..e12e39a8 --- /dev/null +++ b/src/components/channels/browseChannelItem.tsx @@ -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 ( +
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" + > +
+ +
+
+ {channel.name} + {channel.about} +
+
+ +
+
+ ); +}; diff --git a/src/components/channels/channelList.tsx b/src/components/channels/channelList.tsx new file mode 100644 index 00000000..626c729e --- /dev/null +++ b/src/components/channels/channelList.tsx @@ -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 ( +
+ +
+ +
+
+
Browse channels
+
+ + {list.map((item) => ( + + ))} + +
+ ); +} diff --git a/src/components/channels/channelListItem.tsx b/src/components/channels/channelListItem.tsx new file mode 100644 index 00000000..230728a9 --- /dev/null +++ b/src/components/channels/channelListItem.tsx @@ -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 ( +
openChannel(data.eventId)} + className="inline-flex items-center gap-2 rounded-md px-2.5 py-1.5 hover:bg-zinc-900" + > +
+ +
+
+
{channel.name}
+
+
+ ); +}; diff --git a/src/components/channels/createChannelModal.tsx b/src/components/channels/createChannelModal.tsx new file mode 100644 index 00000000..ab09699c --- /dev/null +++ b/src/components/channels/createChannelModal.tsx @@ -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 ( + + +
+
+ +
+
+
Add a new channel
+
+
+
+ + + +
+
+
+
+
# Create channel
+ + + +
+
+
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+