Note style changes
This commit is contained in:
parent
fd7e00c8d4
commit
ed4a0678e1
@ -10,7 +10,6 @@
|
||||
"@reduxjs/toolkit": "^1.9.1",
|
||||
"bech32": "^2.0.0",
|
||||
"light-bolt11-decoder": "^2.1.0",
|
||||
"moment": "^2.29.4",
|
||||
"qr-code-styling": "^1.6.0-rc.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
|
21
src/Text.js
21
src/Text.js
@ -2,14 +2,13 @@ import { Link } from "react-router-dom";
|
||||
|
||||
import Invoice from "./element/Invoice";
|
||||
import { UrlRegex, FileExtensionRegex, MentionRegex, InvoiceRegex, YoutubeUrlRegex } from "./Const";
|
||||
import { eventLink, profileLink } from "./Util";
|
||||
import { eventLink, hexToBech32, profileLink } from "./Util";
|
||||
|
||||
function transformHttpLink(a) {
|
||||
try {
|
||||
const url = new URL(a);
|
||||
const vParam = url.searchParams.get('v')
|
||||
const youtubeId = YoutubeUrlRegex.test(a) && RegExp.$1
|
||||
const extension = FileExtensionRegex.test(url.pathname.toLowerCase()) && RegExp.$1
|
||||
const youtubeId = YoutubeUrlRegex.test(a) && RegExp.$1;
|
||||
const extension = FileExtensionRegex.test(url.pathname.toLowerCase()) && RegExp.$1;
|
||||
if (extension) {
|
||||
switch (extension) {
|
||||
case "gif":
|
||||
@ -28,7 +27,7 @@ function transformHttpLink(a) {
|
||||
return <video key={url} src={url} controls />
|
||||
}
|
||||
default:
|
||||
return <a key={url} href={url}>{url.toString()}</a>
|
||||
return <a key={url} href={url} onClick={(e) => e.stopPropagation()}>{url.toString()}</a>
|
||||
}
|
||||
} else if (youtubeId) {
|
||||
return (
|
||||
@ -37,7 +36,7 @@ function transformHttpLink(a) {
|
||||
<iframe
|
||||
src={`https://www.youtube.com/embed/${youtubeId}`}
|
||||
title="YouTube video player"
|
||||
frameborder="0"
|
||||
frameBorder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
allowfullscreen=""
|
||||
/>
|
||||
@ -45,7 +44,7 @@ function transformHttpLink(a) {
|
||||
</>
|
||||
)
|
||||
} else {
|
||||
return <a key={url} href={url}>{url.toString()}</a>
|
||||
return <a key={url} href={url} onClick={(e) => e.stopPropagation()}>{url.toString()}</a>
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(`Not a valid url: ${a}`);
|
||||
@ -77,12 +76,12 @@ export function extractMentions(fragments, tags, users) {
|
||||
if (ref) {
|
||||
switch (ref.Key) {
|
||||
case "p": {
|
||||
let pUser = users[ref.PubKey]?.name ?? ref.PubKey.substring(0, 8);
|
||||
return <Link key={ref.PubKey} to={profileLink(ref.PubKey)} onClick={(ev) => ev.stopPropagation()}>@{pUser}</Link>;
|
||||
let pUser = users[ref.PubKey]?.name ?? hexToBech32("npub", ref.PubKey).substring(0, 12);
|
||||
return <Link key={ref.PubKey} to={profileLink(ref.PubKey)} onClick={(e) => e.stopPropagation()}>@{pUser}</Link>;
|
||||
}
|
||||
case "e": {
|
||||
let eText = ref.Event.substring(0, 8);
|
||||
return <Link key={ref.Event} to={eventLink(ref.Event)}>#{eText}</Link>;
|
||||
let eText = hexToBech32("note", ref.Event).substring(0, 12);
|
||||
return <Link key={ref.Event} to={eventLink(ref.Event)} onClick={(e) => e.stopPropagation()}>#{eText}</Link>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
13
src/Util.js
13
src/Util.js
@ -50,8 +50,16 @@ export function bech32ToText(str) {
|
||||
* @returns
|
||||
*/
|
||||
export function eventLink(hex) {
|
||||
return `/e/${hexToBech32("note", hex)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert hex to bech32
|
||||
* @param {string} hex
|
||||
*/
|
||||
export function hexToBech32(hrp, hex) {
|
||||
let buf = secp.utils.hexToBytes(hex);
|
||||
return `/e/${bech32.encode("note", bech32.toWords(buf))}`;
|
||||
return bech32.encode(hrp, bech32.toWords(buf));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -60,8 +68,7 @@ export function eventLink(hex) {
|
||||
* @returns
|
||||
*/
|
||||
export function profileLink(hex) {
|
||||
let buf = secp.utils.hexToBytes(hex);
|
||||
return `/p/${bech32.encode("npub", bech32.toWords(buf))}`;
|
||||
return `/p/${hexToBech32("npub", hex)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,7 +1,7 @@
|
||||
import "./Invoice.css";
|
||||
import { decode as invoiceDecode } from "light-bolt11-decoder";
|
||||
import { useMemo } from "react";
|
||||
import moment from "moment";
|
||||
import NoteTime from "./NoteTime";
|
||||
|
||||
export default function Invoice(props) {
|
||||
const invoice = props.invoice;
|
||||
@ -48,7 +48,7 @@ export default function Invoice(props) {
|
||||
<div className="note-invoice flex">
|
||||
<div className="f-grow flex f-col">
|
||||
{header()}
|
||||
{info?.expire ? <small>{info?.expired ? "Expired" : "Expires"} {moment(info.expire * 1000).fromNow()}</small> : null}
|
||||
{info?.expire ? <small>{info?.expired ? "Expired" : "Expires"} <NoteTime from={info.expire * 1000} /></small> : null}
|
||||
</div>
|
||||
|
||||
{info?.expired ? <div className="btn">Expired</div> :
|
||||
|
@ -13,10 +13,13 @@
|
||||
|
||||
.note > .header .reply {
|
||||
font-size: small;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.note > .header > .info {
|
||||
font-size: small;
|
||||
white-space: nowrap;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.note > .body {
|
||||
|
@ -1,14 +1,14 @@
|
||||
import "./Note.css";
|
||||
import { useCallback } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import moment from "moment";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import Event from "../nostr/Event";
|
||||
import ProfileImage from "./ProfileImage";
|
||||
import { extractLinks, extractMentions, extractInvoices } from "../Text";
|
||||
import { eventLink } from "../Util";
|
||||
import { eventLink, hexToBech32 } from "../Util";
|
||||
import NoteFooter from "./NoteFooter";
|
||||
import NoteTime from "./NoteTime";
|
||||
|
||||
export default function Note(props) {
|
||||
const navigate = useNavigate();
|
||||
@ -57,11 +57,14 @@ export default function Note(props) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const maxMentions = 2;
|
||||
let replyId = ev.Thread?.ReplyTo?.Event;
|
||||
let mentions = ev.Thread?.PubKeys?.map(a => [a, users[a]])?.map(a => a[1]?.name ?? a[0].substring(0, 8));
|
||||
let mentions = ev.Thread?.PubKeys?.map(a => [a, users[a]])?.map(a => a[1]?.name ?? hexToBech32("npub", a[0]).substring(0, 12))
|
||||
.sort((a, b) => a.startsWith("npub") ? 1 : -1);
|
||||
let pubMentions = mentions.length > maxMentions ? `${mentions?.slice(0, maxMentions).join(", ")} & ${mentions.length - maxMentions} others` : mentions?.join(", ");
|
||||
return (
|
||||
<div className="reply" onClick={(e) => goToEvent(e, replyId)}>
|
||||
➡️ {mentions?.join(", ") ?? replyId?.substring(0, 8)}
|
||||
<div className="reply">
|
||||
➡️ {pubMentions ?? hexToBech32("note", replyId).substring(0, 12)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -84,7 +87,7 @@ export default function Note(props) {
|
||||
<ProfileImage pubkey={ev.RootPubKey} subHeader={replyTag()} />
|
||||
{options.showTime ?
|
||||
<div className="info">
|
||||
{moment(ev.CreatedAt * 1000).fromNow()}
|
||||
<NoteTime from={ev.CreatedAt * 1000} />
|
||||
</div> : null}
|
||||
</div> : null}
|
||||
<div className="body" onClick={(e) => goToEvent(e, ev.Id)}>
|
||||
|
@ -1,12 +1,12 @@
|
||||
import "./NoteReaction.css";
|
||||
import moment from "moment";
|
||||
import EventKind from "../nostr/EventKind";
|
||||
import Note from "./Note";
|
||||
import ProfileImage from "./ProfileImage";
|
||||
import Event from "../nostr/Event";
|
||||
import { eventLink } from "../Util";
|
||||
import { eventLink, hexToBech32 } from "../Util";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useMemo } from "react";
|
||||
import NoteTime from "./NoteTime";
|
||||
|
||||
export default function NoteReaction(props) {
|
||||
const ev = props["data-ev"] || Event.FromObject(props.data);
|
||||
@ -68,12 +68,12 @@ export default function NoteReaction(props) {
|
||||
<div className="header flex">
|
||||
<ProfileImage pubkey={ev.RootPubKey} subHeader={tagLine()} />
|
||||
<div className="info">
|
||||
{moment(ev.CreatedAt * 1000).fromNow()}
|
||||
<NoteTime from={ev.CreatedAt * 1000} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{root ? <Note data={root} options={opt} /> : null}
|
||||
{!root && refEvent ? <p><Link to={eventLink(refEvent)}>#{refEvent.substring(0, 8)}</Link></p> : null}
|
||||
{!root && refEvent ? <p><Link to={eventLink(refEvent)}>#{hexToBech32("note", refEvent).substring(0, 12)}</Link></p> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
40
src/element/NoteTime.js
Normal file
40
src/element/NoteTime.js
Normal file
@ -0,0 +1,40 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
const MinuteInMs = 1_000 * 60;
|
||||
const HourInMs = MinuteInMs * 60;
|
||||
const DayInMs = HourInMs * 24;
|
||||
|
||||
export default function NoteTime(props) {
|
||||
const from = props.from;
|
||||
const [time, setTime] = useState("");
|
||||
|
||||
function calcTime() {
|
||||
let fromDate = new Date(from);
|
||||
let ago = (new Date().getTime()) - from;
|
||||
let absAgo = Math.abs(ago);
|
||||
if (absAgo > DayInMs) {
|
||||
return fromDate.toLocaleDateString(undefined, { year: "2-digit", month: "short", day: "2-digit", weekday: "short" });
|
||||
} else if (absAgo > HourInMs) {
|
||||
return `${fromDate.getHours().toString().padStart(2, '0')}:${fromDate.getMinutes().toString().padStart(2, '0')}`;
|
||||
} else {
|
||||
let mins = parseInt(absAgo / MinuteInMs);
|
||||
return `${mins} mins ago`;
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setTime(calcTime());
|
||||
let t = setInterval(() => {
|
||||
setTime(s => {
|
||||
let newTime = calcTime();
|
||||
if (newTime !== s) {
|
||||
return newTime;
|
||||
}
|
||||
return s;
|
||||
})
|
||||
}, MinuteInMs);
|
||||
return () => clearInterval(t);
|
||||
}, [from]);
|
||||
|
||||
return <>{time}</>
|
||||
}
|
@ -6,7 +6,7 @@
|
||||
.pfp > img {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin-right: 20px;
|
||||
margin-right: 10px;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
@ -6037,11 +6037,6 @@ mkdirp@~0.5.1:
|
||||
dependencies:
|
||||
minimist "^1.2.6"
|
||||
|
||||
moment@^2.29.4:
|
||||
version "2.29.4"
|
||||
resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108"
|
||||
integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==
|
||||
|
||||
ms@2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
|
||||
|
Loading…
x
Reference in New Issue
Block a user