DMs
This commit is contained in:
parent
eb94a239e4
commit
593c8a4fa9
@ -7,6 +7,7 @@
|
||||
"@fortawesome/free-solid-svg-icons": "^6.2.1",
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"@noble/secp256k1": "^1.7.0",
|
||||
"@protobufjs/base64": "^1.1.2",
|
||||
"@reduxjs/toolkit": "^1.9.1",
|
||||
"@types/jest": "^29.2.5",
|
||||
"@types/node": "^18.11.18",
|
||||
|
17
src/Text.js
17
src/Text.js
@ -6,6 +6,7 @@ import { UrlRegex, FileExtensionRegex, MentionRegex, InvoiceRegex, YoutubeUrlReg
|
||||
import { eventLink, hexToBech32, profileLink } from "./Util";
|
||||
import LazyImage from "./element/LazyImage";
|
||||
import Hashtag from "./element/Hashtag";
|
||||
import { useMemo } from "react";
|
||||
|
||||
function transformHttpLink(a) {
|
||||
try {
|
||||
@ -135,3 +136,19 @@ export function extractHashtags(fragments) {
|
||||
return f;
|
||||
}).flat();
|
||||
}
|
||||
|
||||
export default function Text({ content, transforms }) {
|
||||
const transformed = useMemo(() => {
|
||||
let fragments = [content];
|
||||
transforms?.forEach(a => {
|
||||
fragments = a(fragments);
|
||||
});
|
||||
fragments = extractLinks(fragments);
|
||||
fragments = extractInvoices(fragments);
|
||||
fragments = extractHashtags(fragments);
|
||||
|
||||
return fragments;
|
||||
}, [content]);
|
||||
|
||||
return transformed;
|
||||
}
|
26
src/element/DM.css
Normal file
26
src/element/DM.css
Normal file
@ -0,0 +1,26 @@
|
||||
.dm {
|
||||
padding: 8px;
|
||||
background-color: var(--gray);
|
||||
margin-bottom: 5px;
|
||||
border-radius: 5px;
|
||||
width: fit-content;
|
||||
min-width: 100px;
|
||||
max-width: 90%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dm > div:first-child {
|
||||
color: var(--gray-light);
|
||||
font-size: small;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.dm.me {
|
||||
align-self: flex-end;
|
||||
background-color: var(--gray-secondary);
|
||||
}
|
||||
|
||||
.dm img, .dm video, .dm iframe {
|
||||
max-width: 100%;
|
||||
max-height: 500px;
|
||||
}
|
41
src/element/DM.tsx
Normal file
41
src/element/DM.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
import "./DM.css";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
// @ts-ignore
|
||||
import useEventPublisher from "../feed/EventPublisher";
|
||||
// @ts-ignore
|
||||
import Event from "../nostr/Event";
|
||||
// @ts-ignore
|
||||
import NoteTime from "./NoteTime";
|
||||
// @ts-ignore
|
||||
import Text from "../Text";
|
||||
|
||||
export type DMProps = {
|
||||
data: any
|
||||
}
|
||||
|
||||
export default function DM(props: DMProps) {
|
||||
const pubKey = useSelector<any>(s => s.login.publicKey);
|
||||
const publisher = useEventPublisher();
|
||||
const [content, setContent] = useState("Loading...");
|
||||
|
||||
async function decrypt() {
|
||||
let e = Event.FromObject(props.data);
|
||||
let decrypted = await publisher.decryptDm(e);
|
||||
setContent(decrypted);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
decrypt().catch(console.error);
|
||||
}, [props.data]);
|
||||
|
||||
return (
|
||||
<div className={`flex dm f-col${props.data.pubkey === pubKey ? " me" : ""}`}>
|
||||
<div><NoteTime from={props.data.created_at * 1000} /></div>
|
||||
<div className="w-max">
|
||||
<Text content={content} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -5,7 +5,7 @@ import { useNavigate } from "react-router-dom";
|
||||
|
||||
import Event from "../nostr/Event";
|
||||
import ProfileImage from "./ProfileImage";
|
||||
import { extractLinks, extractMentions, extractInvoices, extractHashtags } from "../Text";
|
||||
import Text, { extractMentions } from "../Text";
|
||||
import { eventLink, hexToBech32 } from "../Util";
|
||||
import NoteFooter from "./NoteFooter";
|
||||
import NoteTime from "./NoteTime";
|
||||
@ -28,19 +28,13 @@ export default function Note(props) {
|
||||
|
||||
const transformBody = useCallback(() => {
|
||||
let body = ev?.Content ?? "";
|
||||
|
||||
let fragments = extractLinks([body]);
|
||||
fragments = extractMentions(fragments, ev.Tags, users);
|
||||
fragments = extractInvoices(fragments);
|
||||
fragments = extractHashtags(fragments);
|
||||
if (deletion?.length > 0) {
|
||||
return (
|
||||
<>
|
||||
<b className="error">Deleted</b>
|
||||
</>
|
||||
);
|
||||
return (<b className="error">Deleted</b>);
|
||||
}
|
||||
return fragments;
|
||||
const mentions = (fragments) => {
|
||||
return extractMentions(fragments, ev.Tags, users);
|
||||
}
|
||||
return <Text content={body} transforms={[mentions]} />;
|
||||
}, [data, dataEvent, reactions, deletion]);
|
||||
|
||||
function goToEvent(e, id) {
|
||||
|
@ -4,8 +4,7 @@ const MinuteInMs = 1_000 * 60;
|
||||
const HourInMs = MinuteInMs * 60;
|
||||
const DayInMs = HourInMs * 24;
|
||||
|
||||
export default function NoteTime(props) {
|
||||
const from = props.from;
|
||||
export default function NoteTime({ from }) {
|
||||
const [time, setTime] = useState("");
|
||||
|
||||
function calcTime() {
|
||||
|
@ -7,7 +7,7 @@ import useProfile from "../feed/ProfileFeed";
|
||||
import { hexToBech32, profileLink } from "../Util";
|
||||
import LazyImage from "./LazyImage";
|
||||
|
||||
export default function ProfileImage({ pubkey, subHeader, showUsername = true, className }) {
|
||||
export default function ProfileImage({ pubkey, subHeader, showUsername = true, className, link }) {
|
||||
const navigate = useNavigate();
|
||||
const user = useProfile(pubkey);
|
||||
|
||||
@ -23,12 +23,12 @@ export default function ProfileImage({ pubkey, subHeader, showUsername = true, c
|
||||
}, [user]);
|
||||
|
||||
return (
|
||||
<div className={`pfp ${className ?? ""}`}>
|
||||
<LazyImage src={hasImage ? user.picture : Nostrich} onClick={() => navigate(profileLink(pubkey))} />
|
||||
<div className={`pfp ${className}`}>
|
||||
<LazyImage src={hasImage ? user.picture : Nostrich} onClick={() => navigate(link ?? profileLink(pubkey))} />
|
||||
{showUsername && (<div className="f-grow">
|
||||
<Link key={pubkey} to={profileLink(pubkey)}>{name}</Link>
|
||||
<Link key={pubkey} to={link ?? profileLink(pubkey)}>{name}</Link>
|
||||
{subHeader ? <>{subHeader}</> : null}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
@ -20,7 +20,7 @@ export default function useEventPublisher() {
|
||||
async function signEvent(ev) {
|
||||
if (hasNip07 && !privKey) {
|
||||
ev.Id = await ev.CreateId();
|
||||
let tmpEv = await window.nostr.signEvent(ev.ToObject());
|
||||
let tmpEv = await barierNip07(() => window.nostr.signEvent(ev.ToObject()));
|
||||
return Event.FromObject(tmpEv);
|
||||
} else {
|
||||
await ev.Sign(privKey);
|
||||
@ -72,7 +72,7 @@ export default function useEventPublisher() {
|
||||
ev.Tags.push(new Tag(["e", replyTo.Id, "", "reply"], ev.Tags.length));
|
||||
ev.Tags.push(new Tag(["p", replyTo.PubKey], ev.Tags.length));
|
||||
for (let pk of thread.PubKeys) {
|
||||
if(pk === pubKey) {
|
||||
if (pk === pubKey) {
|
||||
continue; // dont tag self in replies
|
||||
}
|
||||
ev.Tags.push(new Tag(["p", pk], ev.Tags.length));
|
||||
@ -152,6 +152,62 @@ export default function useEventPublisher() {
|
||||
ev.Tags.push(new Tag(["e", note.Id]));
|
||||
ev.Tags.push(new Tag(["p", note.PubKey]));
|
||||
return await signEvent(ev);
|
||||
},
|
||||
decryptDm: async (note) => {
|
||||
if (note.PubKey !== pubKey && !note.Tags.some(a => a.PubKey === pubKey)) {
|
||||
return "<CANT DECRYPT>";
|
||||
}
|
||||
try {
|
||||
let otherPubKey = note.PubKey === pubKey ? note.Tags.filter(a => a.Key === "p")[0].PubKey : note.PubKey;
|
||||
if (hasNip07 && !privKey) {
|
||||
return await barierNip07(() => window.nostr.nip04.decrypt(otherPubKey, note.Content));
|
||||
} else if(privKey) {
|
||||
await note.DecryptDm(privKey, otherPubKey);
|
||||
return note.Content;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Decyrption failed", e);
|
||||
return "<DECRYPTION FAILED>";
|
||||
}
|
||||
return "test";
|
||||
},
|
||||
sendDm: async (content, to) => {
|
||||
let ev = Event.ForPubKey(pubKey);
|
||||
ev.Kind = EventKind.DirectMessage;
|
||||
ev.Content = content;
|
||||
ev.Tags.push(new Tag(["p", to]));
|
||||
|
||||
try {
|
||||
if (hasNip07 && !privKey) {
|
||||
let ev = await barierNip07(() => window.nostr.nip04.encrypt(to, content));
|
||||
return await signEvent(ev);
|
||||
} else if(privKey) {
|
||||
await ev.EncryptDmForPubkey(to, privKey);
|
||||
return await signEvent(ev);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Encryption failed", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let isNip07Busy = false;
|
||||
|
||||
const delay = (t) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(resolve, t);
|
||||
});
|
||||
}
|
||||
|
||||
const barierNip07 = async (then) => {
|
||||
while (isNip07Busy) {
|
||||
await delay(10);
|
||||
}
|
||||
isNip07Busy = true;
|
||||
try {
|
||||
return await then();
|
||||
} finally {
|
||||
isNip07Busy = false;
|
||||
}
|
||||
};
|
@ -2,7 +2,7 @@ import { useEffect, useMemo } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import EventKind from "../nostr/EventKind";
|
||||
import { Subscriptions } from "../nostr/Subscriptions";
|
||||
import { addNotifications, setFollows, setRelays } from "../state/Login";
|
||||
import { addDirectMessage, addNotifications, setFollows, setRelays } from "../state/Login";
|
||||
import { setUserData } from "../state/Users";
|
||||
import useSubscription from "./Subscription";
|
||||
import { mapEventToProfile } from "./UsersFeed";
|
||||
@ -24,9 +24,11 @@ export default function useLoginFeed() {
|
||||
sub.Authors.add(pubKey);
|
||||
sub.Kinds.add(EventKind.ContactList);
|
||||
sub.Kinds.add(EventKind.SetMetadata);
|
||||
sub.Kinds.add(EventKind.DirectMessage);
|
||||
|
||||
let notifications = new Subscriptions();
|
||||
notifications.Kinds.add(EventKind.TextNote);
|
||||
notifications.Kinds.add(EventKind.DirectMessage);
|
||||
notifications.PTags.add(pubKey);
|
||||
notifications.Limit = 100;
|
||||
sub.AddSubscription(notifications);
|
||||
@ -40,6 +42,7 @@ export default function useLoginFeed() {
|
||||
let contactList = notes.filter(a => a.kind === EventKind.ContactList);
|
||||
let notifications = notes.filter(a => a.kind === EventKind.TextNote);
|
||||
let metadata = notes.filter(a => a.kind === EventKind.SetMetadata).map(a => mapEventToProfile(a));
|
||||
let dms = notes.filter(a => a.kind === EventKind.DirectMessage);
|
||||
|
||||
for (let cl of contactList) {
|
||||
if (cl.content !== "") {
|
||||
@ -58,5 +61,6 @@ export default function useLoginFeed() {
|
||||
}
|
||||
dispatch(addNotifications(notifications));
|
||||
dispatch(setUserData(metadata));
|
||||
dispatch(addDirectMessage(dms));
|
||||
}, [notes]);
|
||||
}
|
10
src/index.js
10
src/index.js
@ -20,6 +20,8 @@ import NewUserPage from './pages/NewUserPage';
|
||||
import SettingsPage from './pages/SettingsPage';
|
||||
import ErrorPage from './pages/ErrorPage';
|
||||
import VerificationPage from './pages/Verification';
|
||||
import MessagesPage from './pages/MessagesPage';
|
||||
import ChatPage from './pages/ChatPage';
|
||||
|
||||
/**
|
||||
* Nostr websocket managment system
|
||||
@ -62,6 +64,14 @@ const router = createBrowserRouter([
|
||||
{
|
||||
path: "/verification",
|
||||
element: <VerificationPage />
|
||||
},
|
||||
{
|
||||
path: "/messages",
|
||||
element: <MessagesPage />
|
||||
},
|
||||
{
|
||||
path: "/messages/:id",
|
||||
element: <ChatPage />
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import * as secp from '@noble/secp256k1';
|
||||
import base64 from "@protobufjs/base64"
|
||||
import EventKind from "./EventKind";
|
||||
import Tag from './Tag';
|
||||
import Thread from './Thread';
|
||||
@ -165,4 +166,48 @@ export default class Event {
|
||||
ev.PubKey = pubKey;
|
||||
return ev;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt the message content in place
|
||||
* @param {string} pubkey
|
||||
* @param {string} privkey
|
||||
*/
|
||||
async EncryptDmForPubkey(pubkey, privkey) {
|
||||
let key = await this._GetDmSharedKey(pubkey, privkey);
|
||||
let iv = window.crypto.getRandomValues(new Uint8Array(16));
|
||||
let data = new TextEncoder().encode(this.Content);
|
||||
let result = await window.crypto.subtle.encrypt({
|
||||
name: "AES-CBC",
|
||||
iv: iv
|
||||
}, key, data);
|
||||
let uData = new Uint8Array(result);
|
||||
this.Content = `${base64.encode(uData, 0, result.byteLength)}?iv=${base64.encode(iv, 0, 16)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt the content of this message in place
|
||||
* @param {string} privkey
|
||||
* @param {string} pubkey
|
||||
*/
|
||||
async DecryptDm(privkey, pubkey) {
|
||||
let key = await this._GetDmSharedKey(pubkey, privkey);
|
||||
let cSplit = this.Content.split("?iv=");
|
||||
let data = new Uint8Array(base64.length(cSplit[0]));
|
||||
base64.decode(cSplit[0], data, 0);
|
||||
|
||||
let iv = new Uint8Array(base64.length(cSplit[1]));
|
||||
base64.decode(cSplit[1], iv, 0);
|
||||
|
||||
let result = await window.crypto.subtle.decrypt({
|
||||
name: "AES-CBC",
|
||||
iv: iv
|
||||
}, key, data);
|
||||
this.Content = new TextDecoder().decode(result);
|
||||
}
|
||||
|
||||
async _GetDmSharedKey(pubkey, privkey) {
|
||||
let sharedPoint = secp.getSharedSecret(privkey, '02' + pubkey);
|
||||
let sharedX = sharedPoint.slice(1, 33);
|
||||
return await window.crypto.subtle.importKey("raw", sharedX, { name: "AES-CBC" }, false, ["encrypt", "decrypt"])
|
||||
}
|
||||
}
|
9
src/nostr/index.ts
Normal file
9
src/nostr/index.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export interface RawEvent {
|
||||
id: string,
|
||||
pubkey: string,
|
||||
created_at: number,
|
||||
kind: number,
|
||||
tags: string[][],
|
||||
content: string,
|
||||
sig: string
|
||||
}
|
32
src/pages/ChatPage.css
Normal file
32
src/pages/ChatPage.css
Normal file
@ -0,0 +1,32 @@
|
||||
.dm-list {
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
height: calc(100vh - 66px - 50px - 70px);
|
||||
}
|
||||
|
||||
.dm-list > div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.write-dm {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
background-color: var(--gray-light);
|
||||
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;
|
||||
}
|
64
src/pages/ChatPage.tsx
Normal file
64
src/pages/ChatPage.tsx
Normal file
@ -0,0 +1,64 @@
|
||||
import "./ChatPage.css";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
// @ts-ignore
|
||||
import ProfileImage from "../element/ProfileImage";
|
||||
// @ts-ignore
|
||||
import { bech32ToHex } from "../Util";
|
||||
// @ts-ignore
|
||||
import useEventPublisher from "../feed/EventPublisher";
|
||||
|
||||
import DM from "../element/DM";
|
||||
import { RawEvent } from "../nostr";
|
||||
|
||||
type RouterParams = {
|
||||
id: string
|
||||
}
|
||||
|
||||
export default function ChatPage() {
|
||||
const params = useParams<RouterParams>();
|
||||
const publisher = useEventPublisher();
|
||||
const id = bech32ToHex(params.id);
|
||||
const dms = useSelector<any, RawEvent[]>(s => filterDms(s.login.dms, s.login.publicKey));
|
||||
const [content, setContent] = useState<string>();
|
||||
|
||||
function filterDms(dms: RawEvent[], myPubkey: string) {
|
||||
return dms.filter(a => {
|
||||
if (a.pubkey === myPubkey && a.tags.some(b => b[0] === "p" && b[1] === id)) {
|
||||
return true;
|
||||
} else if (a.pubkey === id && a.tags.some(b => b[0] === "p" && b[1] === myPubkey)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
const sortedDms = useMemo<any[]>(() => {
|
||||
return [...dms].sort((a, b) => a.created_at - b.created_at)
|
||||
}, [dms]);
|
||||
|
||||
async function sendDm() {
|
||||
let ev = await publisher.sendDm(content, id);
|
||||
console.debug(ev);
|
||||
publisher.broadcast(ev);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ProfileImage pubkey={id} className="f-grow mb10" />
|
||||
<div className="dm-list">
|
||||
<div>
|
||||
{sortedDms.slice(-10).map(a => <DM data={a} 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)}></textarea>
|
||||
<div className="btn" onClick={() => sendDm()}>Send</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
@ -12,4 +12,4 @@
|
||||
background-color: var(--error);
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
@ -2,7 +2,7 @@ import "./Layout.css";
|
||||
import { useEffect } from "react"
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { Outlet, useNavigate } from "react-router-dom";
|
||||
import { faBell } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faBell, faMessage } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { System } from ".."
|
||||
@ -57,12 +57,15 @@ export default function Layout(props) {
|
||||
const unreadNotifications = notifications?.filter(a => (a.created_at * 1000) > readNotifications).length;
|
||||
return (
|
||||
<>
|
||||
<div className="btn btn-rnd notifications" onClick={(e) => goToNotifications(e)}>
|
||||
<div className="btn btn-rnd mr10" onClick={(e) => navigate("/messages")}>
|
||||
<FontAwesomeIcon icon={faMessage} size="xl" />
|
||||
</div>
|
||||
<div className={`btn btn-rnd${unreadNotifications === 0 ? " mr10" : ""}`} onClick={(e) => goToNotifications(e)}>
|
||||
<FontAwesomeIcon icon={faBell} size="xl" />
|
||||
</div>
|
||||
<span className="unread-count">
|
||||
{unreadNotifications > 1000 ? "..." : unreadNotifications}
|
||||
</span>
|
||||
{unreadNotifications > 0 && (<span className="unread-count">
|
||||
{unreadNotifications > 100 ? ">99" : unreadNotifications}
|
||||
</span>)}
|
||||
<ProfileImage pubkey={key} showUsername={false} />
|
||||
</>
|
||||
)
|
||||
|
36
src/pages/MessagesPage.tsx
Normal file
36
src/pages/MessagesPage.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
import { useMemo } from "react";
|
||||
import { useSelector } from "react-redux"
|
||||
|
||||
import { RawEvent } from "../nostr";
|
||||
|
||||
// @ts-ignore
|
||||
import ProfileImage from "../element/ProfileImage";
|
||||
// @ts-ignore
|
||||
import { hexToBech32 } from "../Util";
|
||||
|
||||
export default function MessagesPage() {
|
||||
const pubKey = useSelector<any, string>(s => s.login.publicKey);
|
||||
const dms = useSelector<any, RawEvent[]>(s => s.login.dms);
|
||||
|
||||
const pubKeys = useMemo(() => {
|
||||
return Array.from(new Set<string>(dms.map(a => a.pubkey)));
|
||||
}, [dms]);
|
||||
|
||||
function person(pubkey: string) {
|
||||
return (
|
||||
<div className="flex" key={pubkey}>
|
||||
<ProfileImage pubkey={pubkey} className="f-grow" link={`/messages/${hexToBech32("npub", pubkey)}`} />
|
||||
<span className="pill">
|
||||
{dms?.filter(a => a.pubkey === pubkey && a.pubkey !== pubKey).length}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<h3>Messages</h3>
|
||||
{pubKeys.map(person)}
|
||||
</>
|
||||
)
|
||||
}
|
@ -43,6 +43,11 @@ const LoginSlice = createSlice({
|
||||
* Timestamp of last read notification
|
||||
*/
|
||||
readNotifications: 0,
|
||||
|
||||
/**
|
||||
* Encrypted DM's
|
||||
*/
|
||||
dms: []
|
||||
},
|
||||
reducers: {
|
||||
init: (state) => {
|
||||
@ -126,6 +131,25 @@ const LoginSlice = createSlice({
|
||||
];
|
||||
}
|
||||
},
|
||||
addDirectMessage: (state, action) => {
|
||||
let n = action.payload;
|
||||
if (!Array.isArray(n)) {
|
||||
n = [n];
|
||||
}
|
||||
|
||||
let didChange = false;
|
||||
for (let x of n) {
|
||||
if (!state.dms.some(a => a.id === x.id)) {
|
||||
state.dms.push(x);
|
||||
didChange = true;
|
||||
}
|
||||
}
|
||||
if (didChange) {
|
||||
state.dms = [
|
||||
...state.dms
|
||||
];
|
||||
}
|
||||
},
|
||||
logout: (state) => {
|
||||
window.localStorage.removeItem(PrivateKeyItem);
|
||||
window.localStorage.removeItem(PublicKeyItem);
|
||||
@ -144,5 +168,5 @@ const LoginSlice = createSlice({
|
||||
}
|
||||
});
|
||||
|
||||
export const { init, setPrivateKey, setPublicKey, setRelays, removeRelay, setFollows, addNotifications, logout, markNotificationsRead } = LoginSlice.actions;
|
||||
export const { init, setPrivateKey, setPublicKey, setRelays, removeRelay, setFollows, addNotifications, addDirectMessage, logout, markNotificationsRead } = LoginSlice.actions;
|
||||
export const reducer = LoginSlice.reducer;
|
@ -1594,6 +1594,11 @@
|
||||
schema-utils "^3.0.0"
|
||||
source-map "^0.7.3"
|
||||
|
||||
"@protobufjs/base64@^1.1.2":
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@protobufjs/base64/-/base64-1.1.2.tgz#4c85730e59b9a1f1f349047dbf24296034bb2735"
|
||||
integrity sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==
|
||||
|
||||
"@reduxjs/toolkit@^1.9.1":
|
||||
version "1.9.1"
|
||||
resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-1.9.1.tgz#4c34dc4ddcec161535288c60da5c19c3ef15180e"
|
||||
|
Loading…
x
Reference in New Issue
Block a user