Unread DMS count
This commit is contained in:
@ -3,14 +3,11 @@ import { useEffect, useState } from "react";
|
|||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
import { useInView } from 'react-intersection-observer';
|
import { useInView } from 'react-intersection-observer';
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
import useEventPublisher from "../feed/EventPublisher";
|
import useEventPublisher from "../feed/EventPublisher";
|
||||||
// @ts-ignore
|
|
||||||
import Event from "../nostr/Event";
|
import Event from "../nostr/Event";
|
||||||
// @ts-ignore
|
|
||||||
import NoteTime from "./NoteTime";
|
import NoteTime from "./NoteTime";
|
||||||
// @ts-ignore
|
|
||||||
import Text from "./Text";
|
import Text from "./Text";
|
||||||
|
import { lastReadDm, setLastReadDm } from "../pages/MessagesPage";
|
||||||
|
|
||||||
export type DMProps = {
|
export type DMProps = {
|
||||||
data: any
|
data: any
|
||||||
@ -22,9 +19,13 @@ export default function DM(props: DMProps) {
|
|||||||
const [content, setContent] = useState("Loading...");
|
const [content, setContent] = useState("Loading...");
|
||||||
const [decrypted, setDecrypted] = useState(false);
|
const [decrypted, setDecrypted] = useState(false);
|
||||||
const { ref, inView, entry } = useInView();
|
const { ref, inView, entry } = useInView();
|
||||||
|
const isMe = props.data.pubkey === pubKey;
|
||||||
|
|
||||||
async function decrypt() {
|
async function decrypt() {
|
||||||
let e = new Event(props.data);
|
let e = new Event(props.data);
|
||||||
|
if (!isMe) {
|
||||||
|
setLastReadDm(e.PubKey);
|
||||||
|
}
|
||||||
let decrypted = await publisher.decryptDm(e);
|
let decrypted = await publisher.decryptDm(e);
|
||||||
setContent(decrypted || "<ERROR>");
|
setContent(decrypted || "<ERROR>");
|
||||||
}
|
}
|
||||||
@ -37,7 +38,7 @@ export default function DM(props: DMProps) {
|
|||||||
}, [inView, props.data]);
|
}, [inView, props.data]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`flex dm f-col${props.data.pubkey === pubKey ? " me" : ""}`} ref={ref}>
|
<div className={`flex dm f-col${isMe ? " me" : ""}`} ref={ref}>
|
||||||
<div><NoteTime from={props.data.created_at * 1000} fallback={'Just now'} /></div>
|
<div><NoteTime from={props.data.created_at * 1000} fallback={'Just now'} /></div>
|
||||||
<div className="w-max">
|
<div className="w-max">
|
||||||
<Text content={content} tags={[]} users={new Map()} />
|
<Text content={content} tags={[]} users={new Map()} />
|
||||||
|
@ -11,9 +11,7 @@ import {
|
|||||||
CheckRegisterResponse
|
CheckRegisterResponse
|
||||||
} from "../nip05/ServiceProvider";
|
} from "../nip05/ServiceProvider";
|
||||||
import AsyncButton from "./AsyncButton";
|
import AsyncButton from "./AsyncButton";
|
||||||
// @ts-ignore
|
|
||||||
import LNURLTip from "./LNURLTip";
|
import LNURLTip from "./LNURLTip";
|
||||||
// @ts-ignore
|
|
||||||
import Copy from "./Copy";
|
import Copy from "./Copy";
|
||||||
import useProfile from "../feed/ProfileFeed";
|
import useProfile from "../feed/ProfileFeed";
|
||||||
import useEventPublisher from "../feed/EventPublisher";
|
import useEventPublisher from "../feed/EventPublisher";
|
||||||
|
@ -10,8 +10,9 @@ import ProfileImage from "../element/ProfileImage";
|
|||||||
import { init } from "../state/Login";
|
import { init } from "../state/Login";
|
||||||
import useLoginFeed from "../feed/LoginFeed";
|
import useLoginFeed from "../feed/LoginFeed";
|
||||||
import { RootState } from "../state/Store";
|
import { RootState } from "../state/Store";
|
||||||
import { HexKey, TaggedRawEvent } from "../nostr";
|
import { HexKey, RawEvent, TaggedRawEvent } from "../nostr";
|
||||||
import { RelaySettings } from "../nostr/Connection";
|
import { RelaySettings } from "../nostr/Connection";
|
||||||
|
import { totalUnread } from "./MessagesPage";
|
||||||
|
|
||||||
export default function Layout() {
|
export default function Layout() {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
@ -21,6 +22,7 @@ export default function Layout() {
|
|||||||
const relays = useSelector<RootState, Record<string, RelaySettings>>(s => s.login.relays);
|
const relays = useSelector<RootState, Record<string, RelaySettings>>(s => s.login.relays);
|
||||||
const notifications = useSelector<RootState, TaggedRawEvent[]>(s => s.login.notifications);
|
const notifications = useSelector<RootState, TaggedRawEvent[]>(s => s.login.notifications);
|
||||||
const readNotifications = useSelector<RootState, number>(s => s.login.readNotifications);
|
const readNotifications = useSelector<RootState, number>(s => s.login.readNotifications);
|
||||||
|
const dms = useSelector<RootState, RawEvent[]>(s => s.login.dms);
|
||||||
useLoginFeed();
|
useLoginFeed();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -56,11 +58,15 @@ export default function Layout() {
|
|||||||
|
|
||||||
function accountHeader() {
|
function accountHeader() {
|
||||||
const unreadNotifications = notifications?.filter(a => (a.created_at * 1000) > readNotifications).length;
|
const unreadNotifications = notifications?.filter(a => (a.created_at * 1000) > readNotifications).length;
|
||||||
|
const unreadDms = key ? totalUnread(dms, key) : 0;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="btn btn-rnd mr10" onClick={(e) => navigate("/messages")}>
|
<div className={`btn btn-rnd${unreadDms === 0 ? " mr10" : ""}`} onClick={(e) => navigate("/messages")}>
|
||||||
<FontAwesomeIcon icon={faMessage} size="xl" />
|
<FontAwesomeIcon icon={faMessage} size="xl" />
|
||||||
</div>
|
</div>
|
||||||
|
{unreadDms > 0 && (<span className="unread-count">
|
||||||
|
{unreadDms > 100 ? ">99" : unreadDms}
|
||||||
|
</span>)}
|
||||||
<div className={`btn btn-rnd${unreadNotifications === 0 ? " mr10" : ""}`} onClick={(e) => goToNotifications(e)}>
|
<div className={`btn btn-rnd${unreadNotifications === 0 ? " mr10" : ""}`} onClick={(e) => goToNotifications(e)}>
|
||||||
<FontAwesomeIcon icon={faBell} size="xl" />
|
<FontAwesomeIcon icon={faBell} size="xl" />
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,27 +1,31 @@
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { useSelector } from "react-redux"
|
import { useSelector } from "react-redux"
|
||||||
|
|
||||||
import { RawEvent } from "../nostr";
|
import { HexKey, RawEvent } from "../nostr";
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
import ProfileImage from "../element/ProfileImage";
|
import ProfileImage from "../element/ProfileImage";
|
||||||
// @ts-ignore
|
|
||||||
import { hexToBech32 } from "../Util";
|
import { hexToBech32 } from "../Util";
|
||||||
|
|
||||||
|
type DmChat = {
|
||||||
|
pubkey: HexKey,
|
||||||
|
lastRead: number,
|
||||||
|
unreadMessages: number,
|
||||||
|
newestMessage: number
|
||||||
|
}
|
||||||
|
|
||||||
export default function MessagesPage() {
|
export default function MessagesPage() {
|
||||||
const pubKey = useSelector<any, string>(s => s.login.publicKey);
|
const myPubKey = useSelector<any, string>(s => s.login.publicKey);
|
||||||
const dms = useSelector<any, RawEvent[]>(s => s.login.dms);
|
const dms = useSelector<any, RawEvent[]>(s => s.login.dms);
|
||||||
|
|
||||||
const pubKeys = useMemo(() => {
|
const chats = useMemo(() => {
|
||||||
return Array.from(new Set<string>(dms.map(a => [a.pubkey, ...a.tags.filter(b => b[0] === "p").map(b => b[1])]).flat()));
|
return extractChats(dms, myPubKey);
|
||||||
}, [dms]);
|
}, [dms]);
|
||||||
|
|
||||||
function person(pubkey: string) {
|
function person(chat: DmChat) {
|
||||||
return (
|
return (
|
||||||
<div className="flex mb10" key={pubkey}>
|
<div className="flex mb10" key={chat.pubkey}>
|
||||||
<ProfileImage pubkey={pubkey} className="f-grow" link={`/messages/${hexToBech32("npub", pubkey)}`} />
|
<ProfileImage pubkey={chat.pubkey} className="f-grow" link={`/messages/${hexToBech32("npub", chat.pubkey)}`} />
|
||||||
<span className="pill">
|
<span className="pill">
|
||||||
{dms?.filter(a => a.pubkey === pubkey && a.pubkey !== pubKey).length}
|
{chat.unreadMessages}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@ -30,7 +34,50 @@ export default function MessagesPage() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h3>Messages</h3>
|
<h3>Messages</h3>
|
||||||
{pubKeys.map(person)}
|
{chats.sort((a, b) => b.newestMessage - a.newestMessage).map(person)}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function lastReadDm(pk: HexKey) {
|
||||||
|
let k = `dm:seen:${pk}`;
|
||||||
|
return parseInt(window.localStorage.getItem(k) ?? "0");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setLastReadDm(pk: HexKey) {
|
||||||
|
const now = Math.floor(new Date().getTime() / 1000);
|
||||||
|
let current = lastReadDm(pk);
|
||||||
|
if (current >= now) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let k = `dm:seen:${pk}`;
|
||||||
|
window.localStorage.setItem(k, now.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
export function totalUnread(dms: RawEvent[], myPubKey: HexKey) {
|
||||||
|
return extractChats(dms, myPubKey).reduce((acc, v) => acc += v.unreadMessages, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function unreadDms(dms: RawEvent[], myPubKey: HexKey, pk: HexKey) {
|
||||||
|
let lastRead = lastReadDm(pk);
|
||||||
|
return dms?.filter(a => a.pubkey === pk && a.pubkey !== myPubKey && a.created_at >= lastRead).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function newestMessage(dms: RawEvent[], myPubKey: HexKey, pk: HexKey) {
|
||||||
|
return dms.filter(a => a.pubkey === pk && a.pubkey !== myPubKey).reduce((acc, v) => acc = v.created_at > acc ? v.created_at : acc, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function extractChats(dms: RawEvent[], myPubKey: HexKey) {
|
||||||
|
const keys = dms.map(a => [a.pubkey, ...a.tags.filter(b => b[0] === "p").map(b => b[1])]).flat();
|
||||||
|
const filteredKeys = Array.from(new Set<string>(keys));
|
||||||
|
return filteredKeys.map(a => {
|
||||||
|
return {
|
||||||
|
pubkey: a,
|
||||||
|
lastRead: lastReadDm(a),
|
||||||
|
unreadMessages: unreadDms(dms, myPubKey, a),
|
||||||
|
newestMessage: newestMessage(dms, myPubKey, a)
|
||||||
|
} as DmChat;
|
||||||
|
})
|
||||||
|
}
|
Reference in New Issue
Block a user