Added mobile
This commit is contained in:
parent
82a14f65fa
commit
80114d1d66
125 changed files with 8200 additions and 542 deletions
|
|
@ -25,6 +25,7 @@ export default function Message(props: {
|
|||
}) {
|
||||
const { sharedSecret } = useChat();
|
||||
const { decrypt } = useCrypto();
|
||||
const sharedSecretValue = sharedSecret();
|
||||
|
||||
const [decodedContent, setDecodedContent] = React.useState("");
|
||||
const [isReady, setIsReady] = React.useState(false);
|
||||
|
|
@ -37,7 +38,7 @@ export default function Message(props: {
|
|||
}
|
||||
|
||||
const content = props.message.content;
|
||||
const secret = sharedSecret();
|
||||
const secret = sharedSecretValue;
|
||||
let active = true;
|
||||
|
||||
if (!secret) {
|
||||
|
|
@ -45,8 +46,6 @@ export default function Message(props: {
|
|||
return;
|
||||
}
|
||||
|
||||
setIsReady(false);
|
||||
|
||||
decrypt(secret, content)
|
||||
.then((value) => {
|
||||
if (!active) {
|
||||
|
|
@ -73,14 +72,14 @@ export default function Message(props: {
|
|||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}, [decrypt, props.message.content, props.notEncrypted, sharedSecret]);
|
||||
}, [decrypt, props.message.content, props.notEncrypted, sharedSecretValue]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`w-full flex justify-start ${(props.message.failed || props.message.message_state === "awaiting") && "opacity-50"}`}
|
||||
className={`w-full flex justify-start transition-opacity duration-150 ${(props.message.failed || props.message.message_state === "awaiting") && "opacity-50"}`}
|
||||
>
|
||||
<div
|
||||
className={`flex gap-1 items-center justify-center animate-in fade-in duration-200 max-w-[80%] rounded-lg px-2 py-px whitespace-pre-wrap wrap-break-word ${
|
||||
className={`flex gap-1 items-center justify-center max-w-[80%] rounded-lg px-2 py-px whitespace-pre-wrap wrap-break-word ${
|
||||
props.message.sent_by_self
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "bg-muted"
|
||||
|
|
@ -91,9 +90,7 @@ export default function Message(props: {
|
|||
<TooltipContent>
|
||||
<p>Failed to send message</p>
|
||||
</TooltipContent>
|
||||
<TooltipTrigger asChild>
|
||||
<AlertTriangle size={17} />
|
||||
</TooltipTrigger>
|
||||
<TooltipTrigger render={<AlertTriangle size={17} />} />
|
||||
</Tooltip>
|
||||
)}
|
||||
{props.message.message_state === "awaiting" &&
|
||||
|
|
|
|||
|
|
@ -20,6 +20,33 @@ export const context = createContext<contextType | undefined>(undefined);
|
|||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
function updateMessageStateByTimestamp<
|
||||
T extends { timestamp: number; message_state: RawMessage["message_state"] },
|
||||
>(
|
||||
messages: T[],
|
||||
timestamp: number,
|
||||
messageState: RawMessage["message_state"],
|
||||
): { next: T[]; updated: boolean } {
|
||||
let updated = false;
|
||||
|
||||
const next = messages.map((item) => {
|
||||
if (item.timestamp !== timestamp || item.message_state === messageState) {
|
||||
return item;
|
||||
}
|
||||
|
||||
updated = true;
|
||||
return {
|
||||
...item,
|
||||
message_state: messageState,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
next,
|
||||
updated,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes Provider.
|
||||
* @param props Parameter props.
|
||||
|
|
@ -94,6 +121,19 @@ export default function Provider(props: { children: ReactNode }) {
|
|||
|
||||
const rawMessages = messages.data.messages;
|
||||
const sorted = [...rawMessages].sort((a, b) => a.timestamp - b.timestamp);
|
||||
|
||||
if (sorted.length > 0) {
|
||||
const fetchedTimestamps = new Set(sorted.map((item) => item.timestamp));
|
||||
|
||||
setLiveMessagesState((prev) => {
|
||||
const filtered = prev.filter(
|
||||
(liveMessage) => !fetchedTimestamps.has(liveMessage.timestamp),
|
||||
);
|
||||
|
||||
return filtered.length === prev.length ? prev : filtered;
|
||||
});
|
||||
}
|
||||
|
||||
return sorted;
|
||||
},
|
||||
[send, userIdValue],
|
||||
|
|
@ -137,36 +177,36 @@ export default function Provider(props: { children: ReactNode }) {
|
|||
return;
|
||||
}
|
||||
|
||||
const nextState = message.data as {
|
||||
chat_partner_id: number;
|
||||
timestamp: number;
|
||||
const rawData = message.data as {
|
||||
chat_partner_id: unknown;
|
||||
timestamp: unknown;
|
||||
message_state: RawMessage["message_state"];
|
||||
};
|
||||
|
||||
const nextState = {
|
||||
chat_partner_id: Number(rawData.chat_partner_id),
|
||||
timestamp: Number(rawData.timestamp),
|
||||
message_state: rawData.message_state,
|
||||
};
|
||||
|
||||
if (
|
||||
!Number.isFinite(nextState.chat_partner_id) ||
|
||||
!Number.isFinite(nextState.timestamp)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
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 { next, updated } = updateMessageStateByTimestamp(
|
||||
prev,
|
||||
nextState.timestamp,
|
||||
nextState.message_state,
|
||||
);
|
||||
return updated ? next : prev;
|
||||
});
|
||||
|
||||
const queryKey = ["chat-messages", String(userIdValue)] as const;
|
||||
|
|
@ -179,23 +219,19 @@ export default function Provider(props: { children: ReactNode }) {
|
|||
|
||||
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;
|
||||
}
|
||||
const pages = current.pages.map((page) => {
|
||||
const nextPage = updateMessageStateByTimestamp(
|
||||
page,
|
||||
nextState.timestamp,
|
||||
nextState.message_state,
|
||||
);
|
||||
|
||||
if (nextPage.updated) {
|
||||
updated = true;
|
||||
return {
|
||||
...historicalMessage,
|
||||
message_state: nextState.message_state,
|
||||
};
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return nextPage.next;
|
||||
});
|
||||
|
||||
if (!updated) {
|
||||
return current;
|
||||
|
|
|
|||
|
|
@ -54,10 +54,22 @@ export default function Screen() {
|
|||
setPrependAnchor(null);
|
||||
}, [chatUserId, clearLiveMessages]);
|
||||
|
||||
const messages = React.useMemo(
|
||||
() => [...historicalMessages, ...liveMessages()],
|
||||
[historicalMessages, liveMessages],
|
||||
);
|
||||
const liveMessagesSnapshot = liveMessages();
|
||||
|
||||
const messages = React.useMemo(() => {
|
||||
if (historicalMessages.length === 0) {
|
||||
return liveMessagesSnapshot;
|
||||
}
|
||||
|
||||
const historicalTimestamps = new Set(
|
||||
historicalMessages.map((message) => message.timestamp),
|
||||
);
|
||||
const liveWithoutDuplicates = liveMessagesSnapshot.filter(
|
||||
(message) => !historicalTimestamps.has(message.timestamp),
|
||||
);
|
||||
|
||||
return [...historicalMessages, ...liveWithoutDuplicates];
|
||||
}, [historicalMessages, liveMessagesSnapshot]);
|
||||
|
||||
const virtualizer = useVirtualizer({
|
||||
count: messages.length,
|
||||
|
|
@ -91,7 +103,7 @@ export default function Screen() {
|
|||
await messagesQuery.fetchNextPage();
|
||||
}, [messagesQuery, virtualizer]);
|
||||
|
||||
React.useEffect(() => {
|
||||
React.useLayoutEffect(() => {
|
||||
if (
|
||||
!scrollRef.current ||
|
||||
hasScrolledToBottomInitially ||
|
||||
|
|
@ -100,37 +112,25 @@ export default function Screen() {
|
|||
return;
|
||||
}
|
||||
|
||||
queueMicrotask(() => {
|
||||
if (!scrollRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
||||
setHasScrolledToBottomInitially(true);
|
||||
});
|
||||
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
||||
setHasScrolledToBottomInitially(true);
|
||||
}, [hasScrolledToBottomInitially, messages]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const count = liveMessages().length;
|
||||
React.useLayoutEffect(() => {
|
||||
const count = liveMessagesSnapshot.length;
|
||||
|
||||
if (!scrollRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (count > lastLiveMessageCount) {
|
||||
queueMicrotask(() => {
|
||||
if (!scrollRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
||||
});
|
||||
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
||||
}
|
||||
|
||||
setLastLiveMessageCount(count);
|
||||
}, [lastLiveMessageCount, liveMessages]);
|
||||
}, [lastLiveMessageCount, liveMessagesSnapshot.length]);
|
||||
|
||||
React.useEffect(() => {
|
||||
React.useLayoutEffect(() => {
|
||||
if (
|
||||
!scrollRef.current ||
|
||||
!prependAnchor ||
|
||||
|
|
@ -139,15 +139,9 @@ export default function Screen() {
|
|||
return;
|
||||
}
|
||||
|
||||
queueMicrotask(() => {
|
||||
if (!scrollRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const delta = virtualizer.getTotalSize() - prependAnchor.totalSize;
|
||||
scrollRef.current.scrollTop = prependAnchor.scrollTop + delta;
|
||||
setPrependAnchor(null);
|
||||
});
|
||||
const delta = virtualizer.getTotalSize() - prependAnchor.totalSize;
|
||||
scrollRef.current.scrollTop = prependAnchor.scrollTop + delta;
|
||||
setPrependAnchor(null);
|
||||
}, [messagesQuery.isFetchingNextPage, prependAnchor, virtualizer]);
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
import { z } from "zod";
|
||||
import { ttp } from "@tensamin/shared/data";
|
||||
|
||||
export type RawMessages = z.infer<
|
||||
typeof ttp.messages_get.response
|
||||
>["messages"];
|
||||
export type RawMessages = z.infer<typeof ttp.messages_get.response>["messages"];
|
||||
|
||||
export type RawMessage = RawMessages[number];
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue