mirror of
https://github.com/fromchat-messenger/web.git
synced 2026-09-22 19:15:08 +03:00
Improve code and type safety
This commit is contained in:
@@ -45,6 +45,7 @@ class MessageFile(Base):
|
|||||||
id = Column(Integer, primary_key=True, index=True)
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
message_id = Column(Integer, ForeignKey("message.id"), nullable=False, index=True)
|
message_id = Column(Integer, ForeignKey("message.id"), nullable=False, index=True)
|
||||||
path = Column(Text, nullable=False)
|
path = Column(Text, nullable=False)
|
||||||
|
name = Column(Text, nullable=False)
|
||||||
|
|
||||||
message = relationship("Message", back_populates="files")
|
message = relationship("Message", back_populates="files")
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ def convert_message(msg: Message) -> dict:
|
|||||||
{
|
{
|
||||||
"path": f"/api/uploads/files/normal/{Path(f.path).name}",
|
"path": f"/api/uploads/files/normal/{Path(f.path).name}",
|
||||||
"id": f.id,
|
"id": f.id,
|
||||||
|
"name": f.name,
|
||||||
"message_id": f.message_id
|
"message_id": f.message_id
|
||||||
}
|
}
|
||||||
for f in (msg.files or [])
|
for f in (msg.files or [])
|
||||||
@@ -64,7 +65,7 @@ async def send_message(
|
|||||||
# Expect JSON: {"type":"text","data":{"content": str}, "reply_to_id": number|null}
|
# Expect JSON: {"type":"text","data":{"content": str}, "reply_to_id": number|null}
|
||||||
try:
|
try:
|
||||||
obj = json.loads(payload)
|
obj = json.loads(payload)
|
||||||
content = obj.get("data", {}).get("content", "")
|
content = obj.get("content", "")
|
||||||
reply_to_id = obj.get("reply_to_id", None)
|
reply_to_id = obj.get("reply_to_id", None)
|
||||||
request = SendMessageRequest(content=content, reply_to_id=reply_to_id)
|
request = SendMessageRequest(content=content, reply_to_id=reply_to_id)
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -147,6 +148,7 @@ async def send_message(
|
|||||||
|
|
||||||
mf = MessageFile(
|
mf = MessageFile(
|
||||||
message_id=new_message.id,
|
message_id=new_message.id,
|
||||||
|
name=original_name,
|
||||||
path=str(out_path)
|
path=str(out_path)
|
||||||
)
|
)
|
||||||
db.add(mf)
|
db.add(mf)
|
||||||
|
|||||||
Vendored
+6
-4
@@ -1,13 +1,15 @@
|
|||||||
export type Platform = "win32" | "darwin" | "linux"
|
export type Platform = "win32" | "darwin" | "linux"
|
||||||
|
|
||||||
export interface ElectronNotifications {
|
export interface NotificationShowOptions {
|
||||||
requestPermission: () => Promise<NotificationPermission>;
|
|
||||||
show: (options: {
|
|
||||||
title: string;
|
title: string;
|
||||||
body: string;
|
body: string;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
tag?: string;
|
tag?: string;
|
||||||
}) => Promise<boolean>;
|
}
|
||||||
|
|
||||||
|
export interface ElectronNotifications {
|
||||||
|
requestPermission: () => Promise<NotificationPermission>;
|
||||||
|
show: (options: NotificationShowOptions) => Promise<boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ElectronInterface {
|
export interface ElectronInterface {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { app, BrowserWindow, Notification, ipcMain } from 'electron';
|
import { app, BrowserWindow, Notification, ipcMain } from 'electron';
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
import { NotificationShowOptions } from '../electron';
|
||||||
|
|
||||||
let mainWindow: BrowserWindow | null = null;
|
let mainWindow: BrowserWindow | null = null;
|
||||||
|
|
||||||
@@ -35,7 +36,7 @@ app.whenReady().then(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Handle showing notifications
|
// Handle showing notifications
|
||||||
ipcMain.handle('show-notification', async (event, options) => {
|
ipcMain.handle('show-notification', async (event, options: NotificationShowOptions) => {
|
||||||
if (Notification.isSupported()) {
|
if (Notification.isSupported()) {
|
||||||
try {
|
try {
|
||||||
const notification = new Notification({
|
const notification = new Notification({
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
import { contextBridge, ipcRenderer } from "electron";
|
import { contextBridge, ipcRenderer } from "electron";
|
||||||
import type { ElectronInterface, Platform } from "../electron";
|
import type { ElectronInterface, Platform } from "../electron";
|
||||||
|
|
||||||
const electronInterface: ElectronInterface = {
|
contextBridge.exposeInMainWorld("electronInterface", {
|
||||||
desktop: true,
|
desktop: true,
|
||||||
platform: process.platform as Platform,
|
platform: process.platform as Platform,
|
||||||
notifications: {
|
notifications: {
|
||||||
requestPermission: () => ipcRenderer.invoke('request-notification-permission'),
|
requestPermission: () => ipcRenderer.invoke('request-notification-permission'),
|
||||||
show: (options: any) => ipcRenderer.invoke('show-notification', options)
|
show: (options) => ipcRenderer.invoke('show-notification', options)
|
||||||
}
|
}
|
||||||
}
|
} satisfies ElectronInterface);
|
||||||
|
|
||||||
contextBridge.exposeInMainWorld("electronInterface", electronInterface);
|
|
||||||
@@ -5,7 +5,7 @@ import { importAesGcmKey, aesGcmEncrypt, aesGcmDecrypt } from "../utils/crypto/s
|
|||||||
import { randomBytes } from "../utils/crypto/kdf";
|
import { randomBytes } from "../utils/crypto/kdf";
|
||||||
import { getCurrentKeys } from "../auth/crypto";
|
import { getCurrentKeys } from "../auth/crypto";
|
||||||
import { request } from "../core/websocket";
|
import { request } from "../core/websocket";
|
||||||
import type { SendDMRequest, DmEnvelope, User, DMEditWebSocketMessage, DmEncryptedJSON, BaseDmEnvelope } from "../core/types";
|
import type { SendDMRequest, DmEnvelope, User, DMEditRequest, DmEncryptedJSON, BaseDmEnvelope } from "../core/types";
|
||||||
import { b64, ub64 } from "../utils/utils";
|
import { b64, ub64 } from "../utils/utils";
|
||||||
|
|
||||||
export async function decryptDm(envelope: DmEnvelope, senderPublicKeyB64: string): Promise<string> {
|
export async function decryptDm(envelope: DmEnvelope, senderPublicKeyB64: string): Promise<string> {
|
||||||
@@ -159,7 +159,7 @@ export async function editDmEnvelope(id: number, recipientPublicKeyB64: string,
|
|||||||
wrappedMk: b64(wrap.ciphertext),
|
wrappedMk: b64(wrap.ciphertext),
|
||||||
salt: b64(wkSalt)
|
salt: b64(wkSalt)
|
||||||
}
|
}
|
||||||
} as DMEditWebSocketMessage);
|
} as DMEditRequest);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteDmEnvelope(id: number, recipientId: number, authToken: string): Promise<void> {
|
export async function deleteDmEnvelope(id: number, recipientId: number, authToken: string): Promise<void> {
|
||||||
|
|||||||
Vendored
+66
-11
@@ -235,10 +235,10 @@ export interface DmEncryptedJSON {
|
|||||||
* @property {any} [data] - Message payload data
|
* @property {any} [data] - Message payload data
|
||||||
* @property {WebSocketError} [error] - Error information if applicable
|
* @property {WebSocketError} [error] - Error information if applicable
|
||||||
*/
|
*/
|
||||||
export interface WebSocketMessage {
|
export interface WebSocketMessage<T> {
|
||||||
type: string;
|
type: string;
|
||||||
credentials?: WebSocketCredentials;
|
credentials?: WebSocketCredentials;
|
||||||
data?: any;
|
data?: T;
|
||||||
error?: WebSocketError;
|
error?: WebSocketError;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,26 +264,81 @@ export interface WebSocketCredentials {
|
|||||||
credentials: string;
|
credentials: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DMEditWebSocketMessage extends WebSocketMessage {
|
export interface Attachment {
|
||||||
type: "dmEdit",
|
path: string;
|
||||||
data: {
|
encrypted: boolean;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------
|
||||||
|
// WebSocket message types
|
||||||
|
// -----------------------
|
||||||
|
|
||||||
|
// Utils
|
||||||
|
export interface DMEditPayload {
|
||||||
id: number;
|
id: number;
|
||||||
iv: string;
|
iv: string;
|
||||||
ciphertext: string;
|
ciphertext: string;
|
||||||
iv2: string;
|
iv2: string;
|
||||||
wrappedMk: string;
|
wrappedMk: string;
|
||||||
salt: string;
|
salt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Requests
|
||||||
|
export interface DMEditRequest extends WebSocketMessage {
|
||||||
|
type: "dmEdit",
|
||||||
|
credentials: WebSocketCredentials;
|
||||||
|
data: DMEditPayload
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SendMessageRequest extends WebSocketMessage {
|
||||||
|
type: "sendMessage",
|
||||||
|
credentials: WebSocketCredentials;
|
||||||
|
data: {
|
||||||
|
content: string;
|
||||||
|
reply_to_id: number | null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Attachment {
|
// Messages
|
||||||
path: string;
|
export interface DMNewWebSocketMessage extends WebSocketMessage {
|
||||||
encrypted: boolean;
|
type: "dmNew",
|
||||||
filename?: string;
|
data: DmEnvelope
|
||||||
content_type?: string;
|
|
||||||
size?: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DMEditedWebSocketMessage extends WebSocketMessage {
|
||||||
|
type: "dmEdited",
|
||||||
|
data: DMEditPayload
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DMDeletedWebSocketMessage extends WebSocketMessage {
|
||||||
|
type: "dmDeleted",
|
||||||
|
data: {
|
||||||
|
id: number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MessageEditedWebSocketMessage extends WebSocketMessage {
|
||||||
|
type: "messageEdited",
|
||||||
|
data: Partial<Message> & { id: number }
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MessageDeletedWebSocketMessage extends WebSocketMessage {
|
||||||
|
type: "messageDeleted",
|
||||||
|
data: {
|
||||||
|
message_id: number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NewMessageWebSocketMessage extends WebSocketMessage {
|
||||||
|
type: "newMessage",
|
||||||
|
data: Message
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shared types
|
||||||
|
export type DMWebSocketMessage = DMNewWebSocketMessage | DMEditedWebSocketMessage | DMDeletedWebSocketMessage
|
||||||
|
export type ChatWebSocketMessage = MessageEditedWebSocketMessage | MessageDeletedWebSocketMessage | NewMessageWebSocketMessage
|
||||||
|
|
||||||
// -----------
|
// -----------
|
||||||
// Encrypted message JSON (plaintext structure before encryption)
|
// Encrypted message JSON (plaintext structure before encryption)
|
||||||
// -----------
|
// -----------
|
||||||
|
|||||||
@@ -33,17 +33,17 @@ export let websocket: WebSocket = create();
|
|||||||
* Global WebSocket message handler reference
|
* Global WebSocket message handler reference
|
||||||
* This will be set by the active panel to handle incoming messages
|
* This will be set by the active panel to handle incoming messages
|
||||||
*/
|
*/
|
||||||
let globalMessageHandler: ((response: WebSocketMessage) => void) | null = null;
|
let globalMessageHandler: ((response: WebSocketMessage<any>) => void) | null = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the global WebSocket message handler
|
* Set the global WebSocket message handler
|
||||||
* @param handler - Function to handle WebSocket messages
|
* @param handler - Function to handle WebSocket messages
|
||||||
*/
|
*/
|
||||||
export function setGlobalMessageHandler(handler: ((response: WebSocketMessage) => void) | null): void {
|
export function setGlobalMessageHandler(handler: ((response: WebSocketMessage<any>) => void) | null): void {
|
||||||
globalMessageHandler = handler;
|
globalMessageHandler = handler;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function request(payload: WebSocketMessage): Promise<WebSocketMessage> {
|
export function request<Request, Response = any>(payload: WebSocketMessage<Request>): Promise<WebSocketMessage<Response>> {
|
||||||
console.log("WebSocket request:", payload);
|
console.log("WebSocket request:", payload);
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
function requestInner() {
|
function requestInner() {
|
||||||
@@ -95,7 +95,7 @@ async function onError() {
|
|||||||
|
|
||||||
websocket.addEventListener("message", (e) => {
|
websocket.addEventListener("message", (e) => {
|
||||||
try {
|
try {
|
||||||
const response: WebSocketMessage = JSON.parse(e.data);
|
const response: WebSocketMessage<any> = JSON.parse(e.data);
|
||||||
|
|
||||||
// Route message to global handler if set
|
// Route message to global handler if set
|
||||||
if (globalMessageHandler) {
|
if (globalMessageHandler) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { RichTextArea } from "../core/RichTextArea";
|
|||||||
import type { Message } from "../../../core/types";
|
import type { Message } from "../../../core/types";
|
||||||
import Quote from "../core/Quote";
|
import Quote from "../core/Quote";
|
||||||
import AnimatedHeight from "../core/animations/AnimatedHeight";
|
import AnimatedHeight from "../core/animations/AnimatedHeight";
|
||||||
|
import { useImmer } from "use-immer";
|
||||||
|
|
||||||
interface ChatInputWrapperProps {
|
interface ChatInputWrapperProps {
|
||||||
onSendMessage: (message: string, files: File[]) => void;
|
onSendMessage: (message: string, files: File[]) => void;
|
||||||
@@ -35,7 +36,7 @@ export function ChatInputWrapper(
|
|||||||
}: ChatInputWrapperProps
|
}: ChatInputWrapperProps
|
||||||
) {
|
) {
|
||||||
const [message, setMessage] = useState("");
|
const [message, setMessage] = useState("");
|
||||||
const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
|
const [selectedFiles, setSelectedFiles] = useImmer<File[]>([]);
|
||||||
const [attachmentsVisible, setAttachmentsVisible] = useState(false);
|
const [attachmentsVisible, setAttachmentsVisible] = useState(false);
|
||||||
const [errorOpen, setErrorOpen] = useState(false);
|
const [errorOpen, setErrorOpen] = useState(false);
|
||||||
|
|
||||||
@@ -44,8 +45,7 @@ export function ChatInputWrapper(
|
|||||||
if (onProvideFileAdder) {
|
if (onProvideFileAdder) {
|
||||||
const addFiles = (files: File[]) => {
|
const addFiles = (files: File[]) => {
|
||||||
if (!files || files.length === 0) return;
|
if (!files || files.length === 0) return;
|
||||||
setSelectedFiles(prev => [...prev, ...files]);
|
setSelectedFiles(draft => { draft.push(...files) });
|
||||||
setAttachmentsVisible(true);
|
|
||||||
};
|
};
|
||||||
onProvideFileAdder(addFiles);
|
onProvideFileAdder(addFiles);
|
||||||
}
|
}
|
||||||
@@ -53,18 +53,12 @@ export function ChatInputWrapper(
|
|||||||
|
|
||||||
// When entering edit mode, preload the message content
|
// When entering edit mode, preload the message content
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (editingMessage) {
|
setMessage(editingMessage ? editingMessage.content || "" : "");
|
||||||
setMessage(editingMessage.content || "");
|
|
||||||
} else {
|
|
||||||
setMessage("");
|
|
||||||
}
|
|
||||||
}, [editingMessage]);
|
}, [editingMessage]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedFiles.length > 0) {
|
setAttachmentsVisible(selectedFiles.length > 0);
|
||||||
setAttachmentsVisible(true);
|
}, [selectedFiles]);
|
||||||
}
|
|
||||||
}, [selectedFiles])
|
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent | Event) => {
|
const handleSubmit = async (e: React.FormEvent | Event) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -94,10 +88,9 @@ export function ChatInputWrapper(
|
|||||||
const input = document.createElement("input");
|
const input = document.createElement("input");
|
||||||
input.type = "file";
|
input.type = "file";
|
||||||
input.multiple = true;
|
input.multiple = true;
|
||||||
input.onchange = () => {
|
input.addEventListener("change", () => {
|
||||||
const files = Array.from(input.files || []);
|
setSelectedFiles(draft => { draft.push(...Array.from(input.files || [])) });
|
||||||
setSelectedFiles(files);
|
});
|
||||||
};
|
|
||||||
input.click();
|
input.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,16 +126,22 @@ export function ChatInputWrapper(
|
|||||||
<div className="attachments-preview contextual-preview">
|
<div className="attachments-preview contextual-preview">
|
||||||
<mdui-icon name="attach_file" />
|
<mdui-icon name="attach_file" />
|
||||||
<div className="attachments-chips">
|
<div className="attachments-chips">
|
||||||
{selectedFiles.map((f, i) => (
|
{selectedFiles.map((file, i) => (
|
||||||
<mdui-chip
|
<mdui-chip
|
||||||
key={i}
|
key={i}
|
||||||
variant="input"
|
variant="input"
|
||||||
end-icon="close"
|
end-icon="close"
|
||||||
title={`${f.name} (${Math.round(f.size/1024/1024)} MB)`}
|
title={`${file.name} (${Math.round(file.size/1024/1024)} MB)`}
|
||||||
onClick={() => setSelectedFiles(prev => prev.filter((_, idx) => idx !== i))}
|
onClick={() => {
|
||||||
|
if (selectedFiles.length == 1) {
|
||||||
|
setAttachmentsVisible(false);
|
||||||
|
} else {
|
||||||
|
setSelectedFiles(draft => { draft.splice(i) })
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<mdui-icon slot="icon" name="attach_file"></mdui-icon>
|
<mdui-icon slot="icon" name="attach_file"></mdui-icon>
|
||||||
<span className="name">{f.name}</span>
|
<span className="name">{file.name}</span>
|
||||||
</mdui-chip>
|
</mdui-chip>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -142,16 +142,16 @@ export function Message({ message, isAuthor, onProfileClick, onContextMenu, isLo
|
|||||||
{message.files && message.files.length > 0 && (
|
{message.files && message.files.length > 0 && (
|
||||||
<mdui-list className="message-attachments">
|
<mdui-list className="message-attachments">
|
||||||
{message.files.map((file, idx) => {
|
{message.files.map((file, idx) => {
|
||||||
const isImage = !file.encrypted && (file.content_type?.startsWith("image/") || /\.(png|jpg|jpeg|gif|webp)$/i.test(file.filename || ""));
|
const isImage = !file.encrypted && /\.(png|jpg|jpeg|gif|webp)$/i.test(file.name || "");
|
||||||
const downloadUrl = decryptedFiles.get(file.path) || file.path;
|
const downloadUrl = decryptedFiles.get(file.path) || file.path;
|
||||||
return (
|
return (
|
||||||
<div className="attachment" key={idx}>
|
<div className="attachment" key={idx}>
|
||||||
{isImage ? (
|
{isImage ? (
|
||||||
<img src={file.path} alt={file.filename || "image"} style={{ maxWidth: "200px", borderRadius: "8px" }} />
|
<img src={file.path} alt={file.name || "image"} style={{ maxWidth: "200px", borderRadius: "8px" }} />
|
||||||
) : (
|
) : (
|
||||||
<a
|
<a
|
||||||
href={downloadUrl}
|
href={downloadUrl}
|
||||||
download={file.filename || "file"}
|
download={file.name || "file"}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
onClick={async (e) => {
|
onClick={async (e) => {
|
||||||
@@ -161,13 +161,15 @@ export function Message({ message, isAuthor, onProfileClick, onContextMenu, isLo
|
|||||||
if (decryptedUrl) {
|
if (decryptedUrl) {
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
link.href = decryptedUrl;
|
link.href = decryptedUrl;
|
||||||
link.download = file.filename || "file";
|
link.download = file.name || "file";
|
||||||
link.click();
|
link.click();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<mdui-list-item icon="download--filled">{(file.filename || file.path.split("/").pop() || "Имя файла неизвестно").replace(/\d+_\d+_/, "")}</mdui-list-item>
|
<mdui-list-item icon="download--filled">
|
||||||
|
{(file.name || file.path.split("/").pop() || "Имя файла неизвестно").replace(/\d+_\d+_/, "")}
|
||||||
|
</mdui-list-item>
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -22,8 +22,7 @@ export default function AnimatedHeight({ visible, duration = 0.25, onFinish, chi
|
|||||||
setIsAnimating(false);
|
setIsAnimating(false);
|
||||||
}, duration * 1000);
|
}, duration * 1000);
|
||||||
}, 0);
|
}, 0);
|
||||||
} else {
|
} else if (shouldRender) {
|
||||||
if (shouldRender) {
|
|
||||||
setIsAnimating(true);
|
setIsAnimating(true);
|
||||||
if (measureRef.current) {
|
if (measureRef.current) {
|
||||||
const contentHeight = measureRef.current.scrollHeight;
|
const contentHeight = measureRef.current.scrollHeight;
|
||||||
@@ -42,7 +41,6 @@ export default function AnimatedHeight({ visible, duration = 0.25, onFinish, chi
|
|||||||
}
|
}
|
||||||
}, duration * 1000);
|
}, duration * 1000);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}, [visible, shouldRender]);
|
}, [visible, shouldRender]);
|
||||||
|
|
||||||
// Don't render if not visible and not animating
|
// Don't render if not visible and not animating
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
editDmEnvelope,
|
editDmEnvelope,
|
||||||
deleteDmEnvelope
|
deleteDmEnvelope
|
||||||
} from "../../api/dmApi";
|
} from "../../api/dmApi";
|
||||||
import type { DmEncryptedJSON, DmEnvelope, EncryptedMessageJson, Message, WebSocketMessage } from "../../core/types";
|
import type { DmEncryptedJSON, DmEnvelope, DMWebSocketMessage, EncryptedMessageJson, Message } from "../../core/types";
|
||||||
import type { UserState } from "../state";
|
import type { UserState } from "../state";
|
||||||
|
|
||||||
export interface DMPanelData {
|
export interface DMPanelData {
|
||||||
@@ -58,19 +58,14 @@ export class DMPanel extends MessagePanel {
|
|||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
const dmMsg: Message & { dmEnvelope?: { salt: string; iv2: string; wrappedMk: string } } = {
|
const dmMsg: Message = {
|
||||||
id: env.id,
|
id: env.id,
|
||||||
content: content,
|
content: content,
|
||||||
username: username,
|
username: username,
|
||||||
timestamp: env.timestamp,
|
timestamp: env.timestamp,
|
||||||
is_read: false,
|
is_read: false,
|
||||||
is_edited: false,
|
is_edited: false,
|
||||||
files: env.files?.map(file => { return {"filename": file.name, "encrypted": true, "path": file.path} }) || [],
|
files: env.files?.map(file => { return {"name": file.name, "encrypted": true, "path": file.path} }) || []
|
||||||
dmEnvelope: {
|
|
||||||
salt: env.salt,
|
|
||||||
iv2: env.iv2,
|
|
||||||
wrappedMk: env.wrappedMk
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (reply_to_id) {
|
if (reply_to_id) {
|
||||||
@@ -165,9 +160,9 @@ export class DMPanel extends MessagePanel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle incoming WebSocket DM messages
|
// Handle incoming WebSocket DM messages
|
||||||
handleWebSocketMessage = async (response: WebSocketMessage): Promise<void> => {
|
handleWebSocketMessage = async (response: DMWebSocketMessage): Promise<void> => {
|
||||||
if (response.type === "dmNew" && this.dmData) {
|
if (response.type === "dmNew" && this.dmData) {
|
||||||
const envelope = response.data as DmEnvelope;
|
const envelope = response.data;
|
||||||
|
|
||||||
// If this is for the active DM conversation
|
// If this is for the active DM conversation
|
||||||
if (envelope.senderId === this.dmData.userId || envelope.recipientId === this.dmData.userId) {
|
if (envelope.senderId === this.dmData.userId || envelope.recipientId === this.dmData.userId) {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { MessagePanel } from "./MessagePanel";
|
|||||||
import { API_BASE_URL } from "../../core/config";
|
import { API_BASE_URL } from "../../core/config";
|
||||||
import { getAuthHeaders } from "../../auth/api";
|
import { getAuthHeaders } from "../../auth/api";
|
||||||
import { request } from "../../core/websocket";
|
import { request } from "../../core/websocket";
|
||||||
import type { Message, WebSocketMessage } from "../../core/types";
|
import type { ChatWebSocketMessage, Message, SendMessageRequest } from "../../core/types";
|
||||||
import type { UserState } from "../state";
|
import type { UserState } from "../state";
|
||||||
|
|
||||||
export class PublicChatPanel extends MessagePanel {
|
export class PublicChatPanel extends MessagePanel {
|
||||||
@@ -74,13 +74,16 @@ export class PublicChatPanel extends MessagePanel {
|
|||||||
credentials: this.currentUser.authToken
|
credentials: this.currentUser.authToken
|
||||||
},
|
},
|
||||||
type: "sendMessage"
|
type: "sendMessage"
|
||||||
});
|
} satisfies SendMessageRequest);
|
||||||
if (response.error) {
|
if (response.error) {
|
||||||
console.error("Error sending message:", response.error);
|
console.error("Error sending message:", response.error);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const form = new FormData();
|
const form = new FormData();
|
||||||
form.append("payload", JSON.stringify({ type: "text", data: { content: content.trim() }, reply_to_id: replyToId ?? null }));
|
form.append("payload", JSON.stringify({
|
||||||
|
content: content.trim(),
|
||||||
|
reply_to_id: replyToId ?? null
|
||||||
|
} satisfies SendMessageRequest["data"]));
|
||||||
for (const f of files) form.append("files", f, f.name);
|
for (const f of files) form.append("files", f, f.name);
|
||||||
const res = await fetch(`${API_BASE_URL}/send_message`, {
|
const res = await fetch(`${API_BASE_URL}/send_message`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -97,7 +100,7 @@ export class PublicChatPanel extends MessagePanel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle incoming WebSocket messages
|
// Handle incoming WebSocket messages
|
||||||
handleWebSocketMessage = (response: WebSocketMessage): void => {
|
handleWebSocketMessage = (response: ChatWebSocketMessage): void => {
|
||||||
switch (response.type) {
|
switch (response.type) {
|
||||||
case 'messageEdited':
|
case 'messageEdited':
|
||||||
if (response.data) {
|
if (response.data) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { API_BASE_URL } from "../core/config";
|
import { API_BASE_URL } from "../core/config";
|
||||||
import { isElectron } from "../electron/electron";
|
import { isElectron } from "../electron/electron";
|
||||||
import { websocket } from "../core/websocket";
|
import { websocket } from "../core/websocket";
|
||||||
import type { WebSocketMessage } from "../core/types";
|
import type { NewMessageWebSocketMessage, WebSocketMessage } from "../core/types";
|
||||||
|
|
||||||
export interface PushSubscriptionData {
|
export interface PushSubscriptionData {
|
||||||
endpoint: string;
|
endpoint: string;
|
||||||
@@ -124,10 +124,11 @@ async function showMessageNotification(message: any): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleWebSocketMessage(response: WebSocketMessage): Promise<void> {
|
async function handleWebSocketMessage(response: WebSocketMessage<any>): Promise<void> {
|
||||||
// Handle notifications for new messages
|
// Handle notifications for new messages
|
||||||
if (response.type === "newMessage" && response.data) {
|
if (response.type === "newMessage" && response.data) {
|
||||||
await showMessageNotification(response.data);
|
const newResponse = response as NewMessageWebSocketMessage;
|
||||||
|
await showMessageNotification(newResponse.data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,7 +243,7 @@ export async function startElectronReceiver(): Promise<void> {
|
|||||||
// Add our own message listener to the existing WebSocket
|
// Add our own message listener to the existing WebSocket
|
||||||
messageListener = (event: MessageEvent) => {
|
messageListener = (event: MessageEvent) => {
|
||||||
try {
|
try {
|
||||||
const response: WebSocketMessage = JSON.parse(event.data);
|
const response: WebSocketMessage<any> = JSON.parse(event.data);
|
||||||
handleWebSocketMessage(response);
|
handleWebSocketMessage(response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to parse WebSocket message:', error);
|
console.error('Failed to parse WebSocket message:', error);
|
||||||
|
|||||||
Reference in New Issue
Block a user