Added live message_state updates, fixed sonner theme
This commit is contained in:
parent
0d214de5ed
commit
7ebe028629
5 changed files with 138 additions and 21 deletions
|
|
@ -1,13 +1,22 @@
|
||||||
import * as React from "react";
|
import {
|
||||||
|
createContext,
|
||||||
|
useMemo,
|
||||||
|
useEffect,
|
||||||
|
useCallback,
|
||||||
|
useState,
|
||||||
|
useContext,
|
||||||
|
type ReactNode,
|
||||||
|
} from "react";
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
import { useRouterState } from "@tanstack/react-router";
|
import { useRouterState } from "@tanstack/react-router";
|
||||||
|
import type { InfiniteData } from "@tanstack/react-query";
|
||||||
import type { LiveMessage, RawMessage, RawMessages } from "./values";
|
import type { LiveMessage, RawMessage, RawMessages } from "./values";
|
||||||
import { useCrypto } from "@tensamin/crypto/context";
|
import { useCrypto } from "@tensamin/crypto/context";
|
||||||
import { useUser } from "@tensamin/user/context";
|
import { useUser } from "@tensamin/user/context";
|
||||||
import { useStorage } from "@tensamin/storage/context";
|
import { useStorage } from "@tensamin/storage/context";
|
||||||
import { useSocket } from "@tensamin/ttp/context";
|
import { useSocket } from "@tensamin/ttp/context";
|
||||||
|
|
||||||
export const context = React.createContext<contextType | undefined>(undefined);
|
export const context = createContext<contextType | undefined>(undefined);
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
|
|
@ -16,27 +25,25 @@ const queryClient = new QueryClient();
|
||||||
* @param props Parameter props.
|
* @param props Parameter props.
|
||||||
* @returns unknown.
|
* @returns unknown.
|
||||||
*/
|
*/
|
||||||
export default function Provider(props: { children: React.ReactNode }) {
|
export default function Provider(props: { children: ReactNode }) {
|
||||||
const { getSharedSecret } = useCrypto();
|
const { getSharedSecret } = useCrypto();
|
||||||
const { get } = useUser();
|
const { get } = useUser();
|
||||||
const { load } = useStorage();
|
const { load } = useStorage();
|
||||||
const { send } = useSocket();
|
const { send, subscribePush } = useSocket();
|
||||||
|
|
||||||
const [liveMessagesState, setLiveMessagesState] = React.useState<
|
const [liveMessagesState, setLiveMessagesState] = useState<LiveMessage[]>([]);
|
||||||
LiveMessage[]
|
const [currentSharedSecret, setCurrentSharedSecret] = useState("");
|
||||||
>([]);
|
|
||||||
const [currentSharedSecret, setCurrentSharedSecret] = React.useState("");
|
|
||||||
|
|
||||||
const locationSearch = useRouterState({
|
const locationSearch = useRouterState({
|
||||||
select: (state) => state.location.search,
|
select: (state) => state.location.search,
|
||||||
});
|
});
|
||||||
|
|
||||||
const userIdValue = React.useMemo(() => {
|
const userIdValue = useMemo(() => {
|
||||||
const rawId = (locationSearch as unknown as { id?: unknown })?.id;
|
const rawId = (locationSearch as unknown as { id?: unknown })?.id;
|
||||||
return Number(rawId ?? 0);
|
return Number(rawId ?? 0);
|
||||||
}, [locationSearch]);
|
}, [locationSearch]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
useEffect(() => {
|
||||||
const recipientId = userIdValue;
|
const recipientId = userIdValue;
|
||||||
|
|
||||||
if (!recipientId) {
|
if (!recipientId) {
|
||||||
|
|
@ -73,7 +80,7 @@ export default function Provider(props: { children: React.ReactNode }) {
|
||||||
};
|
};
|
||||||
}, [get, getSharedSecret, load, userIdValue]);
|
}, [get, getSharedSecret, load, userIdValue]);
|
||||||
|
|
||||||
const customGetMessages = React.useCallback(
|
const customGetMessages = useCallback(
|
||||||
async (amount: number, offset: number) => {
|
async (amount: number, offset: number) => {
|
||||||
const messages = await send("messages_get", {
|
const messages = await send("messages_get", {
|
||||||
amount,
|
amount,
|
||||||
|
|
@ -92,7 +99,7 @@ export default function Provider(props: { children: React.ReactNode }) {
|
||||||
[send, userIdValue],
|
[send, userIdValue],
|
||||||
);
|
);
|
||||||
|
|
||||||
const addLiveMessage = React.useCallback((message: RawMessage) => {
|
const addLiveMessage = useCallback((message: RawMessage) => {
|
||||||
const localId =
|
const localId =
|
||||||
globalThis.crypto?.randomUUID?.() ??
|
globalThis.crypto?.randomUUID?.() ??
|
||||||
`${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
`${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
||||||
|
|
@ -119,11 +126,91 @@ export default function Provider(props: { children: React.ReactNode }) {
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const clearLiveMessages = React.useCallback(() => {
|
const clearLiveMessages = useCallback(() => {
|
||||||
setLiveMessagesState([]);
|
setLiveMessagesState([]);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const value = React.useMemo<contextType>(
|
// Get live updates for message states
|
||||||
|
useEffect(() => {
|
||||||
|
return subscribePush((message) => {
|
||||||
|
if (message.type !== "message_state") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextState = message.data as {
|
||||||
|
chat_partner_id: number;
|
||||||
|
timestamp: number;
|
||||||
|
message_state: RawMessage["message_state"];
|
||||||
|
};
|
||||||
|
|
||||||
|
if (nextState.chat_partner_id !== userIdValue) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLiveMessagesState((prev) => {
|
||||||
|
let updated = false;
|
||||||
|
|
||||||
|
const mapped = prev.map((liveMessage) => {
|
||||||
|
if (liveMessage.timestamp !== nextState.timestamp) {
|
||||||
|
return liveMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (liveMessage.message_state === nextState.message_state) {
|
||||||
|
return liveMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
updated = true;
|
||||||
|
return {
|
||||||
|
...liveMessage,
|
||||||
|
message_state: nextState.message_state,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return updated ? mapped : prev;
|
||||||
|
});
|
||||||
|
|
||||||
|
const queryKey = ["chat-messages", String(userIdValue)] as const;
|
||||||
|
queryClient.setQueryData<InfiniteData<RawMessages>>(
|
||||||
|
queryKey,
|
||||||
|
(current) => {
|
||||||
|
if (!current) {
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
let updated = false;
|
||||||
|
|
||||||
|
const pages = current.pages.map((page) =>
|
||||||
|
page.map((historicalMessage) => {
|
||||||
|
if (historicalMessage.timestamp !== nextState.timestamp) {
|
||||||
|
return historicalMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (historicalMessage.message_state === nextState.message_state) {
|
||||||
|
return historicalMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
updated = true;
|
||||||
|
return {
|
||||||
|
...historicalMessage,
|
||||||
|
message_state: nextState.message_state,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!updated) {
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...current,
|
||||||
|
pages,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}, [subscribePush, userIdValue]);
|
||||||
|
|
||||||
|
const value = useMemo<contextType>(
|
||||||
() => ({
|
() => ({
|
||||||
getMessages: customGetMessages,
|
getMessages: customGetMessages,
|
||||||
liveMessages: () => liveMessagesState,
|
liveMessages: () => liveMessagesState,
|
||||||
|
|
@ -166,7 +253,7 @@ type contextType = {
|
||||||
* @returns contextType.
|
* @returns contextType.
|
||||||
*/
|
*/
|
||||||
export function useChat(): contextType {
|
export function useChat(): contextType {
|
||||||
const ctx = React.useContext(context);
|
const ctx = useContext(context);
|
||||||
if (!ctx) {
|
if (!ctx) {
|
||||||
throw new Error("useChat must be used within a ChatProvider");
|
throw new Error("useChat must be used within a ChatProvider");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -188,7 +188,7 @@ export default function Screen() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={virtualRow.key}
|
key={message.timestamp}
|
||||||
data-index={virtualRow.index}
|
data-index={virtualRow.index}
|
||||||
ref={virtualizer.measureElement}
|
ref={virtualizer.measureElement}
|
||||||
style={{
|
style={{
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ const message = z.object({
|
||||||
tint: z.string().length(7).startsWith("#").optional(),
|
tint: z.string().length(7).startsWith("#").optional(),
|
||||||
avatar: z.boolean().optional(),
|
avatar: z.boolean().optional(),
|
||||||
display: z.boolean().optional(),
|
display: z.boolean().optional(),
|
||||||
message_state: z.enum(["read", "received", "sent", "sending", "awaiting"]),
|
message_state: z.enum(["read", "received", "sent", "sending", "awaiting"]), // awaiting for 'internal' use
|
||||||
});
|
});
|
||||||
|
|
||||||
export const failedUser = {
|
export const failedUser = {
|
||||||
|
|
@ -148,6 +148,18 @@ export const socket = {
|
||||||
}),
|
}),
|
||||||
response: z.object({}),
|
response: z.object({}),
|
||||||
},
|
},
|
||||||
|
message_state: {
|
||||||
|
request: z.object({
|
||||||
|
chat_partner_id: z.number(),
|
||||||
|
timestamp: z.number(),
|
||||||
|
message_state: message.shape.message_state,
|
||||||
|
}),
|
||||||
|
response: z.object({
|
||||||
|
chat_partner_id: z.number(),
|
||||||
|
message_state: message.shape.message_state,
|
||||||
|
timestamp: z.number(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
} satisfies Record<string, { request: z.ZodType; response: z.ZodType }>;
|
} satisfies Record<string, { request: z.ZodType; response: z.ZodType }>;
|
||||||
|
|
||||||
export type Socket = typeof socket;
|
export type Socket = typeof socket;
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import { useCrypto } from "@tensamin/crypto/context";
|
||||||
import { log } from "@tensamin/shared/log";
|
import { log } from "@tensamin/shared/log";
|
||||||
import { useStorage } from "@tensamin/storage/context";
|
import { useStorage } from "@tensamin/storage/context";
|
||||||
import { createTransportClient, READY_STATE, type BoundSendFn } from "./core";
|
import { createTransportClient, READY_STATE, type BoundSendFn } from "./core";
|
||||||
|
import type { PushHandler } from "./core";
|
||||||
import {
|
import {
|
||||||
PING_INTERVAL,
|
PING_INTERVAL,
|
||||||
RECONNECT_RESET,
|
RECONNECT_RESET,
|
||||||
|
|
@ -132,6 +133,7 @@ function getProtocolErrorDetails(error: unknown) {
|
||||||
|
|
||||||
type ContextType = {
|
type ContextType = {
|
||||||
send: BoundSendFn<Schemas>;
|
send: BoundSendFn<Schemas>;
|
||||||
|
subscribePush: (handler: PushHandler) => () => void;
|
||||||
readyState: () => number;
|
readyState: () => number;
|
||||||
ownPing: () => number;
|
ownPing: () => number;
|
||||||
iotaPing: () => number;
|
iotaPing: () => number;
|
||||||
|
|
@ -202,6 +204,21 @@ export default function Provider(props: {
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribes to unsolicited push events from the active transport client.
|
||||||
|
* @param handler Callback invoked for each push message.
|
||||||
|
* @returns Unsubscribe function.
|
||||||
|
*/
|
||||||
|
const subscribePush = useCallback((handler: PushHandler) => {
|
||||||
|
const client = clientRef.current;
|
||||||
|
|
||||||
|
if (!client) {
|
||||||
|
return () => {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return client.subscribePush(handler);
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!connected || !identified) {
|
if (!connected || !identified) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -536,12 +553,13 @@ export default function Provider(props: {
|
||||||
const contextValue = useMemo<ContextType>(
|
const contextValue = useMemo<ContextType>(
|
||||||
() => ({
|
() => ({
|
||||||
send,
|
send,
|
||||||
|
subscribePush,
|
||||||
readyState: () => readyState,
|
readyState: () => readyState,
|
||||||
ownPing: () => ownPing,
|
ownPing: () => ownPing,
|
||||||
iotaPing: () => iotaPing,
|
iotaPing: () => iotaPing,
|
||||||
identified: () => identified,
|
identified: () => identified,
|
||||||
}),
|
}),
|
||||||
[identified, iotaPing, ownPing, readyState, send],
|
[identified, iotaPing, ownPing, readyState, send, subscribePush],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (error !== "" && errorDescription !== "") {
|
if (error !== "" && errorDescription !== "") {
|
||||||
|
|
|
||||||
|
|
@ -26,9 +26,9 @@ const Toaster = ({ ...props }: ToasterProps) => {
|
||||||
}}
|
}}
|
||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
"--normal-bg": "var(--popover)",
|
"--normal-bg": "var(--color-popover)",
|
||||||
"--normal-text": "var(--popover-foreground)",
|
"--normal-text": "var(--color-popover-foreground)",
|
||||||
"--normal-border": "var(--border)",
|
"--normal-border": "var(--color-border)",
|
||||||
"--border-radius": "var(--radius)",
|
"--border-radius": "var(--radius)",
|
||||||
} as React.CSSProperties
|
} as React.CSSProperties
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue