feat: note creator button on deck
This commit is contained in:
parent
65552e126b
commit
f0110e9009
@ -80,7 +80,7 @@ export default function WriteMessage({ chat }: { chat: Chat }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<button className="btn-rnd" onClick={() => attachFile()}>
|
<button className="circle flex flex-center" onClick={() => attachFile()}>
|
||||||
{uploading ? <Spinner width={20} /> : <Icon name="attachment" size={20} />}
|
{uploading ? <Spinner width={20} /> : <Icon name="attachment" size={20} />}
|
||||||
</button>
|
</button>
|
||||||
<div className="w-max">
|
<div className="w-max">
|
||||||
@ -97,7 +97,7 @@ export default function WriteMessage({ chat }: { chat: Chat }) {
|
|||||||
/>
|
/>
|
||||||
{error && <b className="error">{error}</b>}
|
{error && <b className="error">{error}</b>}
|
||||||
</div>
|
</div>
|
||||||
<button className="btn-rnd" onClick={() => sendMessage()}>
|
<button className="circle flex flex-center" onClick={() => sendMessage()}>
|
||||||
{sending ? <Spinner width={20} /> : <Icon name="arrow-right" size={20} />}
|
{sending ? <Spinner width={20} /> : <Icon name="arrow-right" size={20} />}
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
|
@ -5,13 +5,13 @@ import "./Nav.css";
|
|||||||
import Icon from "Icons/Icon";
|
import Icon from "Icons/Icon";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { profileLink } from "SnortUtils";
|
import { profileLink } from "SnortUtils";
|
||||||
|
import { NoteCreatorButton } from "Element/Event/NoteCreatorButton";
|
||||||
|
|
||||||
export function DeckNav() {
|
export function DeckNav() {
|
||||||
const { publicKey } = useLogin();
|
const { publicKey } = useLogin();
|
||||||
const profile = useUserProfile(publicKey);
|
const profile = useUserProfile(publicKey);
|
||||||
|
|
||||||
const unreadDms = 0;
|
const unreadDms = 0;
|
||||||
const hasNotifications = false;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className="deck flex-column f-space">
|
<nav className="deck flex-column f-space">
|
||||||
@ -20,15 +20,12 @@ export function DeckNav() {
|
|||||||
<Icon name="mail" size={24} />
|
<Icon name="mail" size={24} />
|
||||||
{unreadDms > 0 && <span className="has-unread"></span>}
|
{unreadDms > 0 && <span className="has-unread"></span>}
|
||||||
</Link>
|
</Link>
|
||||||
<Link className="btn" to="/notifications">
|
<NoteCreatorButton />
|
||||||
<Icon name="bell-02" size={24} />
|
|
||||||
{hasNotifications && <span className="has-unread"></span>}
|
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-column f-center g16">
|
<div className="flex-column f-center g16">
|
||||||
<Link className="btn" to="/">
|
{/*<Link className="btn" to="/">
|
||||||
<Icon name="grid-01" size={24} />
|
<Icon name="grid-01" size={24} />
|
||||||
</Link>
|
</Link>*/}
|
||||||
<Link className="btn" to="/settings">
|
<Link className="btn" to="/settings">
|
||||||
<Icon name="settings-02" size={24} />
|
<Icon name="settings-02" size={24} />
|
||||||
</Link>
|
</Link>
|
||||||
|
@ -84,27 +84,6 @@
|
|||||||
height: 32px;
|
height: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.light .note-creator textarea {
|
.light .note-creator textarea {
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
}
|
}
|
||||||
|
20
packages/app/src/Element/Event/NoteCreatorButton.css
Normal file
20
packages/app/src/Element/Event/NoteCreatorButton.css
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
.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;
|
||||||
|
}
|
||||||
|
}
|
51
packages/app/src/Element/Event/NoteCreatorButton.tsx
Normal file
51
packages/app/src/Element/Event/NoteCreatorButton.tsx
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import "./NoteCreatorButton.css";
|
||||||
|
import { useRef, useMemo } from "react";
|
||||||
|
import { useLocation } from "react-router-dom";
|
||||||
|
|
||||||
|
import { isFormElement } from "SnortUtils";
|
||||||
|
import useKeyboardShortcut from "Hooks/useKeyboardShortcut";
|
||||||
|
import useLogin from "Hooks/useLogin";
|
||||||
|
import Icon from "Icons/Icon";
|
||||||
|
import { useNoteCreator } from "State/NoteCreator";
|
||||||
|
import { NoteCreator } from "./NoteCreator";
|
||||||
|
|
||||||
|
export const NoteCreatorButton = ({ className }: { className?: string }) => {
|
||||||
|
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||||
|
const location = useLocation();
|
||||||
|
const { readonly } = useLogin(s => ({ readonly: s.readonly }));
|
||||||
|
const { show, replyTo, update } = useNoteCreator(v => ({ show: v.show, replyTo: v.replyTo, update: v.update }));
|
||||||
|
|
||||||
|
useKeyboardShortcut("n", event => {
|
||||||
|
// if event happened in a form element, do nothing, otherwise focus on search input
|
||||||
|
if (event.target && !isFormElement(event.target as HTMLElement)) {
|
||||||
|
event.preventDefault();
|
||||||
|
if (buttonRef.current) {
|
||||||
|
buttonRef.current.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const shouldHideNoteCreator = useMemo(() => {
|
||||||
|
const isReplyNoteCreatorShowing = replyTo && show;
|
||||||
|
const hideOn = ["/settings", "/messages", "/new", "/login", "/donate", "/e", "/subscribe"];
|
||||||
|
return readonly || isReplyNoteCreatorShowing || hideOn.some(a => location.pathname.startsWith(a));
|
||||||
|
}, [location, readonly]);
|
||||||
|
|
||||||
|
if (shouldHideNoteCreator) return null;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
ref={buttonRef}
|
||||||
|
className={`primary circle${className ? ` ${className}` : ""}`}
|
||||||
|
onClick={() =>
|
||||||
|
update(v => {
|
||||||
|
v.replyTo = undefined;
|
||||||
|
v.show = true;
|
||||||
|
})
|
||||||
|
}>
|
||||||
|
<Icon name="plus" size={16} />
|
||||||
|
</button>
|
||||||
|
<NoteCreator key="global-note-creator" />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -1,5 +1,5 @@
|
|||||||
import "./Layout.css";
|
import "./Layout.css";
|
||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { Link, Outlet, useLocation, useNavigate } from "react-router-dom";
|
import { Link, Outlet, useLocation, useNavigate } from "react-router-dom";
|
||||||
import { FormattedMessage, useIntl } from "react-intl";
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import { useUserProfile } from "@snort/system-react";
|
import { useUserProfile } from "@snort/system-react";
|
||||||
@ -9,7 +9,6 @@ import messages from "./messages";
|
|||||||
|
|
||||||
import Icon from "Icons/Icon";
|
import Icon from "Icons/Icon";
|
||||||
import useLoginFeed from "Feed/LoginFeed";
|
import useLoginFeed from "Feed/LoginFeed";
|
||||||
import { NoteCreator } from "Element/Event/NoteCreator";
|
|
||||||
import { mapPlanName } from "./subscribe";
|
import { mapPlanName } from "./subscribe";
|
||||||
import useLogin from "Hooks/useLogin";
|
import useLogin from "Hooks/useLogin";
|
||||||
import Avatar from "Element/User/Avatar";
|
import Avatar from "Element/User/Avatar";
|
||||||
@ -20,10 +19,10 @@ import Spinner from "Icons/Spinner";
|
|||||||
import { fetchNip05Pubkey } from "Nip05/Verifier";
|
import { fetchNip05Pubkey } from "Nip05/Verifier";
|
||||||
import { useTheme } from "Hooks/useTheme";
|
import { useTheme } from "Hooks/useTheme";
|
||||||
import { useLoginRelays } from "Hooks/useLoginRelays";
|
import { useLoginRelays } from "Hooks/useLoginRelays";
|
||||||
import { useNoteCreator } from "State/NoteCreator";
|
|
||||||
import { LoginUnlock } from "Element/PinPrompt";
|
import { LoginUnlock } from "Element/PinPrompt";
|
||||||
import useKeyboardShortcut from "Hooks/useKeyboardShortcut";
|
import useKeyboardShortcut from "Hooks/useKeyboardShortcut";
|
||||||
import { LoginStore } from "Login";
|
import { LoginStore } from "Login";
|
||||||
|
import { NoteCreatorButton } from "Element/Event/NoteCreatorButton";
|
||||||
|
|
||||||
export default function Layout() {
|
export default function Layout() {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
@ -69,7 +68,7 @@ export default function Layout() {
|
|||||||
</header>
|
</header>
|
||||||
)}
|
)}
|
||||||
<Outlet />
|
<Outlet />
|
||||||
<NoteCreatorButton />
|
<NoteCreatorButton className="note-create-button" />
|
||||||
<Toaster />
|
<Toaster />
|
||||||
</div>
|
</div>
|
||||||
<LoginUnlock />
|
<LoginUnlock />
|
||||||
@ -79,7 +78,7 @@ export default function Layout() {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
LoginStore.removeSession(id);
|
LoginStore.removeSession(id);
|
||||||
}}>
|
}}>
|
||||||
<button type="button" className="btn btn-rnd">
|
<button type="button" className="circle flex flex-center">
|
||||||
<Icon name="close" />
|
<Icon name="close" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -88,47 +87,6 @@ export default function Layout() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const NoteCreatorButton = () => {
|
|
||||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
|
||||||
const location = useLocation();
|
|
||||||
const { readonly } = useLogin(s => ({ readonly: s.readonly }));
|
|
||||||
const { show, replyTo, update } = useNoteCreator(v => ({ show: v.show, replyTo: v.replyTo, update: v.update }));
|
|
||||||
|
|
||||||
useKeyboardShortcut("n", event => {
|
|
||||||
// if event happened in a form element, do nothing, otherwise focus on search input
|
|
||||||
if (event.target && !isFormElement(event.target as HTMLElement)) {
|
|
||||||
event.preventDefault();
|
|
||||||
if (buttonRef.current) {
|
|
||||||
buttonRef.current.click();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const shouldHideNoteCreator = useMemo(() => {
|
|
||||||
const isReplyNoteCreatorShowing = replyTo && show;
|
|
||||||
const hideOn = ["/settings", "/messages", "/new", "/login", "/donate", "/e", "/subscribe"];
|
|
||||||
return readonly || isReplyNoteCreatorShowing || hideOn.some(a => location.pathname.startsWith(a));
|
|
||||||
}, [location, readonly]);
|
|
||||||
|
|
||||||
if (shouldHideNoteCreator) return;
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<button
|
|
||||||
ref={buttonRef}
|
|
||||||
className="primary note-create-button"
|
|
||||||
onClick={() =>
|
|
||||||
update(v => {
|
|
||||||
v.replyTo = undefined;
|
|
||||||
v.show = true;
|
|
||||||
})
|
|
||||||
}>
|
|
||||||
<Icon name="plus" size={16} />
|
|
||||||
</button>
|
|
||||||
<NoteCreator key="global-note-creator" />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const AccountHeader = () => {
|
const AccountHeader = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
|
@ -33,7 +33,7 @@
|
|||||||
bottom: -32px;
|
bottom: -32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings .image-settings .avatar-stack .btn-rnd {
|
.settings .image-settings .avatar-stack button {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
width: 32px;
|
width: 32px;
|
||||||
|
@ -205,7 +205,11 @@ export default function ProfileSettings(props: ProfileSettingsProps) {
|
|||||||
{(props.avatar ?? true) && (
|
{(props.avatar ?? true) && (
|
||||||
<div className="avatar-stack">
|
<div className="avatar-stack">
|
||||||
<Avatar pubkey={id} user={user} image={picture} />
|
<Avatar pubkey={id} user={user} image={picture} />
|
||||||
<AsyncButton type="button" className="btn-rnd" onClick={() => setNewAvatar()} disabled={readonly}>
|
<AsyncButton
|
||||||
|
type="button"
|
||||||
|
className="circle flex flex-center"
|
||||||
|
onClick={() => setNewAvatar()}
|
||||||
|
disabled={readonly}>
|
||||||
<Icon name="upload-01" />
|
<Icon name="upload-01" />
|
||||||
</AsyncButton>
|
</AsyncButton>
|
||||||
</div>
|
</div>
|
||||||
|
@ -364,12 +364,9 @@ button.icon:hover {
|
|||||||
padding: 5px;
|
padding: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-rnd {
|
.circle {
|
||||||
border-radius: 100%;
|
border-radius: 100%;
|
||||||
aspect-ratio: 1;
|
aspect-ratio: 1;
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
textarea {
|
textarea {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user