Note style changes
This commit is contained in:
parent
fd7e00c8d4
commit
ed4a0678e1
@ -10,7 +10,6 @@
|
|||||||
"@reduxjs/toolkit": "^1.9.1",
|
"@reduxjs/toolkit": "^1.9.1",
|
||||||
"bech32": "^2.0.0",
|
"bech32": "^2.0.0",
|
||||||
"light-bolt11-decoder": "^2.1.0",
|
"light-bolt11-decoder": "^2.1.0",
|
||||||
"moment": "^2.29.4",
|
|
||||||
"qr-code-styling": "^1.6.0-rc.1",
|
"qr-code-styling": "^1.6.0-rc.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^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 Invoice from "./element/Invoice";
|
||||||
import { UrlRegex, FileExtensionRegex, MentionRegex, InvoiceRegex, YoutubeUrlRegex } from "./Const";
|
import { UrlRegex, FileExtensionRegex, MentionRegex, InvoiceRegex, YoutubeUrlRegex } from "./Const";
|
||||||
import { eventLink, profileLink } from "./Util";
|
import { eventLink, hexToBech32, profileLink } from "./Util";
|
||||||
|
|
||||||
function transformHttpLink(a) {
|
function transformHttpLink(a) {
|
||||||
try {
|
try {
|
||||||
const url = new URL(a);
|
const url = new URL(a);
|
||||||
const vParam = url.searchParams.get('v')
|
const youtubeId = YoutubeUrlRegex.test(a) && RegExp.$1;
|
||||||
const youtubeId = YoutubeUrlRegex.test(a) && RegExp.$1
|
const extension = FileExtensionRegex.test(url.pathname.toLowerCase()) && RegExp.$1;
|
||||||
const extension = FileExtensionRegex.test(url.pathname.toLowerCase()) && RegExp.$1
|
|
||||||
if (extension) {
|
if (extension) {
|
||||||
switch (extension) {
|
switch (extension) {
|
||||||
case "gif":
|
case "gif":
|
||||||
@ -28,7 +27,7 @@ function transformHttpLink(a) {
|
|||||||
return <video key={url} src={url} controls />
|
return <video key={url} src={url} controls />
|
||||||
}
|
}
|
||||||
default:
|
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) {
|
} else if (youtubeId) {
|
||||||
return (
|
return (
|
||||||
@ -37,7 +36,7 @@ function transformHttpLink(a) {
|
|||||||
<iframe
|
<iframe
|
||||||
src={`https://www.youtube.com/embed/${youtubeId}`}
|
src={`https://www.youtube.com/embed/${youtubeId}`}
|
||||||
title="YouTube video player"
|
title="YouTube video player"
|
||||||
frameborder="0"
|
frameBorder="0"
|
||||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||||
allowfullscreen=""
|
allowfullscreen=""
|
||||||
/>
|
/>
|
||||||
@ -45,7 +44,7 @@ function transformHttpLink(a) {
|
|||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
} else {
|
} 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) {
|
} catch (e) {
|
||||||
console.warn(`Not a valid url: ${a}`);
|
console.warn(`Not a valid url: ${a}`);
|
||||||
@ -77,12 +76,12 @@ export function extractMentions(fragments, tags, users) {
|
|||||||
if (ref) {
|
if (ref) {
|
||||||
switch (ref.Key) {
|
switch (ref.Key) {
|
||||||
case "p": {
|
case "p": {
|
||||||
let pUser = users[ref.PubKey]?.name ?? ref.PubKey.substring(0, 8);
|
let pUser = users[ref.PubKey]?.name ?? hexToBech32("npub", ref.PubKey).substring(0, 12);
|
||||||
return <Link key={ref.PubKey} to={profileLink(ref.PubKey)} onClick={(ev) => ev.stopPropagation()}>@{pUser}</Link>;
|
return <Link key={ref.PubKey} to={profileLink(ref.PubKey)} onClick={(e) => e.stopPropagation()}>@{pUser}</Link>;
|
||||||
}
|
}
|
||||||
case "e": {
|
case "e": {
|
||||||
let eText = ref.Event.substring(0, 8);
|
let eText = hexToBech32("note", ref.Event).substring(0, 12);
|
||||||
return <Link key={ref.Event} to={eventLink(ref.Event)}>#{eText}</Link>;
|
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
|
* @returns
|
||||||
*/
|
*/
|
||||||
export function eventLink(hex) {
|
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);
|
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
|
* @returns
|
||||||
*/
|
*/
|
||||||
export function profileLink(hex) {
|
export function profileLink(hex) {
|
||||||
let buf = secp.utils.hexToBytes(hex);
|
return `/p/${hexToBech32("npub", hex)}`;
|
||||||
return `/p/${bech32.encode("npub", bech32.toWords(buf))}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import "./Invoice.css";
|
import "./Invoice.css";
|
||||||
import { decode as invoiceDecode } from "light-bolt11-decoder";
|
import { decode as invoiceDecode } from "light-bolt11-decoder";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import moment from "moment";
|
import NoteTime from "./NoteTime";
|
||||||
|
|
||||||
export default function Invoice(props) {
|
export default function Invoice(props) {
|
||||||
const invoice = props.invoice;
|
const invoice = props.invoice;
|
||||||
@ -48,7 +48,7 @@ export default function Invoice(props) {
|
|||||||
<div className="note-invoice flex">
|
<div className="note-invoice flex">
|
||||||
<div className="f-grow flex f-col">
|
<div className="f-grow flex f-col">
|
||||||
{header()}
|
{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>
|
</div>
|
||||||
|
|
||||||
{info?.expired ? <div className="btn">Expired</div> :
|
{info?.expired ? <div className="btn">Expired</div> :
|
||||||
|
@ -13,10 +13,13 @@
|
|||||||
|
|
||||||
.note > .header .reply {
|
.note > .header .reply {
|
||||||
font-size: small;
|
font-size: small;
|
||||||
|
color: #999;
|
||||||
}
|
}
|
||||||
|
|
||||||
.note > .header > .info {
|
.note > .header > .info {
|
||||||
font-size: small;
|
font-size: small;
|
||||||
|
white-space: nowrap;
|
||||||
|
color: #999;
|
||||||
}
|
}
|
||||||
|
|
||||||
.note > .body {
|
.note > .body {
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
import "./Note.css";
|
import "./Note.css";
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
import moment from "moment";
|
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
import Event from "../nostr/Event";
|
import Event from "../nostr/Event";
|
||||||
import ProfileImage from "./ProfileImage";
|
import ProfileImage from "./ProfileImage";
|
||||||
import { extractLinks, extractMentions, extractInvoices } from "../Text";
|
import { extractLinks, extractMentions, extractInvoices } from "../Text";
|
||||||
import { eventLink } from "../Util";
|
import { eventLink, hexToBech32 } from "../Util";
|
||||||
import NoteFooter from "./NoteFooter";
|
import NoteFooter from "./NoteFooter";
|
||||||
|
import NoteTime from "./NoteTime";
|
||||||
|
|
||||||
export default function Note(props) {
|
export default function Note(props) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -57,11 +57,14 @@ export default function Note(props) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const maxMentions = 2;
|
||||||
let replyId = ev.Thread?.ReplyTo?.Event;
|
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 (
|
return (
|
||||||
<div className="reply" onClick={(e) => goToEvent(e, replyId)}>
|
<div className="reply">
|
||||||
➡️ {mentions?.join(", ") ?? replyId?.substring(0, 8)}
|
➡️ {pubMentions ?? hexToBech32("note", replyId).substring(0, 12)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -84,7 +87,7 @@ export default function Note(props) {
|
|||||||
<ProfileImage pubkey={ev.RootPubKey} subHeader={replyTag()} />
|
<ProfileImage pubkey={ev.RootPubKey} subHeader={replyTag()} />
|
||||||
{options.showTime ?
|
{options.showTime ?
|
||||||
<div className="info">
|
<div className="info">
|
||||||
{moment(ev.CreatedAt * 1000).fromNow()}
|
<NoteTime from={ev.CreatedAt * 1000} />
|
||||||
</div> : null}
|
</div> : null}
|
||||||
</div> : null}
|
</div> : null}
|
||||||
<div className="body" onClick={(e) => goToEvent(e, ev.Id)}>
|
<div className="body" onClick={(e) => goToEvent(e, ev.Id)}>
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import "./NoteReaction.css";
|
import "./NoteReaction.css";
|
||||||
import moment from "moment";
|
|
||||||
import EventKind from "../nostr/EventKind";
|
import EventKind from "../nostr/EventKind";
|
||||||
import Note from "./Note";
|
import Note from "./Note";
|
||||||
import ProfileImage from "./ProfileImage";
|
import ProfileImage from "./ProfileImage";
|
||||||
import Event from "../nostr/Event";
|
import Event from "../nostr/Event";
|
||||||
import { eventLink } from "../Util";
|
import { eventLink, hexToBech32 } from "../Util";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
|
import NoteTime from "./NoteTime";
|
||||||
|
|
||||||
export default function NoteReaction(props) {
|
export default function NoteReaction(props) {
|
||||||
const ev = props["data-ev"] || Event.FromObject(props.data);
|
const ev = props["data-ev"] || Event.FromObject(props.data);
|
||||||
@ -68,12 +68,12 @@ export default function NoteReaction(props) {
|
|||||||
<div className="header flex">
|
<div className="header flex">
|
||||||
<ProfileImage pubkey={ev.RootPubKey} subHeader={tagLine()} />
|
<ProfileImage pubkey={ev.RootPubKey} subHeader={tagLine()} />
|
||||||
<div className="info">
|
<div className="info">
|
||||||
{moment(ev.CreatedAt * 1000).fromNow()}
|
<NoteTime from={ev.CreatedAt * 1000} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{root ? <Note data={root} options={opt} /> : null}
|
{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>
|
</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 {
|
.pfp > img {
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
margin-right: 20px;
|
margin-right: 10px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
@ -6037,11 +6037,6 @@ mkdirp@~0.5.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
minimist "^1.2.6"
|
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:
|
ms@2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
|
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user