(fix): live reactions, message and call invites
(fix): call ui
This commit is contained in:
parent
5a26c2d401
commit
f1874042ba
10 changed files with 203 additions and 88 deletions
|
|
@ -332,7 +332,7 @@ function createCallTrayIcon(color: string, speaking: boolean) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function CallInit() {
|
function CallInit() {
|
||||||
useInitializeCall();
|
const callInvitePopup = useInitializeCall();
|
||||||
|
|
||||||
const { load } = useStorage();
|
const { load } = useStorage();
|
||||||
const {
|
const {
|
||||||
|
|
@ -395,7 +395,7 @@ function CallInit() {
|
||||||
});
|
});
|
||||||
}, [inCall, primaryColor, speaking]);
|
}, [inCall, primaryColor, speaking]);
|
||||||
|
|
||||||
return null;
|
return callInvitePopup;
|
||||||
}
|
}
|
||||||
|
|
||||||
const rootRoute = createRootRoute({
|
const rootRoute = createRootRoute({
|
||||||
|
|
|
||||||
41
packages/cache/src/sync.tsx
vendored
41
packages/cache/src/sync.tsx
vendored
|
|
@ -18,7 +18,6 @@ export default function CacheSync() {
|
||||||
const { load } = useStorage();
|
const { load } = useStorage();
|
||||||
const [accountId, setAccountId] = useState(0);
|
const [accountId, setAccountId] = useState(0);
|
||||||
const queueRef = useRef(Promise.resolve());
|
const queueRef = useRef(Promise.resolve());
|
||||||
const reactionPartnersRef = useRef(new Map<number, number>());
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void load("user_id").then(setAccountId);
|
void load("user_id").then(setAccountId);
|
||||||
|
|
@ -195,23 +194,6 @@ export default function CacheSync() {
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === "MessageGet") {
|
|
||||||
const message = result as unknown as CachedMessage;
|
|
||||||
const mappedPartner = reactionPartnersRef.current.get(message.SendTime);
|
|
||||||
reactionPartnersRef.current.delete(message.SendTime);
|
|
||||||
if (mappedPartner) {
|
|
||||||
await insertMessage(mappedPartner, message);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const windows = await secureCache().conversations.list();
|
|
||||||
const window = windows.find((candidate) =>
|
|
||||||
candidate.Messages.some(
|
|
||||||
(cached) => cached.SendTime === message.SendTime,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
if (window) await insertMessage(window.UserId, message);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[accountId, insertMessage, removeMessage, replaceMessage, secureCache],
|
[accountId, insertMessage, removeMessage, replaceMessage, secureCache],
|
||||||
);
|
);
|
||||||
|
|
@ -259,13 +241,28 @@ export default function CacheSync() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (message.type === "MessageReactionLive") {
|
if (message.type === "MessageReactionLive") {
|
||||||
reactionPartnersRef.current.set(
|
const partnerId = Number(data.ChatPartnerId);
|
||||||
Number(data.SendTime),
|
const sendTime = Number(data.SendTime);
|
||||||
Number(data.ChatPartnerId),
|
const senderId = Number(data.SenderId);
|
||||||
|
const reaction = String(data.Reaction);
|
||||||
|
const cache = secureCache();
|
||||||
|
const window = await cache.conversations.get(partnerId);
|
||||||
|
const target = window?.Messages.find(
|
||||||
|
(candidate) => candidate.SendTime === sendTime,
|
||||||
);
|
);
|
||||||
|
if (!window || !target) return;
|
||||||
|
|
||||||
|
const reactions = (target.Reactions ?? []).filter(
|
||||||
|
(candidate) =>
|
||||||
|
candidate.SenderId !== senderId || candidate.Reaction !== reaction,
|
||||||
|
);
|
||||||
|
if (data.Accepted === true) {
|
||||||
|
reactions.push({ SenderId: senderId, Reaction: reaction });
|
||||||
|
}
|
||||||
|
await replaceMessage(partnerId, sendTime, { Reactions: reactions });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[accountId, insertMessage, removeMessage, replaceMessage],
|
[accountId, insertMessage, removeMessage, replaceMessage, secureCache],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,10 @@ export default function InvitePopup({
|
||||||
loading={null}
|
loading={null}
|
||||||
component={(user) => (
|
component={(user) => (
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
<DialogContent className="flex flex-col gap-5 items-center justify-center w-65 h-80">
|
<DialogContent
|
||||||
|
showCloseButton={false}
|
||||||
|
className="flex flex-col gap-5 items-center justify-center w-65 h-80"
|
||||||
|
>
|
||||||
<Avatar className="size-30">
|
<Avatar className="size-30">
|
||||||
<AvatarImage src={user.Avatar} />
|
<AvatarImage src={user.Avatar} />
|
||||||
<AvatarFallback className="text-5xl">
|
<AvatarFallback className="text-5xl">
|
||||||
|
|
@ -36,14 +39,14 @@ export default function InvitePopup({
|
||||||
<p className="text-xl font-medium">{user.Display}</p>
|
<p className="text-xl font-medium">{user.Display}</p>
|
||||||
<div className="w-full flex justify-center gap-3">
|
<div className="w-full flex justify-center gap-3">
|
||||||
<Button
|
<Button
|
||||||
className="w-14 h-14"
|
className="w-13 h-13!"
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
onClick={() => onAccept(false)}
|
onClick={() => onAccept(false)}
|
||||||
>
|
>
|
||||||
<X className="size-5" />
|
<X className="size-5" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
className="w-14 h-14"
|
className="w-13 h-13!"
|
||||||
variant="subtleDefault"
|
variant="subtleDefault"
|
||||||
onClick={() => onAccept(true)}
|
onClick={() => onAccept(true)}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -268,7 +268,7 @@ export default function Base({
|
||||||
flush || fill ? "rounded-none" : "rounded-md"
|
flush || fill ? "rounded-none" : "rounded-md"
|
||||||
} ${
|
} ${
|
||||||
type === "user" && isSpeaking
|
type === "user" && isSpeaking
|
||||||
? "border-4 border-(--primary-foreground-alt)/75"
|
? "border-4 border-(--primary-foreground-alt)/75!"
|
||||||
: fill
|
: fill
|
||||||
? "border-0"
|
? "border-0"
|
||||||
: "border"
|
: "border"
|
||||||
|
|
|
||||||
|
|
@ -155,8 +155,8 @@ export function getRoom(): Room {
|
||||||
}
|
}
|
||||||
|
|
||||||
const remoteAudioElements = new Map<string, HTMLMediaElement>();
|
const remoteAudioElements = new Map<string, HTMLMediaElement>();
|
||||||
let outgoingJingle: HTMLAudioElement | null = null;
|
let callJingle: HTMLAudioElement | null = null;
|
||||||
let outgoingJingleGeneration = 0;
|
let callJingleGeneration = 0;
|
||||||
|
|
||||||
const CALL_SECRET_VERSION = 1;
|
const CALL_SECRET_VERSION = 1;
|
||||||
const SCREEN_SHARE_PREVIEW_MAX_WIDTH = 320;
|
const SCREEN_SHARE_PREVIEW_MAX_WIDTH = 320;
|
||||||
|
|
@ -164,32 +164,32 @@ const SCREEN_SHARE_PREVIEW_MAX_HEIGHT = 180;
|
||||||
const SCREEN_SHARE_PREVIEW_QUALITY = 0.7;
|
const SCREEN_SHARE_PREVIEW_QUALITY = 0.7;
|
||||||
const SCREEN_SHARE_PREVIEW_TIMEOUT_MS = 5000;
|
const SCREEN_SHARE_PREVIEW_TIMEOUT_MS = 5000;
|
||||||
|
|
||||||
async function startOutgoingJingle() {
|
async function startCallJingle(shouldPlay: () => boolean) {
|
||||||
const generation = ++outgoingJingleGeneration;
|
const generation = ++callJingleGeneration;
|
||||||
stopSound(outgoingJingle);
|
stopSound(callJingle);
|
||||||
outgoingJingle = null;
|
callJingle = null;
|
||||||
|
|
||||||
const jingle = await requireRuntime(useCall.getState().runtime).load(
|
const jingle = await requireRuntime(useCall.getState().runtime).load(
|
||||||
"settings.call_jingle",
|
"settings.call_jingle",
|
||||||
);
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
generation !== outgoingJingleGeneration ||
|
generation !== callJingleGeneration ||
|
||||||
useCall.getState().invitedUserId == null
|
!shouldPlay()
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
outgoingJingle = playSound(
|
callJingle = playSound(
|
||||||
jingle === "jingle_2" ? "call_jingle_2" : "call_jingle_1",
|
jingle === "jingle_2" ? "call_jingle_2" : "call_jingle_1",
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopOutgoingJingle() {
|
function stopCallJingle() {
|
||||||
outgoingJingleGeneration += 1;
|
callJingleGeneration += 1;
|
||||||
stopSound(outgoingJingle);
|
stopSound(callJingle);
|
||||||
outgoingJingle = null;
|
callJingle = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function protocolBytes(bytes: Uint8Array): Uint8Array<ArrayBuffer> {
|
function protocolBytes(bytes: Uint8Array): Uint8Array<ArrayBuffer> {
|
||||||
|
|
@ -918,7 +918,7 @@ export async function connect(callId: string) {
|
||||||
|
|
||||||
// Tear down the active call session and return the store to a closed state.
|
// Tear down the active call session and return the store to a closed state.
|
||||||
export async function disconnect() {
|
export async function disconnect() {
|
||||||
stopOutgoingJingle();
|
stopCallJingle();
|
||||||
disposeSpeakingDetector();
|
disposeSpeakingDetector();
|
||||||
await clearScreenSharePreview();
|
await clearScreenSharePreview();
|
||||||
|
|
||||||
|
|
@ -993,7 +993,7 @@ export async function joinCall(
|
||||||
});
|
});
|
||||||
|
|
||||||
if (sendInvite && !existingCallId) {
|
if (sendInvite && !existingCallId) {
|
||||||
void startOutgoingJingle();
|
void startCallJingle(() => useCall.getState().invitedUserId != null);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (callSecret) {
|
if (callSecret) {
|
||||||
|
|
@ -1231,12 +1231,14 @@ export function useInitializeCall() {
|
||||||
useCall.setState({
|
useCall.setState({
|
||||||
incomingCallInvite: { callId, callSecret, senderId },
|
incomingCallInvite: { callId, callSecret, senderId },
|
||||||
});
|
});
|
||||||
|
void startCallJingle(() => useCall.getState().incomingCallInvite != null);
|
||||||
},
|
},
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
const setInvitePopupOpen = useCallback((open: boolean) => {
|
const setInvitePopupOpen = useCallback((open: boolean) => {
|
||||||
if (!open) {
|
if (!open) {
|
||||||
|
stopCallJingle();
|
||||||
useCall.setState({ incomingCallInvite: null });
|
useCall.setState({ incomingCallInvite: null });
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
@ -1245,6 +1247,7 @@ export function useInitializeCall() {
|
||||||
(accepted: boolean) => {
|
(accepted: boolean) => {
|
||||||
const invite = useCall.getState().incomingCallInvite;
|
const invite = useCall.getState().incomingCallInvite;
|
||||||
|
|
||||||
|
stopCallJingle();
|
||||||
useCall.setState({ incomingCallInvite: null });
|
useCall.setState({ incomingCallInvite: null });
|
||||||
|
|
||||||
if (!invite) {
|
if (!invite) {
|
||||||
|
|
@ -1292,6 +1295,11 @@ export function useInitializeCall() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const currentCall = useCall.getState();
|
||||||
|
if (currentCall.callId === CallId && currentCall.state !== "closed") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
showCallingScreen(
|
showCallingScreen(
|
||||||
CallId,
|
CallId,
|
||||||
normalizeWrappedCallSecret(CallSecret),
|
normalizeWrappedCallSecret(CallSecret),
|
||||||
|
|
@ -1434,7 +1442,7 @@ export function useInitializeCall() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const onDisconnected = () => {
|
const onDisconnected = () => {
|
||||||
stopOutgoingJingle();
|
stopCallJingle();
|
||||||
playSound("call_leave");
|
playSound("call_leave");
|
||||||
useCall.setState({ state: "closed" });
|
useCall.setState({ state: "closed" });
|
||||||
syncParticipantState();
|
syncParticipantState();
|
||||||
|
|
@ -1445,7 +1453,7 @@ export function useInitializeCall() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const onParticipantConnected = () => {
|
const onParticipantConnected = () => {
|
||||||
stopOutgoingJingle();
|
stopCallJingle();
|
||||||
playSound("call_join");
|
playSound("call_join");
|
||||||
syncAllRemoteTrackSubscriptions();
|
syncAllRemoteTrackSubscriptions();
|
||||||
syncParticipantState();
|
syncParticipantState();
|
||||||
|
|
|
||||||
|
|
@ -185,7 +185,7 @@ export default function View() {
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Base
|
<Base
|
||||||
fill={isImmersiveFocusedView}
|
fill
|
||||||
flush={isImmersiveFocusedView || isFocusedTileFlush}
|
flush={isImmersiveFocusedView || isFocusedTileFlush}
|
||||||
type={focusedTileType}
|
type={focusedTileType}
|
||||||
participant={focusedParticipant}
|
participant={focusedParticipant}
|
||||||
|
|
|
||||||
|
|
@ -102,6 +102,34 @@ function updateMessagesBySendTime<T extends EditableMessage>(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateMessageReaction<T extends EditableMessage>(
|
||||||
|
message: T,
|
||||||
|
reaction: string,
|
||||||
|
senderId: number,
|
||||||
|
accepted: boolean,
|
||||||
|
): T {
|
||||||
|
const current = message.Reactions ?? [];
|
||||||
|
const withoutReaction = current.filter(
|
||||||
|
(item) => item.Reaction !== reaction || item.SenderId !== senderId,
|
||||||
|
);
|
||||||
|
const next = accepted
|
||||||
|
? [...withoutReaction, { Reaction: reaction, SenderId: senderId }]
|
||||||
|
: withoutReaction;
|
||||||
|
|
||||||
|
if (
|
||||||
|
next.length === current.length &&
|
||||||
|
next.every(
|
||||||
|
(item, index) =>
|
||||||
|
item.Reaction === current[index]?.Reaction &&
|
||||||
|
item.SenderId === current[index]?.SenderId,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...message, Reactions: next };
|
||||||
|
}
|
||||||
|
|
||||||
function assertProtocolSuccess(type: string, response: { type: string }) {
|
function assertProtocolSuccess(type: string, response: { type: string }) {
|
||||||
if (response.type.startsWith("Error")) {
|
if (response.type.startsWith("Error")) {
|
||||||
throw new Error(`${type} failed: ${response.type}`);
|
throw new Error(`${type} failed: ${response.type}`);
|
||||||
|
|
@ -553,6 +581,51 @@ export default function Provider({ children }: { children: ReactNode }) {
|
||||||
[currentChatSecret, userIdValue],
|
[currentChatSecret, userIdValue],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const applyLiveReaction = useCallback(
|
||||||
|
(
|
||||||
|
sendTime: number,
|
||||||
|
reaction: string,
|
||||||
|
senderId: number,
|
||||||
|
accepted: boolean,
|
||||||
|
) => {
|
||||||
|
setLiveMessagesState((current) =>
|
||||||
|
current.map((message) =>
|
||||||
|
message.SendTime === sendTime
|
||||||
|
? updateMessageReaction(message, reaction, senderId, accepted)
|
||||||
|
: message,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const queryKey = [
|
||||||
|
"chat-messages",
|
||||||
|
String(userIdValue),
|
||||||
|
currentChatSecret !== null,
|
||||||
|
] as const;
|
||||||
|
queryClient.setQueryData<InfiniteData<RawMessages>>(
|
||||||
|
queryKey,
|
||||||
|
(current) =>
|
||||||
|
current
|
||||||
|
? {
|
||||||
|
...current,
|
||||||
|
pages: current.pages.map((page) =>
|
||||||
|
page.map((message) =>
|
||||||
|
message.SendTime === sendTime
|
||||||
|
? updateMessageReaction(
|
||||||
|
message,
|
||||||
|
reaction,
|
||||||
|
senderId,
|
||||||
|
accepted,
|
||||||
|
)
|
||||||
|
: message,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
: current,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[currentChatSecret, userIdValue],
|
||||||
|
);
|
||||||
|
|
||||||
const deleteMessage = useCallback(
|
const deleteMessage = useCallback(
|
||||||
async (sendTime: number) => {
|
async (sendTime: number) => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -743,20 +816,28 @@ export default function Provider({ children }: { children: ReactNode }) {
|
||||||
const rawData = message.data as {
|
const rawData = message.data as {
|
||||||
ChatPartnerId: unknown;
|
ChatPartnerId: unknown;
|
||||||
SendTime: unknown;
|
SendTime: unknown;
|
||||||
|
Reaction: string;
|
||||||
|
SenderId: unknown;
|
||||||
|
Accepted: boolean;
|
||||||
};
|
};
|
||||||
const chatPartnerId = Number(rawData.ChatPartnerId);
|
const chatPartnerId = Number(rawData.ChatPartnerId);
|
||||||
const sendTime = Number(rawData.SendTime);
|
const sendTime = Number(rawData.SendTime);
|
||||||
|
const senderId = Number(rawData.SenderId);
|
||||||
|
|
||||||
if (chatPartnerId !== userIdValue || !Number.isFinite(sendTime)) return;
|
if (
|
||||||
|
chatPartnerId !== userIdValue ||
|
||||||
|
!Number.isFinite(sendTime) ||
|
||||||
|
!Number.isFinite(senderId)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
void send("MessageGet", { SendTime: sendTime })
|
applyLiveReaction(
|
||||||
.then((response) => {
|
sendTime,
|
||||||
assertProtocolSuccess("MessageGet", response);
|
rawData.Reaction,
|
||||||
editMessage(sendTime, { Reactions: response.data.Reactions ?? [] });
|
senderId,
|
||||||
})
|
rawData.Accepted,
|
||||||
.catch((err) => {
|
);
|
||||||
log(1, "chat", "red", "Failed to refresh message reactions", err);
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -819,9 +900,9 @@ export default function Provider({ children }: { children: ReactNode }) {
|
||||||
});
|
});
|
||||||
}, [
|
}, [
|
||||||
currentChatSecret,
|
currentChatSecret,
|
||||||
|
applyLiveReaction,
|
||||||
editMessage,
|
editMessage,
|
||||||
removeMessage,
|
removeMessage,
|
||||||
send,
|
|
||||||
subscribePush,
|
subscribePush,
|
||||||
userIdValue,
|
userIdValue,
|
||||||
]);
|
]);
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,19 @@ export type BoundSendFn = <T extends keyof Schemas & string>(
|
||||||
options?: { id?: number },
|
options?: { id?: number },
|
||||||
) => Promise<ProtocolMessage<T>>;
|
) => Promise<ProtocolMessage<T>>;
|
||||||
|
|
||||||
export type PushHandler = (message: ProtocolMessage) => void;
|
export type PushHandler = (
|
||||||
|
message: ProtocolMessage,
|
||||||
|
) => void | Promise<void>;
|
||||||
|
|
||||||
|
const PUSH_TYPES = [
|
||||||
|
"MessageLive",
|
||||||
|
"MessageEditLive",
|
||||||
|
"MessageReactionLive",
|
||||||
|
"MessageDeleteLive",
|
||||||
|
"MessageState",
|
||||||
|
"CallInvite",
|
||||||
|
"ErrorNoIota",
|
||||||
|
] as const;
|
||||||
|
|
||||||
export type MTPExchange = {
|
export type MTPExchange = {
|
||||||
type: keyof Schemas & string;
|
type: keyof Schemas & string;
|
||||||
|
|
@ -157,6 +169,7 @@ export function Provider(props: {
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
const interceptorsRef = useRef(new Set<MTPInterceptor>());
|
const interceptorsRef = useRef(new Set<MTPInterceptor>());
|
||||||
|
const pushHandlersRef = useRef(new Set<PushHandler>());
|
||||||
|
|
||||||
const connected = readyState === ConnectionState.Connected;
|
const connected = readyState === ConnectionState.Connected;
|
||||||
|
|
||||||
|
|
@ -197,28 +210,8 @@ export function Provider(props: {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const subscribePush = useCallback((handler: PushHandler) => {
|
const subscribePush = useCallback((handler: PushHandler) => {
|
||||||
const client = clientRef.current;
|
pushHandlersRef.current.add(handler);
|
||||||
if (!client) {
|
return () => pushHandlersRef.current.delete(handler);
|
||||||
return () => {};
|
|
||||||
}
|
|
||||||
|
|
||||||
const unsubscribers = [
|
|
||||||
"MessageLive",
|
|
||||||
"MessageEditLive",
|
|
||||||
"MessageReactionLive",
|
|
||||||
"MessageDeleteLive",
|
|
||||||
"MessageState",
|
|
||||||
"CallInvite",
|
|
||||||
"ErrorNoIota",
|
|
||||||
].map((type) =>
|
|
||||||
client.subscribe(type, (message) => {
|
|
||||||
handler(validateResponse(type as keyof Schemas & string, message));
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
unsubscribers.forEach((unsubscribe) => unsubscribe());
|
|
||||||
};
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const addInterceptor = useCallback((interceptor: MTPInterceptor) => {
|
const addInterceptor = useCallback((interceptor: MTPInterceptor) => {
|
||||||
|
|
@ -419,6 +412,28 @@ export function Provider(props: {
|
||||||
const activeClient = client;
|
const activeClient = client;
|
||||||
|
|
||||||
clientRef.current = activeClient;
|
clientRef.current = activeClient;
|
||||||
|
for (const type of PUSH_TYPES) {
|
||||||
|
activeClient.subscribe(type, (message) => {
|
||||||
|
let validated: ProtocolMessage;
|
||||||
|
try {
|
||||||
|
validated = validateResponse(type, message);
|
||||||
|
} catch (error) {
|
||||||
|
log(1, "mtp", "red", "Failed to validate push message", error, {
|
||||||
|
type,
|
||||||
|
data: message.data,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const handler of [...pushHandlersRef.current]) {
|
||||||
|
void Promise.resolve()
|
||||||
|
.then(() => handler(validated))
|
||||||
|
.catch((error) => {
|
||||||
|
log(1, "mtp", "red", "Push handler failed", error, { type });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
setReadyState(activeClient.state);
|
setReadyState(activeClient.state);
|
||||||
|
|
||||||
clearReconnectTimer();
|
clearReconnectTimer();
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ export default function Provider(props: { children: React.ReactNode }) {
|
||||||
const { subscribePush, send } = useMTP();
|
const { subscribePush, send } = useMTP();
|
||||||
const { load } = useStorage();
|
const { load } = useStorage();
|
||||||
const { get } = useUser();
|
const { get } = useUser();
|
||||||
const { addLiveMessage, getChatSecret, userId } = useChat();
|
const { addLiveMessage, chatSecret, getChatSecret, userId } = useChat();
|
||||||
const { moveUserIdToTop } = useSession();
|
const { moveUserIdToTop } = useSession();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
|
@ -42,13 +42,15 @@ export default function Provider(props: { children: React.ReactNode }) {
|
||||||
|
|
||||||
if (!data.SenderId) return;
|
if (!data.SenderId) return;
|
||||||
|
|
||||||
const chatSecret = await getChatSecret(data.SenderId);
|
const messageSecret =
|
||||||
|
userId === data.SenderId && chatSecret
|
||||||
|
? chatSecret
|
||||||
|
: await getChatSecret(data.SenderId);
|
||||||
|
|
||||||
if (!data.Message || !chatSecret) return;
|
if (!data.Message || !messageSecret) return;
|
||||||
playSound("message");
|
playSound("message");
|
||||||
const user = await get(data.SenderId);
|
|
||||||
|
|
||||||
void decryptChatText(chatSecret, data.Message.Content)
|
void decryptChatText(messageSecret, data.Message.Content)
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
log(1, "chat", "red", "Failed to decrypt live message", err, {
|
log(1, "chat", "red", "Failed to decrypt live message", err, {
|
||||||
SendTime: data.Message?.SendTime,
|
SendTime: data.Message?.SendTime,
|
||||||
|
|
@ -69,6 +71,7 @@ export default function Provider(props: { children: React.ReactNode }) {
|
||||||
|
|
||||||
// todo: add notification symbol to conversation cards (incl. message start)
|
// todo: add notification symbol to conversation cards (incl. message start)
|
||||||
moveUserIdToTop(data.SenderId);
|
moveUserIdToTop(data.SenderId);
|
||||||
|
const user = await get(data.SenderId);
|
||||||
|
|
||||||
if (await load("settings.receive_confirmations")) {
|
if (await load("settings.receive_confirmations")) {
|
||||||
void send(
|
void send(
|
||||||
|
|
@ -127,6 +130,7 @@ export default function Provider(props: { children: React.ReactNode }) {
|
||||||
});
|
});
|
||||||
}, [
|
}, [
|
||||||
addLiveMessage,
|
addLiveMessage,
|
||||||
|
chatSecret,
|
||||||
get,
|
get,
|
||||||
load,
|
load,
|
||||||
navigate,
|
navigate,
|
||||||
|
|
|
||||||
|
|
@ -277,14 +277,21 @@ export const mtp = {
|
||||||
Reaction: z.string(),
|
Reaction: z.string(),
|
||||||
SendTime: z.number(),
|
SendTime: z.number(),
|
||||||
ChatPartnerId: z.number(),
|
ChatPartnerId: z.number(),
|
||||||
|
SenderId: z.number(),
|
||||||
|
Accepted: z.boolean(),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
MessageLive: {
|
MessageLive: {
|
||||||
request: z.object({}).optional(),
|
request: z.object({}).optional(),
|
||||||
response: z.object({
|
response: z
|
||||||
SenderId: z.number(),
|
.object({
|
||||||
Message,
|
SenderId: z.number(),
|
||||||
}),
|
Message: Message.extend({ SenderId: z.number().optional() }),
|
||||||
|
})
|
||||||
|
.transform(({ SenderId, Message }) => ({
|
||||||
|
SenderId,
|
||||||
|
Message: { ...Message, SenderId: Message.SenderId ?? SenderId },
|
||||||
|
})),
|
||||||
},
|
},
|
||||||
MessageGet: {
|
MessageGet: {
|
||||||
request: z.object({
|
request: z.object({
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue