Help fund the development of Snort
Snort is an open source project built by passionate people in their free time
diff --git a/src/Pages/Layout.css b/src/Pages/Layout.css
index 0648eebb..25922ec7 100644
--- a/src/Pages/Layout.css
+++ b/src/Pages/Layout.css
@@ -1,5 +1,46 @@
.logo {
cursor: pointer;
+ font-weight: 700;
+ font-size: 29px;
+ line-height: 23px;
+}
+
+header {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: space-between;
+ height: 72px;
+ padding: 0 12px;
+}
+
+@media (min-width: 720px) {
+ header {
+ padding: 0;
+ }
+}
+
+header .pfp .avatar-wrapper {
+ margin-right: 0;
+}
+
+.header-actions {
+ display: flex;
+ flex-direction: row;
+}
+
+.header-actions .btn-rnd {
+ position: relative;
+}
+
+.header-actions .btn-rnd .has-unread {
+ background: var(--highlight);
+ border-radius: 100%;
+ width: 9px;
+ height: 9px;
+ position: absolute;
+ top: 0;
+ right: 0;
}
.search {
@@ -13,20 +54,3 @@
.search .btn {
display: none;
}
-
-.unread-count {
- width: 20px;
- height: 20px;
- border: 1px solid;
- border-radius: 100%;
- position: relative;
- padding: 3px;
- line-height: 1.5em;
- top: -10px;
- left: -10px;
- font-size: var(--font-size-small);
- background-color: var(--error);
- color: var(--note-bg);
- font-weight: bold;
- text-align: center;
-}
diff --git a/src/Pages/Layout.tsx b/src/Pages/Layout.tsx
index a7e2cfaa..5cb2ac9f 100644
--- a/src/Pages/Layout.tsx
+++ b/src/Pages/Layout.tsx
@@ -2,8 +2,9 @@ import "./Layout.css";
import { useEffect, useMemo } from "react"
import { useDispatch, useSelector } from "react-redux";
import { Outlet, useNavigate } from "react-router-dom";
-import { faBell, faMessage, faSearch } from "@fortawesome/free-solid-svg-icons";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import Envelope from "Icons/Envelope";
+import Bell from "Icons/Bell";
+import Search from "Icons/Search";
import { RootState } from "State/Store";
import { init, UserPreferences } from "State/Login";
@@ -15,6 +16,8 @@ import useLoginFeed from "Feed/LoginFeed";
import { totalUnread } from "Pages/MessagesPage";
import { SearchRelays } from 'Const';
import useEventPublisher from "Feed/EventPublisher";
+import useModeration from "Hooks/useModeration";
+
export default function Layout() {
const dispatch = useDispatch();
@@ -25,6 +28,8 @@ export default function Layout() {
const notifications = useSelector(s => s.login.notifications);
const readNotifications = useSelector(s => s.login.readNotifications);
const dms = useSelector(s => s.login.dms);
+ const { isMuted } = useModeration();
+ const filteredDms = dms.filter(a => !isMuted(a.pubkey))
const prefs = useSelector(s => s.login.preferences);
const pub = useEventPublisher();
useLoginFeed();
@@ -89,23 +94,22 @@ export default function Layout() {
function accountHeader() {
const unreadNotifications = notifications?.filter(a => (a.created_at * 1000) > readNotifications).length;
- const unreadDms = key ? totalUnread(dms, key) : 0;
+ const unreadDms = key ? totalUnread(filteredDms, key) : 0;
return (
- <>
- navigate("/messages")}>
-
+
+
navigate("/search")}>
+
- {unreadDms > 0 && (
- {unreadDms > 100 ? ">99" : unreadDms}
- )}
-
goToNotifications(e)}>
-
+
navigate("/messages")}>
+
+ {unreadDms > 0 && ()}
+
+
goToNotifications(e)}>
+
+ {unreadNotifications > 0 && ()}
- {unreadNotifications > 0 && (
- {unreadNotifications > 100 ? ">99" : unreadNotifications}
- )}
- >
+
)
}
@@ -114,17 +118,14 @@ export default function Layout() {
}
return (
diff --git a/src/Pages/Login.tsx b/src/Pages/Login.tsx
index 8169591f..b1b02eac 100644
--- a/src/Pages/Login.tsx
+++ b/src/Pages/Login.tsx
@@ -20,7 +20,7 @@ export default function LoginPage() {
if (publicKey) {
navigate("/");
}
- }, [publicKey]);
+ }, [publicKey, navigate]);
async function getNip05PubKey(addr: string) {
let [username, domain] = addr.split("@");
@@ -32,7 +32,7 @@ export default function LoginPage() {
return pKey;
}
}
- throw "User key not found"
+ throw new Error("User key not found")
}
async function doLogin() {
@@ -43,7 +43,7 @@ export default function LoginPage() {
if (secp.utils.isValidPrivateKey(hexKey)) {
dispatch(setPrivateKey(hexKey));
} else {
- throw "INVALID PRIVATE KEY";
+ throw new Error("INVALID PRIVATE KEY");
}
} else if (key.startsWith("npub")) {
let hexKey = bech32ToHex(key);
@@ -55,7 +55,7 @@ export default function LoginPage() {
if (secp.utils.isValidPrivateKey(key)) {
dispatch(setPrivateKey(key));
} else {
- throw "INVALID PRIVATE KEY";
+ throw new Error("INVALID PRIVATE KEY");
}
}
} catch (e) {
@@ -93,24 +93,24 @@ export default function LoginPage() {
<>
Other Login Methods
-
doNip07Login()}>Login with Extension (NIP-07)
+
>
)
}
return (
- <>
+
Login
setKey(e.target.value)} />
{error.length > 0 ?
{error} : null}
-
doLogin()}>Login
-
makeRandomKey()}>Generate Key
+
+
{altLogins()}
- >
+
);
-}
\ No newline at end of file
+}
diff --git a/src/Pages/MessagesPage.tsx b/src/Pages/MessagesPage.tsx
index 5142f016..01c6b392 100644
--- a/src/Pages/MessagesPage.tsx
+++ b/src/Pages/MessagesPage.tsx
@@ -8,6 +8,7 @@ import { hexToBech32 } from "../Util";
import { incDmInteraction } from "State/Login";
import { RootState } from "State/Store";
import NoteToSelf from "Element/NoteToSelf";
+import useModeration from "Hooks/useModeration";
type DmChat = {
pubkey: HexKey,
@@ -20,9 +21,10 @@ export default function MessagesPage() {
const myPubKey = useSelector
(s => s.login.publicKey);
const dms = useSelector(s => s.login.dms);
const dmInteraction = useSelector(s => s.login.dmInteraction);
+ const { isMuted } = useModeration();
const chats = useMemo(() => {
- return extractChats(dms, myPubKey!);
+ return extractChats(dms.filter(a => !isMuted(a.pubkey)), myPubKey!)
}, [dms, myPubKey, dmInteraction]);
function noteToSelf(chat: DmChat) {
@@ -51,17 +53,17 @@ export default function MessagesPage() {
}
return (
- <>
+
Messages
-
markAllRead()}>Mark All Read
+
{chats.sort((a, b) => {
return a.pubkey === myPubKey ? -1 :
b.pubkey === myPubKey ? 1 :
b.newestMessage - a.newestMessage
}).map(person)}
- >
+
)
}
@@ -91,7 +93,7 @@ export function isToSelf(e: RawEvent, pk: HexKey) {
}
export function dmsInChat(dms: RawEvent[], pk: HexKey) {
- return dms.filter(a => a.pubkey === pk || dmTo(a) == pk);
+ return dms.filter(a => a.pubkey === pk || dmTo(a) === pk);
}
export function totalUnread(dms: RawEvent[], myPubKey: HexKey) {
@@ -122,4 +124,4 @@ export function extractChats(dms: RawEvent[], myPubKey: HexKey) {
newestMessage: newestMessage(dms, myPubKey, a)
} as DmChat;
})
-}
\ No newline at end of file
+}
diff --git a/src/Pages/NewUserPage.tsx b/src/Pages/NewUserPage.tsx
index 32c0bf66..f7b7d13c 100644
--- a/src/Pages/NewUserPage.tsx
+++ b/src/Pages/NewUserPage.tsx
@@ -74,9 +74,9 @@ export default function NewUserPage() {
}
return (
- <>
+
{importTwitterFollows()}
{followSomebody()}
- >
+
);
}
\ No newline at end of file
diff --git a/src/Pages/ProfilePage.css b/src/Pages/ProfilePage.css
index 6ba6bd67..53c54e1e 100644
--- a/src/Pages/ProfilePage.css
+++ b/src/Pages/ProfilePage.css
@@ -1,63 +1,73 @@
.profile {
- flex-direction: column;
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
}
.profile .banner {
- width: 100%;
- height: 210px;
- margin-bottom: -80px;
- object-fit: cover;
- mask-image: linear-gradient(to bottom, var(--bg-color) 60%, rgba(0, 0, 0, 0));
- -webkit-mask-image: linear-gradient(to bottom, var(--bg-color) 60%, rgba(0, 0, 0, 0));
- z-index: 0;
+ width: 100%;
+ height: 160px;
+ object-fit: cover;
+ margin-bottom: -60px;
+ mask-image: linear-gradient(to bottom, var(--bg-color) 60%, rgba(0,0,0,0));
+ -webkit-mask-image: linear-gradient(to bottom, var(--bg-color) 60%, rgba(0,0,0,0));
+ z-index: 0;
}
-@media (min-width: 720px) {
- .profile .banner {
- width: 100%;
- max-width: 720px;
- height: 300px;
- margin-bottom: -120px;
- }
+.profile .profile-wrapper {
+ margin: 0 16px;
+ width: calc(100% - 32px);
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ position: relative;
}
+
.profile p {
white-space: pre-wrap;
}
.profile .name h2 {
- margin: 0;
+ margin: 12px 0 0 0;
+ font-weight: 600;
+ font-size: 19px;
+ line-height: 23px;
}
-@media (min-width: 720px) {
- .profile .banner {
- width: 100%;
- max-width: 720px;
- height: 300px;
- margin-bottom: -120px;
- }
+.profile .nip05 {
+ display: flex;
+ font-size: 16px;
+ margin: 0 0 12px 0;
+}
+
+.profile .nip05 .nick {
+ font-weight: normal;
+ color: var(--gray-light);
}
.profile .avatar-wrapper {
- align-self: flex-start;
- z-index: 1;
- margin-left: 4px;
+ z-index: 1;
+}
+
+.profile .avatar-wrapper .avatar {
+ width: 120px;
+ height: 120px;
}
.profile .name {
- display: flex;
- flex-direction: column;
- align-items: flex-start;
-}
-
-.profile .name h2 {
- margin: 0;
+ display: flex;
+ flex-direction: column;
}
.profile .details {
- max-width: 680px;
width: 100%;
margin-top: 12px;
+ background-color: var(--note-bg);
+ padding: 12px 16px;
+ border-radius: 16px;
+ margin: 0 auto;
+ margin-bottom: 12px;
}
.profile .details p {
@@ -76,146 +86,64 @@
.profile .btn-icon {
color: var(--font-color);
padding: 6px;
- margin-left: 4px;
}
.profile .details-wrapper {
display: flex;
flex-direction: column;
- align-items: flex-start;
justify-content: space-between;
- position: relative;
- width: 100%;
- margin-left: 4px;
+ width: calc(100% - 32px);
}
-.profile .copy .body {
- font-size: 12px
-}
-
-@media (min-width: 360px) {
- .profile .copy .body {
- font-size: 14px
- }
-
- .profile .details-wrapper, .profile .avatar-wrapper {
- margin-left: 21px;
- }
-
- .profile .details {
- width: calc(100% - 21px);
- }
-}
-
-@media (min-width: 720px) {
- .profile .details-wrapper, .profile .avatar-wrapper {
- margin-left: 30px;
- }
-
- .profile .details {
- width: calc(100% - 30px);
- }
-}
-
-.profile .p-buttons {
- position: absolute;
- top: -30px;
- right: 20px;
-}
-
-.profile .p-buttons>div {
- margin-right: 10px;
-}
-
-.profile .no-banner .follow-button {
- right: 0px;
-}
-
-.profile .no-banner .message-button {
- right: 54px;
-}
-
-.tabs {
- display: flex;
- justify-content: flex-start;
- width: 100%;
- margin: 10px 0;
-}
-
-.tabs>div {
- margin-right: 0;
-}
-
-.tab {
- margin: 0;
- padding: 8px 0;
- border-bottom: 3px solid var(--gray-secondary);
-}
-
-.tab.active {
- border-bottom: 3px solid var(--highlight);
-}
-
-.profile .no-banner {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- width: 100%;
-}
-
-.profile .no-banner .avatar {
- height: 256px;
- width: 256px;
- margin-bottom: 30px;
-}
-
-.profile .no-banner .avatar-wrapper, .profile .no-banner .details-wrapper {
- margin: 0 auto;
-}
-
-@media (min-width: 720px) {
- .profile .no-banner {
- width: 100%;
- flex-direction: row;
- justify-content: space-around;
- margin-top: 21px;
- }
-
- .profile .no-banner .avatar-wrapper {
- margin: auto 10px;
- }
-
- .profile .no-banner .details-wrapper {
- margin-left: 10px;
- margin-top: 21px;
- max-width: 420px;
- }
+.profile .details .text {
+ font-size: 14px;
}
.profile .links {
- margin: 8px 12px;
+ margin-top: 4px;
+ margin-left: 2px;
+ margin-bottom: 12px;
+}
+
+.profile h3 {
+ color: var(--font-secondary-color);
+ font-size: 10px;
+ letter-spacing: .11em;
+ font-weight: 600;
+ line-height: 12px;
+ text-transform: uppercase;
+ margin-left: 12px;
}
.profile .website {
- color: var(--highlight);
- margin: 6px 0;
+ margin: 4px 0;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+}
+
+.profile .website a {
+ color: var(--font-color);
}
.profile .website a {
text-decoration: none;
}
-.profile .website::before {
- content: '🔗 ';
+.profile .website a:hover {
+ text-decoration: underline;
}
.profile .lnurl {
- color: var(--highlight);
- margin: 6px 0;
cursor: pointer;
}
+.profile .ln-address {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+}
+
.profile .lnurl:hover {
text-decoration: underline;
}
@@ -225,6 +153,63 @@
text-overflow: ellipsis;
}
-.profile .zap {
- margin-right: .3em;
-}
\ No newline at end of file
+.profile .link-icon {
+ color: var(--highlight);
+ margin-right: 8px;
+}
+
+.profile .link-icon svg {
+ width: 12px;
+ height: 12px;
+}
+
+.profile .profile-actions {
+ position: absolute;
+ top: 80px;
+ right: 0;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+}
+
+.profile .icon-actions {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+}
+
+@media (min-width: 520px) {
+ .profile .profile-actions {
+ top: 120px;
+ }
+}
+
+.profile .profile-actions button:not(:last-child) {
+ margin-right: 8px;
+}
+
+.profile .profile-actions button.icon:not(:last-child) {
+ margin-right: 0;
+}
+
+@media (min-width: 520px) {
+ .profile .banner {
+ width: 100%;
+ max-width: 720px;
+ height: 300px;
+ margin-bottom: -100px;
+ }
+ .profile .profile-actions button.icon:not(:last-child) {
+ margin-right: 2px;
+ }
+}
+
+.profile .npub {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+}
+
+.qr-modal .modal-body {
+ width: unset;
+}
diff --git a/src/Pages/ProfilePage.tsx b/src/Pages/ProfilePage.tsx
index e4091c6a..81f997b3 100644
--- a/src/Pages/ProfilePage.tsx
+++ b/src/Pages/ProfilePage.tsx
@@ -2,14 +2,17 @@ import "./ProfilePage.css";
import { useEffect, useMemo, useState } from "react";
import { useSelector } from "react-redux";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { faGear, faEnvelope, faQrcode } from "@fortawesome/free-solid-svg-icons";
import { useNavigate, useParams } from "react-router-dom";
+import Link from "Icons/Link";
+import Qr from "Icons/Qr";
+import Zap from "Icons/Zap";
+import Envelope from "Icons/Envelope";
import { useUserProfile } from "Feed/ProfileFeed";
import FollowButton from "Element/FollowButton";
import { extractLnAddress, parseId, hexToBech32 } from "Util";
import Avatar from "Element/Avatar";
+import LogoutButton from "Element/LogoutButton";
import Timeline from "Element/Timeline";
import Text from 'Element/Text'
import LNURLTip from "Element/LNURLTip";
@@ -17,7 +20,10 @@ import Nip05 from "Element/Nip05";
import Copy from "Element/Copy";
import ProfilePreview from "Element/ProfilePreview";
import FollowersList from "Element/FollowersList";
+import BlockList from "Element/BlockList";
+import MutedList from "Element/MutedList";
import FollowsList from "Element/FollowsList";
+import IconButton from "Element/IconButton";
import { RootState } from "State/Store";
import { HexKey } from "Nostr";
import FollowsYou from "Element/FollowsYou"
@@ -28,7 +34,9 @@ enum ProfileTab {
Notes = "Notes",
Reactions = "Reactions",
Followers = "Followers",
- Follows = "Follows"
+ Follows = "Follows",
+ Muted = "Muted",
+ Blocked = "Blocked"
};
export default function ProfilePage() {
@@ -36,13 +44,16 @@ export default function ProfilePage() {
const navigate = useNavigate();
const id = useMemo(() => parseId(params.id!), [params]);
const user = useUserProfile(id);
+ const loggedOut = useSelector(s => s.login.loggedOut);
const loginPubKey = useSelector(s => s.login.publicKey);
const follows = useSelector(s => s.login.follows);
const isMe = loginPubKey === id;
const [showLnQr, setShowLnQr] = useState(false);
const [tab, setTab] = useState(ProfileTab.Notes);
const [showProfileQr, setShowProfileQr] = useState(false);
- const about = Text({ content: user?.about || '', tags: [], users: new Map() })
+ const aboutText = user?.about || ''
+ const about = Text({ content: aboutText, tags: [], users: new Map() })
+ const lnurl = extractLnAddress(user?.lud16 || user?.lud06 || "");
useEffect(() => {
setTab(ProfileTab.Notes);
@@ -55,50 +66,52 @@ export default function ProfilePage() {
{user?.display_name || user?.name || 'Nostrich'}
-
{user?.nip05 && }
+
+ {links()}
)
}
+ function links() {
+ return (
+
+ {user?.website && (
+
+ )}
+
+
setShowLnQr(false)} />
+
+ )
+ }
+
function bio() {
- const lnurl = extractLnAddress(user?.lud16 || user?.lud06 || "");
- return (
-
-
{about}
-
-
- {user?.website && (
-
- )}
-
- {lnurl && (
-
setShowLnQr(true)}>
- ⚡️
-
- {lnurl}
-
-
- )}
-
-
setShowLnQr(false)} />
-
+ return aboutText.length > 0 && (
+ <>
+
Bio
+
+ {about}
+
+ >
)
}
function tabContent() {
switch (tab) {
case ProfileTab.Notes:
- return
;
+ return
;
case ProfileTab.Follows: {
if (isMe) {
return (
- <>
+
Following {follows.length}
{follows.map(a =>
)}
- >
+
);
} else {
return
;
@@ -107,6 +120,12 @@ export default function ProfilePage() {
case ProfileTab.Followers: {
return
}
+ case ProfileTab.Muted: {
+ return isMe ?
:
+ }
+ case ProfileTab.Blocked: {
+ return isMe ?
: null
+ }
}
}
@@ -118,58 +137,71 @@ export default function ProfilePage() {
)
}
+ function renderIcons() {
+ return (
+
+ setShowProfileQr(true)}>
+
+
+ {showProfileQr && (
+ setShowProfileQr(false)}>
+
+
+ )}
+ {isMe ? (
+ <>
+
+
+ >
+ ) : (
+ <>
+ setShowLnQr(true)}>
+
+
+ {!loggedOut && (
+ <>
+ navigate(`/messages/${hexToBech32("npub", id)}`)}>
+
+
+ >
+ )}
+ >
+ )}
+
+ )
+ }
+
function userDetails() {
return (
{username()}
-
-
-
setShowProfileQr(true)}>
-
-
- {showProfileQr && (
setShowProfileQr(false)}>
-
-
-
- )}
- {isMe ? (
-
navigate("/settings")}>
-
-
- ) : <>
-
navigate(`/messages/${hexToBech32("npub", id)}`)}>
-
-
-
- >
- }
+
+ {renderIcons()}
+ {!isMe && }
-
{bio()}
)
}
+ function renderTab(v: ProfileTab) {
+ return
setTab(v)}>{v}
+ }
+
return (
<>
- {user?.banner &&
}
- {user?.banner ? (
- <>
- {avatar()}
- {userDetails()}
- >
- ) : (
-
- {avatar()}
- {userDetails()}
-
- )}
+ {user?.banner &&
}
+
+ {avatar()}
+ {userDetails()}
+
- {[ProfileTab.Notes, ProfileTab.Followers, ProfileTab.Follows].map(v => {
- return
setTab(v)}>{v}
- })}
+ {[ProfileTab.Notes, ProfileTab.Followers, ProfileTab.Follows, ProfileTab.Muted].map(renderTab)}
+ {isMe && renderTab(ProfileTab.Blocked)}
{tabContent()}
>
diff --git a/src/Pages/Root.tsx b/src/Pages/Root.tsx
index 75383348..739e8e24 100644
--- a/src/Pages/Root.tsx
+++ b/src/Pages/Root.tsx
@@ -16,6 +16,7 @@ const RootTab = {
};
export default function RootPage() {
+ const [show, setShow] = useState(false)
const [loggedOut, pubKey, follows] = useSelector
(s => [s.login.loggedOut, s.login.publicKey, s.login.follows]);
const [tab, setTab] = useState(RootTab.Posts);
@@ -32,20 +33,20 @@ export default function RootPage() {
return (
<>
{pubKey ? <>
-
-
-
setTab(RootTab.Posts)}>
+
+
setTab(RootTab.Posts)}>
Posts
-
setTab(RootTab.PostsAndReplies)}>
- Posts & Replies
+
setTab(RootTab.PostsAndReplies)}>
+ Conversations
-
setTab(RootTab.Global)}>
+
setTab(RootTab.Global)}>
Global
> : null}
{followHints()}
+
>
);
-}
\ No newline at end of file
+}
diff --git a/src/Pages/SettingsPage.tsx b/src/Pages/SettingsPage.tsx
index 4ceaddd7..daf0b76c 100644
--- a/src/Pages/SettingsPage.tsx
+++ b/src/Pages/SettingsPage.tsx
@@ -9,10 +9,10 @@ export default function SettingsPage() {
const navigate = useNavigate();
return (
- <>
+
navigate("/settings")} className="pointer">Settings
- >
+
);
}
diff --git a/src/Pages/Verification.tsx b/src/Pages/Verification.tsx
index 683c9025..3732bca1 100644
--- a/src/Pages/Verification.tsx
+++ b/src/Pages/Verification.tsx
@@ -24,7 +24,7 @@ export default function VerificationPage() {
];
return (
-
+
Get Verified
NIP-05 is a DNS based verification spec which helps to validate you as a real user.
diff --git a/src/Pages/settings/Index.tsx b/src/Pages/settings/Index.tsx
index 5fc6ae1b..d6b9877d 100644
--- a/src/Pages/settings/Index.tsx
+++ b/src/Pages/settings/Index.tsx
@@ -1,12 +1,23 @@
-import { faCircleDollarToSlot, faGear, faPlug, faUser } from "@fortawesome/free-solid-svg-icons";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { useNavigate } from "react-router-dom";
import "./Index.css";
+import { useDispatch } from "react-redux";
+import { useNavigate } from "react-router-dom";
+import { faRightFromBracket, faCircleDollarToSlot, faGear, faPlug, faUser } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+
+import { logout } from "State/Login";
+
const SettingsIndex = () => {
+ const dispatch = useDispatch();
const navigate = useNavigate();
+ function handleLogout() {
+ dispatch(logout())
+ navigate("/")
+ }
+
return (
+ <>
navigate("profile")}>
@@ -24,7 +35,12 @@ const SettingsIndex = () => {
Donate
+
+
+ Log Out
+
+ >
)
}
diff --git a/src/Pages/settings/Profile.tsx b/src/Pages/settings/Profile.tsx
index 09ffd469..a88f399f 100644
--- a/src/Pages/settings/Profile.tsx
+++ b/src/Pages/settings/Profile.tsx
@@ -2,14 +2,15 @@ import "./Profile.css";
import Nostrich from "nostrich.jpg";
import { useEffect, useState } from "react";
-import { useDispatch, useSelector } from "react-redux";
+import { useSelector } from "react-redux";
import { useNavigate } from "react-router-dom";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faShop } from "@fortawesome/free-solid-svg-icons";
import useEventPublisher from "Feed/EventPublisher";
import { useUserProfile } from "Feed/ProfileFeed";
-import { logout } from "State/Login";
+import VoidUpload from "Feed/VoidUpload";
+import LogoutButton from "Element/LogoutButton";
import { hexToBech32, openFile } from "Util";
import Copy from "Element/Copy";
import { RootState } from "State/Store";
@@ -20,7 +21,6 @@ export default function ProfileSettings() {
const navigate = useNavigate();
const id = useSelector
(s => s.login.publicKey);
const privKey = useSelector(s => s.login.privateKey);
- const dispatch = useDispatch();
const user = useUserProfile(id!);
const publisher = useEventPublisher();
const uploader = useFileUpload();
@@ -130,11 +130,11 @@ export default function ProfileSettings() {
NIP-05:
setNip05(e.target.value)} />
-
navigate("/verification")}>
+
+
@@ -145,10 +145,10 @@ export default function ProfileSettings() {
-
{ dispatch(logout()); navigate("/"); }}>Logout
+
-
saveProfile()}>Save
+
diff --git a/src/Pages/settings/Relays.tsx b/src/Pages/settings/Relays.tsx
index b5e2d241..f8c83383 100644
--- a/src/Pages/settings/Relays.tsx
+++ b/src/Pages/settings/Relays.tsx
@@ -27,7 +27,7 @@ const RelaySettingsPage = () => {
setNewRelay(e.target.value)} />
-
addNewRelay()}>Add
+
>
)
}
@@ -49,16 +49,16 @@ const RelaySettingsPage = () => {
return (
<>
Relays
-
+
{Object.keys(relays || {}).map(a => )}
-
saveRelays()}>Save
+
{addRelay()}
>
)
}
-export default RelaySettingsPage;
\ No newline at end of file
+export default RelaySettingsPage;
diff --git a/src/State/Login.ts b/src/State/Login.ts
index 66950a3b..d7c756db 100644
--- a/src/State/Login.ts
+++ b/src/State/Login.ts
@@ -3,6 +3,7 @@ import * as secp from '@noble/secp256k1';
import { DefaultRelays } from 'Const';
import { HexKey, TaggedRawEvent } from 'Nostr';
import { RelaySettings } from 'Nostr/Connection';
+import type { AppDispatch, RootState } from "State/Store";
const PrivateKeyItem = "secret";
const PublicKeyItem = "pubkey";
@@ -11,6 +12,13 @@ const UserPreferencesKey = "preferences";
const RelayListKey = "last-relays";
const FollowList = "last-follows";
+export interface NotificationRequest {
+ title: string
+ body: string
+ icon: string
+ timestamp: number
+}
+
export interface UserPreferences {
/**
* Enable reactions / reposts / zaps
@@ -79,6 +87,26 @@ export interface LoginStore {
*/
follows: HexKey[],
+ /**
+ * Newest relay list timestamp
+ */
+ latestFollows: number,
+
+ /**
+ * A list of pubkeys this user has muted
+ */
+ muted: HexKey[],
+
+ /**
+ * Last seen mute list event timestamp
+ */
+ latestMuted: number,
+
+ /**
+ * A list of pubkeys this user has muted privately
+ */
+ blocked: HexKey[],
+
/**
* Notifications for this login session
*/
@@ -112,6 +140,10 @@ const InitState = {
relays: {},
latestRelays: 0,
follows: [],
+ latestFollows: 0,
+ muted: [],
+ blocked: [],
+ latestMuted: 0,
notifications: [],
readNotifications: new Date().getTime(),
dms: [],
@@ -132,6 +164,11 @@ export interface SetRelaysPayload {
createdAt: number
};
+export interface SetFollowsPayload {
+ keys: HexKey[]
+ createdAt: number
+};
+
const LoginSlice = createSlice({
name: "Login",
initialState: InitState,
@@ -212,9 +249,14 @@ const LoginSlice = createSlice({
state.relays = { ...state.relays };
window.localStorage.setItem(RelayListKey, JSON.stringify(state.relays));
},
- setFollows: (state, action: PayloadAction
) => {
+ setFollows: (state, action: PayloadAction) => {
+ const { keys, createdAt } = action.payload
+ if (state.latestFollows > createdAt) {
+ return;
+ }
+
let existing = new Set(state.follows);
- let update = Array.isArray(action.payload) ? action.payload : [action.payload];
+ let update = Array.isArray(keys) ? keys : [keys];
let changes = false;
for (let pk of update.filter(a => a.length === 64)) {
@@ -232,28 +274,26 @@ const LoginSlice = createSlice({
if (changes) {
state.follows = Array.from(existing);
+ state.latestFollows = createdAt;
}
window.localStorage.setItem(FollowList, JSON.stringify(state.follows));
},
- addNotifications: (state, action: PayloadAction) => {
- let n = action.payload;
- if (!Array.isArray(n)) {
- n = [n];
- }
-
- let didChange = false;
- for (let x of n) {
- if (!state.notifications.some(a => a.id === x.id)) {
- state.notifications.push(x);
- didChange = true;
- }
- }
- if (didChange) {
- state.notifications = [
- ...state.notifications
- ];
- }
+ setMuted(state, action: PayloadAction<{createdAt: number, keys: HexKey[]}>) {
+ const { createdAt, keys } = action.payload
+ if (createdAt >= state.latestMuted) {
+ const muted = new Set([...keys])
+ state.muted = Array.from(muted)
+ state.latestMuted = createdAt
+ }
+ },
+ setBlocked(state, action: PayloadAction<{createdAt: number, keys: HexKey[]}>) {
+ const { createdAt, keys } = action.payload
+ if (createdAt >= state.latestMuted) {
+ const blocked = new Set([...keys])
+ state.blocked = Array.from(blocked)
+ state.latestMuted = createdAt
+ }
},
addDirectMessage: (state, action: PayloadAction>) => {
let n = action.payload;
@@ -268,6 +308,7 @@ const LoginSlice = createSlice({
didChange = true;
}
}
+
if (didChange) {
state.dms = [
...state.dms
@@ -301,11 +342,36 @@ export const {
setRelays,
removeRelay,
setFollows,
- addNotifications,
+ setMuted,
+ setBlocked,
addDirectMessage,
incDmInteraction,
logout,
markNotificationsRead,
- setPreferences
+ setPreferences,
} = LoginSlice.actions;
+
+export function sendNotification({ title, body, icon, timestamp }: NotificationRequest) {
+ return async (dispatch: AppDispatch, getState: () => RootState) => {
+ const state = getState()
+ const { readNotifications } = state.login
+ const hasPermission = "Notification" in window && Notification.permission === "granted"
+ const shouldShowNotification = hasPermission && timestamp > readNotifications
+ if (shouldShowNotification) {
+ try {
+ let worker = await navigator.serviceWorker.ready;
+ worker.showNotification(title, {
+ tag: "notification",
+ vibrate: [500],
+ body,
+ icon,
+ timestamp,
+ });
+ } catch (error) {
+ console.warn(error)
+ }
+ }
+ }
+}
+
export const reducer = LoginSlice.reducer;
diff --git a/src/index.css b/src/index.css
index 9e9b5d55..419060ef 100644
--- a/src/index.css
+++ b/src/index.css
@@ -1,4 +1,4 @@
-@import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@400;700&display=swap');
+@import url('https://fonts.googleapis.com/css2?family=Be+Bietnam+Pro:wght@400;500;600;700&display=swap');
:root {
--bg-color: #000;
@@ -9,10 +9,8 @@
--font-size-small: 14px;
--font-size-tiny: 12px;
--modal-bg-color: rgba(0, 0, 0, 0.8);
- --note-bg: #111;
- --highlight-light: #ffd342;
- --highlight: #ffc400;
- --highlight-dark: #dba800;
+ --note-bg: #0C0C0C;
+ --highlight: #8B5CF6;
--error: #FF6053;
--success: #2AD544;
@@ -22,8 +20,10 @@
--gray: #333;
--gray-secondary: #222;
--gray-tertiary: #444;
+ --gray-dark: #2B2B2B;
+ --gray-superdark: #171717;
--gray-gradient: linear-gradient(to bottom right, var(--gray-superlight), var(--gray), var(--gray-light));
- --snort-gradient: linear-gradient(to bottom right, var(--highlight-light), var(--highlight), var(--highlight-dark));
+ --snort-gradient: linear-gradient(180deg, #FFC7B7 0%, #4F1B73 100%);
--nostrplebs-gradient: linear-gradient(to bottom right, #ff3cac, #2b86c5);
--strike-army-gradient: linear-gradient(to bottom right, #CCFF00, #a1c900);
}
@@ -46,11 +46,13 @@ html.light {
--gray-tertiary: #EEE;
--gray-superlight: #333;
--gray-light: #555;
+ --gray-dark: #2B2B2B;
+ --gray-superdark: #171717;
}
body {
margin: 0;
- font-family: 'Montserrat', sans-serif;
+ font-family: 'Be Vietnam Pro', sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background-color: var(--bg-color);
@@ -63,25 +65,17 @@ code {
}
.page {
- width: 720px;
+ width: 100vw;
margin-left: auto;
margin-right: auto;
}
-.page>.header {
- display: flex;
- align-items: center;
- margin: 10px 0;
-}
-
-.page>.header>div:nth-child(1) {
- font-size: x-large;
- flex-grow: 1;
-}
-
-.page>.header>div:nth-child(2) {
- display: flex;
- align-items: center;
+@media (min-width: 720px) {
+ .page {
+ width: 720px;
+ margin-left: auto;
+ margin-right: auto;
+ }
}
.card {
@@ -93,7 +87,7 @@ code {
@media (min-width: 720px) {
.card {
- margin-bottom: 24px;
+ margin-bottom: 16px;
padding: 12px 24px;
}
}
@@ -111,14 +105,90 @@ html.light .card {
.card>.footer {
display: flex;
- flex-direction: row-reverse;
- margin-top: 12px;
+ flex-direction: row;
+}
+
+button {
+ cursor: pointer;
+ padding: 6px 12px;
+ font-weight: 700;
+ color: white;
+ font-size: var(--font-size);
+ background-color: var(--highlight);
+ border: none;
+ border-radius: 16px;
+ outline: none;
+}
+
+button:disabled {
+ cursor: not-allowed;
+ color: var(--gray);
+}
+
+.light button.transparent {
+ color: var(--font-color);
+}
+
+.light button:disabled {
+ color: var(--font-color);
+}
+
+button:hover {
+ background-color: var(--font-color);
+ color: var(--bg-color);
+}
+
+button.secondary {
+ color: var(--font-color);
+ background-color: var(--gray-dark);
+}
+
+button.transparent {
+ background-color: transparent;
+ border: 1px solid var(--gray-superdark);
+}
+
+.light button.secondary {
+ background-color: var(--gray);
+}
+
+button.secondary:hover {
+ border: none;
+ color: var(--font-color);
+ background-color: var(--gray-superdark);
+}
+
+button.transparent:hover {
+ color: var(--bg-color);
+ background-color: var(--font-color);
+}
+
+.light button.secondary:hover {
+ background-color: var(--gray-secondary);
+}
+
+button.icon {
+ border: none;
+ background: none;
+ color: var(--font-color);
+ min-height: 28px;
+}
+
+button.icon .icon-wrapper {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+button.icon:hover {
+ color: var(--highlight);
}
.btn {
padding: 10px;
border-radius: 5px;
cursor: pointer;
+ color: var(--font-color);
user-select: none;
background-color: var(--bg-color);
color: var(--font-color);
@@ -138,7 +208,7 @@ html.light .card {
border: 2px solid;
background-color: var(--gray-secondary);
color: var(--font-color);
- font-weight: bold;
+ font-weight: 700;
}
.btn.disabled {
@@ -155,6 +225,17 @@ html.light .card {
.btn-rnd {
border-radius: 100%;
+ border-color: var(--gray-superdark);
+ width: 32px;
+ height: 32px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-right: 16px;
+}
+
+.light .btn-rnd {
+ border-color: var(--gray);
}
textarea {
@@ -345,16 +426,12 @@ body.scroll-lock {
}
.tabs>div {
- margin-right: 10px;
cursor: pointer;
-}
-
-.tabs>div:last-child {
margin: 0;
}
.tabs .active {
- font-weight: bold;
+ font-weight: 700;
}
.error {
@@ -369,18 +446,24 @@ body.scroll-lock {
background-color: var(--success);
}
-.root-tabs {
+.tabs {
padding: 0;
align-items: center;
justify-content: flex-start;
+ margin-bottom: 16px;
}
-.root-tab {
- border-bottom: 3px solid var(--gray-secondary);
+.tab {
+ border-bottom: 1px solid var(--gray-secondary);
+ font-weight: 700;
+ line-height: 19px;
+ color: var(--font-secondary-color);
+ padding: 8px 0;
}
-.root-tab.active {
- border-bottom: 3px solid var(--highlight);
+.tab.active {
+ border-bottom: 1px solid var(--highlight);
+ color: var(--font-color);
}
.tweet {
@@ -402,10 +485,6 @@ body.scroll-lock {
}
@media(max-width: 720px) {
- .page {
- width: calc(100vw - 8px);
- }
-
div.form-group {
flex-direction: column;
align-items: flex-start;
@@ -414,4 +493,18 @@ body.scroll-lock {
.highlight {
color: var(--highlight);
-}
\ No newline at end of file
+}
+
+.main-content {
+ padding: 0 12px;
+}
+
+@media (min-width: 720px) {
+ .main-content {
+ padding: 0;
+ }
+}
+
+.bold {
+ font-weight: 700;
+}