From 8d882a08448881bed7a692874bdcbb2411cb88a7 Mon Sep 17 00:00:00 2001 From: Kieran Date: Mon, 9 Oct 2023 16:32:14 +0100 Subject: [PATCH] Profile hover cards --- packages/app/package.json | 1 + packages/app/src/Element/Text.tsx | 10 ++- .../app/src/Element/User/FollowButton.css | 2 - .../app/src/Element/User/FollowButton.tsx | 10 ++- .../app/src/Element/User/ProfileImage.css | 12 +++ .../app/src/Element/User/ProfileImage.tsx | 87 ++++++++++++++++--- .../app/src/Element/User/ProfilePreview.css | 4 - .../app/src/Element/User/ProfilePreview.tsx | 2 + .../app/src/Element/User/UserWebsiteLink.css | 13 +++ .../app/src/Element/User/UserWebsiteLink.tsx | 29 +++++++ .../app/src/Hooks/useTextTransformCache.tsx | 13 +-- packages/app/src/Pages/ProfilePage.css | 8 -- packages/app/src/Pages/ProfilePage.tsx | 25 +----- packages/app/src/index.css | 30 +++---- packages/system/src/text.ts | 2 +- yarn.lock | 11 +++ 16 files changed, 184 insertions(+), 75 deletions(-) delete mode 100644 packages/app/src/Element/User/FollowButton.css create mode 100644 packages/app/src/Element/User/UserWebsiteLink.css create mode 100644 packages/app/src/Element/User/UserWebsiteLink.tsx diff --git a/packages/app/package.json b/packages/app/package.json index c68200643..0a5dd2530 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -16,6 +16,7 @@ "@snort/system-web": "workspace:*", "@szhsin/react-menu": "^3.3.1", "@types/use-sync-external-store": "^0.0.4", + "@uidotdev/usehooks": "^2.3.1", "@void-cat/api": "^1.0.4", "debug": "^4.3.4", "dexie": "^3.2.4", diff --git a/packages/app/src/Element/Text.tsx b/packages/app/src/Element/Text.tsx index 06f00ffd6..fb31ad9bf 100644 --- a/packages/app/src/Element/Text.tsx +++ b/packages/app/src/Element/Text.tsx @@ -248,9 +248,13 @@ export default function Text({ chunks.push(); } if (element.type === "link" || (element.type === "media" && element.mimeType?.startsWith("unknown"))) { - chunks.push( - , - ); + if (disableMedia ?? false) { + chunks.push(); + } else { + chunks.push( + , + ); + } } if (element.type === "custom_emoji") { chunks.push(); diff --git a/packages/app/src/Element/User/FollowButton.css b/packages/app/src/Element/User/FollowButton.css deleted file mode 100644 index 66f055260..000000000 --- a/packages/app/src/Element/User/FollowButton.css +++ /dev/null @@ -1,2 +0,0 @@ -.follow-button { -} diff --git a/packages/app/src/Element/User/FollowButton.tsx b/packages/app/src/Element/User/FollowButton.tsx index 5952b7fdf..89c7059a5 100644 --- a/packages/app/src/Element/User/FollowButton.tsx +++ b/packages/app/src/Element/User/FollowButton.tsx @@ -1,4 +1,3 @@ -import "./FollowButton.css"; import FormattedMessage from "Element/FormattedMessage"; import { HexKey } from "@snort/system"; @@ -20,7 +19,7 @@ export default function FollowButton(props: FollowButtonProps) { const publisher = useEventPublisher(); const { follows, relays, readonly } = useLogin(s => ({ follows: s.follows, relays: s.relays, readonly: s.readonly })); const isFollowing = follows.item.includes(pubkey); - const baseClassname = `${props.className ? ` ${props.className}` : ""}follow-button`; + const baseClassname = props.className ? `${props.className} ` : ""; async function follow(pubkey: HexKey) { if (publisher) { @@ -42,9 +41,12 @@ export default function FollowButton(props: FollowButtonProps) { return ( (isFollowing ? unfollow(pubkey) : follow(pubkey))}> + onClick={e => { + e.stopPropagation(); + isFollowing ? unfollow(pubkey) : follow(pubkey); + }}> {isFollowing ? : } ); diff --git a/packages/app/src/Element/User/ProfileImage.css b/packages/app/src/Element/User/ProfileImage.css index 29d1c906c..a6f289885 100644 --- a/packages/app/src/Element/User/ProfileImage.css +++ b/packages/app/src/Element/User/ProfileImage.css @@ -43,3 +43,15 @@ a.pfp { background-color: var(--gray-superdark); transform: rotate(135deg); } + +.profile-card { + width: 360px; + border-radius: 16px; + background: var(--gray-superdark); + box-shadow: 0px 2px 4px 0px rgba(0, 0, 0, 0.05); +} + +.profile-card > div { + color: white; + padding: 8px 12px; +} diff --git a/packages/app/src/Element/User/ProfileImage.tsx b/packages/app/src/Element/User/ProfileImage.tsx index 7fc6d6b13..1a7b97aa3 100644 --- a/packages/app/src/Element/User/ProfileImage.tsx +++ b/packages/app/src/Element/User/ProfileImage.tsx @@ -1,9 +1,11 @@ import "./ProfileImage.css"; -import React, { ReactNode } from "react"; +import React, { ReactNode, useEffect, useState } from "react"; import { Link } from "react-router-dom"; import { HexKey, UserMetadata } from "@snort/system"; import { useUserProfile } from "@snort/system-react"; +import { useHover } from "@uidotdev/usehooks"; +import { ControlledMenu } from "@szhsin/react-menu"; import { profileLink } from "SnortUtils"; import Avatar from "Element/User/Avatar"; @@ -11,6 +13,9 @@ import Nip05 from "Element/User/Nip05"; import useLogin from "Hooks/useLogin"; import Icon from "Icons/Icon"; import DisplayName from "./DisplayName"; +import Text from "Element/Text"; +import FollowButton from "Element/User/FollowButton"; +import { UserWebsiteLink } from "Element/User/UserWebsiteLink"; export interface ProfileImageProps { pubkey: HexKey; @@ -27,6 +32,7 @@ export interface ProfileImageProps { imageOverlay?: ReactNode; showFollowingMark?: boolean; icons?: ReactNode; + showProfileCard?: boolean; } export default function ProfileImage({ @@ -44,11 +50,29 @@ export default function ProfileImage({ onClick, showFollowingMark = true, icons, + showProfileCard, }: ProfileImageProps) { const user = useUserProfile(profile ? "" : pubkey) ?? profile; const nip05 = defaultNip ? defaultNip : user?.nip05; const { follows } = useLogin(); const doesFollow = follows.item.includes(pubkey); + const [ref, hovering] = useHover(); + const [showProfileMenu, setShowProfileMenu] = useState(false); + const [t, setT] = useState>(); + + useEffect(() => { + if (hovering) { + const tn = setTimeout(() => { + setShowProfileMenu(true); + }, 1000); + setT(tn); + } else { + if (t) { + clearTimeout(t); + setT(undefined); + } + } + }, [hovering]); function handleClick(e: React.MouseEvent) { if (link === "") { @@ -60,7 +84,7 @@ export default function ProfileImage({ function inner() { return ( <> -
+
setShowProfileMenu(false)}> +
+
+ +
+ {/**/} + +
+
+ + +
+ + ); + } + return null; + } + if (link === "") { return ( -
- {inner()} -
+ <> +
+ {inner()} +
+ {profileCard()} + ); } else { return ( - - {inner()} - + <> + + {inner()} + + {profileCard()} + ); } } diff --git a/packages/app/src/Element/User/ProfilePreview.css b/packages/app/src/Element/User/ProfilePreview.css index 00211cc0e..67f7cc3ee 100644 --- a/packages/app/src/Element/User/ProfilePreview.css +++ b/packages/app/src/Element/User/ProfilePreview.css @@ -15,7 +15,3 @@ overflow: hidden; text-overflow: ellipsis; } - -.profile-preview button { - min-width: 98px; -} diff --git a/packages/app/src/Element/User/ProfilePreview.tsx b/packages/app/src/Element/User/ProfilePreview.tsx index 1fd655178..19ba98b10 100644 --- a/packages/app/src/Element/User/ProfilePreview.tsx +++ b/packages/app/src/Element/User/ProfilePreview.tsx @@ -12,6 +12,7 @@ export interface ProfilePreviewProps { options?: { about?: boolean; linkToProfile?: boolean; + profileCards?: boolean; }; profile?: UserMetadata; actions?: ReactNode; @@ -45,6 +46,7 @@ export default function ProfilePreview(props: ProfilePreviewProps) { profile={props.profile} link={options.linkToProfile ?? true ? undefined : ""} subHeader={options.about ?
{user?.about}
: undefined} + showProfileCard={options.profileCards} /> {props.actions ?? (
diff --git a/packages/app/src/Element/User/UserWebsiteLink.css b/packages/app/src/Element/User/UserWebsiteLink.css new file mode 100644 index 000000000..0f81ef932 --- /dev/null +++ b/packages/app/src/Element/User/UserWebsiteLink.css @@ -0,0 +1,13 @@ +.user-profile-link { + display: flex; + align-items: center; + gap: 8px; +} + +.user-profile-link a { + text-decoration: none; +} + +.user-profile-link a:hover { + text-decoration: underline; +} diff --git a/packages/app/src/Element/User/UserWebsiteLink.tsx b/packages/app/src/Element/User/UserWebsiteLink.tsx new file mode 100644 index 000000000..e5dbbd7a4 --- /dev/null +++ b/packages/app/src/Element/User/UserWebsiteLink.tsx @@ -0,0 +1,29 @@ +import "./UserWebsiteLink.css"; +import { MetadataCache, UserMetadata } from "@snort/system"; +import Icon from "Icons/Icon"; + +export function UserWebsiteLink({ user }: { user?: MetadataCache | UserMetadata }) { + const website_url = + user?.website && !user.website.startsWith("http") ? "https://" + user.website : user?.website || ""; + + function tryFormatWebsite(url: string) { + try { + const u = new URL(url); + return `${u.hostname}${u.pathname !== "/" ? u.pathname : ""}`; + } catch { + // ignore + } + return url; + } + + if (user?.website) { + return ( + + ); + } +} diff --git a/packages/app/src/Hooks/useTextTransformCache.tsx b/packages/app/src/Hooks/useTextTransformCache.tsx index 07442043d..4c562658a 100644 --- a/packages/app/src/Hooks/useTextTransformCache.tsx +++ b/packages/app/src/Hooks/useTextTransformCache.tsx @@ -3,11 +3,14 @@ import { ParsedFragment, transformText } from "@snort/system"; const TextCache = new Map>(); export function transformTextCached(id: string, content: string, tags: Array>) { - const cached = TextCache.get(id); - if (cached) return cached; - const newCache = transformText(content, tags); - TextCache.set(id, newCache); - return newCache; + if (content.length > 0) { + const cached = TextCache.get(id); + if (cached) return cached; + const newCache = transformText(content, tags); + TextCache.set(id, newCache); + return newCache; + } + return []; } export function useTextTransformer(id: string, content: string, tags: Array>) { diff --git a/packages/app/src/Pages/ProfilePage.css b/packages/app/src/Pages/ProfilePage.css index eb4e3c5ef..6c0851eb2 100644 --- a/packages/app/src/Pages/ProfilePage.css +++ b/packages/app/src/Pages/ProfilePage.css @@ -138,14 +138,6 @@ gap: 8px; } -.profile .website a { - text-decoration: none; -} - -.profile .website a:hover { - text-decoration: underline; -} - .profile .link svg { color: var(--highlight); } diff --git a/packages/app/src/Pages/ProfilePage.tsx b/packages/app/src/Pages/ProfilePage.tsx index 5c2501ade..1660f4622 100644 --- a/packages/app/src/Pages/ProfilePage.tsx +++ b/packages/app/src/Pages/ProfilePage.tsx @@ -59,6 +59,7 @@ import { useStatusFeed } from "Feed/StatusFeed"; import messages from "./messages"; import { SpotlightMediaModal } from "Element/Deck/SpotlightMedia"; +import { UserWebsiteLink } from "Element/User/UserWebsiteLink"; const NOTES = 0; const REACTIONS = 1; @@ -135,8 +136,6 @@ export default function ProfilePage() { const showBadges = login.preferences.showBadges ?? false; const showStatus = login.preferences.showStatus ?? true; - const website_url = - user?.website && !user.website.startsWith("http") ? "https://" + user.website : user?.website || ""; // feeds const { blocked } = useModeration(); const pinned = usePinnedFeed(id); @@ -295,28 +294,10 @@ export default function ProfilePage() { ); } - function tryFormatWebsite(url: string) { - try { - const u = new URL(url); - return `${u.hostname}${u.pathname !== "/" ? u.pathname : ""}`; - } catch { - // ignore - } - return url; - } - function links() { return ( <> - {user?.website && ( - - )} - + {lnurl && (
setShowLnQr(true)}> @@ -433,7 +414,7 @@ export default function ProfilePage() { setModalImage(user?.picture || "")} className="pointer" />
{renderIcons()} - {!isMe && id && } + {!isMe && id && }
); diff --git a/packages/app/src/index.css b/packages/app/src/index.css index 456f4bba2..3f498f57f 100644 --- a/packages/app/src/index.css +++ b/packages/app/src/index.css @@ -113,6 +113,21 @@ code { font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace; } +a { + color: inherit; + line-height: 1.3em; + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +a.ext { + word-break: break-all; + white-space: initial; +} + #root { overflow-x: hidden; } @@ -498,21 +513,6 @@ input:disabled { max-width: -moz-available; } -a { - color: inherit; - line-height: 1.3em; - text-decoration: none; -} - -a:hover { - text-decoration: underline; -} - -a.ext { - word-break: break-all; - white-space: initial; -} - div.form { display: grid; grid-auto-flow: row; diff --git a/packages/system/src/text.ts b/packages/system/src/text.ts index 5988b40ad..b6aebd7dc 100644 --- a/packages/system/src/text.ts +++ b/packages/system/src/text.ts @@ -233,7 +233,7 @@ export function transformText(body: string, tags: Array>) { fragments = fragments .map(a => { if (typeof a === "string") { - if (a.trim().length > 0) { + if (a.length > 0) { return { type: "text", content: a } as ParsedFragment; } } else { diff --git a/yarn.lock b/yarn.lock index 00f048c7b..5f2ef34ff 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2714,6 +2714,7 @@ __metadata: "@types/webtorrent": ^0.109.3 "@typescript-eslint/eslint-plugin": ^6.1.0 "@typescript-eslint/parser": ^6.1.0 + "@uidotdev/usehooks": ^2.3.1 "@void-cat/api": ^1.0.4 "@webbtc/webln-types": ^1.0.10 "@webpack-cli/generators": ^3.0.4 @@ -3852,6 +3853,16 @@ __metadata: languageName: node linkType: hard +"@uidotdev/usehooks@npm:^2.3.1": + version: 2.3.1 + resolution: "@uidotdev/usehooks@npm:2.3.1" + peerDependencies: + react: ">=18.0.0" + react-dom: ">=18.0.0" + checksum: a1339b91bdb4176f59fc2dd8273065fccacb17749b7022879982ff874bda8e4e54a3f8d74f126e6224164fb2ad422f1cc40dac8705467960df525b207fcd3a79 + languageName: node + linkType: hard + "@void-cat/api@npm:^1.0.4": version: 1.0.7 resolution: "@void-cat/api@npm:1.0.7"