Fix DM rooms showing as regular rooms for invited users by syncing m.direct on join and routing push navigation correctly.
This commit is contained in:
parent
f96c80f829
commit
adb0012834
7 changed files with 179 additions and 46 deletions
65
docs/ai/desired_features.md
Normal file
65
docs/ai/desired_features.md
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
1. Звонки по аудиосвязи в чатах
|
||||
|
||||
2. Понятное сообщение при регистрации на сервере с забаненным ником
|
||||
|
||||
3. Хочу скинуть ник-диплинк и чтобы сразу открылся сразу чат с юзером в приложении
|
||||
|
||||
4. Разобраться с текущими доменами и навести там порядок. Сейчас caddy заведен неправильно, web тоже не совсем правильно. Предложение
|
||||
Тогда структура:
|
||||
vojo.chat — красивый лендинг с описанием проекта, скриншоты, кнопка "Попробовать"
|
||||
app.vojo.chat — сам мессенджер (Cinny)
|
||||
docs.vojo.chat — документация
|
||||
matrix.vojo.chat — Matrix API (технический поддомен, пользователи туда не ходят)
|
||||
matrix-rtc.vojo.chat — LiveKit (технический)
|
||||
|
||||
5. Связанное ограничение по федерации (сейчас в env lk-jwt-service):
|
||||
`LIVEKIT_FULL_ACCESS_HOMESERVERS: "vojo.chat"` — инициировать звонки могут только
|
||||
юзеры `@…:vojo.chat`, федеративные — только join к существующим. При первых
|
||||
реальных кросс-homeserver кейсах ослабить до `"*"` или whitelist (правка в env,
|
||||
клиент не меняется).
|
||||
|
||||
6. Миграция unstable-префикса MSC4143 → stable, когда спека финализируется.
|
||||
Сейчас `.well-known/matrix/client` объявляет RTC-фокусы под ключом
|
||||
`org.matrix.msc4143.rtc_foci` (unstable prefix). Когда MSC4143 примут, ключ
|
||||
станет `m.rtc_foci`. Что понадобится:
|
||||
- В `~/vojo/caddy/Caddyfile` блок `.well-known/matrix/client` — добавить
|
||||
оба ключа на переходный период, потом выпилить старый.
|
||||
- В клиенте [src/app/hooks/useLivekitSupport.ts](src/app/hooks/useLivekitSupport.ts)
|
||||
сейчас читается только `org.matrix.msc4143.rtc_foci` — расширить на оба
|
||||
ключа (или дождаться matrix-js-sdk апдейта, который сделает это сам).
|
||||
- Этот же паттерн относится к о всем msc-событиям в RTC-стеке: MSC4075
|
||||
(`org.matrix.msc4075.rtc.notification`), MSC4195, MSC4310
|
||||
(`org.matrix.msc4310.rtc.decline`). При stable-переходе спек все
|
||||
префиксы поменяются синхронно — миграцию делать вместе.
|
||||
|
||||
7. Пофиксить follow страницы с чатами 1-1. Потому что сейчас они не автоскролятся вниз. Происходит это на нативе когда открываешь клавиатуру (она не сдвигает ui вверх как это делают обычные приложения с клавиатурой, по ощущениям просто перекрывает часть страницы сдвигая просто форму с вводом)
|
||||
|
||||
8. **(из Phase 2 DM-звонков, 2026-04-19)** Верифицировать на сервере: шифрует ли
|
||||
Element Call widget **сам** `m.rtc.notification` при отправке в encrypted room
|
||||
(ring от A → B). Теоретически SDK шифрует любой `sendEvent` в encrypted room
|
||||
прозрачно, и widget должен идти через тот же путь — но `sendRtcDecline`
|
||||
(наш, не widget) делегирует в `client.sendEvent` и это проверено
|
||||
([client.js:2457](../../node_modules/matrix-js-sdk/lib/client.js#L2457)),
|
||||
а `m.rtc.notification` widget шлёт через свой postMessage канал — не
|
||||
перепроверяли. Как проверить: в encrypted DM у A нажать startCall,
|
||||
в DevTools Network tab смотреть PUT на `/rooms/{roomId}/send/...` — body
|
||||
должно быть encrypted (ключи `algorithm`, `ciphertext`, `sender_key`,
|
||||
`session_id`), не plain JSON с `"m.rtc.notification"`. Если plain —
|
||||
widget игнорит encryption, нужен апстрим-фикс Element Call либо наша
|
||||
обёртка. См. [docs/plans/dm_calls_techdebt.md](../plans/dm_calls_techdebt.md) 5.9.
|
||||
|
||||
9. Когда открываешь чат уведомления из пуш бара не исчезают
|
||||
|
||||
10. Пофиксить цвет полос снизу и сверху при разных бекграундах цветовых (Сейчас они не синхронны на темной теме)
|
||||
|
||||
11. Как то по умнее схлопывать переходы назад. Возможно уникальные переходы схлопывать. Контекст такой: приложение сохраняет все походы юзера по экрану и так можно навигироваться без конца. Особенно это странно работает при входящих уведомлениях из одного чата: можно положить в этот стеш очень много ивентов этого окна и сидеть жать кнопку назад очень долго
|
||||
|
||||
12. Не очень понятно что делать со звонками в пуше на андроиде. Если пришел пуш-звонок, надо показывать на экране чата внизу сам эвент со звонком как на вебе?
|
||||
|
||||
13. UI friendly боты (в т.ч. телеграм бот)
|
||||
|
||||
14. Убрать странное читат чат
|
||||
|
||||
15. ~~Баг с присоединться в комнату, перекидывает не в чат, а в комнаты. При чем у одного юзера отображается чат, а у другого комната. Из-за этого нельзя позовнить например~~ — пофикшено: глобальный `useAutoDirectSync` listener, прямая навигация в Invites, smart push routing, HomeRouteRoomProvider redirect.
|
||||
|
||||
16. **Тех-долг:** one-shot repair для исторически сломанных DM-комнат. После фикса #15 новые join-ы корректно обновляют `m.direct`, но комнаты у юзеров, которые уже приняли инвайт до фикса, остаются в "Комнатах". Нужна миграция или команда `/repairDMs`, которая пройдёт по joined rooms, проверит `getDMInviter()`, и добавит в `m.direct`. Осложнение: `prev_content.is_direct` не исчезает после `/converttoroom`, поэтому наивный sweep откатит ручную конвертацию — нужен tombstone-маркер или whitelist.
|
||||
39
src/app/hooks/useAutoDirectSync.ts
Normal file
39
src/app/hooks/useAutoDirectSync.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import { MatrixClient, Room, RoomEvent } from 'matrix-js-sdk';
|
||||
import { useEffect } from 'react';
|
||||
import { AccountDataEvent } from '../../types/matrix/accountData';
|
||||
import { Membership } from '../../types/matrix/room';
|
||||
import { getAccountData, getMDirects } from '../utils/room';
|
||||
import { addRoomIdToMDirect } from '../utils/matrix';
|
||||
|
||||
export function useAutoDirectSync(mx: MatrixClient): void {
|
||||
useEffect(() => {
|
||||
const handleMembership = (
|
||||
room: Room,
|
||||
_membership: string,
|
||||
prevMembership?: string
|
||||
) => {
|
||||
if (prevMembership !== Membership.Invite) return;
|
||||
if (room.getMyMembership() !== Membership.Join) return;
|
||||
|
||||
const dmInviter = room.getDMInviter();
|
||||
if (!dmInviter) return;
|
||||
|
||||
const joinedAndInvited =
|
||||
room.getJoinedMemberCount() + room.getInvitedMemberCount();
|
||||
if (joinedAndInvited > 2) return;
|
||||
|
||||
const mDirectEvent = getAccountData(mx, AccountDataEvent.Direct);
|
||||
if (mDirectEvent) {
|
||||
const directs = getMDirects(mDirectEvent);
|
||||
if (directs.has(room.roomId)) return;
|
||||
}
|
||||
|
||||
addRoomIdToMDirect(mx, room.roomId, dmInviter).catch(() => undefined);
|
||||
};
|
||||
|
||||
mx.on(RoomEvent.MyMembership, handleMembership);
|
||||
return () => {
|
||||
mx.removeListener(RoomEvent.MyMembership, handleMembership);
|
||||
};
|
||||
}, [mx]);
|
||||
}
|
||||
|
|
@ -20,8 +20,9 @@ import {
|
|||
unregisterPusher,
|
||||
urlBase64ToUint8Array,
|
||||
} from '../utils/push';
|
||||
import { getDirectRoomPath, getHomeRoomPath, getInboxInvitesPath } from '../pages/pathUtils';
|
||||
import { getDirectRoomPath, getInboxInvitesPath } from '../pages/pathUtils';
|
||||
import { pendingCallActionAtom } from '../state/pendingCallAction';
|
||||
import { useRoomNavigate } from './useRoomNavigate';
|
||||
|
||||
const noop = (): void => undefined;
|
||||
|
||||
|
|
@ -274,6 +275,7 @@ export function usePushNotificationsLifecycle(): void {
|
|||
const mx = useMatrixClient();
|
||||
const clientConfig = useClientConfig();
|
||||
const navigate = useNavigate();
|
||||
const { navigateRoom } = useRoomNavigate();
|
||||
const setPendingCallAction = useSetAtom(pendingCallActionAtom);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -300,7 +302,7 @@ export function usePushNotificationsLifecycle(): void {
|
|||
navigate(getInboxInvitesPath());
|
||||
return;
|
||||
}
|
||||
if (detail?.roomId) navigate(getHomeRoomPath(detail.roomId));
|
||||
if (detail?.roomId) navigateRoom(detail.roomId);
|
||||
};
|
||||
const onSubChange = () => {
|
||||
if (isPushEnabled()) register().catch(noop);
|
||||
|
|
@ -312,7 +314,7 @@ export function usePushNotificationsLifecycle(): void {
|
|||
window.removeEventListener('vojo:pushNavigate', onNavigate);
|
||||
window.removeEventListener('vojo:pushSubscriptionChange', onSubChange);
|
||||
};
|
||||
}, [navigate, register]);
|
||||
}, [navigate, navigateRoom, register]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isNativePlatform()) return undefined;
|
||||
|
|
@ -383,7 +385,7 @@ export function usePushNotificationsLifecycle(): void {
|
|||
return;
|
||||
}
|
||||
|
||||
if (data.room_id) navigate(getHomeRoomPath(data.room_id));
|
||||
if (data.room_id) navigateRoom(data.room_id);
|
||||
}
|
||||
);
|
||||
})
|
||||
|
|
@ -473,7 +475,7 @@ export function usePushNotificationsLifecycle(): void {
|
|||
cancelled = true;
|
||||
cleanups.forEach((c) => c());
|
||||
};
|
||||
}, [navigate, mx, clientConfig, setPendingCallAction]);
|
||||
}, [navigate, navigateRoom, mx, clientConfig, setPendingCallAction]);
|
||||
}
|
||||
|
||||
export { isPushEnabled, getPushPlatform } from '../utils/push';
|
||||
|
|
|
|||
|
|
@ -1,21 +1,31 @@
|
|||
import React, { ReactNode } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { Navigate, useParams } from 'react-router-dom';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { useSelectedRoom } from '../../../hooks/router/useSelectedRoom';
|
||||
import { IsDirectRoomProvider, RoomProvider } from '../../../hooks/useRoom';
|
||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||
import { JoinBeforeNavigate } from '../../../features/join-before-navigate';
|
||||
import { useHomeRooms } from './useHomeRooms';
|
||||
import { useSearchParamsViaServers } from '../../../hooks/router/useSearchParamsViaServers';
|
||||
import { mDirectAtom } from '../../../state/mDirectList';
|
||||
import { getDirectRoomPath } from '../../pathUtils';
|
||||
import { getCanonicalAliasOrRoomId } from '../../../utils/matrix';
|
||||
|
||||
export function HomeRouteRoomProvider({ children }: { children: ReactNode }) {
|
||||
const mx = useMatrixClient();
|
||||
const rooms = useHomeRooms();
|
||||
const mDirects = useAtomValue(mDirectAtom);
|
||||
|
||||
const { roomIdOrAlias, eventId } = useParams();
|
||||
const viaServers = useSearchParamsViaServers();
|
||||
const roomId = useSelectedRoom();
|
||||
const room = mx.getRoom(roomId);
|
||||
|
||||
if (room && mDirects.has(room.roomId)) {
|
||||
const alias = getCanonicalAliasOrRoomId(mx, room.roomId);
|
||||
return <Navigate to={getDirectRoomPath(alias, eventId)} replace />;
|
||||
}
|
||||
|
||||
if (!room || !rooms.includes(room.roomId)) {
|
||||
return (
|
||||
<JoinBeforeNavigate
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import { useTranslation } from 'react-i18next';
|
|||
import { RoomTopicEventContent } from 'matrix-js-sdk/lib/types';
|
||||
import FocusTrap from 'focus-trap-react';
|
||||
import { MatrixClient, MatrixError, Room } from 'matrix-js-sdk';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Page,
|
||||
PageContent,
|
||||
|
|
@ -48,10 +49,12 @@ import { nameInitials } from '../../../utils/common';
|
|||
import { RoomAvatar } from '../../../components/room-avatar';
|
||||
import {
|
||||
addRoomIdToMDirect,
|
||||
getCanonicalAliasOrRoomId,
|
||||
getMxIdLocalPart,
|
||||
guessDmRoomUserId,
|
||||
rateLimitedActions,
|
||||
} from '../../../utils/matrix';
|
||||
import { getDirectRoomPath } from '../../pathUtils';
|
||||
import { Time } from '../../../components/message';
|
||||
import { useElementSizeObserver } from '../../../hooks/useElementSizeObserver';
|
||||
import { onEnterOrSpace, stopPropagation } from '../../../utils/keyboard';
|
||||
|
|
@ -162,6 +165,7 @@ function InviteCard({
|
|||
}: InviteCardProps) {
|
||||
const { t } = useTranslation();
|
||||
const mx = useMatrixClient();
|
||||
const navigate = useNavigate();
|
||||
const userId = mx.getSafeUserId();
|
||||
|
||||
const [viewTopic, setViewTopic] = useState(false);
|
||||
|
|
@ -177,9 +181,12 @@ function InviteCard({
|
|||
await mx.joinRoom(invite.roomId);
|
||||
if (dmUserId) {
|
||||
await addRoomIdToMDirect(mx, invite.roomId, dmUserId);
|
||||
const alias = getCanonicalAliasOrRoomId(mx, invite.roomId);
|
||||
navigate(getDirectRoomPath(alias));
|
||||
return;
|
||||
}
|
||||
onNavigate(invite.roomId, invite.isSpace);
|
||||
}, [mx, invite, userId, onNavigate])
|
||||
}, [mx, invite, userId, onNavigate, navigate])
|
||||
);
|
||||
const [leaveState, leave] = useAsyncCallback<Record<string, never>, MatrixError, []>(
|
||||
useCallback(() => mx.leave(invite.roomId), [mx, invite])
|
||||
|
|
|
|||
|
|
@ -5,9 +5,11 @@ import { mDirectAtom, useBindMDirectAtom } from '../mDirectList';
|
|||
import { roomToUnreadAtom, useBindRoomToUnreadAtom } from '../room/roomToUnread';
|
||||
import { roomToParentsAtom, useBindRoomToParentsAtom } from '../room/roomToParents';
|
||||
import { roomIdToTypingMembersAtom, useBindRoomIdToTypingMembersAtom } from '../typingMembers';
|
||||
import { useAutoDirectSync } from '../../hooks/useAutoDirectSync';
|
||||
|
||||
export const useBindAtoms = (mx: MatrixClient) => {
|
||||
useBindMDirectAtom(mx, mDirectAtom);
|
||||
useAutoDirectSync(mx);
|
||||
useBindAllInvitesAtom(mx, allInvitesAtom);
|
||||
useBindAllRoomsAtom(mx, allRoomsAtom);
|
||||
useBindRoomToParentsAtom(mx, roomToParentsAtom);
|
||||
|
|
|
|||
|
|
@ -252,57 +252,65 @@ export const guessDmRoomUserId = (room: Room, myUserId: string): string => {
|
|||
return member1?.userId ?? myUserId;
|
||||
};
|
||||
|
||||
export const addRoomIdToMDirect = async (
|
||||
const mDirectQueues = new WeakMap<MatrixClient, Promise<void>>();
|
||||
const enqueueMDirectWrite = (mx: MatrixClient, fn: () => Promise<void>): Promise<void> => {
|
||||
const prev = mDirectQueues.get(mx) ?? Promise.resolve();
|
||||
const next = prev.then(fn, fn);
|
||||
mDirectQueues.set(mx, next);
|
||||
return next;
|
||||
};
|
||||
|
||||
export const addRoomIdToMDirect = (
|
||||
mx: MatrixClient,
|
||||
roomId: string,
|
||||
userId: string
|
||||
): Promise<void> => {
|
||||
const mDirectsEvent = mx.getAccountData(AccountDataEvent.Direct as any);
|
||||
let userIdToRoomIds: Record<string, string[]> = {};
|
||||
): Promise<void> =>
|
||||
enqueueMDirectWrite(mx, async () => {
|
||||
const mDirectsEvent = mx.getAccountData(AccountDataEvent.Direct as any);
|
||||
let userIdToRoomIds: Record<string, string[]> = {};
|
||||
|
||||
if (typeof mDirectsEvent !== 'undefined')
|
||||
userIdToRoomIds = structuredClone(mDirectsEvent.getContent());
|
||||
if (typeof mDirectsEvent !== 'undefined')
|
||||
userIdToRoomIds = structuredClone(mDirectsEvent.getContent());
|
||||
|
||||
// remove it from the lists of any others users
|
||||
// (it can only be a DM room for one person)
|
||||
Object.keys(userIdToRoomIds).forEach((targetUserId) => {
|
||||
const roomIds = userIdToRoomIds[targetUserId];
|
||||
Object.keys(userIdToRoomIds).forEach((targetUserId) => {
|
||||
const roomIds = userIdToRoomIds[targetUserId];
|
||||
|
||||
if (targetUserId !== userId) {
|
||||
if (targetUserId !== userId) {
|
||||
const indexOfRoomId = roomIds.indexOf(roomId);
|
||||
if (indexOfRoomId > -1) {
|
||||
roomIds.splice(indexOfRoomId, 1);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const roomIds = userIdToRoomIds[userId] || [];
|
||||
if (roomIds.indexOf(roomId) === -1) {
|
||||
roomIds.push(roomId);
|
||||
}
|
||||
userIdToRoomIds[userId] = roomIds;
|
||||
|
||||
await mx.setAccountData(AccountDataEvent.Direct as any, userIdToRoomIds as any);
|
||||
});
|
||||
|
||||
export const removeRoomIdFromMDirect = (mx: MatrixClient, roomId: string): Promise<void> =>
|
||||
enqueueMDirectWrite(mx, async () => {
|
||||
const mDirectsEvent = mx.getAccountData(AccountDataEvent.Direct as any);
|
||||
let userIdToRoomIds: Record<string, string[]> = {};
|
||||
|
||||
if (typeof mDirectsEvent !== 'undefined')
|
||||
userIdToRoomIds = structuredClone(mDirectsEvent.getContent());
|
||||
|
||||
Object.keys(userIdToRoomIds).forEach((targetUserId) => {
|
||||
const roomIds = userIdToRoomIds[targetUserId];
|
||||
const indexOfRoomId = roomIds.indexOf(roomId);
|
||||
if (indexOfRoomId > -1) {
|
||||
roomIds.splice(indexOfRoomId, 1);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await mx.setAccountData(AccountDataEvent.Direct as any, userIdToRoomIds as any);
|
||||
});
|
||||
|
||||
const roomIds = userIdToRoomIds[userId] || [];
|
||||
if (roomIds.indexOf(roomId) === -1) {
|
||||
roomIds.push(roomId);
|
||||
}
|
||||
userIdToRoomIds[userId] = roomIds;
|
||||
|
||||
await mx.setAccountData(AccountDataEvent.Direct as any, userIdToRoomIds as any);
|
||||
};
|
||||
|
||||
export const removeRoomIdFromMDirect = async (mx: MatrixClient, roomId: string): Promise<void> => {
|
||||
const mDirectsEvent = mx.getAccountData(AccountDataEvent.Direct as any);
|
||||
let userIdToRoomIds: Record<string, string[]> = {};
|
||||
|
||||
if (typeof mDirectsEvent !== 'undefined')
|
||||
userIdToRoomIds = structuredClone(mDirectsEvent.getContent());
|
||||
|
||||
Object.keys(userIdToRoomIds).forEach((targetUserId) => {
|
||||
const roomIds = userIdToRoomIds[targetUserId];
|
||||
const indexOfRoomId = roomIds.indexOf(roomId);
|
||||
if (indexOfRoomId > -1) {
|
||||
roomIds.splice(indexOfRoomId, 1);
|
||||
}
|
||||
});
|
||||
|
||||
await mx.setAccountData(AccountDataEvent.Direct as any, userIdToRoomIds as any);
|
||||
};
|
||||
|
||||
export const mxcUrlToHttp = (
|
||||
mx: MatrixClient,
|
||||
mxcUrl: string,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue