1
0
forked from Kieran/snort

mobile footer

This commit is contained in:
Martti Malmi 2023-11-30 17:12:52 +02:00
parent 7443012995
commit e9f70cd040
13 changed files with 122 additions and 117 deletions

View File

@ -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;
}
}

View File

@ -1,4 +1,3 @@
import "./NoteCreatorButton.css";
import { useRef, useMemo } from "react";
import { useLocation } from "react-router-dom";
import classNames from "classnames";

View 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;

View 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>
);
}
}

View File

@ -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%;

View File

@ -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 = [
{

View File

@ -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;

View File

@ -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>
);
}

View File

@ -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 ? (

View File

@ -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" />

View File

@ -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;

View File

@ -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"
},

View File

@ -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",