diff --git a/packages/app/config/default.json b/packages/app/config/default.json index c0db1394..36e4cd12 100644 --- a/packages/app/config/default.json +++ b/packages/app/config/default.json @@ -6,6 +6,7 @@ "nip05Domain": "snort.social", "favicon": "public/favicon.ico", "appleTouchIconUrl": "/nostrich_512.png", + "navLogo": null, "publicDir": "public/snort", "httpCache": "", "animalNamePlaceholders": false, diff --git a/packages/app/config/iris.json b/packages/app/config/iris.json index 18eb7149..a117f1fe 100644 --- a/packages/app/config/iris.json +++ b/packages/app/config/iris.json @@ -6,6 +6,7 @@ "nip05Domain": "iris.to", "favicon": "public/iris/favicon.ico", "appleTouchIconUrl": "/img/apple-touch-icon.png", + "navLogo": "/img/icon128.png", "publicDir": "public/iris", "httpCache": "https://api.iris.to", "animalNamePlaceholders": true, diff --git a/packages/app/custom.d.ts b/packages/app/custom.d.ts index dc8d941f..c1c1ac32 100644 --- a/packages/app/custom.d.ts +++ b/packages/app/custom.d.ts @@ -48,6 +48,7 @@ declare const CONFIG: { nip05Domain: string; favicon: string; appleTouchIconUrl: string; + navLogo: string | null; httpCache: string; animalNamePlaceholders: boolean; defaultZapPoolFee?: number; diff --git a/packages/app/jest.config.js b/packages/app/jest.config.js deleted file mode 100644 index 928edeee..00000000 --- a/packages/app/jest.config.js +++ /dev/null @@ -1,9 +0,0 @@ -/** @type {import('ts-jest').JestConfigWithTsJest} */ -module.exports = { - bail: true, - preset: "ts-jest", - testEnvironment: "jsdom", - roots: ["src"], - moduleDirectories: ["src", "node_modules"], - setupFiles: ["./src/setupTests.ts"], -}; diff --git a/packages/app/package.json b/packages/app/package.json index 9c291ae8..a6a8ab19 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -15,12 +15,13 @@ "@snort/system-wasm": "workspace:*", "@snort/system-web": "workspace:*", "@szhsin/react-menu": "^3.3.1", - "@uidotdev/usehooks": "^2.3.1", + "@uidotdev/usehooks": "^2.4.1", "@void-cat/api": "^1.0.10", "classnames": "^2.3.2", "debug": "^4.3.4", "dexie": "^3.2.4", "emojilib": "^3.0.10", + "fuse.js": "^7.0.0", "highlight.js": "^11.8.0", "light-bolt11-decoder": "^2.1.0", "marked": "^9.1.0", @@ -49,7 +50,8 @@ "start": "vite", "build": "yarn eslint --fix && vite build", "serve": "vite preview", - "test": "jest --runInBand", + "test": "vitest run", + "test:watch": "vitest watch", "intl-extract": "formatjs extract 'src/**/*.ts*' --ignore='**/*.d.ts' --out-file src/lang.json --flatten true", "intl-compile": "formatjs compile src/lang.json --out-file src/translations/en.json", "eslint": "eslint ." @@ -75,10 +77,8 @@ }, "devDependencies": { "@formatjs/cli": "^6.1.3", - "@jest/globals": "^29.6.1", "@types/config": "^3.3.3", "@types/debug": "^4.1.8", - "@types/jest": "^29.5.1", "@types/node": "^20.4.1", "@types/react": "^18.0.26", "@types/react-dom": "^18.0.10", @@ -96,8 +96,6 @@ "config": "^3.3.9", "eslint": "^8.48.0", "eslint-plugin-formatjs": "^4.11.3", - "jest": "^29.5.0", - "jest-environment-jsdom": "^29.5.0", "postcss": "^8.4.31", "postcss-preset-env": "^9.2.0", "prettier": "2.8.3", @@ -105,10 +103,10 @@ "rollup-plugin-visualizer": "^5.9.2", "tailwindcss": "^3.3.3", "tinybench": "^2.5.1", - "ts-jest": "^29.1.1", "typescript": "^5.2.2", "vite": "^5.0.0", "vite-plugin-pwa": "^0.17.0", - "vite-plugin-version-mark": "^0.0.10" + "vite-plugin-version-mark": "^0.0.10", + "vitest": "^0.34.6" } } diff --git a/packages/app/public/iris/img/icon128.png b/packages/app/public/iris/img/icon128.png new file mode 100644 index 00000000..9546ad50 Binary files /dev/null and b/packages/app/public/iris/img/icon128.png differ diff --git a/packages/app/src/Cache/FollowsFeed.ts b/packages/app/src/Cache/FollowsFeed.ts index dcc0d979..89e657f9 100644 --- a/packages/app/src/Cache/FollowsFeed.ts +++ b/packages/app/src/Cache/FollowsFeed.ts @@ -27,10 +27,12 @@ export class FollowsFeedCache extends RefreshFeedCache { } buildSub(session: LoginSession, rb: RequestBuilder): void { + const authors = session.follows.item; + authors.push(session.publicKey); const since = this.newest(); rb.withFilter() .kinds(this.#kinds) - .authors(session.follows.item) + .authors(authors) .since(since === 0 ? unixNow() - WindowSize : since); } @@ -67,9 +69,11 @@ export class FollowsFeedCache extends RefreshFeedCache { async loadMore(system: SystemInterface, session: LoginSession, before: number) { if (this.#oldest && before <= this.#oldest) { const rb = new RequestBuilder(`${this.name}-loadmore`); + const authors = session.follows.item; + authors.push(session.publicKey); rb.withFilter() .kinds(this.#kinds) - .authors(session.follows.item) + .authors(authors) .until(before) .since(before - WindowSize); await system.Fetch(rb, async evs => { diff --git a/packages/app/src/Element/Deck/Articles.tsx b/packages/app/src/Element/Articles.tsx similarity index 96% rename from packages/app/src/Element/Deck/Articles.tsx rename to packages/app/src/Element/Articles.tsx index 4cd8d6ca..0cc436cb 100644 --- a/packages/app/src/Element/Deck/Articles.tsx +++ b/packages/app/src/Element/Articles.tsx @@ -3,7 +3,7 @@ import { useReactions } from "@snort/system-react"; import { useArticles } from "@/Feed/ArticlesFeed"; import { orderDescending } from "@/SnortUtils"; -import Note from "../Event/Note"; +import Note from "./Event/Note"; import { useContext } from "react"; import { DeckContext } from "@/Pages/DeckLayout"; diff --git a/packages/app/src/Element/Chat/ChatParticipant.tsx b/packages/app/src/Element/Chat/ChatParticipant.tsx index 9cd8bc0c..20d52b27 100644 --- a/packages/app/src/Element/Chat/ChatParticipant.tsx +++ b/packages/app/src/Element/Chat/ChatParticipant.tsx @@ -10,5 +10,12 @@ export function ChatParticipantProfile({ participant }: { participant: ChatParti if (participant.id === publicKey) { return ; } - return ; + return ( + + ); } diff --git a/packages/app/src/Element/Chat/DM.css b/packages/app/src/Element/Chat/DM.css index 9c5a17ba..766672f0 100644 --- a/packages/app/src/Element/Chat/DM.css +++ b/packages/app/src/Element/Chat/DM.css @@ -1,37 +1,7 @@ -.dm { - margin-top: 16px; - min-width: 100px; - max-width: 90%; - white-space: pre-wrap; - color: var(--font-color); -} - -.dm a { - color: var(--font-color) !important; -} - -.dm > div:last-child { - color: var(--gray-light); - font-size: small; - margin-top: 3px; -} - -.dm.other > div:first-child { - padding: 12px 16px; - background: var(--gray-secondary); - border-radius: 16px 16px 16px 0px; -} - -.dm.me { - align-self: flex-end; -} - -.dm.me > div:first-child { - padding: 12px 16px; +.dm-gradient { background: var(--dm-gradient); - border-radius: 16px 16px 0px 16px; } -.dm.me > div:last-child { - text-align: end; +.other { + background: var(--gray-superdark); } diff --git a/packages/app/src/Element/Chat/DM.tsx b/packages/app/src/Element/Chat/DM.tsx index 831d7a66..06999176 100644 --- a/packages/app/src/Element/Chat/DM.tsx +++ b/packages/app/src/Element/Chat/DM.tsx @@ -1,4 +1,5 @@ import "./DM.css"; + import { useEffect, useState } from "react"; import { FormattedMessage, useIntl } from "react-intl"; import { useInView } from "react-intersection-observer"; @@ -55,8 +56,19 @@ export default function DM(props: DMProps) { }, [inView]); return ( -
-
+
+
{sender()} {content ? ( @@ -64,7 +76,7 @@ export default function DM(props: DMProps) { )}
-
+
diff --git a/packages/app/src/Element/Chat/DmWindow.css b/packages/app/src/Element/Chat/DmWindow.css deleted file mode 100644 index 1fbefc88..00000000 --- a/packages/app/src/Element/Chat/DmWindow.css +++ /dev/null @@ -1,32 +0,0 @@ -.dm-window { - display: flex; - flex-direction: column; - height: 100%; -} -.dm-window > div:nth-child(1) { - padding: 12px 0; -} - -.dm-window > div:nth-child(2) { - overflow-y: auto; - padding: 0 10px 10px 10px; - flex-grow: 1; - display: flex; - flex-direction: column-reverse; -} - -.dm-window > div:nth-child(3) { - display: flex; - align-items: center; - gap: 10px; - padding: 5px 10px; -} - -.pfp-overlap .pfp:not(:last-of-type) { - margin-right: -20px; -} - -.pfp-overlap .avatar { - width: 32px; - height: 32px; -} diff --git a/packages/app/src/Element/Chat/DmWindow.tsx b/packages/app/src/Element/Chat/DmWindow.tsx index c7c07062..f8e52495 100644 --- a/packages/app/src/Element/Chat/DmWindow.tsx +++ b/packages/app/src/Element/Chat/DmWindow.tsx @@ -1,6 +1,4 @@ -import "./DmWindow.css"; -import { useMemo } from "react"; - +import { useEffect, useMemo, useRef } from "react"; import ProfileImage from "@/Element/User/ProfileImage"; import DM from "@/Element/Chat/DM"; import useLogin from "@/Hooks/useLogin"; @@ -18,7 +16,7 @@ export default function DmWindow({ id }: { id: string }) { return ; } else { return ( -
+
{chat.participants.map(v => ( ))} @@ -29,12 +27,12 @@ export default function DmWindow({ id }: { id: string }) { } return ( -
-
{sender()}
-
+
+
{sender()}
+
{chat && }
-
+
@@ -43,6 +41,9 @@ export default function DmWindow({ id }: { id: string }) { function DmChatSelected({ chat }: { chat: Chat }) { const { publicKey: myPubKey } = useLogin(s => ({ publicKey: s.publicKey })); + const messagesContainerRef = useRef(null); + const messagesEndRef = useRef(null); + const sortedDms = useMemo(() => { const myDms = chat?.messages; if (myPubKey && myDms) { @@ -52,11 +53,37 @@ function DmChatSelected({ chat }: { chat: Chat }) { return []; }, [chat, myPubKey]); + const scrollToBottom = () => { + messagesEndRef.current?.scrollIntoView({ behavior: "instant" }); + }; + + useEffect(() => { + const observer = new ResizeObserver(() => { + scrollToBottom(); + }); + + // Start observing the element that you want to keep in view + if (messagesContainerRef.current) { + observer.observe(messagesContainerRef.current); + } + + // Make sure to scroll to bottom on initial load + scrollToBottom(); + + // Clean up the observer on component unmount + return () => { + if (messagesContainerRef.current) { + observer.unobserve(messagesContainerRef.current); + } + }; + }, [sortedDms]); + return ( - <> +
{sortedDms.map(a => ( ))} - +
+
); } diff --git a/packages/app/src/Element/Chat/WriteMessage.tsx b/packages/app/src/Element/Chat/WriteMessage.tsx index b7d51189..1340cf47 100644 --- a/packages/app/src/Element/Chat/WriteMessage.tsx +++ b/packages/app/src/Element/Chat/WriteMessage.tsx @@ -1,54 +1,12 @@ import { useState } from "react"; -import { NostrEvent, NostrLink, NostrPrefix } from "@snort/system"; import useEventPublisher from "@/Hooks/useEventPublisher"; -import useFileUpload from "@/Upload"; -import { openFile } from "@/SnortUtils"; import Textarea from "../Textarea"; import { Chat } from "@/chat"; import { AsyncIcon } from "@/Element/AsyncIcon"; export default function WriteMessage({ chat }: { chat: Chat }) { const [msg, setMsg] = useState(""); - const [otherEvents, setOtherEvents] = useState>([]); - const [error, setError] = useState(""); const { publisher, system } = useEventPublisher(); - const uploader = useFileUpload(); - - async function attachFile() { - try { - const file = await openFile(); - if (file) { - uploadFile(file); - } - } catch (e) { - if (e instanceof Error) { - setError(e.message); - } - } - } - - async function uploadFile(file: File | Blob) { - try { - if (file) { - const rx = await uploader.upload(file, file.name); - if (rx.header) { - const link = `nostr:${new NostrLink(NostrPrefix.Event, rx.header.id, rx.header.kind).encode( - CONFIG.eventLinkPrefix, - )}`; - setMsg(`${msg ? `${msg}\n` : ""}${link}`); - setOtherEvents([...otherEvents, rx.header]); - } else if (rx.url) { - setMsg(`${msg ? `${msg}\n` : ""}${rx.url}`); - } else if (rx?.error) { - setError(rx.error); - } - } - } catch (e) { - if (e instanceof Error) { - setError(e.message); - } - } - } async function sendMessage() { if (msg && publisher && chat) { @@ -72,7 +30,6 @@ export default function WriteMessage({ chat }: { chat: Chat }) { return ( <> - attachFile()} />