(feat): migrate ttp to mtp
(wip): crypto migration
This commit is contained in:
parent
4e69b8ef77
commit
930663d495
30 changed files with 559 additions and 749 deletions
|
|
@ -11,13 +11,13 @@ import type { RawMessages } from "../values";
|
|||
*/
|
||||
export async function getMessages(
|
||||
send: BoundSendFn,
|
||||
amount: number,
|
||||
offset: number,
|
||||
Amount: number,
|
||||
Offset: number,
|
||||
UserId: number,
|
||||
): Promise<RawMessages> {
|
||||
const messages = await send("messages_get", {
|
||||
amount: amount,
|
||||
offset: offset,
|
||||
const messages = await send("MessagesGet", {
|
||||
Amount,
|
||||
Offset,
|
||||
UserId,
|
||||
});
|
||||
|
||||
|
|
@ -25,5 +25,5 @@ export async function getMessages(
|
|||
throw new Error(messages.type);
|
||||
}
|
||||
|
||||
return messages.data.messages;
|
||||
return messages.data.Messages;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,10 +72,9 @@ export default function InputComponent({
|
|||
log(3, "chat", "purple", "Message send init, adding live message ...");
|
||||
|
||||
const reference = addLiveMessage({
|
||||
height: 0,
|
||||
NotEncrypted: true,
|
||||
SendTime: time,
|
||||
content: currentValue,
|
||||
Content: currentValue,
|
||||
SentBySelf: true,
|
||||
MessageState: "awaiting",
|
||||
});
|
||||
|
|
@ -94,9 +93,8 @@ export default function InputComponent({
|
|||
|
||||
log(3, "chat", "purple", "Content encrypted, sending message...");
|
||||
|
||||
send("message_send", {
|
||||
height: 0,
|
||||
content: encryptedContext,
|
||||
send("MessageSend", {
|
||||
Content: encryptedContext,
|
||||
ReceiverId: userId,
|
||||
SendTime: time,
|
||||
}).catch((e) => {
|
||||
|
|
|
|||
|
|
@ -64,14 +64,14 @@ function MessageComponent({
|
|||
const [isValidURL, setIsValidURL] = useState(false);
|
||||
useEffect(() => {
|
||||
try {
|
||||
if (message.content.split(" ").length > 1) throw new Error();
|
||||
if (message.Content.split(" ").length > 1) throw new Error();
|
||||
|
||||
new URL(message.content);
|
||||
new URL(message.Content);
|
||||
setIsValidURL(true);
|
||||
} catch {
|
||||
setIsValidURL(false);
|
||||
}
|
||||
}, [message.content]);
|
||||
}, [message.Content]);
|
||||
|
||||
// Message states
|
||||
const { load } = useStorage();
|
||||
|
|
@ -82,13 +82,13 @@ function MessageComponent({
|
|||
if (readConfirmations) {
|
||||
if (!user?.UserId) return;
|
||||
|
||||
await send("message_state", {
|
||||
await send("MessageState", {
|
||||
ChatPartnerId: user?.UserId,
|
||||
SendTime: message.SendTime,
|
||||
MessageState: "read",
|
||||
});
|
||||
} else {
|
||||
await send("message_state", {
|
||||
await send("MessageState", {
|
||||
ChatPartnerId: user?.UserId,
|
||||
SendTime: message.SendTime,
|
||||
MessageState: "received",
|
||||
|
|
@ -131,7 +131,7 @@ function MessageComponent({
|
|||
className={`${grouped ? "" : "pt-3"} w-full flex justify-start transition-opacity duration-150 ${opacityClass}`}
|
||||
>
|
||||
<MessageContextMenu
|
||||
content={message.content}
|
||||
content={message.Content}
|
||||
messageId={message.SendTime}
|
||||
>
|
||||
<div
|
||||
|
|
@ -154,9 +154,9 @@ function MessageComponent({
|
|||
</p>
|
||||
) : (
|
||||
<Avatar className="mr-1 mb-auto mt-1 w-10">
|
||||
<AvatarImage src={user.avatar} />
|
||||
<AvatarImage src={user.Avatar} />
|
||||
<AvatarFallback>
|
||||
{user.display.slice(0, 2).toUpperCase()}
|
||||
{user.Display.slice(0, 2).toUpperCase()}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
)}
|
||||
|
|
@ -190,7 +190,7 @@ function MessageComponent({
|
|||
</Card>
|
||||
{!grouped && (
|
||||
<div className="flex items-center gap-1">
|
||||
<p className="font-medium">{user.display}</p>
|
||||
<p className="font-medium">{user.Display}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{new Date(message.SendTime).toLocaleString([], {
|
||||
hour: "2-digit",
|
||||
|
|
@ -214,9 +214,9 @@ function MessageComponent({
|
|||
</div>
|
||||
)}
|
||||
{isValidURL ? (
|
||||
<Media link={message.content} />
|
||||
<Media link={message.Content} />
|
||||
) : (
|
||||
<Text value={message.content} />
|
||||
<Text value={message.Content} />
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
|
|
@ -232,8 +232,7 @@ function MessageComponent({
|
|||
export default React.memo(MessageComponent, (prev, next) => {
|
||||
return (
|
||||
prev.message.SendTime === next.message.SendTime &&
|
||||
prev.message.content === next.message.content &&
|
||||
prev.message.height === next.message.height &&
|
||||
prev.message.Content === next.message.Content &&
|
||||
prev.message.SentBySelf === next.message.SentBySelf &&
|
||||
prev.message.MessageState === next.message.MessageState &&
|
||||
prev.message.failed === next.message.failed &&
|
||||
|
|
|
|||
|
|
@ -50,18 +50,16 @@ function updateMessageStateBySendTime<
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes Provider.
|
||||
* @param props Parameter props.
|
||||
* @returns unknown.
|
||||
*/
|
||||
export default function Provider(props: { children: ReactNode }) {
|
||||
export default function Provider({ children }: { children: ReactNode }) {
|
||||
const { getSharedSecret, decryptText } = useCrypto();
|
||||
const { get } = useUser();
|
||||
const { load } = useStorage();
|
||||
const { send, subscribePush } = useMTP();
|
||||
const { moveUserIdToTop } = useSession();
|
||||
|
||||
const [error, setError] = useState("");
|
||||
const [errorDescription, setErrorDescription] = useState("");
|
||||
|
||||
const [liveMessagesState, setLiveMessagesState] = useState<LiveMessage[]>([]);
|
||||
const [currentSharedSecretState, setCurrentSharedSecretState] = useState<{
|
||||
userId: number;
|
||||
|
|
@ -101,21 +99,40 @@ export default function Provider(props: { children: ReactNode }) {
|
|||
try {
|
||||
const recipientData = await get(userIdValue);
|
||||
const ownId = await load("user_id");
|
||||
const privateKey = await load("private_key");
|
||||
const privateKey = await load("mtp_keyring");
|
||||
const ownData = await get(ownId);
|
||||
|
||||
log(3, "chat", "purple", "Getting shared secret...", {
|
||||
recipientData,
|
||||
ownData,
|
||||
});
|
||||
|
||||
const sharedSecret = await getSharedSecret(
|
||||
privateKey,
|
||||
ownData.PublicKey,
|
||||
recipientData.PublicKey,
|
||||
);
|
||||
|
||||
log(2, "chat", "purple", "Got shared secret", {
|
||||
sharedSecret,
|
||||
});
|
||||
|
||||
if (active) {
|
||||
setCurrentSharedSecretState({
|
||||
userId: userIdValue,
|
||||
value: sharedSecret,
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
} catch (err) {
|
||||
log(
|
||||
1,
|
||||
"chat",
|
||||
"red",
|
||||
"An unknown error occured while getting a shared secret",
|
||||
err,
|
||||
);
|
||||
setError(err instanceof Error ? err.name : "Unknown Error");
|
||||
setErrorDescription(err instanceof Error ? err.message : String(err));
|
||||
if (active) {
|
||||
setCurrentSharedSecretState({
|
||||
userId: userIdValue,
|
||||
|
|
@ -132,9 +149,9 @@ export default function Provider(props: { children: ReactNode }) {
|
|||
|
||||
const getMessages = useCallback(
|
||||
async (amount: number, offset: number) => {
|
||||
const messages = await send("messages_get", {
|
||||
amount,
|
||||
offset,
|
||||
const messages = await send("MessagesGet", {
|
||||
Amount: amount,
|
||||
Offset: offset,
|
||||
UserId: userIdValue,
|
||||
});
|
||||
|
||||
|
|
@ -142,7 +159,7 @@ export default function Provider(props: { children: ReactNode }) {
|
|||
throw new Error(messages.type);
|
||||
}
|
||||
|
||||
const rawMessages = messages.data.messages;
|
||||
const rawMessages = messages.data.Messages;
|
||||
const sorted = [...rawMessages].sort((a, b) => a.SendTime - b.SendTime);
|
||||
|
||||
if (sorted.length > 0) {
|
||||
|
|
@ -162,7 +179,7 @@ export default function Provider(props: { children: ReactNode }) {
|
|||
try {
|
||||
return {
|
||||
...message,
|
||||
content: await decryptText(currentSharedSecret, message.content),
|
||||
content: await decryptText(currentSharedSecret, message.Content),
|
||||
};
|
||||
} catch {
|
||||
return message;
|
||||
|
|
@ -214,7 +231,7 @@ export default function Provider(props: { children: ReactNode }) {
|
|||
// Get live updates for message states
|
||||
useEffect(() => {
|
||||
return subscribePush((message) => {
|
||||
if (message.type !== "message_state") {
|
||||
if (message.type !== "MessageState") {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -318,9 +335,11 @@ export default function Provider(props: { children: ReactNode }) {
|
|||
sharedSecret: currentSharedSecret,
|
||||
userId: userIdValue,
|
||||
inputBoxRef,
|
||||
error,
|
||||
errorDescription,
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
{children}
|
||||
</context.Provider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
|
|
@ -336,13 +355,10 @@ type contextType = {
|
|||
sharedSecret: string;
|
||||
userId: number;
|
||||
inputBoxRef: React.RefObject<HTMLDivElement | null>;
|
||||
error: string;
|
||||
errorDescription: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Executes useChat.
|
||||
* @param none This function has no parameters.
|
||||
* @returns contextType.
|
||||
*/
|
||||
export function useChat(): contextType {
|
||||
const ctx = useContext(context);
|
||||
if (!ctx) {
|
||||
|
|
|
|||
|
|
@ -22,12 +22,6 @@ type MessageChunk = {
|
|||
startIndex: number;
|
||||
};
|
||||
|
||||
function getEstimatedMessageHeight(message: { height?: number }) {
|
||||
return typeof message.height === "number" && Number.isFinite(message.height)
|
||||
? Math.max(1, Math.ceil(message.height))
|
||||
: FALLBACK_MESSAGE_HEIGHT;
|
||||
}
|
||||
|
||||
function shouldFetchPreviousPage({
|
||||
entry,
|
||||
hasNextPage,
|
||||
|
|
@ -80,8 +74,15 @@ function buildMessageChunks(
|
|||
* @returns Chat screen JSX.
|
||||
*/
|
||||
export default function Screen() {
|
||||
const { getMessages, liveMessages, clearLiveMessages, userId, sharedSecret } =
|
||||
useChat();
|
||||
const {
|
||||
getMessages,
|
||||
liveMessages,
|
||||
clearLiveMessages,
|
||||
userId,
|
||||
sharedSecret,
|
||||
error,
|
||||
errorDescription,
|
||||
} = useChat();
|
||||
const { get: getUser } = useUser();
|
||||
const { load } = useStorage();
|
||||
|
||||
|
|
@ -251,16 +252,7 @@ export default function Screen() {
|
|||
return FALLBACK_MESSAGE_HEIGHT;
|
||||
}
|
||||
|
||||
const chunk = messageChunks[index];
|
||||
|
||||
if (!chunk) {
|
||||
return FALLBACK_MESSAGE_HEIGHT;
|
||||
}
|
||||
|
||||
return chunk.messages.reduce(
|
||||
(total, message) => total + getEstimatedMessageHeight(message),
|
||||
0,
|
||||
);
|
||||
return FALLBACK_MESSAGE_HEIGHT;
|
||||
},
|
||||
[messageChunks, shouldShowConversationStart],
|
||||
);
|
||||
|
|
@ -479,112 +471,122 @@ export default function Screen() {
|
|||
|
||||
if (!hasValidChatUser) {
|
||||
return (
|
||||
<div className="w-full h-full flex items-center justify-center text-xl text-foreground/80">
|
||||
Invalid user
|
||||
<div className="w-full h-full flex flex-col gap-2 items-center justify-center">
|
||||
<p className="font-semibold text-xl">Invalid User</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative flex h-full min-h-0 w-full flex-col overflow-hidden">
|
||||
<div
|
||||
ref={scrollRef}
|
||||
id="chat_container"
|
||||
className="min-h-0 flex-1 overflow-y-auto"
|
||||
style={{
|
||||
overflowAnchor: "none",
|
||||
paddingTop: "22px",
|
||||
transform: "scaleY(-1)",
|
||||
}}
|
||||
onScroll={handleContainerScroll}
|
||||
>
|
||||
<div
|
||||
className="relative w-full"
|
||||
style={{ height: `${contentHeight}px` }}
|
||||
>
|
||||
{error !== "" && errorDescription !== "" ? (
|
||||
<div className="w-full h-full flex flex-col gap-2 items-center justify-center">
|
||||
<p className="font-semibold text-xl">{error}</p>
|
||||
<p className="text-muted-foreground text-lg">{errorDescription}</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div
|
||||
ref={topSentinelRef}
|
||||
className="absolute bottom-0 left-0 h-px w-full"
|
||||
/>
|
||||
{virtualizer.getVirtualItems().map((virtualRow) => {
|
||||
if (
|
||||
shouldShowConversationStart &&
|
||||
virtualRow.index === messageChunks.length
|
||||
) {
|
||||
return (
|
||||
<div
|
||||
key="conversation-start"
|
||||
data-index={virtualRow.index}
|
||||
ref={virtualizer.measureElement}
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: "100%",
|
||||
transform: `translateY(${verticalOffset + virtualRow.start}px)`,
|
||||
}}
|
||||
>
|
||||
<div className="w-full flex justify-start scale-y-[-1]">
|
||||
<div className="text-sm text-foreground/55 px-2.5">
|
||||
Conversation start
|
||||
ref={scrollRef}
|
||||
id="chat_container"
|
||||
className="min-h-0 flex-1 overflow-y-auto"
|
||||
style={{
|
||||
overflowAnchor: "none",
|
||||
paddingTop: "22px",
|
||||
transform: "scaleY(-1)",
|
||||
}}
|
||||
onScroll={handleContainerScroll}
|
||||
>
|
||||
<div
|
||||
className="relative w-full"
|
||||
style={{ height: `${contentHeight}px` }}
|
||||
>
|
||||
<div
|
||||
ref={topSentinelRef}
|
||||
className="absolute bottom-0 left-0 h-px w-full"
|
||||
/>
|
||||
{virtualizer.getVirtualItems().map((virtualRow) => {
|
||||
if (
|
||||
shouldShowConversationStart &&
|
||||
virtualRow.index === messageChunks.length
|
||||
) {
|
||||
return (
|
||||
<div
|
||||
key="conversation-start"
|
||||
data-index={virtualRow.index}
|
||||
ref={virtualizer.measureElement}
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: "100%",
|
||||
transform: `translateY(${verticalOffset + virtualRow.start}px)`,
|
||||
}}
|
||||
>
|
||||
<div className="w-full flex justify-start scale-y-[-1]">
|
||||
<div className="text-sm text-foreground/55 px-2.5">
|
||||
Conversation start
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const chunkIndex = virtualRow.index;
|
||||
const chunk = messageChunks[chunkIndex];
|
||||
if (!chunk) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={chunk.key}
|
||||
data-index={virtualRow.index}
|
||||
className="flex flex-col"
|
||||
ref={virtualizer.measureElement}
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: "100%",
|
||||
transform: `translateY(${verticalOffset + virtualRow.start}px)`,
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col scale-y-[-1]">
|
||||
{chunk.messages.map((message, chunkMessageIndex) => {
|
||||
const messageIndex =
|
||||
chunk.startIndex + chunkMessageIndex;
|
||||
const lastMessage = messages[messageIndex - 1];
|
||||
const isGrouped =
|
||||
lastMessage &&
|
||||
lastMessage.SentBySelf === message.SentBySelf &&
|
||||
Math.round(lastMessage.SendTime / 10000) ===
|
||||
Math.round(message.SendTime / 10000);
|
||||
|
||||
return (
|
||||
<Message
|
||||
key={getMessageRenderKey(message)}
|
||||
grouped={isGrouped}
|
||||
message={message}
|
||||
user={
|
||||
message.SentBySelf
|
||||
? (messageUsers.own ?? null)
|
||||
: (messageUsers.peer ?? null)
|
||||
}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const chunkIndex = virtualRow.index;
|
||||
const chunk = messageChunks[chunkIndex];
|
||||
if (!chunk) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={chunk.key}
|
||||
data-index={virtualRow.index}
|
||||
className="flex flex-col"
|
||||
ref={virtualizer.measureElement}
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: "100%",
|
||||
transform: `translateY(${verticalOffset + virtualRow.start}px)`,
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col scale-y-[-1]">
|
||||
{chunk.messages.map((message, chunkMessageIndex) => {
|
||||
const messageIndex = chunk.startIndex + chunkMessageIndex;
|
||||
const lastMessage = messages[messageIndex - 1];
|
||||
const isGrouped =
|
||||
lastMessage &&
|
||||
lastMessage.SentBySelf === message.SentBySelf &&
|
||||
Math.round(lastMessage.SendTime / 10000) ===
|
||||
Math.round(message.SendTime / 10000);
|
||||
|
||||
return (
|
||||
<Message
|
||||
key={getMessageRenderKey(message)}
|
||||
grouped={isGrouped}
|
||||
message={message}
|
||||
user={
|
||||
message.SentBySelf
|
||||
? (messageUsers.own ?? null)
|
||||
: (messageUsers.peer ?? null)
|
||||
}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className="z-10 shrink-0">
|
||||
<InputComponent setValue={setValue} value={value} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className="z-10 shrink-0">
|
||||
<InputComponent setValue={setValue} value={value} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { z } from "zod";
|
||||
import { mtp } from "@tensamin/shared/data";
|
||||
|
||||
export type RawMessages = z.infer<typeof mtp.messages_get.response>["messages"];
|
||||
export type RawMessages = z.infer<typeof mtp.MessagesGet.response>["Messages"];
|
||||
|
||||
export type RawMessage = RawMessages[number];
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue