feat: upgrade dm styles

This commit is contained in:
Kieran 2023-05-11 15:25:01 +01:00
parent fdcf77ad55
commit 663c2ea433
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
19 changed files with 388 additions and 230 deletions

View File

@ -151,6 +151,8 @@
<symbol id="edit" viewBox="0 0 23 23" fill="none">
<path d="M10 3.99998H5.8C4.11984 3.99998 3.27976 3.99998 2.63803 4.32696C2.07354 4.61458 1.6146 5.07353 1.32698 5.63801C1 6.27975 1 7.11983 1 8.79998V17.2C1 18.8801 1 19.7202 1.32698 20.362C1.6146 20.9264 2.07354 21.3854 2.63803 21.673C3.27976 22 4.11984 22 5.8 22H14.2C15.8802 22 16.7202 22 17.362 21.673C17.9265 21.3854 18.3854 20.9264 18.673 20.362C19 19.7202 19 18.8801 19 17.2V13M6.99997 16H8.67452C9.1637 16 9.40829 16 9.63846 15.9447C9.84254 15.8957 10.0376 15.8149 10.2166 15.7053C10.4184 15.5816 10.5914 15.4086 10.9373 15.0627L20.5 5.49998C21.3284 4.67156 21.3284 3.32841 20.5 2.49998C19.6716 1.67156 18.3284 1.67155 17.5 2.49998L7.93723 12.0627C7.59133 12.4086 7.41838 12.5816 7.29469 12.7834C7.18504 12.9624 7.10423 13.1574 7.05523 13.3615C6.99997 13.5917 6.99997 13.8363 6.99997 14.3255V16Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</symbol>
<symbol id="arrow-right" viewBox="0 0 14 14" fill="none">
<path d="M1.16663 6.99935H12.8333M12.8333 6.99935L6.99996 1.16602M12.8333 6.99935L6.99996 12.8327" stroke="currentColor" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/>
</symbol>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 39 KiB

View File

@ -10,6 +10,7 @@
http-equiv="Content-Security-Policy"
content="default-src 'self'; manifest-src *; child-src 'none'; worker-src 'self'; frame-src youtube.com www.youtube.com https://platform.twitter.com https://embed.tidal.com https://w.soundcloud.com https://www.mixcloud.com https://open.spotify.com https://player.twitch.tv https://embed.music.apple.com https://nostrnests.com https://embed.wavlake.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; connect-src *; img-src * data:; font-src https://fonts.gstatic.com; media-src *; script-src 'self' 'wasm-unsafe-eval' https://static.cloudflareinsights.com https://platform.twitter.com https://embed.tidal.com;" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://imgproxy.snort.social" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/nostrich_512.png" />

View File

@ -1,23 +1,37 @@
.dm {
padding: 8px;
background-color: var(--gray);
margin-bottom: 5px;
border-radius: 5px;
width: fit-content;
margin-top: 16px;
min-width: 100px;
max-width: 90%;
overflow: hidden;
min-height: 40px;
white-space: pre-wrap;
color: var(--font-color);
}
.dm > div:first-child {
.dm a {
color: var(--font-color) !important;
}
.dm > div:last-child {
color: var(--gray-light);
font-size: small;
margin-bottom: 3px;
margin-top: 3px;
}
.dm.other > div:first-child {
padding: 12px 16px;
background: var(--gray-secondary);
border-radius: 16px 16px 16px 0px;
}
.dm.me {
align-self: flex-end;
background-color: var(--gray-secondary);
}
.dm.me > div:first-child {
padding: 12px 16px;
background: var(--dm-gradient);
border-radius: 16px 16px 0px 16px;
}
.dm.me > div:last-child {
text-align: end;
}

View File

@ -45,13 +45,13 @@ export default function DM(props: DMProps) {
}, [inView, props.data]);
return (
<div className={`flex dm f-col${isMe ? " me" : ""}`} ref={ref}>
<div className={isMe ? "dm me" : "dm other"} ref={ref}>
<div>
<Text content={content} tags={[]} creator={otherPubkey} />
</div>
<div>
<NoteTime from={props.data.created_at * 1000} fallback={formatMessage(messages.JustNow)} />
</div>
<div className="w-max">
<Text content={content} tags={[]} creator={otherPubkey} />
</div>
</div>
);
}

View File

@ -0,0 +1,19 @@
.dm-window {
display: flex;
flex-direction: column;
height: 100%;
}
.dm-window > div:nth-child(2) {
overflow-y: auto;
padding: 0 10px 10px 10px;
flex-grow: 1;
}
.dm-window > div:nth-child(3) {
display: flex;
align-items: center;
background-color: var(--bg-color);
gap: 10px;
padding: 5px 10px;
}

View File

@ -0,0 +1,84 @@
import "./DmWindow.css";
import { useEffect, useMemo, useRef } from "react";
import { TaggedRawEvent } from "@snort/nostr";
import ProfileImage from "Element/ProfileImage";
import DM from "Element/DM";
import { dmsForLogin, dmsInChat, isToSelf } from "Pages/MessagesPage";
import NoteToSelf from "Element/NoteToSelf";
import { useDmCache } from "Hooks/useDmsCache";
import useLogin from "Hooks/useLogin";
import WriteDm from "Element/WriteDm";
import { unwrap } from "Util";
export default function DmWindow({ id }: { id: string }) {
const pubKey = useLogin().publicKey;
const dmListRef = useRef<HTMLDivElement>(null);
function resize(chatList: HTMLDivElement) {
const scrollWrap = unwrap(chatList.parentElement);
const h = scrollWrap.scrollHeight;
const s = scrollWrap.clientHeight + scrollWrap.scrollTop;
const pos = Math.abs(h - s);
const atBottom = pos === 0;
//console.debug("Resize", h, s, pos, atBottom);
if (atBottom) {
scrollWrap.scrollTo(0, scrollWrap.scrollHeight);
}
}
useEffect(() => {
if (dmListRef.current) {
const scrollWrap = dmListRef.current;
const chatList = unwrap(scrollWrap.parentElement);
chatList.onscroll = () => {
resize(dmListRef.current as HTMLDivElement);
};
new ResizeObserver(() => resize(dmListRef.current as HTMLDivElement)).observe(scrollWrap);
return () => {
chatList.onscroll = null;
new ResizeObserver(() => resize(dmListRef.current as HTMLDivElement)).unobserve(scrollWrap);
};
}
}, [dmListRef]);
return (
<div className="dm-window">
<div>
{(id === pubKey && <NoteToSelf className="f-grow mb-10" pubkey={id} />) || (
<ProfileImage pubkey={id} className="f-grow mb10" />
)}
</div>
<div>
<div className="flex f-col" ref={dmListRef}>
<DmChatSelected chatPubKey={id} />
</div>
</div>
<div>
<WriteDm chatPubKey={id} />
</div>
</div>
);
}
function DmChatSelected({ chatPubKey }: { chatPubKey: string }) {
const dms = useDmCache();
const { publicKey: myPubKey } = useLogin();
const sortedDms = useMemo(() => {
if (myPubKey) {
const myDms = dmsForLogin(dms, myPubKey);
// filter dms in this chat, or dms to self
const thisDms = myPubKey === chatPubKey ? myDms.filter(d => isToSelf(d, myPubKey)) : myDms;
return [...dmsInChat(thisDms, chatPubKey)].sort((a, b) => a.created_at - b.created_at);
}
return [];
}, [dms, myPubKey, chatPubKey]);
return (
<>
{sortedDms.map(a => (
<DM data={a as TaggedRawEvent} key={a.id} />
))}
</>
);
}

View File

@ -68,7 +68,7 @@ const Nip05 = ({ nip05, pubkey, verifyNip = true }: Nip05Params) => {
const { isVerified, couldNotVerify } = useIsVerified(pubkey, nip05, !verifyNip);
return (
<div className={`flex nip05${couldNotVerify ? " failed" : ""}`} onClick={ev => ev.stopPropagation()}>
<div className={`flex nip05${couldNotVerify ? " failed" : ""}`}>
{!isDefaultUser && isVerified && <span className="nick">{`${name}@`}</span>}
{isVerified && (
<>

View File

@ -28,7 +28,6 @@ export default function NoteTime(props: NoteTimeProps) {
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")}`;

View File

@ -1,6 +1,6 @@
import "./ProfileImage.css";
import { useMemo } from "react";
import React, { useMemo } from "react";
import { HexKey, NostrPrefix } from "@snort/nostr";
import { useUserProfile } from "Hooks/useUserProfile";
@ -38,8 +38,18 @@ export default function ProfileImage({
return overrideUsername ?? getDisplayName(user, pubkey);
}, [user, pubkey, overrideUsername]);
function handleClick(e: React.MouseEvent) {
if (link === "") {
e.preventDefault();
}
}
return (
<Link className={`pfp${className ? ` ${className}` : ""}`} to={link === undefined ? profileLink(pubkey) : link}>
<Link
className={`pfp${className ? ` ${className}` : ""}`}
to={link === undefined ? profileLink(pubkey) : link}
onClick={handleClick}
replace={true}>
<div className="avatar-wrapper">
<Avatar user={user} />
</div>

View File

@ -1,13 +1,10 @@
import "./Text.css";
import { useMemo } from "react";
import { Link, useLocation } from "react-router-dom";
import ReactMarkdown from "react-markdown";
import { visit, SKIP } from "unist-util-visit";
import * as unist from "unist";
import { HexKey, NostrPrefix } from "@snort/nostr";
import { MentionRegex, InvoiceRegex, HashtagRegex, CashuRegex } from "Const";
import { eventLink, hexToBech32, splitByUrl, unwrap, validateNostrLink } from "Util";
import { eventLink, hexToBech32, splitByUrl, validateNostrLink } from "Util";
import Invoice from "Element/Invoice";
import Hashtag from "Element/Hashtag";
import Mention from "Element/Mention";
@ -159,19 +156,6 @@ export default function Text({ content, tags, creator, disableMedia, depth }: Te
.flat();
}
function transformLi(frag: TextFragment) {
const fragments = transformText(frag);
return <li>{fragments}</li>;
}
function transformParagraph(frag: TextFragment) {
const fragments = transformText(frag);
if (fragments.every(f => typeof f === "string")) {
return <p>{fragments}</p>;
}
return <>{fragments}</>;
}
function transformText(frag: TextFragment) {
let fragments = extractMentions(frag);
fragments = extractLinks(fragments);
@ -181,41 +165,8 @@ export default function Text({ content, tags, creator, disableMedia, depth }: Te
return fragments;
}
const components = {
p: (x: { children?: React.ReactNode[] }) => transformParagraph({ body: x.children ?? [], tags }),
a: (x: { href?: string }) => <HyperText link={x.href ?? ""} creator={creator} />,
li: (x: { children?: Fragment[] }) => transformLi({ body: x.children ?? [], tags }),
};
interface Node extends unist.Node<unist.Data> {
value: string;
}
const disableMarkdownLinks = () => (tree: Node) => {
visit(tree, (node, index, parent) => {
if (
parent &&
typeof index === "number" &&
(node.type === "link" ||
node.type === "linkReference" ||
node.type === "image" ||
node.type === "imageReference" ||
node.type === "definition")
) {
node.type = "text";
const position = unwrap(node.position);
node.value = content.slice(position.start.offset, position.end.offset).replace(/\)$/, " )");
return SKIP;
}
});
};
const element = useMemo(() => {
return (
<ReactMarkdown className="text" components={components} remarkPlugins={[disableMarkdownLinks]}>
{content}
</ReactMarkdown>
);
return <div className="text">{transformText({ body: [content], tags })}</div>;
}, [content]);
return <div dir="auto">{element}</div>;

View File

@ -3,7 +3,7 @@
font-size: var(--font-size-small);
display: inline-block;
background-color: var(--gray-secondary);
padding: 2px 10px;
padding: 2px 8px;
border-radius: 10px;
user-select: none;
margin: 2px 5px;
@ -17,3 +17,7 @@
.pill:hover {
cursor: pointer;
}
.light .pill.unread {
color: white;
}

View File

@ -0,0 +1,102 @@
import { encodeTLV, NostrPrefix, RawEvent } from "@snort/nostr";
import useEventPublisher from "Feed/EventPublisher";
import Icon from "Icons/Icon";
import Spinner from "Icons/Spinner";
import { useState } from "react";
import useFileUpload from "Upload";
import { openFile } from "Util";
import Textarea from "./Textarea";
export default function WriteDm({ chatPubKey }: { chatPubKey: string }) {
const [msg, setMsg] = useState("");
const [sending, setSending] = useState(false);
const [uploading, setUploading] = useState(false);
const [otherEvents, setOtherEvents] = useState<Array<RawEvent>>([]);
const [error, setError] = useState("");
const publisher = useEventPublisher();
const uploader = useFileUpload();
async function attachFile() {
try {
const file = await openFile();
if (file) {
uploadFile(file);
}
} catch (e) {
if (e instanceof Error) {
setError(e.message);
}
}
}
async function uploadFile(file: File | Blob) {
setUploading(true);
try {
if (file) {
const rx = await uploader.upload(file, file.name);
if (rx.header) {
const link = `nostr:${encodeTLV(rx.header.id, NostrPrefix.Event, undefined, rx.header.kind)}`;
setMsg(`${msg ? `${msg}\n` : ""}${link}`);
setOtherEvents([...otherEvents, rx.header]);
} else if (rx.url) {
setMsg(`${msg ? `${msg}\n` : ""}${rx.url}`);
} else if (rx?.error) {
setError(rx.error);
}
}
} catch (e) {
if (e instanceof Error) {
setError(e.message);
}
} finally {
setUploading(false);
}
}
async function sendDm() {
if (msg && publisher) {
setSending(true);
const ev = await publisher.sendDm(msg, chatPubKey);
publisher.broadcast(ev);
setMsg("");
setSending(false);
}
}
function onChange(e: React.ChangeEvent<HTMLTextAreaElement>) {
if (!sending) {
setMsg(e.target.value);
}
}
async function onEnter(e: React.KeyboardEvent<HTMLTextAreaElement>) {
const isEnter = e.code === "Enter";
if (isEnter && !e.shiftKey) {
await sendDm();
}
}
return (
<>
<button className="btn-rnd" onClick={() => attachFile()}>
{uploading ? <Spinner width={20} /> : <Icon name="attachment" size={20} />}
</button>
<div className="w-max">
<Textarea
autoFocus={true}
className=""
value={msg}
onChange={e => onChange(e)}
onKeyDown={e => onEnter(e)}
onFocus={() => {
// ignored
}}
/>
{error && <b className="error">{error}</b>}
</div>
<button className="btn-rnd" onClick={() => sendDm()}>
{sending ? <Spinner width={20} /> : <Icon name="arrow-right" size={20} />}
</button>
</>
);
}

View File

@ -1,7 +1,7 @@
import { useRef, useState, useEffect } from "react";
export default function usePageWidth() {
const ref = useRef<HTMLDivElement | null>(document.querySelector(".page"));
const ref = useRef<HTMLDivElement | null>(document.querySelector("#root"));
const [width, setWidth] = useState(0);
useEffect(() => {

View File

@ -1,36 +1,3 @@
.dm-list {
overflow-y: auto;
overflow-x: hidden;
--header-height: 72px;
--profile-container-height: 58px;
--write-dm-input-height: 86px;
height: calc(100vh - var(--header-height) - var(--profile-container-height) - var(--write-dm-input-height));
}
.dm-list > div {
display: flex;
flex-direction: column;
margin-bottom: 10px;
scroll-padding-bottom: 40px;
}
.write-dm {
position: fixed;
bottom: 0;
background-color: var(--gray);
width: inherit;
border-radius: 5px 5px 0 0;
}
.write-dm .inner {
display: flex;
align-items: center;
padding: 10px 5px;
}
.write-dm textarea {
resize: none;
}
.write-dm-spacer {
margin-bottom: 80px;
.chat-page {
height: calc(100vh - 57px);
}

View File

@ -1,86 +1,15 @@
import "./ChatPage.css";
import { KeyboardEvent, useEffect, useMemo, useRef, useState } from "react";
import DmWindow from "Element/DmWindow";
import { useParams } from "react-router-dom";
import { FormattedMessage } from "react-intl";
import { TaggedRawEvent } from "@snort/nostr";
import ProfileImage from "Element/ProfileImage";
import { bech32ToHex } from "Util";
import useEventPublisher from "Feed/EventPublisher";
import DM from "Element/DM";
import { dmsForLogin, dmsInChat, isToSelf } from "Pages/MessagesPage";
import NoteToSelf from "Element/NoteToSelf";
import { useDmCache } from "Hooks/useDmsCache";
import useLogin from "Hooks/useLogin";
type RouterParams = {
id: string;
};
import "./ChatPage.css";
export default function ChatPage() {
const params = useParams<RouterParams>();
const publisher = useEventPublisher();
const id = bech32ToHex(params.id ?? "");
const pubKey = useLogin().publicKey;
const [content, setContent] = useState<string>();
const dmListRef = useRef<HTMLDivElement>(null);
const dms = useDmCache();
const sortedDms = useMemo(() => {
if (pubKey) {
const myDms = dmsForLogin(dms, pubKey);
// filter dms in this chat, or dms to self
const thisDms = id === pubKey ? myDms.filter(d => isToSelf(d, pubKey)) : myDms;
return [...dmsInChat(thisDms, id)].sort((a, b) => a.created_at - b.created_at);
}
return [];
}, [dms, pubKey]);
useEffect(() => {
if (dmListRef.current) {
dmListRef.current.scroll(0, dmListRef.current.scrollHeight);
}
}, [dmListRef.current?.scrollHeight]);
async function sendDm() {
if (content && publisher) {
const ev = await publisher.sendDm(content, id);
publisher.broadcast(ev);
setContent("");
}
}
async function onEnter(e: KeyboardEvent) {
const isEnter = e.code === "Enter";
if (isEnter && !e.shiftKey) {
await sendDm();
}
}
const { id } = useParams();
return (
<>
{(id === pubKey && <NoteToSelf className="f-grow mb-10" pubkey={id} />) || (
<ProfileImage pubkey={id} className="f-grow mb10" />
)}
<div className="dm-list" ref={dmListRef}>
<div>
{sortedDms.map(a => (
<DM data={a as TaggedRawEvent} key={a.id} />
))}
</div>
</div>
<div className="write-dm">
<div className="inner">
<textarea
className="f-grow mr10"
value={content}
onChange={e => setContent(e.target.value)}
onKeyDown={e => onEnter(e)}></textarea>
<button type="button" onClick={() => sendDm()}>
<FormattedMessage defaultMessage="Send" description="Send DM button" />
</button>
</div>
</div>
</>
<div className="chat-page">
<DmWindow id={bech32ToHex(id ?? "")} />
</div>
);
}

View File

@ -57,7 +57,8 @@ export default function Layout() {
}, [location]);
useEffect(() => {
if (location.pathname.startsWith("/login")) {
const widePage = ["/login", "/messages"];
if (widePage.some(a => location.pathname.startsWith(a))) {
setPageClass("");
} else {
setPageClass("page");
@ -162,7 +163,7 @@ export default function Layout() {
{!shouldHideNoteCreator && (
<>
<button className="note-create-button" type="button" onClick={handleNoteCreatorButtonClick}>
<button className="note-create-button" onClick={handleNoteCreatorButtonClick}>
<Icon name="plus" size={16} />
</button>
<NoteCreator />

View File

@ -0,0 +1,39 @@
.dm-page {
display: grid;
grid-template-columns: 350px auto;
height: calc(100vh - 57px);
/* 100vh - header - padding */
overflow: hidden;
}
/* These should match what is in code too */
@media (max-width: 768px) {
.dm-page {
grid-template-columns: 100vw;
}
.dm-page > div:nth-child(1) {
margin: 0 !important;
}
}
@media (min-width: 1500px) {
.dm-page {
grid-template-columns: 350px auto 350px;
}
}
/* User list */
.dm-page > div:nth-child(1) {
overflow-y: auto;
margin: 0 10px;
padding: 0 10px 0 0;
}
/* Chat window */
.dm-page > div:nth-child(2) {
height: calc(100vh - 57px);
}
/* Profile pannel */
.dm-page > div:nth-child(3) {
margin: 0 10px;
}

View File

@ -1,6 +1,7 @@
import { useMemo } from "react";
import { FormattedMessage } from "react-intl";
import { HexKey, RawEvent } from "@snort/nostr";
import React, { useMemo, useState } from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { useNavigate } from "react-router-dom";
import { HexKey, RawEvent, NostrPrefix } from "@snort/nostr";
import UnreadCount from "Element/UnreadCount";
import ProfileImage from "Element/ProfileImage";
@ -9,9 +10,16 @@ import NoteToSelf from "Element/NoteToSelf";
import useModeration from "Hooks/useModeration";
import { useDmCache } from "Hooks/useDmsCache";
import useLogin from "Hooks/useLogin";
import usePageWidth from "Hooks/usePageWidth";
import NoteTime from "Element/NoteTime";
import DmWindow from "Element/DmWindow";
import "./MessagesPage.css";
import messages from "./messages";
const TwoCol = 768;
const ThreeCol = 1500;
type DmChat = {
pubkey: HexKey;
unreadMessages: number;
@ -21,7 +29,11 @@ type DmChat = {
export default function MessagesPage() {
const login = useLogin();
const { isMuted } = useModeration();
const { formatMessage } = useIntl();
const navigate = useNavigate();
const dms = useDmCache();
const [chat, setChat] = useState<string>();
const pageWidth = usePageWidth();
const chats = useMemo(() => {
if (login.publicKey) {
@ -35,25 +47,39 @@ export default function MessagesPage() {
const unreadCount = useMemo(() => chats.reduce((p, c) => p + c.unreadMessages, 0), [chats]);
function openChat(e: React.MouseEvent<HTMLDivElement>, pubkey: string) {
e.stopPropagation();
e.preventDefault();
if (pageWidth < TwoCol) {
navigate(`/messages/${hexToBech32(NostrPrefix.PublicKey, pubkey)}`);
} else {
setChat(pubkey);
}
}
function noteToSelf(chat: DmChat) {
return (
<div className="flex mb10" key={chat.pubkey}>
<NoteToSelf
clickable={true}
className="f-grow"
link={`/messages/${hexToBech32("npub", chat.pubkey)}`}
pubkey={chat.pubkey}
/>
<div className="flex mb10" key={chat.pubkey} onClick={e => openChat(e, chat.pubkey)}>
<NoteToSelf clickable={true} className="f-grow" link="" pubkey={chat.pubkey} />
</div>
);
}
function person(chat: DmChat) {
if (!login.publicKey) return null;
if (chat.pubkey === login.publicKey) return noteToSelf(chat);
return (
<div className="flex mb10" key={chat.pubkey}>
<ProfileImage pubkey={chat.pubkey} className="f-grow" link={`/messages/${hexToBech32("npub", chat.pubkey)}`} />
<UnreadCount unread={chat.unreadMessages} />
<div className="flex mb10" key={chat.pubkey} onClick={e => openChat(e, chat.pubkey)}>
<ProfileImage pubkey={chat.pubkey} className="f-grow" link="" />
<div className="nowrap">
<small>
<NoteTime
from={newestMessage(dms, login.publicKey, chat.pubkey) * 1000}
fallback={formatMessage({ defaultMessage: "Just now" })}
/>
</small>
{chat.unreadMessages > 0 && <UnreadCount unread={chat.unreadMessages} />}
</div>
</div>
);
}
@ -65,24 +91,28 @@ export default function MessagesPage() {
}
return (
<div className="main-content">
<div className="flex">
<h3 className="f-grow">
<FormattedMessage {...messages.Messages} />
</h3>
<button disabled={unreadCount <= 0} type="button" onClick={() => markAllRead()}>
<FormattedMessage {...messages.MarkAllRead} />
</button>
<div className="dm-page">
<div>
<div className="flex">
<h3 className="f-grow">
<FormattedMessage {...messages.Messages} />
</h3>
<button disabled={unreadCount <= 0} type="button" onClick={() => markAllRead()}>
<FormattedMessage {...messages.MarkAllRead} />
</button>
</div>
{chats
.sort((a, b) => {
return a.pubkey === login.publicKey
? -1
: b.pubkey === login.publicKey
? 1
: b.newestMessage - a.newestMessage;
})
.map(person)}
</div>
{chats
.sort((a, b) => {
return a.pubkey === login.publicKey
? -1
: b.pubkey === login.publicKey
? 1
: b.newestMessage - a.newestMessage;
})
.map(person)}
{pageWidth >= TwoCol && chat && <DmWindow id={chat} />}
{pageWidth >= ThreeCol && <div></div>}
</div>
);
}
@ -126,7 +156,7 @@ function unreadDms(dms: RawEvent[], myPubKey: HexKey, pk: HexKey) {
return dmsInChat(dms, pk).filter(a => a.created_at >= lastRead && a.pubkey !== myPubKey).length;
}
function newestMessage(dms: RawEvent[], myPubKey: HexKey, pk: HexKey) {
function newestMessage(dms: readonly RawEvent[], myPubKey: HexKey, pk: HexKey) {
if (pk === myPubKey) {
return dmsInChat(
dms.filter(d => isToSelf(d, myPubKey)),

View File

@ -23,7 +23,8 @@
--gray-dark: #2b2b2b;
--gray-superdark: #1a1a1a;
--gray-gradient: linear-gradient(to bottom right, var(--gray-superlight), var(--gray), var(--gray-light));
--snort-gradient: linear-gradient(90deg, #a178ff 0%, #ff6baf 108.33%);
--snort-gradient: linear-gradient(90deg, #a178ff 0%, #ff6baf 100%);
--dm-gradient: linear-gradient(90deg, #5722d2 0%, #db1771 100%);
--invoice-gradient: linear-gradient(
45deg,
var(--note-bg) 50%,
@ -65,8 +66,9 @@ html.light {
--gray-dark: #2b2b2b;
--gray-superdark: #eee;
--invoice-gradient: linear-gradient(45deg, var(--note-bg) 50%, rgb(247, 183, 51, 0.2), rgb(252, 74, 26, 0.2));
--paid-invoice-gradient: linear-gradient(45deg, var(--note-bg) 50%, rgb(247, 183, 51, 0.6), rgb(252, 74, 26, 0.6));
--dm-gradient: var(--gray);
--invoice-gradient: linear-gradient(45deg, var(--note-bg) 50%, #f7b73333, #fc4a1a33);
--paid-invoice-gradient: linear-gradient(45deg, var(--note-bg) 50%, #f7b73399, #fc4a1a99);
}
body {
@ -89,6 +91,9 @@ code {
margin-right: auto;
}
body #root > div:not(.page) header {
padding: 2px 10px;
}
@media (min-width: 720px) {
.page {
width: 586px;
@ -258,22 +263,13 @@ button.icon:hover {
}
.btn-rnd {
border: none;
border-radius: 100%;
width: 21px;
height: 21px;
aspect-ratio: 1;
display: flex;
align-items: center;
justify-content: center;
}
@media (min-width: 520px) {
.btn-rnd {
width: 32px;
height: 32px;
}
}
textarea {
font: inherit;
}
@ -358,6 +354,11 @@ input:disabled {
align-items: flex-start !important;
}
.f-col-end {
flex-direction: column;
align-items: flex-end !important;
}
.f-end {
justify-content: flex-end;
}
@ -615,3 +616,8 @@ button.tall {
opacity: 1;
}
}
.rta__textarea {
/* Fix width calculation to account for 12px padding on input */
width: calc(100% - 24px) !important;
}