Merge pull request 'Modal quick keys, profile picture & banner modal' (#640) from mmalmi/snort:main into main
Some checks failed
continuous-integration/drone/push Build is failing
Reviewed-on: #640
@ -3,5 +3,6 @@
|
|||||||
"appNameCapitalized": "Snort",
|
"appNameCapitalized": "Snort",
|
||||||
"appTitle": "Snort - Nostr",
|
"appTitle": "Snort - Nostr",
|
||||||
"nip05Domain": "snort.social",
|
"nip05Domain": "snort.social",
|
||||||
"favicon": "public/favicon.ico"
|
"favicon": "public/favicon.ico",
|
||||||
|
"appleTouchIconUrl": "/nostrich_512.png"
|
||||||
}
|
}
|
||||||
|
@ -3,5 +3,6 @@
|
|||||||
"appNameCapitalized": "Iris",
|
"appNameCapitalized": "Iris",
|
||||||
"appTitle": "iris",
|
"appTitle": "iris",
|
||||||
"nip05Domain": "iris.to",
|
"nip05Domain": "iris.to",
|
||||||
"favicon": "public/iris.ico"
|
"favicon": "public/iris/favicon.ico",
|
||||||
|
"appleTouchIconUrl": "/img/apple-touch-icon.png"
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
name="keywords"
|
name="keywords"
|
||||||
content="nostr snort fast decentralized social media censorship resistant open source software" />
|
content="nostr snort fast decentralized social media censorship resistant open source software" />
|
||||||
<link rel="preconnect" href="https://imgproxy.snort.social" />
|
<link rel="preconnect" href="https://imgproxy.snort.social" />
|
||||||
<link rel="apple-touch-icon" href="/nostrich_512.png" />
|
<link rel="apple-touch-icon" href="<%= htmlWebpackPlugin.options.templateParameters.appleTouchIconUrl %>" />
|
||||||
<link rel="manifest" href="/manifest.json" />
|
<link rel="manifest" href="/manifest.json" />
|
||||||
<title><%= htmlWebpackPlugin.options.templateParameters.appTitle %></title>
|
<title><%= htmlWebpackPlugin.options.templateParameters.appTitle %></title>
|
||||||
</head>
|
</head>
|
||||||
|
12
packages/app/public/iris/.well-known/assetlinks.json
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"relation": ["delegate_permission/common.handle_all_urls"],
|
||||||
|
"target": {
|
||||||
|
"namespace": "android_app",
|
||||||
|
"package_name": "to.iris.twa",
|
||||||
|
"sha256_cert_fingerprints": [
|
||||||
|
"63:B5:70:E8:F1:75:7E:D6:EF:81:11:66:F4:9D:47:AB:49:3C:2E:00:B9:67:92:40:89:A5:03:0B:96:B9:40:09"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
BIN
packages/app/public/iris/img/android-chrome-192x192.png
Normal file
After Width: | Height: | Size: 23 KiB |
BIN
packages/app/public/iris/img/android-chrome-512x512.png
Normal file
After Width: | Height: | Size: 49 KiB |
BIN
packages/app/public/iris/img/apple-touch-icon.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
packages/app/public/iris/img/irisconnects.png
Normal file
After Width: | Height: | Size: 64 KiB |
BIN
packages/app/public/iris/img/maskable_icon.png
Normal file
After Width: | Height: | Size: 71 KiB |
BIN
packages/app/public/iris/img/maskable_icon_x192.png
Normal file
After Width: | Height: | Size: 12 KiB |
41
packages/app/public/iris/manifest_iris.json
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"short_name": "Iris",
|
||||||
|
"name": "Iris",
|
||||||
|
"description": "Fast nostr web ui",
|
||||||
|
"id": "/",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/img/android-chrome-192x192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/img/android-chrome-512x512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "any"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/img/maskable_icon.png",
|
||||||
|
"sizes": "640x640",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "maskable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/img/maskable_icon_x192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "maskable"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"start_url": "/",
|
||||||
|
"display": "standalone",
|
||||||
|
"theme_color": "#000000",
|
||||||
|
"background_color": "#000000",
|
||||||
|
"protocol_handlers": [
|
||||||
|
{
|
||||||
|
"protocol": "web+nostr",
|
||||||
|
"url": "/%s"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
import "./SpotlightMedia.css";
|
import "./SpotlightMedia.css";
|
||||||
import { useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import Modal from "Element/Modal";
|
import Modal from "Element/Modal";
|
||||||
import Icon from "Icons/Icon";
|
import Icon from "Icons/Icon";
|
||||||
import { ProxyImg } from "Element/ProxyImg";
|
import { ProxyImg } from "Element/ProxyImg";
|
||||||
@ -17,6 +17,24 @@ export function SpotlightMedia(props: SpotlightMediaProps) {
|
|||||||
return props.images.at(idx % props.images.length);
|
return props.images.at(idx % props.images.length);
|
||||||
}, [idx, props]);
|
}, [idx, props]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
switch (e.key) {
|
||||||
|
case "ArrowLeft":
|
||||||
|
case "ArrowUp":
|
||||||
|
dec();
|
||||||
|
break;
|
||||||
|
case "ArrowRight":
|
||||||
|
case "ArrowDown":
|
||||||
|
inc();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("keydown", handleKeyDown);
|
||||||
|
return () => document.removeEventListener("keydown", handleKeyDown);
|
||||||
|
}, []);
|
||||||
|
|
||||||
function dec() {
|
function dec() {
|
||||||
setIdx(s => {
|
setIdx(s => {
|
||||||
if (s - 1 === -1) {
|
if (s - 1 === -1) {
|
||||||
@ -36,6 +54,7 @@ export function SpotlightMedia(props: SpotlightMediaProps) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="spotlight">
|
<div className="spotlight">
|
||||||
<ProxyImg src={image} />
|
<ProxyImg src={image} />
|
||||||
|
@ -4,14 +4,25 @@ import { ReactNode, useEffect } from "react";
|
|||||||
export interface ModalProps {
|
export interface ModalProps {
|
||||||
id: string;
|
id: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
onClose?: (e: React.MouseEvent) => void;
|
onClose?: (e: React.MouseEvent | KeyboardEvent) => void;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Modal(props: ModalProps) {
|
export default function Modal(props: ModalProps) {
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === "Escape" && props.onClose) {
|
||||||
|
props.onClose(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.body.classList.add("scroll-lock");
|
document.body.classList.add("scroll-lock");
|
||||||
return () => document.body.classList.remove("scroll-lock");
|
document.addEventListener("keydown", handleKeyDown);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.body.classList.remove("scroll-lock");
|
||||||
|
document.removeEventListener("keydown", handleKeyDown);
|
||||||
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -15,9 +15,10 @@ interface AvatarProps {
|
|||||||
image?: string;
|
image?: string;
|
||||||
imageOverlay?: ReactNode;
|
imageOverlay?: ReactNode;
|
||||||
icons?: ReactNode;
|
icons?: ReactNode;
|
||||||
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Avatar = ({ pubkey, user, size, onClick, image, imageOverlay, icons }: AvatarProps) => {
|
const Avatar = ({ pubkey, user, size, onClick, image, imageOverlay, icons, className }: AvatarProps) => {
|
||||||
const [url, setUrl] = useState("");
|
const [url, setUrl] = useState("");
|
||||||
const { proxy } = useImgProxy();
|
const { proxy } = useImgProxy();
|
||||||
|
|
||||||
@ -43,7 +44,7 @@ const Avatar = ({ pubkey, user, size, onClick, image, imageOverlay, icons }: Ava
|
|||||||
<div
|
<div
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
style={style}
|
style={style}
|
||||||
className={`avatar${imageOverlay ? " with-overlay" : ""}`}
|
className={`avatar${imageOverlay ? " with-overlay" : ""} ${className ?? ""}`}
|
||||||
data-domain={domain?.toLowerCase()}
|
data-domain={domain?.toLowerCase()}
|
||||||
title={getDisplayName(user, "")}>
|
title={getDisplayName(user, "")}>
|
||||||
{icons && <div className="icons">{icons}</div>}
|
{icons && <div className="icons">{icons}</div>}
|
||||||
|
@ -58,6 +58,7 @@ import { ZapTarget } from "Zapper";
|
|||||||
import { useStatusFeed } from "Feed/StatusFeed";
|
import { useStatusFeed } from "Feed/StatusFeed";
|
||||||
|
|
||||||
import messages from "./messages";
|
import messages from "./messages";
|
||||||
|
import { SpotlightMediaModal } from "../Element/Deck/SpotlightMedia";
|
||||||
|
|
||||||
const NOTES = 0;
|
const NOTES = 0;
|
||||||
const REACTIONS = 1;
|
const REACTIONS = 1;
|
||||||
@ -120,6 +121,7 @@ export default function ProfilePage() {
|
|||||||
const isMe = loginPubKey === id;
|
const isMe = loginPubKey === id;
|
||||||
const [showLnQr, setShowLnQr] = useState<boolean>(false);
|
const [showLnQr, setShowLnQr] = useState<boolean>(false);
|
||||||
const [showProfileQr, setShowProfileQr] = useState<boolean>(false);
|
const [showProfileQr, setShowProfileQr] = useState<boolean>(false);
|
||||||
|
const [modalImage, setModalImage] = useState<string>("");
|
||||||
const aboutText = user?.about || "";
|
const aboutText = user?.about || "";
|
||||||
const npub = !id?.startsWith(NostrPrefix.PublicKey) ? hexToBech32(NostrPrefix.PublicKey, id || undefined) : id;
|
const npub = !id?.startsWith(NostrPrefix.PublicKey) ? hexToBech32(NostrPrefix.PublicKey, id || undefined) : id;
|
||||||
|
|
||||||
@ -428,7 +430,7 @@ export default function ProfilePage() {
|
|||||||
function avatar() {
|
function avatar() {
|
||||||
return (
|
return (
|
||||||
<div className="avatar-wrapper w-max">
|
<div className="avatar-wrapper w-max">
|
||||||
<Avatar pubkey={id ?? ""} user={user} />
|
<Avatar pubkey={id ?? ""} user={user} onClick={() => setModalImage(user?.picture || "")} className="pointer" />
|
||||||
<div className="profile-actions">
|
<div className="profile-actions">
|
||||||
{renderIcons()}
|
{renderIcons()}
|
||||||
{!isMe && id && <FollowButton className="primary" pubkey={id} />}
|
{!isMe && id && <FollowButton className="primary" pubkey={id} />}
|
||||||
@ -506,7 +508,15 @@ export default function ProfilePage() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="profile">
|
<div className="profile">
|
||||||
{user?.banner && <ProxyImg alt="banner" className="banner" src={user.banner} size={w} />}
|
{user?.banner && (
|
||||||
|
<ProxyImg
|
||||||
|
alt="banner"
|
||||||
|
className="banner pointer"
|
||||||
|
src={user.banner}
|
||||||
|
size={w}
|
||||||
|
onClick={() => setModalImage(user.banner || "")}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<div className="profile-wrapper w-max">
|
<div className="profile-wrapper w-max">
|
||||||
{avatar()}
|
{avatar()}
|
||||||
{userDetails()}
|
{userDetails()}
|
||||||
@ -520,6 +530,7 @@ export default function ProfilePage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="main-content">{tabContent()}</div>
|
<div className="main-content">{tabContent()}</div>
|
||||||
|
{modalImage && <SpotlightMediaModal onClose={() => setModalImage("")} images={[modalImage]} idx={0} />}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,23 @@ const appConfig = require("config");
|
|||||||
|
|
||||||
const isProduction = process.env.NODE_ENV == "production";
|
const isProduction = process.env.NODE_ENV == "production";
|
||||||
|
|
||||||
|
const appTitle = appConfig.get("appTitle");
|
||||||
|
|
||||||
|
const copyPatterns = [
|
||||||
|
{ from: "public/robots.txt" },
|
||||||
|
{ from: "public/nostrich_512.png" },
|
||||||
|
{ from: "public/nostrich_256.png" },
|
||||||
|
{ from: "_headers" },
|
||||||
|
];
|
||||||
|
|
||||||
|
if (appTitle === "iris") {
|
||||||
|
copyPatterns.push({ from: "public/iris/manifest_iris.json", to: "manifest.json" });
|
||||||
|
copyPatterns.push({ from: "public/iris/img", to: "img" });
|
||||||
|
copyPatterns.push({ from: "public/iris/.well-known", to: ".well-known" });
|
||||||
|
} else {
|
||||||
|
copyPatterns.push({ from: "public/manifest.json" });
|
||||||
|
}
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
entry: {
|
entry: {
|
||||||
main: "./src/index.tsx",
|
main: "./src/index.tsx",
|
||||||
@ -39,20 +56,15 @@ const config = {
|
|||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
new CopyPlugin({
|
new CopyPlugin({
|
||||||
patterns: [
|
patterns: copyPatterns,
|
||||||
{ from: "public/manifest.json" },
|
|
||||||
{ from: "public/robots.txt" },
|
|
||||||
{ from: "public/nostrich_512.png" },
|
|
||||||
{ from: "public/nostrich_256.png" },
|
|
||||||
{ from: "_headers" },
|
|
||||||
],
|
|
||||||
}),
|
}),
|
||||||
new HtmlWebpackPlugin({
|
new HtmlWebpackPlugin({
|
||||||
template: "public/index.html",
|
template: "public/index.html",
|
||||||
favicon: appConfig.get("favicon"),
|
favicon: appConfig.get("favicon"),
|
||||||
excludeChunks: ["pow", "bench"],
|
excludeChunks: ["pow", "bench"],
|
||||||
templateParameters: {
|
templateParameters: {
|
||||||
appTitle: appConfig.get("appTitle"),
|
appTitle,
|
||||||
|
appleTouchIconUrl: appConfig.get("appleTouchIconUrl"),
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
new HtmlWebpackPlugin({
|
new HtmlWebpackPlugin({
|
||||||
|