refactor channel

This commit is contained in:
Ren Amamiya
2023-06-17 12:28:46 +07:00
parent 0a6865431d
commit c9f7d942a0
21 changed files with 275 additions and 291 deletions

View File

@@ -15,7 +15,7 @@
"dependencies": {
"@floating-ui/react": "^0.23.1",
"@headlessui/react": "^1.7.15",
"@nostr-dev-kit/ndk": "^0.5.2",
"@nostr-dev-kit/ndk": "^0.5.3",
"@tanstack/react-virtual": "3.0.0-beta.54",
"@tauri-apps/api": "^1.4.0",
"@vidstack/react": "^0.4.5",

98
pnpm-lock.yaml generated
View File

@@ -8,8 +8,8 @@ dependencies:
specifier: ^1.7.15
version: 1.7.15(react-dom@18.2.0)(react@18.2.0)
'@nostr-dev-kit/ndk':
specifier: ^0.5.2
version: 0.5.2(typescript@4.9.5)
specifier: ^0.5.3
version: 0.5.3(typescript@4.9.5)
'@tanstack/react-virtual':
specifier: 3.0.0-beta.54
version: 3.0.0-beta.54(react@18.2.0)
@@ -370,13 +370,13 @@ packages:
requiresBuild: true
optional: true
/@eslint-community/eslint-utils@4.4.0(eslint@8.42.0):
/@eslint-community/eslint-utils@4.4.0(eslint@8.43.0):
resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies:
eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
dependencies:
eslint: 8.42.0
eslint: 8.43.0
eslint-visitor-keys: 3.4.1
dev: false
@@ -402,8 +402,8 @@ packages:
- supports-color
dev: false
/@eslint/js@8.42.0:
resolution: {integrity: sha512-6SWlXpWU5AvId8Ac7zjzmIOqMOba/JWY8XZ4A7q7Gn1Vlfg/SFFIlrtHXt9nPn4op9ZPAkl91Jao+QQv3r/ukw==}
/@eslint/js@8.43.0:
resolution: {integrity: sha512-s2UHCoiXfxMvmfzqoN+vrQ84ahUSYde9qNO1MdxmoEhyHWsfmwOpFlwYV+ePJEVc7gFnATGUi376WowX1N7tFg==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
dev: false
@@ -571,20 +571,20 @@ packages:
'@nodelib/fs.scandir': 2.1.5
fastq: 1.15.0
/@nostr-dev-kit/ndk@0.5.2(typescript@4.9.5):
resolution: {integrity: sha512-ZTcEGHxe/Yoazr2SHB+6wQ/1ZkLvaJVdEFNOwg2Ll1lD6C7ttnP/rPD1ex17bQFaU//4yvsdgEREDJqo+mLedw==}
/@nostr-dev-kit/ndk@0.5.3(typescript@4.9.5):
resolution: {integrity: sha512-GLmuAoor4oMxxKjFeZ4viHR9XEI61m0wBm78vTUzY1Ev+bBdDzorv6heBz7TWmQirtoJ32r/zIgWdzHsHC6h3A==}
dependencies:
'@noble/hashes': 1.3.1
'@noble/secp256k1': 2.0.0
'@scure/base': 1.1.1
'@typescript-eslint/eslint-plugin': 5.59.11(@typescript-eslint/parser@5.59.11)(eslint@8.42.0)(typescript@4.9.5)
'@typescript-eslint/parser': 5.59.11(eslint@8.42.0)(typescript@4.9.5)
'@typescript-eslint/eslint-plugin': 5.59.11(@typescript-eslint/parser@5.59.11)(eslint@8.43.0)(typescript@4.9.5)
'@typescript-eslint/parser': 5.59.11(eslint@8.43.0)(typescript@4.9.5)
debug: 4.3.4
esbuild: 0.17.19
esbuild-plugin-alias: 0.2.1
eslint: 8.42.0
eslint-config-prettier: 8.8.0(eslint@8.42.0)
eslint-plugin-import: 2.27.5(@typescript-eslint/parser@5.59.11)(eslint@8.42.0)
eslint: 8.43.0
eslint-config-prettier: 8.8.0(eslint@8.43.0)
eslint-plugin-import: 2.27.5(@typescript-eslint/parser@5.59.11)(eslint@8.43.0)
esm-loader-typescript: 1.0.4
eventemitter3: 5.0.1
light-bolt11-decoder: 3.0.0
@@ -1023,7 +1023,7 @@ packages:
resolution: {integrity: sha512-W8F4eoTIvzXeNrT3JroQPimZLXnlJA8smYygHZUKFPVoYwgs/OhJkA1VBhL3iSs57OQkuINqHlY4SmMT5wtnJg==}
dev: true
/@typescript-eslint/eslint-plugin@5.59.11(@typescript-eslint/parser@5.59.11)(eslint@8.42.0)(typescript@4.9.5):
/@typescript-eslint/eslint-plugin@5.59.11(@typescript-eslint/parser@5.59.11)(eslint@8.43.0)(typescript@4.9.5):
resolution: {integrity: sha512-XxuOfTkCUiOSyBWIvHlUraLw/JT/6Io1365RO6ZuI88STKMavJZPNMU0lFcUTeQXEhHiv64CbxYxBNoDVSmghg==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies:
@@ -1035,12 +1035,12 @@ packages:
optional: true
dependencies:
'@eslint-community/regexpp': 4.5.1
'@typescript-eslint/parser': 5.59.11(eslint@8.42.0)(typescript@4.9.5)
'@typescript-eslint/parser': 5.59.11(eslint@8.43.0)(typescript@4.9.5)
'@typescript-eslint/scope-manager': 5.59.11
'@typescript-eslint/type-utils': 5.59.11(eslint@8.42.0)(typescript@4.9.5)
'@typescript-eslint/utils': 5.59.11(eslint@8.42.0)(typescript@4.9.5)
'@typescript-eslint/type-utils': 5.59.11(eslint@8.43.0)(typescript@4.9.5)
'@typescript-eslint/utils': 5.59.11(eslint@8.43.0)(typescript@4.9.5)
debug: 4.3.4
eslint: 8.42.0
eslint: 8.43.0
grapheme-splitter: 1.0.4
ignore: 5.2.4
natural-compare-lite: 1.4.0
@@ -1051,7 +1051,7 @@ packages:
- supports-color
dev: false
/@typescript-eslint/parser@5.59.11(eslint@8.42.0)(typescript@4.9.5):
/@typescript-eslint/parser@5.59.11(eslint@8.43.0)(typescript@4.9.5):
resolution: {integrity: sha512-s9ZF3M+Nym6CAZEkJJeO2TFHHDsKAM3ecNkLuH4i4s8/RCPnF5JRip2GyviYkeEAcwGMJxkqG9h2dAsnA1nZpA==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies:
@@ -1065,7 +1065,7 @@ packages:
'@typescript-eslint/types': 5.59.11
'@typescript-eslint/typescript-estree': 5.59.11(typescript@4.9.5)
debug: 4.3.4
eslint: 8.42.0
eslint: 8.43.0
typescript: 4.9.5
transitivePeerDependencies:
- supports-color
@@ -1079,7 +1079,7 @@ packages:
'@typescript-eslint/visitor-keys': 5.59.11
dev: false
/@typescript-eslint/type-utils@5.59.11(eslint@8.42.0)(typescript@4.9.5):
/@typescript-eslint/type-utils@5.59.11(eslint@8.43.0)(typescript@4.9.5):
resolution: {integrity: sha512-LZqVY8hMiVRF2a7/swmkStMYSoXMFlzL6sXV6U/2gL5cwnLWQgLEG8tjWPpaE4rMIdZ6VKWwcffPlo1jPfk43g==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies:
@@ -1090,9 +1090,9 @@ packages:
optional: true
dependencies:
'@typescript-eslint/typescript-estree': 5.59.11(typescript@4.9.5)
'@typescript-eslint/utils': 5.59.11(eslint@8.42.0)(typescript@4.9.5)
'@typescript-eslint/utils': 5.59.11(eslint@8.43.0)(typescript@4.9.5)
debug: 4.3.4
eslint: 8.42.0
eslint: 8.43.0
tsutils: 3.21.0(typescript@4.9.5)
typescript: 4.9.5
transitivePeerDependencies:
@@ -1125,19 +1125,19 @@ packages:
- supports-color
dev: false
/@typescript-eslint/utils@5.59.11(eslint@8.42.0)(typescript@4.9.5):
/@typescript-eslint/utils@5.59.11(eslint@8.43.0)(typescript@4.9.5):
resolution: {integrity: sha512-didu2rHSOMUdJThLk4aZ1Or8IcO3HzCw/ZvEjTTIfjIrcdd5cvSIwwDy2AOlE7htSNp7QIZ10fLMyRCveesMLg==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies:
eslint: ^6.0.0 || ^7.0.0 || ^8.0.0
dependencies:
'@eslint-community/eslint-utils': 4.4.0(eslint@8.42.0)
'@eslint-community/eslint-utils': 4.4.0(eslint@8.43.0)
'@types/json-schema': 7.0.12
'@types/semver': 7.5.0
'@typescript-eslint/scope-manager': 5.59.11
'@typescript-eslint/types': 5.59.11
'@typescript-eslint/typescript-estree': 5.59.11(typescript@4.9.5)
eslint: 8.42.0
eslint: 8.43.0
eslint-scope: 5.1.1
semver: 7.5.2
transitivePeerDependencies:
@@ -1185,16 +1185,16 @@ packages:
resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==}
dev: false
/acorn-jsx@5.3.2(acorn@8.8.2):
/acorn-jsx@5.3.2(acorn@8.9.0):
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
peerDependencies:
acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
dependencies:
acorn: 8.8.2
acorn: 8.9.0
dev: false
/acorn@8.8.2:
resolution: {integrity: sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==}
/acorn@8.9.0:
resolution: {integrity: sha512-jaVNAFBHNLXspO543WnNNPZFRtavh3skAkITqD0/2aeMkKZTN+254PyhwxFYrk3vQ1xfY+2wbesJMs/JC8/PwQ==}
engines: {node: '>=0.4.0'}
hasBin: true
@@ -1415,7 +1415,7 @@ packages:
hasBin: true
dependencies:
caniuse-lite: 1.0.30001503
electron-to-chromium: 1.4.431
electron-to-chromium: 1.4.433
node-releases: 2.0.12
update-browserslist-db: 1.0.11(browserslist@4.21.9)
dev: true
@@ -1780,8 +1780,8 @@ packages:
/eastasianwidth@0.2.0:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
/electron-to-chromium@1.4.431:
resolution: {integrity: sha512-m232JTVmCawA2vG+1azVxhKZ9Sv1Q//xxNv5PkP5rWxGgQE8c3CiZFrh8Xnp+d1NmNxlu3QQrGIfdeW5TtXX5w==}
/electron-to-chromium@1.4.433:
resolution: {integrity: sha512-MGO1k0w1RgrfdbLVwmXcDhHHuxCn2qRgR7dYsJvWFKDttvYPx6FNzCGG0c/fBBvzK2LDh3UV7Tt9awnHnvAAUQ==}
dev: true
/emoji-regex@8.0.0:
@@ -1951,13 +1951,13 @@ packages:
engines: {node: '>=10'}
dev: false
/eslint-config-prettier@8.8.0(eslint@8.42.0):
/eslint-config-prettier@8.8.0(eslint@8.43.0):
resolution: {integrity: sha512-wLbQiFre3tdGgpDv67NQKnJuTlcUVYHas3k+DZCc2U2BadthoEY4B7hLPvAxaqdyOGCzuLfii2fqGph10va7oA==}
hasBin: true
peerDependencies:
eslint: '>=7.0.0'
dependencies:
eslint: 8.42.0
eslint: 8.43.0
dev: false
/eslint-formatter-pretty@4.1.0:
@@ -1984,7 +1984,7 @@ packages:
- supports-color
dev: false
/eslint-module-utils@2.8.0(@typescript-eslint/parser@5.59.11)(eslint-import-resolver-node@0.3.7)(eslint@8.42.0):
/eslint-module-utils@2.8.0(@typescript-eslint/parser@5.59.11)(eslint-import-resolver-node@0.3.7)(eslint@8.43.0):
resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==}
engines: {node: '>=4'}
peerDependencies:
@@ -2005,15 +2005,15 @@ packages:
eslint-import-resolver-webpack:
optional: true
dependencies:
'@typescript-eslint/parser': 5.59.11(eslint@8.42.0)(typescript@4.9.5)
'@typescript-eslint/parser': 5.59.11(eslint@8.43.0)(typescript@4.9.5)
debug: 3.2.7
eslint: 8.42.0
eslint: 8.43.0
eslint-import-resolver-node: 0.3.7
transitivePeerDependencies:
- supports-color
dev: false
/eslint-plugin-import@2.27.5(@typescript-eslint/parser@5.59.11)(eslint@8.42.0):
/eslint-plugin-import@2.27.5(@typescript-eslint/parser@5.59.11)(eslint@8.43.0):
resolution: {integrity: sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow==}
engines: {node: '>=4'}
peerDependencies:
@@ -2023,15 +2023,15 @@ packages:
'@typescript-eslint/parser':
optional: true
dependencies:
'@typescript-eslint/parser': 5.59.11(eslint@8.42.0)(typescript@4.9.5)
'@typescript-eslint/parser': 5.59.11(eslint@8.43.0)(typescript@4.9.5)
array-includes: 3.1.6
array.prototype.flat: 1.3.1
array.prototype.flatmap: 1.3.1
debug: 3.2.7
doctrine: 2.1.0
eslint: 8.42.0
eslint: 8.43.0
eslint-import-resolver-node: 0.3.7
eslint-module-utils: 2.8.0(@typescript-eslint/parser@5.59.11)(eslint-import-resolver-node@0.3.7)(eslint@8.42.0)
eslint-module-utils: 2.8.0(@typescript-eslint/parser@5.59.11)(eslint-import-resolver-node@0.3.7)(eslint@8.43.0)
has: 1.0.3
is-core-module: 2.12.1
is-glob: 4.0.3
@@ -2071,15 +2071,15 @@ packages:
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
dev: false
/eslint@8.42.0:
resolution: {integrity: sha512-ulg9Ms6E1WPf67PHaEY4/6E2tEn5/f7FXGzr3t9cBMugOmf1INYvuUwwh1aXQN4MfJ6a5K2iNwP3w4AColvI9A==}
/eslint@8.43.0:
resolution: {integrity: sha512-aaCpf2JqqKesMFGgmRPessmVKjcGXqdlAYLLC3THM8t5nBRZRQ+st5WM/hoJXkdioEXLLbXgclUpM0TXo5HX5Q==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
hasBin: true
dependencies:
'@eslint-community/eslint-utils': 4.4.0(eslint@8.42.0)
'@eslint-community/eslint-utils': 4.4.0(eslint@8.43.0)
'@eslint-community/regexpp': 4.5.1
'@eslint/eslintrc': 2.0.3
'@eslint/js': 8.42.0
'@eslint/js': 8.43.0
'@humanwhocodes/config-array': 0.11.10
'@humanwhocodes/module-importer': 1.0.1
'@nodelib/fs.walk': 1.2.8
@@ -2132,8 +2132,8 @@ packages:
resolution: {integrity: sha512-7OASN1Wma5fum5SrNhFMAMJxOUAbhyfQ8dQ//PJaJbNw0URTPWqIghHWt1MmAANKhHZIYOHruW4Kw4ruUWOdGw==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
dependencies:
acorn: 8.8.2
acorn-jsx: 5.3.2(acorn@8.8.2)
acorn: 8.9.0
acorn-jsx: 5.3.2(acorn@8.9.0)
eslint-visitor-keys: 3.4.1
dev: false
@@ -4686,7 +4686,7 @@ packages:
'@brillout/json-serializer': 0.5.3
'@brillout/picocolors': 1.0.4
'@brillout/vite-plugin-import-build': 0.2.18
acorn: 8.8.2
acorn: 8.9.0
cac: 6.7.14
es-module-lexer: 0.10.5
esbuild: 0.17.19

View File

@@ -18,25 +18,6 @@ VALUES
1681898574
);
INSERT
OR IGNORE INTO channels (
event_id,
pubkey,
name,
about,
picture,
created_at
)
VALUES
(
"42224859763652914db53052103f0b744df79dfc4efef7e950fc0802fc3df3c5",
"460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c",
"Amethyst Users",
"General discussion about the Amethyst Nostr client for Android",
"https://nostr.build/i/5970.png",
1674092111
);
INSERT
OR IGNORE INTO channels (
event_id,

View File

@@ -0,0 +1,15 @@
-- Add migration script here
CREATE TABLE
channel_messages (
id INTEGER NOT NULL PRIMARY KEY,
channel_id TEXT NOT NULL,
event_id TEXT NOT NULL UNIQUE,
pubkey TEXT NOT NULL,
kind INTEGER NOT NULL,
content TEXT NOT NULL,
tags JSON,
mute BOOLEAN DEFAULT 0,
hide BOOLEAN DEFAULT 0,
created_at INTEGER NOT NULL,
FOREIGN KEY (channel_id) REFERENCES channels (event_id)
);

View File

@@ -93,6 +93,12 @@ fn main() {
sql: include_str!("../migrations/20230521092300_add_block_model.sql"),
kind: MigrationKind::Up,
},
Migration {
version: 20230617003135,
description: "add channel messages",
sql: include_str!("../migrations/20230617003135_add_channel_messages.sql"),
kind: MigrationKind::Up,
},
],
)
.build(),

View File

@@ -1,72 +0,0 @@
import { ChannelMessageItem } from "@app/channel/components/messages/item";
import { useChannelMessages } from "@stores/channels";
import { getHourAgo } from "@utils/date";
import { useCallback, useRef } from "react";
import { Virtuoso } from "react-virtuoso";
export function ChannelMessageList() {
const now = useRef(new Date());
const virtuosoRef = useRef(null);
const messages = useChannelMessages((state: any) => state.messages);
const itemContent: any = useCallback(
(index: string | number) => {
return <ChannelMessageItem data={messages[index]} />;
},
[messages],
);
const computeItemKey = useCallback(
(index: string | number) => {
return messages[index].id;
},
[messages],
);
return (
<div className="h-full w-full">
<Virtuoso
ref={virtuosoRef}
data={[]}
itemContent={itemContent}
components={{
Header: () => (
<div className="relative py-4">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-zinc-800" />
</div>
<div className="relative flex justify-center">
<div className="inline-flex items-center gap-x-1.5 rounded-full bg-zinc-900 px-3 py-1.5 text-base font-medium text-zinc-400 shadow-sm ring-1 ring-inset ring-zinc-800">
{getHourAgo(24, now.current).toLocaleDateString("en-US", {
weekday: "long",
year: "numeric",
month: "long",
day: "numeric",
})}
</div>
</div>
</div>
),
EmptyPlaceholder: () => (
<div className="flex flex-col gap-1 text-center">
<h3 className="text-base font-semibold leading-none text-white">
Nothing to see here yet
</h3>
<p className="text-base leading-none text-zinc-400">
Be the first to share a message in this channel.
</p>
</div>
),
}}
computeItemKey={computeItemKey}
initialTopMostItemIndex={messages.length - 1}
alignToBottom={true}
followOutput={true}
overscan={50}
increaseViewportBy={{ top: 200, bottom: 200 }}
className="scrollbar-hide h-full w-full overflow-y-auto"
/>
</div>
);
}

View File

@@ -1,6 +1,7 @@
import { UserReply } from "@app/channel/components/messages/userReply";
import { NDKEvent, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
import { CancelIcon } from "@shared/icons";
import { CancelIcon, EnterIcon } from "@shared/icons";
import { MediaUploader } from "@shared/mediaUploader";
import { RelayContext } from "@shared/relayProvider";
import { useActiveAccount } from "@stores/accounts";
import { useChannelMessages } from "@stores/channels";
@@ -17,7 +18,7 @@ export function ChannelMessageForm({ channelID }: { channelID: string }) {
state.closeReply,
]);
const submitEvent = () => {
const submit = () => {
let tags: string[][];
if (replyTo.id !== null) {
@@ -51,7 +52,7 @@ export function ChannelMessageForm({ channelID }: { channelID: string }) {
const handleEnterPress = (e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
submitEvent();
submit();
}
};
@@ -60,11 +61,7 @@ export function ChannelMessageForm({ channelID }: { channelID: string }) {
};
return (
<div
className={`relative ${
replyTo.id ? "h-36" : "h-24"
} w-full overflow-hidden before:pointer-events-none before:absolute before:-inset-1 before:rounded-[6px] before:border before:border-fuchsia-500 before:opacity-0 before:ring-2 before:ring-fuchsia-500/20 before:transition after:pointer-events-none after:absolute after:inset-px after:rounded-[7px] after:shadow-highlight after:shadow-white/5 after:transition focus-within:before:opacity-100 focus-within:after:shadow-fuchsia-500/100 dark:focus-within:after:shadow-fuchsia-500/20`}
>
<div className={`relative w-full ${replyTo.id ? "h-36" : "h-24"}`}>
{replyTo.id && (
<div className="absolute left-0 top-0 z-10 h-16 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">
@@ -92,23 +89,19 @@ export function ChannelMessageForm({ channelID }: { channelID: string }) {
placeholder="Message"
className={`relative ${
replyTo.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-base shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-white dark:shadow-black/10 dark:placeholder:text-zinc-500`}
} w-full resize-none rounded-md px-5 !outline-none bg-zinc-800 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">
<div className="flex items-center gap-2 pl-2" />
</div>
<div className="flex items-center gap-2">
<button
type="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-base font-medium shadow-button hover:bg-fuchsia-600 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50"
>
Send
</button>
</div>
<div className="absolute right-2 bottom-0 h-11">
<div className="h-full flex gap-3 items-center justify-end text-zinc-500">
<MediaUploader setState={setValue} />
<button
type="button"
onClick={submit}
className="inline-flex items-center gap-1 text-sm leading-none"
>
<EnterIcon width={14} height={14} className="" />
Send
</button>
</div>
</div>
</div>

View File

@@ -53,7 +53,7 @@ export function MessageHideButton({ id }: { id: string }) {
onClick={openModal}
className="inline-flex h-7 w-7 items-center justify-center rounded hover:bg-zinc-800"
>
<HideIcon width={16} height={16} className="text-white" />
<HideIcon width={16} height={16} className="text-zinc-200" />
</button>
</Tooltip>
<Transition appear show={isOpen} as={Fragment}>

View File

@@ -2,22 +2,22 @@ import { MessageHideButton } from "@app/channel/components/messages/hideButton";
import { MessageMuteButton } from "@app/channel/components/messages/muteButton";
import { MessageReplyButton } from "@app/channel/components/messages/replyButton";
import { ChannelMessageUser } from "@app/channel/components/messages/user";
import { NDKEvent } from "@nostr-dev-kit/ndk";
import { MentionNote } from "@shared/notes/mentions/note";
import { ImagePreview } from "@shared/notes/preview/image";
import { LinkPreview } from "@shared/notes/preview/link";
import { VideoPreview } from "@shared/notes/preview/video";
import { parser } from "@utils/parser";
import { useMemo } from "react";
import { LumeEvent } from "@utils/types";
export function ChannelMessageItem({ data }: { data: NDKEvent }) {
const content = useMemo(() => parser(data), [data]);
export function ChannelMessageItem({ data }: { data: LumeEvent }) {
const content = parser(data);
return (
<div className="group relative flex h-min min-h-min w-full select-text flex-col px-5 py-3 hover:bg-black/20">
<div className="flex flex-col">
<ChannelMessageUser pubkey={data.pubkey} time={data.created_at} />
<div className="-mt-[20px] pl-[49px]">
<p className="whitespace-pre-line break-words text-base leading-tight">
<p className="select-text whitespace-pre-line break-words text-base text-zinc-100">
{content.parsed}
</p>
{Array.isArray(content.images) && content.images.length ? (
@@ -30,6 +30,11 @@ export function ChannelMessageItem({ data }: { data: NDKEvent }) {
) : (
<></>
)}
{Array.isArray(content.links) && content.links.length ? (
<LinkPreview urls={content.links} />
) : (
<></>
)}
{Array.isArray(content.notes) && content.notes.length ? (
content.notes.map((note: string) => (
<MentionNote key={note} id={note} />

View File

@@ -53,7 +53,7 @@ export function MessageMuteButton({ pubkey }: { pubkey: string }) {
onClick={() => openModal()}
className="inline-flex h-7 w-7 items-center justify-center rounded hover:bg-zinc-800"
>
<MuteIcon width={16} height={16} className="text-white" />
<MuteIcon width={16} height={16} className="text-zinc-200" />
</button>
</Tooltip>
<Transition appear show={isOpen} as={Fragment}>

View File

@@ -20,7 +20,7 @@ export function MessageReplyButton({
onClick={() => createReply()}
className="inline-flex h-7 w-7 items-center justify-center rounded hover:bg-zinc-800"
>
<ReplyMessageIcon width={16} height={16} className="text-white" />
<ReplyMessageIcon width={16} height={16} className="text-zinc-200" />
</button>
</Tooltip>
);

View File

@@ -4,95 +4,73 @@ import { ChannelMessageForm } from "@app/channel/components/messages/form";
import { ChannelMetadata } from "@app/channel/components/metadata";
import { RelayContext } from "@shared/relayProvider";
import { useChannelMessages } from "@stores/channels";
import { useVirtualizer } from "@tanstack/react-virtual";
import { dateToUnix, getHourAgo } from "@utils/date";
import { dateToUnix } from "@utils/date";
import { usePageContext } from "@utils/hooks/usePageContext";
import { LumeEvent } from "@utils/types";
import { useCallback, useContext, useEffect, useRef } from "react";
import { Virtuoso } from "react-virtuoso";
import useSWRSubscription from "swr/subscription";
const now = new Date();
const since = dateToUnix(getHourAgo(24, now));
export function Page() {
const ndk = useContext(RelayContext);
const pageContext = usePageContext();
const virtuosoRef = useRef(null);
const searchParams: any = pageContext.urlParsed.search;
const channelID = searchParams.id;
const [messages, addMessage, fetchMessages, clearMessages]: any =
const [messages, fetchMessages, addMessage, clearMessages] =
useChannelMessages((state: any) => [
state.messages,
state.addMessage,
state.fetch,
state.add,
state.clear,
]);
useSWRSubscription(["channelMessagesSubscribe", channelID], () => {
// subscribe to channel
const sub = ndk.subscribe({
"#e": [channelID],
kinds: [42],
since: dateToUnix(),
});
useSWRSubscription(
channelID ? ["channelMessagesSubscribe", channelID] : null,
() => {
// subscribe to channel
const sub = ndk.subscribe(
{
"#e": [channelID],
kinds: [42],
since: dateToUnix(),
},
{ closeOnEose: false },
);
sub.addListener("event", (event) => {
addMessage(event);
});
sub.addListener("event", (event: LumeEvent) => {
addMessage(channelID, event);
});
return () => {
sub.stop();
};
});
return () => {
sub.stop();
};
},
);
useEffect(() => {
fetchMessages(ndk, channelID, since);
fetchMessages(channelID);
return () => {
clearMessages();
};
}, [fetchMessages]);
const count = messages.length;
const reverseIndex = useCallback((index) => count - 1 - index, [count]);
const parentRef = useRef();
const virtualizerRef = useRef(null);
const itemContent: any = useCallback(
(index: string | number) => {
return <ChannelMessageItem data={messages[index]} />;
},
[messages],
);
if (
virtualizerRef.current &&
count !== virtualizerRef.current.options.count
) {
const delta = count - virtualizerRef.current.options.count;
const nextOffset = virtualizerRef.current.scrollOffset + delta * 200;
virtualizerRef.current.scrollOffset = nextOffset;
virtualizerRef.current.scrollToOffset(nextOffset, { align: "start" });
}
const virtualizer = useVirtualizer({
count,
getScrollElement: () => parentRef.current,
estimateSize: () => 200,
getItemKey: useCallback(
(index) => messages[reverseIndex(index)].id,
[messages, reverseIndex],
),
overscan: 5,
scrollMargin: 50,
});
useEffect(() => {
virtualizerRef.current = virtualizer;
}, []);
const items = virtualizer.getVirtualItems();
const [paddingTop, paddingBottom] =
items.length > 0
? [
Math.max(0, items[0].start - virtualizer.options.scrollMargin),
Math.max(0, virtualizer.getTotalSize() - items[items.length - 1].end),
]
: [0, 0];
const computeItemKey = useCallback(
(index: string | number) => {
return messages[index].event_id;
},
[messages],
);
return (
<div className="h-full w-full grid grid-cols-3">
@@ -105,40 +83,23 @@ export function Page() {
</div>
<div className="w-full flex-1 p-3">
<div className="flex h-full flex-col justify-between rounded-md bg-zinc-900">
<div
ref={parentRef}
className="scrollbar-hide overflow-y-auto h-full w-full"
style={{ contain: "strict" }}
>
{!messages ? (
<p>Loading...</p>
) : (
<div
style={{
overflowAnchor: "none",
paddingTop,
paddingBottom,
}}
>
{items.map((item) => {
const index = reverseIndex(item.index);
const message = messages[index];
return (
<div
key={item.key}
data-index={item.index}
data-reverse-index={index}
ref={virtualizer.measureElement}
>
<ChannelMessageItem data={message} />
</div>
);
})}
</div>
)}
</div>
<div className="w-full inline-flex shrink-0 border-t border-zinc-800">
{!messages ? (
<p>Loading...</p>
) : (
<Virtuoso
ref={virtuosoRef}
data={messages}
itemContent={itemContent}
computeItemKey={computeItemKey}
initialTopMostItemIndex={messages.length - 1}
alignToBottom={true}
followOutput={true}
overscan={50}
increaseViewportBy={{ top: 200, bottom: 200 }}
className="scrollbar-hide overflow-y-auto h-full w-full"
/>
)}
<div className="w-full inline-flex shrink-0 px-5 py-3 border-t border-zinc-800">
<ChannelMessageForm channelID={channelID} />
</div>
</div>

View File

@@ -18,11 +18,11 @@ export function Page() {
const searchParams: any = pageContext.urlParsed.search;
const pubkey = searchParams.pubkey;
const [fetchMessages, clear] = useChatMessages((state: any) => [
const [add, fetchMessages, clear] = useChatMessages((state: any) => [
state.add,
state.fetch,
state.clear,
]);
const add = useChatMessages((state: any) => state.add);
useSWRSubscription(account !== pubkey ? ["chat", pubkey] : null, () => {
const sub = ndk.subscribe({

View File

@@ -1,5 +1,11 @@
import { prefetchEvents } from "@libs/ndk";
import { countTotalNotes, createChat, createNote } from "@libs/storage";
import {
countTotalNotes,
createChannelMessage,
createChat,
createNote,
getChannels,
} from "@libs/storage";
import { NDKFilter } from "@nostr-dev-kit/ndk";
import { LumeIcon } from "@shared/icons";
import { RelayContext } from "@shared/relayProvider";
@@ -98,12 +104,52 @@ export function Page() {
}
}
async function fetchChannelMessages() {
try {
const ids = [];
const channels: any = await getChannels(10, 0);
channels.forEach((channel) => {
ids.push(channel.event_id);
});
const since =
lastLogin === 0 ? dateToUnix(getHourAgo(48, now.current)) : lastLogin;
const filter: NDKFilter = {
"#e": ids,
kinds: [42],
since: since,
};
const events = await prefetchEvents(ndk, filter);
events.forEach((event) => {
const channel_id = event.tags[0][1];
if (channel_id) {
createChannelMessage(
channel_id,
event.id,
event.pubkey,
event.kind,
event.content,
event.tags,
event.created_at,
);
}
});
return true;
} catch (e) {
console.log("error: ", e);
}
}
useEffect(() => {
async function prefetch() {
const notes = await fetchNotes();
if (notes) {
const chats = await fetchChats();
if (chats) {
const channels = await fetchChannelMessages();
if (chats && channels) {
navigate("/app/space", { overwriteLastHistoryEntry: true });
}
}

View File

@@ -35,7 +35,7 @@ export async function prefetchEvents(
});
relaySetSubscription.on("eose", () => {
setTimeout(() => resolve(new Set(events.values())), 3000);
setTimeout(() => resolve(new Set(events.values())), 5000);
});
});
}

View File

@@ -303,6 +303,31 @@ export async function updateChannelMetadata(event_id: string, value: string) {
);
}
// create channel messages
export async function createChannelMessage(
channel_id: string,
event_id: string,
pubkey: string,
kind: number,
content: string,
tags: string[][],
created_at: number,
) {
const db = await connect();
return await db.execute(
"INSERT OR IGNORE INTO channel_messages (channel_id, event_id, pubkey, kind, content, tags, created_at) VALUES (?, ?, ?, ?, ?, ?, ?);",
[channel_id, event_id, pubkey, kind, content, tags, created_at],
);
}
// get channel messages by channel id
export async function getChannelMessages(channel_id: string) {
const db = await connect();
return await db.select(
`SELECT * FROM channel_messages WHERE channel_id = "${channel_id}" ORDER BY created_at ASC;`,
);
}
// get all chats by pubkey
export async function getChatsByPubkey(pubkey: string) {
const db = await connect();

View File

@@ -2,7 +2,7 @@ import { Image } from "@shared/image";
export function ImagePreview({ urls }: { urls: string[] }) {
return (
<div className="mt-3 overflow-hidden">
<div className="mt-3 max-w-[420px] overflow-hidden">
<div className="flex flex-col gap-2">
{urls.map((url) => (
<div key={url} className="min-w-0 grow-0 shrink-0 basis-full">

View File

@@ -6,7 +6,7 @@ export function LinkPreview({ urls }: { urls: string[] }) {
const { data, error, isLoading } = useOpenGraph(urls[0]);
return (
<div className="mt-3 overflow-hidden rounded-lg bg-zinc-800">
<div className="mt-3 max-w-[420px] overflow-hidden rounded-lg bg-zinc-800">
{error && <p>failed to load</p>}
{isLoading || !data ? (
<div className="flex flex-col">

View File

@@ -5,7 +5,7 @@ export function VideoPreview({ urls }: { urls: string[] }) {
<div
onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => e.stopPropagation()}
className="relative mt-2 flex w-full flex-col overflow-hidden rounded-lg bg-zinc-950"
className="relative mt-3 max-w-[420px] flex w-full flex-col overflow-hidden rounded-lg bg-zinc-950"
>
<MediaPlayer src={urls[0]} poster="" controls>
<MediaOutlet />

View File

@@ -1,5 +1,9 @@
import { getChannels } from "@libs/storage";
import NDK, { NDKFilter } from "@nostr-dev-kit/ndk";
import {
createChannelMessage,
getChannelMessages,
getChannels,
} from "@libs/storage";
import { LumeEvent } from "@utils/types";
import { create } from "zustand";
import { immer } from "zustand/middleware/immer";
@@ -38,19 +42,28 @@ export const useChannelMessages = create(
immer((set) => ({
messages: [],
replyTo: { id: null, pubkey: null, content: null },
fetch: async (ndk: NDK, id: string, since: number) => {
const filter: NDKFilter = {
"#e": [id],
kinds: [42],
since: since,
};
const events = await ndk.fetchEvents(filter);
const array = [...events];
set({ messages: array });
fetch: async (id: string) => {
const events = await getChannelMessages(id);
set({ messages: events });
},
add: (message: any) => {
add: (id, event: LumeEvent) => {
set((state: any) => {
state.messages.push(message);
createChannelMessage(
id,
event.id,
event.pubkey,
event.kind,
event.content,
event.tags,
event.created_at,
);
state.messages.push({
event_id: event.id,
channel_id: id,
hide: 0,
mute: 0,
...event,
});
});
},
openReply: (id: string, pubkey: string, content: string) => {

View File

@@ -4,8 +4,19 @@ import getUrls from "get-urls";
import { parseReferences } from "nostr-tools";
import reactStringReplace from "react-string-replace";
function isJsonString(str) {
try {
JSON.parse(str);
} catch (e) {
return false;
}
return true;
}
export function parser(event: any) {
event.tags = destr(event.tags);
if (isJsonString(event.tags)) {
event["tags"] = destr(event.tags);
}
const references = parseReferences(event);
const urls = getUrls(event.content);