This commit is contained in:
parent
7443012995
commit
e9f70cd040
@ -1,20 +0,0 @@
|
||||
.note-create-button {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
color: white;
|
||||
background: linear-gradient(90deg, rgba(239, 150, 68, 1) 0%, rgba(123, 65, 246, 1) 100%);
|
||||
border: none;
|
||||
border-radius: 100%;
|
||||
position: fixed;
|
||||
bottom: 40px;
|
||||
right: calc(((100vw - 640px) / 2) - 75px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.note-create-button {
|
||||
right: 16px;
|
||||
}
|
||||
}
|
@ -1,4 +1,3 @@
|
||||
import "./NoteCreatorButton.css";
|
||||
import { useRef, useMemo } from "react";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import classNames from "classnames";
|
||||
|
74
packages/app/src/Pages/Layout/Footer.tsx
Normal file
74
packages/app/src/Pages/Layout/Footer.tsx
Normal file
@ -0,0 +1,74 @@
|
||||
import React from "react";
|
||||
import { NavLink } from "react-router-dom";
|
||||
import useLogin from "@/Hooks/useLogin";
|
||||
import Icon from "@/Icons/Icon";
|
||||
import { ProfileLink } from "@/Element/User/ProfileLink";
|
||||
import { NoteCreatorButton } from "@/Element/Event/Create/NoteCreatorButton";
|
||||
import classNames from "classnames";
|
||||
import { useUserProfile } from "@snort/system-react";
|
||||
import Avatar from "@/Element/User/Avatar";
|
||||
import { useIntl } from "react-intl";
|
||||
|
||||
const MENU_ITEMS = [
|
||||
{ url: "/", icon: "home" },
|
||||
{ url: "/messages", icon: "mail" },
|
||||
{
|
||||
el: (
|
||||
<div className="flex flex-grow items-center justify-center">
|
||||
<NoteCreatorButton alwaysShow={true} />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{ url: "/search", icon: "search" },
|
||||
];
|
||||
|
||||
const Footer = () => {
|
||||
const { publicKey, readonly } = useLogin(s => ({
|
||||
publicKey: s.publicKey,
|
||||
readonly: s.readonly,
|
||||
}));
|
||||
const profile = useUserProfile(publicKey);
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
const renderButton = item => {
|
||||
if (item.el) {
|
||||
return item.el;
|
||||
}
|
||||
return (
|
||||
<NavLink
|
||||
to={item.url}
|
||||
className={({ isActive }) =>
|
||||
classNames(
|
||||
{ "text-nostr-purple": isActive, "hover:text-nostr-purple": !isActive },
|
||||
"flex flex-grow p-2 justify-center items-center cursor-pointer",
|
||||
)
|
||||
}>
|
||||
<Icon name={item.icon} width={24} />
|
||||
</NavLink>
|
||||
);
|
||||
};
|
||||
|
||||
const readOnlyIcon = readonly && (
|
||||
<span style={{ transform: "rotate(135deg)" }} title={formatMessage({ defaultMessage: "Read-only", id: "djNL6D" })}>
|
||||
<Icon name="openeye" className="text-nostr-red" size={20} />
|
||||
</span>
|
||||
);
|
||||
|
||||
return (
|
||||
<footer className="md:hidden fixed bottom-0 z-10 w-full bg-base-200 pb-safe-area bg-bg-color">
|
||||
<div className="flex">
|
||||
{MENU_ITEMS.map(item => renderButton(item))}
|
||||
{publicKey && (
|
||||
<ProfileLink
|
||||
className="flex flex-grow p-2 justify-center items-center cursor-pointer"
|
||||
pubkey={publicKey}
|
||||
user={profile}>
|
||||
<Avatar pubkey={publicKey} user={profile} icons={readOnlyIcon} size={40} />
|
||||
</ProfileLink>
|
||||
)}
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
27
packages/app/src/Pages/Layout/HasNotificationsMarker.tsx
Normal file
27
packages/app/src/Pages/Layout/HasNotificationsMarker.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import useLogin from "@/Hooks/useLogin";
|
||||
import { useMemo, useSyncExternalStore } from "react";
|
||||
import { Notifications } from "@/Cache";
|
||||
|
||||
export function HasNotificationsMarker() {
|
||||
const readNotifications = useLogin(s => s.readNotifications);
|
||||
const notifications = useSyncExternalStore(
|
||||
c => Notifications.hook(c, "*"),
|
||||
() => Notifications.snapshot(),
|
||||
);
|
||||
const latestNotification = useMemo(
|
||||
() => notifications.reduce((acc, v) => (v.created_at > acc ? v.created_at : acc), 0),
|
||||
[notifications],
|
||||
);
|
||||
const hasNotifications = useMemo(
|
||||
() => latestNotification * 1000 > readNotifications,
|
||||
[notifications, readNotifications],
|
||||
);
|
||||
|
||||
if (hasNotifications) {
|
||||
return (
|
||||
<div className="relative">
|
||||
<span className="has-unread absolute top-0 right-0 rounded-full"></span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@ -15,25 +15,6 @@
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.header-actions .avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.header-actions .btn {
|
||||
border-radius: 0;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.has-unread {
|
||||
background: var(--highlight);
|
||||
border-radius: 100%;
|
||||
|
@ -8,8 +8,8 @@ import { useUserProfile } from "@snort/system-react";
|
||||
import { NoteCreatorButton } from "../../Element/Event/Create/NoteCreatorButton";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import classNames from "classnames";
|
||||
import { HasNotificationsMarker } from "@/Pages/Layout/AccountHeader";
|
||||
import { getCurrentSubscription } from "@/Subscription";
|
||||
import { HasNotificationsMarker } from "@/Pages/Layout/HasNotificationsMarker";
|
||||
|
||||
const MENU_ITEMS = [
|
||||
{
|
||||
|
@ -1,23 +1,17 @@
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { useUserProfile } from "@snort/system-react";
|
||||
import { useMemo, useSyncExternalStore } from "react";
|
||||
import { base64 } from "@scure/base";
|
||||
import { unwrap } from "@snort/shared";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import SearchBox from "@/Element/SearchBox";
|
||||
import { ProfileLink } from "@/Element/User/ProfileLink";
|
||||
import Avatar from "@/Element/User/Avatar";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import Icon from "@/Icons/Icon";
|
||||
import useKeyboardShortcut from "@/Hooks/useKeyboardShortcut";
|
||||
import { isFormElement } from "@/SnortUtils";
|
||||
import useLogin from "@/Hooks/useLogin";
|
||||
import useEventPublisher from "@/Hooks/useEventPublisher";
|
||||
import SnortApi from "@/External/SnortApi";
|
||||
import { Notifications } from "@/Cache";
|
||||
import { HasNotificationsMarker } from "@/Pages/Layout/HasNotificationsMarker";
|
||||
|
||||
const AccountHeader = () => {
|
||||
const NotificationsHeader = () => {
|
||||
const navigate = useNavigate();
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
useKeyboardShortcut("/", event => {
|
||||
// if event happened in a form element, do nothing, otherwise focus on search input
|
||||
@ -27,15 +21,11 @@ const AccountHeader = () => {
|
||||
}
|
||||
});
|
||||
|
||||
const { publicKey, readonly } = useLogin(s => ({
|
||||
const { publicKey } = useLogin(s => ({
|
||||
publicKey: s.publicKey,
|
||||
readonly: s.readonly,
|
||||
}));
|
||||
const profile = useUserProfile(publicKey);
|
||||
const { publisher } = useEventPublisher();
|
||||
|
||||
const unreadDms = useMemo(() => (publicKey ? 0 : 0), [publicKey]);
|
||||
|
||||
async function goToNotifications() {
|
||||
// request permissions to send notifications
|
||||
if ("Notification" in window) {
|
||||
@ -78,54 +68,14 @@ const AccountHeader = () => {
|
||||
);
|
||||
}
|
||||
|
||||
const readOnlyIcon = readonly && (
|
||||
<span style={{ transform: "rotate(135deg)" }} title={formatMessage({ defaultMessage: "Read-only", id: "djNL6D" })}>
|
||||
<Icon name="openeye" className="text-nostr-red" size={20} />
|
||||
</span>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="header-actions">
|
||||
{!location.pathname.startsWith("/search") ? <SearchBox /> : <div className="grow"></div>}
|
||||
{!readonly && (
|
||||
<Link className="btn" to="/messages">
|
||||
<Icon name="mail" size={24} />
|
||||
{unreadDms > 0 && <span className="has-unread"></span>}
|
||||
</Link>
|
||||
)}
|
||||
<div className="flex justify-between">
|
||||
<Link className="btn" to="/notifications" onClick={goToNotifications}>
|
||||
<Icon name="bell-02" size={24} />
|
||||
<HasNotificationsMarker />
|
||||
</Link>
|
||||
<ProfileLink pubkey={publicKey} user={profile}>
|
||||
<Avatar pubkey={publicKey} user={profile} icons={readOnlyIcon} />
|
||||
</ProfileLink>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export function HasNotificationsMarker() {
|
||||
const readNotifications = useLogin(s => s.readNotifications);
|
||||
const notifications = useSyncExternalStore(
|
||||
c => Notifications.hook(c, "*"),
|
||||
() => Notifications.snapshot(),
|
||||
);
|
||||
const latestNotification = useMemo(
|
||||
() => notifications.reduce((acc, v) => (v.created_at > acc ? v.created_at : acc), 0),
|
||||
[notifications],
|
||||
);
|
||||
const hasNotifications = useMemo(
|
||||
() => latestNotification * 1000 > readNotifications,
|
||||
[notifications, readNotifications],
|
||||
);
|
||||
|
||||
if (hasNotifications) {
|
||||
return (
|
||||
<div className="relative">
|
||||
<span className="has-unread absolute top-0 right-0 rounded-full"></span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default AccountHeader;
|
||||
export default NotificationsHeader;
|
@ -11,13 +11,13 @@ import { useLoginRelays } from "@/Hooks/useLoginRelays";
|
||||
import { LoginUnlock } from "@/Element/PinPrompt";
|
||||
import useKeyboardShortcut from "@/Hooks/useKeyboardShortcut";
|
||||
import { LoginStore } from "@/Login";
|
||||
import { NoteCreatorButton } from "@/Element/Event/Create/NoteCreatorButton";
|
||||
import NavSidebar from "./NavSidebar";
|
||||
import AccountHeader from "./AccountHeader";
|
||||
import NotificationsHeader from "./NotificationsHeader";
|
||||
import RightColumn from "./RightColumn";
|
||||
import { LogoHeader } from "./LogoHeader";
|
||||
import useLoginFeed from "@/Feed/LoginFeed";
|
||||
import ErrorBoundary from "@/Element/ErrorBoundary";
|
||||
import Footer from "@/Pages/Layout/Footer";
|
||||
|
||||
export default function Index() {
|
||||
const location = useLocation();
|
||||
@ -69,13 +69,11 @@ export default function Index() {
|
||||
</div>
|
||||
<RightColumn />
|
||||
</div>
|
||||
<div className="md:hidden">
|
||||
<NoteCreatorButton className="note-create-button" />
|
||||
</div>
|
||||
<Toaster />
|
||||
</div>
|
||||
<LoginUnlock />
|
||||
{isStalker && <StalkerModal id={id} />}
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -83,8 +81,8 @@ export default function Index() {
|
||||
function Header() {
|
||||
return (
|
||||
<header className="flex justify-between items-center self-stretch px-4 gap-6 sticky top-0 md:hidden z-10 bg-bg-color py-1">
|
||||
<LogoHeader />
|
||||
<AccountHeader />
|
||||
<LogoHeader showText={true} />
|
||||
<NotificationsHeader />
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
@ -305,8 +305,10 @@ export default function ProfilePage({ id: propId, state }: ProfilePageProps) {
|
||||
{showProfileQr && (
|
||||
<Modal id="profile-qr" className="qr-modal" onClose={() => setShowProfileQr(false)}>
|
||||
<ProfileImage pubkey={id} />
|
||||
<QrCode data={link} className="m10 align-center" />
|
||||
<Copy text={link} className="align-center" />
|
||||
<div className="flex flex-col items-center">
|
||||
<QrCode data={link} className="m10" />
|
||||
<Copy text={link} className="py-3" />
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
{isMe ? (
|
||||
|
@ -220,7 +220,7 @@ export default function ProfileSettings(props: ProfileSettingsProps) {
|
||||
<Avatar pubkey={id} user={user} image={picture} />
|
||||
<AsyncButton
|
||||
type="button"
|
||||
className="circle flex align-center justify-between"
|
||||
className="circle flex items-center justify-center z-10"
|
||||
onClick={() => setNewAvatar()}
|
||||
disabled={readonly}>
|
||||
<Icon name="upload-01" />
|
||||
|
@ -767,12 +767,6 @@ button.tall {
|
||||
min-width: 98px;
|
||||
}
|
||||
|
||||
.align-center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.fade-in {
|
||||
opacity: 1;
|
||||
animation-name: fadeInOpacity;
|
||||
|
@ -294,9 +294,6 @@
|
||||
"9WRlF4": {
|
||||
"defaultMessage": "Send"
|
||||
},
|
||||
"9X9Q2t": {
|
||||
"defaultMessage": "Not followed by anyone you follow"
|
||||
},
|
||||
"9kSari": {
|
||||
"defaultMessage": "Retry publishing"
|
||||
},
|
||||
@ -549,6 +546,9 @@
|
||||
"Ig9/a1": {
|
||||
"defaultMessage": "Sent {n} sats to {name}"
|
||||
},
|
||||
"IgsWFG": {
|
||||
"defaultMessage": "Not followed by anyone you follow"
|
||||
},
|
||||
"IoQq+a": {
|
||||
"defaultMessage": "Click here to load anyway"
|
||||
},
|
||||
|
@ -97,7 +97,6 @@
|
||||
"9HU8vw": "Reply",
|
||||
"9SvQep": "Follows {n}",
|
||||
"9WRlF4": "Send",
|
||||
"9X9Q2t": "Not followed by anyone you follow",
|
||||
"9kSari": "Retry publishing",
|
||||
"9pMqYs": "Nostr Address",
|
||||
"9wO4wJ": "Lightning Invoice",
|
||||
@ -181,6 +180,7 @@
|
||||
"IVbtTS": "Zap all {n} sats",
|
||||
"IWz1ta": "Auto Translate",
|
||||
"Ig9/a1": "Sent {n} sats to {name}",
|
||||
"IgsWFG": "Not followed by anyone you follow",
|
||||
"IoQq+a": "Click here to load anyway",
|
||||
"Ix8l+B": "Trending Notes",
|
||||
"J+dIsA": "Subscriptions",
|
||||
|
Loading…
Reference in New Issue
Block a user