refactor: improve whitelabel config
@ -4,8 +4,7 @@
|
||||
"appTitle": "Snort - Nostr",
|
||||
"hostname": "snort.social",
|
||||
"nip05Domain": "snort.social",
|
||||
"favicon": "public/favicon.ico",
|
||||
"appleTouchIconUrl": "/nostrich_512.png",
|
||||
"icon": "/nostrich_512.png",
|
||||
"navLogo": null,
|
||||
"publicDir": "public/snort",
|
||||
"httpCache": "",
|
||||
|
@ -4,8 +4,7 @@
|
||||
"appTitle": "iris",
|
||||
"hostname": "iris.to",
|
||||
"nip05Domain": "iris.to",
|
||||
"favicon": "public/iris/favicon.ico",
|
||||
"appleTouchIconUrl": "/img/apple-touch-icon.png",
|
||||
"icon": "/img/icon128.png",
|
||||
"navLogo": "/img/icon128.png",
|
||||
"publicDir": "public/iris",
|
||||
"httpCache": "",
|
||||
|
@ -4,10 +4,9 @@
|
||||
"appTitle": "Nostr",
|
||||
"hostname": "nostr.com",
|
||||
"nip05Domain": "nostr.com",
|
||||
"favicon": "public/favicon.ico",
|
||||
"appleTouchIconUrl": "/nostrich_512.png",
|
||||
"icon": "/nostr.jpg",
|
||||
"navLogo": null,
|
||||
"publicDir": "public/snort",
|
||||
"publicDir": "public/nostr",
|
||||
"httpCache": "",
|
||||
"animalNamePlaceholders": false,
|
||||
"features": {
|
||||
@ -40,6 +39,7 @@
|
||||
"defaultRelays": {
|
||||
"wss://relay.snort.social/": { "read": true, "write": true },
|
||||
"wss://nostr.wine/": { "read": true, "write": false },
|
||||
"wss://eden.nostr.land/": { "read": true, "write": false }
|
||||
"wss://eden.nostr.land/": { "read": true, "write": false },
|
||||
"wss://nos.lol/": { "read": true, "write": true }
|
||||
}
|
||||
}
|
||||
|
3
packages/app/custom.d.ts
vendored
@ -47,8 +47,7 @@ declare const CONFIG: {
|
||||
appTitle: string;
|
||||
hostname: string;
|
||||
nip05Domain: string;
|
||||
favicon: string;
|
||||
appleTouchIconUrl: string;
|
||||
icon: string;
|
||||
navLogo: string | null;
|
||||
httpCache: string;
|
||||
animalNamePlaceholders: boolean;
|
||||
|
@ -11,10 +11,12 @@
|
||||
name="keywords"
|
||||
content="nostr snort fast decentralized social media censorship resistant open source software" />
|
||||
<link rel="preconnect" href="https://imgproxy.snort.social" />
|
||||
<link rel="apple-touch-icon" href="" />
|
||||
<link rel="apple-touch-icon" href="/img/apple-touch-icon.png" />
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<link rel="icon" href="/favicon.png" />
|
||||
<title></title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/index.tsx"></script>
|
||||
|
Before Width: | Height: | Size: 15 KiB |
BIN
packages/app/public/iris/favicon.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
packages/app/public/nostr/favicon.png
Normal file
After Width: | Height: | Size: 165 B |
BIN
packages/app/public/nostr/img/apple-touch-icon.png
Normal file
After Width: | Height: | Size: 405 B |
BIN
packages/app/public/nostr/nostr.jpg
Normal file
After Width: | Height: | Size: 3.9 KiB |
BIN
packages/app/public/snort/favicon.png
Normal file
After Width: | Height: | Size: 2.8 KiB |
BIN
packages/app/public/snort/img/apple-touch-icon.png
Normal file
After Width: | Height: | Size: 44 KiB |
29
packages/app/src/Hooks/useEmptyChatSystem.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
import { unwrap } from "@snort/shared";
|
||||
import { decodeTLV, EventKind, RequestBuilder, TLVEntryType } from "@snort/system";
|
||||
import { useRequestBuilder } from "@snort/system-react";
|
||||
import { useMemo } from "react";
|
||||
|
||||
import { createEmptyChatObject } from "@/chat";
|
||||
|
||||
export function useEmptyChatSystem(id?: string) {
|
||||
const sub = useMemo(() => {
|
||||
if (!id) return;
|
||||
|
||||
if (id.startsWith("chat281")) {
|
||||
const cx = unwrap(decodeTLV(id).find(a => a.type === TLVEntryType.Special)).value as string;
|
||||
const rb = new RequestBuilder(`nip28:${id}`);
|
||||
rb.withFilter().ids([cx]).kinds([EventKind.PublicChatChannel, EventKind.PublicChatMetadata]);
|
||||
rb.withFilter()
|
||||
.tag("e", [cx])
|
||||
.kinds([EventKind.PublicChatChannel, EventKind.PublicChatMessage, EventKind.PublicChatMetadata]);
|
||||
|
||||
return rb;
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
const data = useRequestBuilder(sub);
|
||||
return useMemo(() => {
|
||||
if (!id) return;
|
||||
return createEmptyChatObject(id, data);
|
||||
}, [id, data.length]);
|
||||
}
|
@ -12,6 +12,7 @@ import Modal from "@/Components/Modal/Modal";
|
||||
import QrCode from "@/Components/QrCode";
|
||||
import ProfilePreview from "@/Components/User/ProfilePreview";
|
||||
import SnortApi, { RevenueSplit, RevenueToday } from "@/External/SnortApi";
|
||||
import useLogin from "@/Hooks/useLogin";
|
||||
import { bech32ToHex, unwrap } from "@/Utils";
|
||||
import { ApiHost, DeveloperAccounts, SnortPubKey } from "@/Utils/Const";
|
||||
import { ZapPoolController, ZapPoolRecipientType } from "@/Utils/ZapPoolController";
|
||||
@ -67,6 +68,7 @@ const DonatePage = () => {
|
||||
const [onChain, setOnChain] = useState("");
|
||||
const api = new SnortApi(ApiHost);
|
||||
const navigate = useNavigate();
|
||||
const login = useLogin();
|
||||
|
||||
async function getOnChainAddress() {
|
||||
const { address } = await api.onChainDonation();
|
||||
@ -153,9 +155,10 @@ const DonatePage = () => {
|
||||
return (
|
||||
<AsyncButton
|
||||
onClick={() => {
|
||||
navigate(`/messages/${Nip28ChatSystem.chatId(a.value)}`);
|
||||
const id = Nip28ChatSystem.chatId(a.value);
|
||||
navigate(`/messages/${id}`);
|
||||
}}>
|
||||
<img src={CONFIG.appleTouchIconUrl} width={24} height={24} className="rounded-full" />
|
||||
<img src={CONFIG.icon} width={24} height={24} className="rounded-full" />
|
||||
<FormattedMessage defaultMessage="Nostr Public Chat" id="whSrs+" />
|
||||
</AsyncButton>
|
||||
);
|
||||
|
@ -1,19 +1,18 @@
|
||||
import { useEffect, useMemo, useRef } from "react";
|
||||
import { useMemo } from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
import { Chat, createEmptyChatObject, useChatSystem } from "@/chat";
|
||||
import { Chat, useChat } from "@/chat";
|
||||
import ProfileImage from "@/Components/User/ProfileImage";
|
||||
import useLogin from "@/Hooks/useLogin";
|
||||
import DM from "@/Pages/Messages/DM";
|
||||
import WriteMessage from "@/Pages/Messages/WriteMessage";
|
||||
|
||||
import { ChatParticipantProfile } from "./ChatParticipant";
|
||||
|
||||
export default function DmWindow({ id }: { id: string }) {
|
||||
const dms = useChatSystem();
|
||||
const chat = dms.find(a => a.id === id) ?? createEmptyChatObject(id);
|
||||
const chat = useChat(id);
|
||||
|
||||
function sender() {
|
||||
if (!chat) return;
|
||||
if (chat.participants.length === 1) {
|
||||
return <ChatParticipantProfile participant={chat.participants[0]} />;
|
||||
} else {
|
||||
@ -29,63 +28,28 @@ export default function DmWindow({ id }: { id: string }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-1 flex-col h-[calc(100vh-62px)] md:h-screen">
|
||||
<div className="flex flex-1 flex-col h-full w-full md:h-screen">
|
||||
<div className="p-3">{sender()}</div>
|
||||
<div className="overflow-y-auto hide-scrollbar p-2.5 flex-grow">
|
||||
<div className="flex flex-col">{chat && <DmChatSelected chat={chat} />}</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2.5 p-2.5">
|
||||
<WriteMessage chat={chat} />
|
||||
</div>
|
||||
<div className="overflow-y-auto hide-scrollbar p-2.5 flex-grow">{chat && <DmChatSelected chat={chat} />}</div>
|
||||
<div className="flex items-center gap-2.5 p-2.5">{chat && <WriteMessage chat={chat} />}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DmChatSelected({ chat }: { chat: Chat }) {
|
||||
const { publicKey: myPubKey } = useLogin(s => ({ publicKey: s.publicKey }));
|
||||
const messagesContainerRef = useRef<HTMLDivElement>(null);
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const sortedDms = useMemo(() => {
|
||||
const myDms = chat?.messages;
|
||||
if (myPubKey && myDms) {
|
||||
// filter dms in this chat, or dms to self
|
||||
return [...myDms].sort((a, b) => a.created_at - b.created_at);
|
||||
if (myDms) {
|
||||
return [...myDms].sort((a, b) => (a.created_at > b.created_at ? -1 : 1));
|
||||
}
|
||||
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]);
|
||||
}, [chat]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col" ref={messagesContainerRef}>
|
||||
<div className="flex flex-col-reverse">
|
||||
{sortedDms.map(a => (
|
||||
<DM data={a} key={a.id} chat={chat} />
|
||||
))}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import classNames from "classnames";
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import React, { useMemo } from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
|
||||
@ -13,7 +13,6 @@ import { ChatParticipantProfile } from "@/Pages/Messages/ChatParticipant";
|
||||
import DmWindow from "@/Pages/Messages/DmWindow";
|
||||
import NewChatWindow from "@/Pages/Messages/NewChatWindow";
|
||||
import UnreadCount from "@/Pages/Messages/UnreadCount";
|
||||
import { parseId } from "@/Utils";
|
||||
|
||||
const TwoCol = 768;
|
||||
|
||||
@ -22,13 +21,8 @@ export default function MessagesPage() {
|
||||
const { formatMessage } = useIntl();
|
||||
const navigate = useNavigate();
|
||||
const { id } = useParams();
|
||||
const [chat, setChat] = useState<string>();
|
||||
const pageWidth = usePageWidth();
|
||||
|
||||
useEffect(() => {
|
||||
const parsedId = parseId(id ?? "");
|
||||
setChat(id ? parsedId : undefined);
|
||||
}, [id]);
|
||||
const chats = useChatSystem();
|
||||
|
||||
const unreadCount = useMemo(() => chats.reduce((p, c) => p + c.unread, 0), [chats]);
|
||||
@ -67,7 +61,7 @@ export default function MessagesPage() {
|
||||
const participants = cx.participants.map(a => a.id);
|
||||
if (participants.length === 1 && participants[0] === login.publicKey) return noteToSelf(cx);
|
||||
|
||||
const isActive = cx.id === chat;
|
||||
const isActive = cx.id === id;
|
||||
return (
|
||||
<div
|
||||
className={classNames("flex items-center p cursor-pointer justify-between", { active: isActive })}
|
||||
@ -89,7 +83,7 @@ export default function MessagesPage() {
|
||||
|
||||
return (
|
||||
<div className="flex flex-1 md:h-screen md:overflow-hidden">
|
||||
{(pageWidth >= TwoCol || !chat) && (
|
||||
{pageWidth >= TwoCol && !id && (
|
||||
<div className="overflow-y-auto md:h-screen p-1 w-full md:w-1/3 flex-shrink-0">
|
||||
<div className="flex items-center justify-between p-2">
|
||||
<button disabled={unreadCount <= 0} type="button" className="text-sm font-semibold">
|
||||
@ -109,7 +103,7 @@ export default function MessagesPage() {
|
||||
.map(conversation)}
|
||||
</div>
|
||||
)}
|
||||
{chat ? <DmWindow id={chat} /> : pageWidth >= TwoCol && <div className="flex-1 rt-border"></div>}
|
||||
{id ? <DmWindow id={id} /> : pageWidth >= TwoCol && <div className="flex-1 rt-border"></div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -71,7 +71,7 @@ export function SignIn() {
|
||||
const nip7Login = hasNip7 && !useKey;
|
||||
return (
|
||||
<div className="flex flex-col g24">
|
||||
<img src={CONFIG.appleTouchIconUrl} width={48} height={48} className="br mr-auto ml-auto" />
|
||||
<img src={CONFIG.icon} width={48} height={48} className="br mr-auto ml-auto" />
|
||||
<div className="flex flex-col g16 items-center">
|
||||
<h1>
|
||||
<FormattedMessage defaultMessage="Sign In" id="Ub+AGc" />
|
||||
@ -152,7 +152,7 @@ export function SignUp() {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col g24">
|
||||
<img src={CONFIG.appleTouchIconUrl} width={48} height={48} className="br mr-auto ml-auto" />
|
||||
<img src={CONFIG.icon} width={48} height={48} className="br mr-auto ml-auto" />
|
||||
<div className="flex flex-col g16 items-center">
|
||||
<h1>
|
||||
<FormattedMessage defaultMessage="Sign Up" id="39AHJm" />
|
||||
|
@ -15,6 +15,7 @@ import {
|
||||
import { useSyncExternalStore } from "react";
|
||||
|
||||
import { Chats, GiftsCache } from "@/Cache";
|
||||
import { useEmptyChatSystem } from "@/Hooks/useEmptyChatSystem";
|
||||
import useLogin from "@/Hooks/useLogin";
|
||||
import useModeration from "@/Hooks/useModeration";
|
||||
import { findTag } from "@/Utils";
|
||||
@ -155,15 +156,15 @@ export function createChatLink(type: ChatType, ...params: Array<string>) {
|
||||
throw new Error("Unknown chat type");
|
||||
}
|
||||
|
||||
export function createEmptyChatObject(id: string) {
|
||||
export function createEmptyChatObject(id: string, messages?: Array<TaggedNostrEvent>) {
|
||||
if (id.startsWith("chat41")) {
|
||||
return Nip4ChatSystem.createChatObj(id, []);
|
||||
return Nip4ChatSystem.createChatObj(id, messages ?? []);
|
||||
}
|
||||
if (id.startsWith("chat241")) {
|
||||
return Nip24ChatSystem.createChatObj(id, []);
|
||||
}
|
||||
if (id.startsWith("chat281")) {
|
||||
return Nip28ChatSystem.createChatObj(id, []);
|
||||
return Nip28ChatSystem.createChatObj(id, messages ?? []);
|
||||
}
|
||||
throw new Error("Cant create new empty chat, unknown id");
|
||||
}
|
||||
@ -209,3 +210,24 @@ export function useChatSystem() {
|
||||
return authors.length === 0 || !authors.every(a => isBlocked(a));
|
||||
});
|
||||
}
|
||||
|
||||
export function useChat(id: string) {
|
||||
const getStore = () => {
|
||||
if (id.startsWith("chat41")) {
|
||||
return Nip4Chats;
|
||||
}
|
||||
if (id.startsWith("chat281")) {
|
||||
return Nip28Chats;
|
||||
}
|
||||
throw new Error("Unsupported chat system");
|
||||
};
|
||||
const store = getStore();
|
||||
const ret = useSyncExternalStore(
|
||||
c => store.hook(c),
|
||||
() => {
|
||||
return store.snapshot().find(a => a.id === id);
|
||||
},
|
||||
);
|
||||
const emptyChat = useEmptyChatSystem(ret === undefined ? id : undefined);
|
||||
return ret ?? emptyChat;
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import "./index.css";
|
||||
import "@szhsin/react-menu/dist/index.css";
|
||||
import "@/assets/fonts/inter.css";
|
||||
|
||||
import { encodeTLVEntries, socialGraphInstance } from "@snort/system";
|
||||
import { socialGraphInstance } from "@snort/system";
|
||||
import { SnortContext } from "@snort/system-react";
|
||||
import { StrictMode } from "react";
|
||||
import * as ReactDOM from "react-dom/client";
|
||||
@ -34,6 +34,9 @@ import { RootTabRoutes } from "@/Pages/Root/RootTabRoutes";
|
||||
import SearchPage from "@/Pages/SearchPage";
|
||||
import SettingsRoutes from "@/Pages/settings/Routes";
|
||||
import { SubscribeRoutes } from "@/Pages/subscribe";
|
||||
import WalletPage from "@/Pages/wallet";
|
||||
import { WalletReceivePage } from "@/Pages/wallet/receive";
|
||||
import { WalletSendPage } from "@/Pages/wallet/send";
|
||||
import ZapPoolPage from "@/Pages/ZapPool";
|
||||
import { System } from "@/system";
|
||||
import { storeRefCode, unwrap } from "@/Utils";
|
||||
@ -42,10 +45,6 @@ import { hasWasm, wasmInit, WasmPath } from "@/Utils/wasm";
|
||||
import { Wallets } from "@/Wallet";
|
||||
import { setupWebLNWalletConfig } from "@/Wallet/WebLN";
|
||||
|
||||
import WalletPage from "./Pages/wallet";
|
||||
import { WalletReceivePage } from "./Pages/wallet/receive";
|
||||
import { WalletSendPage } from "./Pages/wallet/send";
|
||||
|
||||
async function initSite() {
|
||||
storeRefCode();
|
||||
if (hasWasm) {
|
||||
@ -222,10 +221,5 @@ root.render(
|
||||
</StrictMode>,
|
||||
);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
window.encodeTLV = encodeTLVEntries;
|
||||
|
||||
// Use react-helmet instead?
|
||||
document.title = CONFIG.appTitle;
|
||||
document.querySelector('link[rel="apple-touch-icon"]')?.setAttribute("href", CONFIG.appleTouchIconUrl);
|
||||
|
@ -272,7 +272,6 @@ function makeNotification(n: PushNotification) {
|
||||
const ret = {
|
||||
body: body(),
|
||||
icon: evx.author.avatar ?? defaultAvatar(evx.author.pubkey),
|
||||
badge: `${location.protocol}//${location.hostname}${CONFIG.appleTouchIconUrl}`,
|
||||
timestamp: evx.created_at * 1000,
|
||||
tag: evx.id,
|
||||
data: JSON.stringify(n),
|
||||
|