Merge remote-tracking branch 'origin/lotus' into fix/filtered-timeline-double-count
CI / Secret scan (gitleaks) (pull_request) Successful in 29s
CI / Build & Quality Checks (pull_request) Failing after 11m33s
CI / Trigger Desktop Build (pull_request) Skipped
CI / Docker image build & smoke test (pull_request) Skipped
CI / Playwright smoke (e2e) (pull_request) Skipped
CI / Secret scan (gitleaks) (pull_request) Successful in 29s
CI / Build & Quality Checks (pull_request) Failing after 11m33s
CI / Trigger Desktop Build (pull_request) Skipped
CI / Docker image build & smoke test (pull_request) Skipped
CI / Playwright smoke (e2e) (pull_request) Skipped
# Conflicts: # src/app/components/CallEmbedProvider.tsx
This commit is contained in:
@@ -40,6 +40,7 @@ import { creatorsSupported } from '../../../utils/matrix';
|
||||
import { useRoomCreators } from '../../../hooks/useRoomCreators';
|
||||
import { BreakWord } from '../../../styles/Text.css';
|
||||
import { useModalStyle } from '../../../hooks/useModalStyle';
|
||||
import { stateToCarry } from '../../../utils/roomUpgradeState';
|
||||
|
||||
function RoomUpgradeDialog({ requestClose }: { requestClose: () => void }) {
|
||||
const mx = useMatrixClient();
|
||||
@@ -63,10 +64,41 @@ function RoomUpgradeDialog({ requestClose }: { requestClose: () => void }) {
|
||||
const [upgradeState, upgrade] = useAsyncCallback(
|
||||
useCallback(
|
||||
async (version: string, newAdditionalCreators?: string[]) => {
|
||||
await mx.http.authedRequest(Method.Post, `/rooms/${room.roomId}/upgrade`, undefined, {
|
||||
new_version: version,
|
||||
additional_creators: newAdditionalCreators,
|
||||
});
|
||||
// [Gitea #327] Synapse doesn't copy Lotus room state (voice limit, call
|
||||
// policy, soundboard, retention, role tags, emoji packs) into the
|
||||
// replacement room: read it first, re-create it after the upgrade.
|
||||
const carried = stateToCarry(
|
||||
Array.from(room.currentState.events.values()).flatMap((byKey) =>
|
||||
Array.from(byKey.values()),
|
||||
),
|
||||
);
|
||||
const res = await mx.http.authedRequest<{ replacement_room?: string }>(
|
||||
Method.Post,
|
||||
`/rooms/${room.roomId}/upgrade`,
|
||||
undefined,
|
||||
{
|
||||
new_version: version,
|
||||
additional_creators: newAdditionalCreators,
|
||||
},
|
||||
);
|
||||
const newRoomId = res?.replacement_room;
|
||||
if (!newRoomId || carried.length === 0) return;
|
||||
const results = await Promise.allSettled(
|
||||
carried.map((st) =>
|
||||
mx.http.authedRequest(
|
||||
Method.Put,
|
||||
`/rooms/${encodeURIComponent(newRoomId)}/state/${encodeURIComponent(st.type)}/${encodeURIComponent(st.stateKey)}`,
|
||||
undefined,
|
||||
st.content,
|
||||
),
|
||||
),
|
||||
);
|
||||
const failed = results.filter((r) => r.status === 'rejected').length;
|
||||
if (failed > 0) {
|
||||
throw new Error(
|
||||
`Room upgraded, but ${failed} of ${carried.length} Lotus settings (voice limit, call policy, soundboard, retention…) could not be copied to the new room.`,
|
||||
);
|
||||
}
|
||||
},
|
||||
[mx, room],
|
||||
),
|
||||
|
||||
@@ -43,6 +43,7 @@ import { DebounceOptions, useDebounce } from '../../hooks/useDebounce';
|
||||
import { VirtualTile } from '../../components/virtualizer';
|
||||
import { stopPropagation } from '../../utils/keyboard';
|
||||
import { MsgTypeFilter } from './useMessageSearch';
|
||||
import { useLocalRoomNames } from '../../hooks/useRoomMeta';
|
||||
|
||||
const MSG_TYPE_FILTER_OPTIONS: { msgType: MsgTypeFilter; label: string; icon: IconSrc }[] = [
|
||||
{ msgType: 'm.image', label: 'Images', icon: Icons.Photo },
|
||||
@@ -137,14 +138,19 @@ type SelectRoomButtonProps = {
|
||||
onChange: (rooms?: string[]) => void;
|
||||
};
|
||||
function SelectRoomButton({ roomList, selectedRooms, onChange }: SelectRoomButtonProps) {
|
||||
const localNames = useLocalRoomNames();
|
||||
const mx = useMatrixClient();
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const [menuAnchor, setMenuAnchor] = useState<RectCords>();
|
||||
const [localSelected, setLocalSelected] = useState(selectedRooms);
|
||||
|
||||
const getRoomNameStr: SearchItemStrGetter<string> = useCallback(
|
||||
(rId) => mx.getRoom(rId)?.name ?? rId,
|
||||
[mx],
|
||||
(rId) => {
|
||||
const name = mx.getRoom(rId)?.name ?? rId;
|
||||
// [Gitea #336] match the personal "Rename for me" name too
|
||||
return localNames[rId] ? [localNames[rId], name] : name;
|
||||
},
|
||||
[mx, localNames],
|
||||
);
|
||||
|
||||
const [searchResult, _searchRoom, resetSearch] = useAsyncSearch(
|
||||
@@ -292,7 +298,7 @@ function SelectRoomButton({ roomList, selectedRooms, onChange }: SelectRoomButto
|
||||
}
|
||||
>
|
||||
<Text truncate size="T300">
|
||||
{room.name}
|
||||
{localNames[room.roomId] ?? room.name}
|
||||
</Text>
|
||||
</MenuItem>
|
||||
</VirtualTile>
|
||||
@@ -701,6 +707,7 @@ export function SearchFilters({
|
||||
pinnedOnly,
|
||||
onTogglePinnedOnly,
|
||||
}: SearchFiltersProps) {
|
||||
const localNames = useLocalRoomNames();
|
||||
const mx = useMatrixClient();
|
||||
|
||||
return (
|
||||
@@ -748,7 +755,7 @@ export function SearchFilters({
|
||||
}
|
||||
after={<Icon size="50" src={Icons.Cross} />}
|
||||
>
|
||||
<Text size="T200">{room.name}</Text>
|
||||
<Text size="T200">{localNames[room.roomId] ?? room.name}</Text>
|
||||
</Chip>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -50,6 +50,7 @@ import {
|
||||
} from '../../hooks/useMemberPowerTag';
|
||||
import { useRoomCreators } from '../../hooks/useRoomCreators';
|
||||
import { useRoomCreatorsTag } from '../../hooks/useRoomCreatorsTag';
|
||||
import { useLocalRoomNames } from '../../hooks/useRoomMeta';
|
||||
|
||||
type SearchResultGroupProps = {
|
||||
room: Room;
|
||||
@@ -73,6 +74,7 @@ export function SearchResultGroup({
|
||||
hour24Clock,
|
||||
dateFormatString,
|
||||
}: SearchResultGroupProps) {
|
||||
const localNames = useLocalRoomNames();
|
||||
const mx = useMatrixClient();
|
||||
const useAuthentication = useMediaAuthentication();
|
||||
const highlightRegex = useMemo(() => makeHighlightRegex(highlights), [highlights]);
|
||||
@@ -212,7 +214,7 @@ export function SearchResultGroup({
|
||||
/>
|
||||
</Avatar>
|
||||
<Text size="H4" truncate>
|
||||
{room.name}
|
||||
{localNames[room.roomId] ?? room.name}
|
||||
</Text>
|
||||
</Box>
|
||||
</Header>
|
||||
|
||||
@@ -160,6 +160,7 @@ import { scheduledMessagesAtom } from '../../state/scheduledMessages';
|
||||
import { createErrorToast, toastQueueAtom } from '../../state/toast';
|
||||
import { getThreadDraftKey } from '../../state/room/thread';
|
||||
import { stripTrackingParamsInHtml, stripTrackingParamsInText } from '../../utils/urlTracking';
|
||||
import { plaintextCachesWiped } from '../../state/plaintextCaches';
|
||||
|
||||
const GifPicker = React.lazy(() =>
|
||||
import('../../components/GifPicker').then((m) => ({ default: m.GifPicker })),
|
||||
@@ -562,6 +563,8 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||
// Persist the current editor state to the draft atom + localStorage; an empty
|
||||
// editor clears both so the draft indicators never claim a draft that isn't there.
|
||||
const persistDraft = useCallback(() => {
|
||||
// [Gitea #319] Logout already wiped the drafts; don't write this one back.
|
||||
if (plaintextCachesWiped()) return;
|
||||
if (!isEmptyEditor(editor)) {
|
||||
const parsedDraft = JSON.parse(JSON.stringify(editor.children));
|
||||
setMsgDraft(parsedDraft);
|
||||
|
||||
@@ -122,7 +122,7 @@ import { ThreadSummary } from './thread/ThreadSummary';
|
||||
import { usePowerLevelsContext } from '../../hooks/usePowerLevels';
|
||||
import { GetContentCallback, MessageEvent, StateEvent } from '../../../types/matrix/room';
|
||||
import { useStateEvent } from '../../hooks/useStateEvent';
|
||||
import { RetentionContent, isExpired } from '../../utils/retention';
|
||||
import { RetentionContent, effectiveRetentionMs, isExpired } from '../../utils/retention';
|
||||
import { useKeyDown } from '../../hooks/useKeyDown';
|
||||
import { useDocumentFocusChange } from '../../hooks/useDocumentFocusChange';
|
||||
import { RenderMessageContent } from '../../components/RenderMessageContent';
|
||||
@@ -498,7 +498,10 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
||||
// timeline (unless "show hidden events" is on). Reactive so a policy change
|
||||
// re-renders. `undefined` = no policy.
|
||||
const retentionEvent = useStateEvent(room, StateEvent.RoomRetention);
|
||||
const retentionMs = retentionEvent?.getContent<RetentionContent>().max_lifetime;
|
||||
// [Gitea #298] Same floor as everywhere else: a tiny value must not blank the room.
|
||||
const retentionMs = effectiveRetentionMs(
|
||||
retentionEvent?.getContent<RetentionContent>().max_lifetime,
|
||||
);
|
||||
const [showDeveloperTools] = useSetting(settingsAtom, 'developerTools');
|
||||
const [lotusTerminal] = useSetting(settingsAtom, 'lotusTerminal');
|
||||
|
||||
|
||||
@@ -31,12 +31,13 @@ import React, {
|
||||
MouseEventHandler,
|
||||
ReactNode,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import FocusTrap from 'focus-trap-react';
|
||||
import { useHover, useFocusWithin } from 'react-aria';
|
||||
import { MatrixEvent, Room, EventStatus } from 'matrix-js-sdk';
|
||||
import { MatrixEvent, MatrixEventEvent, Room, EventStatus } from 'matrix-js-sdk';
|
||||
import { Relations } from 'matrix-js-sdk/lib/models/relations';
|
||||
import classNames from 'classnames';
|
||||
import { useAtom, useAtomValue } from 'jotai';
|
||||
@@ -114,6 +115,20 @@ function DeliveryStatus({
|
||||
}) {
|
||||
const mx = useMatrixClient();
|
||||
const offline = useAtomValue(sendOfflineAtom);
|
||||
// [Gitea #306] A local echo's status changes (SENDING → SENT → confirmed,
|
||||
// NOT_SENT → retried) don't re-render the timeline, so the last message in a
|
||||
// quiet room kept "Sending..." (or a stale "Failed to send") indefinitely.
|
||||
// Re-render on the event's own status updates.
|
||||
const [, setStatusTick] = useState(0);
|
||||
useEffect(() => {
|
||||
const bump = () => setStatusTick((t) => t + 1);
|
||||
mEvent.on(MatrixEventEvent.Status, bump);
|
||||
mEvent.on(MatrixEventEvent.LocalEventIdReplaced, bump);
|
||||
return () => {
|
||||
mEvent.off(MatrixEventEvent.Status, bump);
|
||||
mEvent.off(MatrixEventEvent.LocalEventIdReplaced, bump);
|
||||
};
|
||||
}, [mEvent]);
|
||||
const { status } = mEvent;
|
||||
if (status === null) return null; // confirmed by server — read receipts take over
|
||||
let iconSrc: IconSrc;
|
||||
|
||||
@@ -32,6 +32,7 @@ import { useThreadNotificationMode } from '../../../hooks/useThreadNotifications
|
||||
import { useTimestampFormatter } from '../../../hooks/useTimestampFormatter';
|
||||
import { getMemberName } from '../../../utils/room';
|
||||
import { ThreadNotificationMode } from '../../../utils/threadNotifications';
|
||||
import { useLocalRoomNames } from '../../../hooks/useRoomMeta';
|
||||
|
||||
type ThreadPanelHeaderProps = {
|
||||
room: Room;
|
||||
@@ -39,6 +40,7 @@ type ThreadPanelHeaderProps = {
|
||||
requestClose: () => void;
|
||||
};
|
||||
function ThreadPanelHeader({ room, threadId, requestClose }: ThreadPanelHeaderProps) {
|
||||
const localNames = useLocalRoomNames();
|
||||
const mode = useThreadNotificationMode(room.roomId, threadId);
|
||||
const { format } = useTimestampFormatter();
|
||||
// [Gitea #165] Who started the thread and when — the first thing you want
|
||||
@@ -55,7 +57,7 @@ function ThreadPanelHeader({ room, threadId, requestClose }: ThreadPanelHeaderPr
|
||||
Thread
|
||||
</Text>
|
||||
<Text size="T200" truncate style={{ opacity: 0.65 }}>
|
||||
{room.name}
|
||||
{localNames[room.roomId] ?? room.name}
|
||||
{startedBy && root && ` · started by ${startedBy}, ${format(root.getTs())}`}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
@@ -48,6 +48,7 @@ import { useTimestampFormatter } from '../../../hooks/useTimestampFormatter';
|
||||
import { formatRelativeAge } from '../../../utils/formatTimestamp';
|
||||
import { useRoomNavigate } from '../../../hooks/useRoomNavigate';
|
||||
import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize';
|
||||
import { useLocalRoomNames } from '../../../hooks/useRoomMeta';
|
||||
|
||||
// Persisted across panel opens (the panel unmounts on close). getOnInit reads
|
||||
// localStorage synchronously so the chosen filter/sort apply on first render.
|
||||
@@ -235,6 +236,7 @@ export type ThreadsListPanelProps = {
|
||||
onOpenThread: (threadId: string) => void;
|
||||
};
|
||||
export function ThreadsListPanel({ room, onClose, onOpenThread }: ThreadsListPanelProps) {
|
||||
const localNames = useLocalRoomNames();
|
||||
const { navigateRoom } = useRoomNavigate();
|
||||
const screenSize = useScreenSizeContext();
|
||||
const handleJump = useCallback(
|
||||
@@ -361,7 +363,7 @@ export function ThreadsListPanel({ room, onClose, onOpenThread }: ThreadsListPan
|
||||
Threads
|
||||
</Text>
|
||||
<Text size="T200" truncate style={{ opacity: 0.65 }}>
|
||||
{room.name}
|
||||
{localNames[room.roomId] ?? room.name}
|
||||
</Text>
|
||||
</Box>
|
||||
{totalUnread > 0 && (
|
||||
|
||||
Reference in New Issue
Block a user