import { getCanonicalAliasOrRoomId, isRoomAlias } from '../../utils/matrix';
import { getViaServers } from '../../plugins/via-servers';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
+import { useSetting } from '../../state/hooks/settings';
+import { settingsAtom } from '../../state/settings';
type RoomNavItemMenuProps = {
room: Room;
const RoomNavItemMenu = forwardRef<HTMLDivElement, RoomNavItemMenuProps>(
({ room, requestClose }, ref) => {
const mx = useMatrixClient();
+ const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
const unread = useRoomUnread(room.roomId, roomToUnreadAtom);
const powerLevels = usePowerLevels(room);
const { getPowerLevel, canDoAction } = usePowerLevelsAPI(powerLevels);
const canInvite = canDoAction('invite', getPowerLevel(mx.getUserId() ?? ''));
const handleMarkAsRead = () => {
- markAsRead(mx, room.roomId);
+ markAsRead(mx, room.roomId, hideActivity);
requestClose();
};
const mx = useMatrixClient();
const [isDrawer] = useSetting(settingsAtom, 'isPeopleDrawer');
+ const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
const screenSize = useScreenSizeContext();
const powerLevels = usePowerLevels(room);
const members = useRoomMembers(mx, room.roomId);
useCallback(
(evt) => {
if (isKeyHotkey('escape', evt)) {
- markAsRead(mx, room.roomId);
+ markAsRead(mx, room.roomId, hideActivity);
}
},
- [mx, room.roomId]
+ [mx, room.roomId, hideActivity]
)
);
const useAuthentication = useMediaAuthentication();
const [enterForNewline] = useSetting(settingsAtom, 'enterForNewline');
const [isMarkdown] = useSetting(settingsAtom, 'isMarkdown');
+ const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
const commands = useCommands(mx, room);
const emojiBtnRef = useRef<HTMLButtonElement>(null);
const roomToParents = useAtomValue(roomToParentsAtom);
return;
}
- sendTypingStatus(!isEmptyEditor(editor));
+ if (!hideActivity) {
+ sendTypingStatus(!isEmptyEditor(editor));
+ }
const prevWordRange = getPrevWorldRange(editor);
const query = prevWordRange
: undefined;
setAutocompleteQuery(query);
},
- [editor, sendTypingStatus]
+ [editor, sendTypingStatus, hideActivity]
);
const handleCloseAutocomplete = useCallback(() => {
export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimelineProps) {
const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
+ const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
const [messageLayout] = useSetting(settingsAtom, 'messageLayout');
const [messageSpacing] = useSetting(settingsAtom, 'messageSpacing');
const [hideMembershipEvents] = useSetting(settingsAtom, 'hideMembershipEvents');
// Check if the document is in focus (user is actively viewing the app),
// and either there are no unread messages or the latest message is from the current user.
// If either condition is met, trigger the markAsRead function to send a read receipt.
- requestAnimationFrame(() => markAsRead(mx, mEvt.getRoomId()!));
+ requestAnimationFrame(() => markAsRead(mx, mEvt.getRoomId()!, hideActivity));
}
if (!document.hasFocus() && !unreadInfo) {
setUnreadInfo(getRoomUnreadInfo(room));
}
},
- [mx, room, unreadInfo]
+ [mx, room, unreadInfo, hideActivity]
)
);
const tryAutoMarkAsRead = useCallback(() => {
const readUptoEventId = readUptoEventIdRef.current;
if (!readUptoEventId) {
- requestAnimationFrame(() => markAsRead(mx, room.roomId));
+ requestAnimationFrame(() => markAsRead(mx, room.roomId, hideActivity));
return;
}
const evtTimeline = getEventTimeline(room, readUptoEventId);
const latestTimeline = evtTimeline && getFirstLinkedTimeline(evtTimeline, Direction.Forward);
if (latestTimeline === room.getLiveTimeline()) {
- requestAnimationFrame(() => markAsRead(mx, room.roomId));
+ requestAnimationFrame(() => markAsRead(mx, room.roomId, hideActivity));
}
- }, [mx, room]);
+ }, [mx, room, hideActivity]);
const debounceSetAtBottom = useDebounce(
useCallback((entry: IntersectionObserverEntry) => {
};
const handleMarkAsRead = () => {
- markAsRead(mx, room.roomId);
+ markAsRead(mx, room.roomId, hideActivity);
};
const handleOpenReply: MouseEventHandler = useCallback(
/>
)
}
+ hideReadReceipts={hideActivity}
>
{mEvent.isRedacted() ? (
<RedactedContent reason={mEvent.getUnsigned().redacted_because?.content.reason} />
/>
)
}
+ hideReadReceipts={hideActivity}
>
<EncryptedContent mEvent={mEvent}>
{() => {
/>
)
}
+ hideReadReceipts={hideActivity}
>
{mEvent.isRedacted() ? (
<RedactedContent reason={mEvent.getUnsigned().redacted_because?.content.reason} />
highlight={highlighted}
messageSpacing={messageSpacing}
canDelete={canRedact || mEvent.getSender() === mx.getUserId()}
+ hideReadReceipts={hideActivity}
>
<EventContent
messageLayout={messageLayout}
highlight={highlighted}
messageSpacing={messageSpacing}
canDelete={canRedact || mEvent.getSender() === mx.getUserId()}
+ hideReadReceipts={hideActivity}
>
<EventContent
messageLayout={messageLayout}
highlight={highlighted}
messageSpacing={messageSpacing}
canDelete={canRedact || mEvent.getSender() === mx.getUserId()}
+ hideReadReceipts={hideActivity}
>
<EventContent
messageLayout={messageLayout}
highlight={highlighted}
messageSpacing={messageSpacing}
canDelete={canRedact || mEvent.getSender() === mx.getUserId()}
+ hideReadReceipts={hideActivity}
>
<EventContent
messageLayout={messageLayout}
highlight={highlighted}
messageSpacing={messageSpacing}
canDelete={canRedact || mEvent.getSender() === mx.getUserId()}
+ hideReadReceipts={hideActivity}
>
<EventContent
messageLayout={messageLayout}
highlight={highlighted}
messageSpacing={messageSpacing}
canDelete={canRedact || mEvent.getSender() === mx.getUserId()}
+ hideReadReceipts={hideActivity}
>
<EventContent
messageLayout={messageLayout}
import { RoomViewTyping } from './RoomViewTyping';
import { RoomTombstone } from './RoomTombstone';
import { RoomInput } from './RoomInput';
-import { RoomViewFollowing } from './RoomViewFollowing';
+import { RoomViewFollowing, RoomViewFollowingPlaceholder } from './RoomViewFollowing';
import { Page } from '../../components/page';
import { RoomViewHeader } from './RoomViewHeader';
import { useKeyDown } from '../../hooks/useKeyDown';
import { editableActiveElement } from '../../utils/dom';
import navigation from '../../../client/state/navigation';
+import { settingsAtom } from '../../state/settings';
+import { useSetting } from '../../state/hooks/settings';
const FN_KEYS_REGEX = /^F\d+$/;
const shouldFocusMessageField = (evt: KeyboardEvent): boolean => {
const roomInputRef = useRef<HTMLDivElement>(null);
const roomViewRef = useRef<HTMLDivElement>(null);
+ const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
+
const { roomId } = room;
const editor = useEditor();
</>
)}
</div>
- <RoomViewFollowing room={room} />
+ {hideActivity ? <RoomViewFollowingPlaceholder /> : <RoomViewFollowing room={room} />}
</Box>
</Page>
);
+import { style } from '@vanilla-extract/css';
import { recipe } from '@vanilla-extract/recipes';
import { DefaultReset, color, config, toRem } from 'folds';
+export const RoomViewFollowingPlaceholder = style([
+ DefaultReset,
+ {
+ height: toRem(28),
+ },
+]);
+
export const RoomViewFollowing = recipe({
base: [
DefaultReset,
import { EventReaders } from '../../components/event-readers';
import { stopPropagation } from '../../utils/keyboard';
+export function RoomViewFollowingPlaceholder() {
+ return <div className={css.RoomViewFollowingPlaceholder} />;
+}
+
export type RoomViewFollowingProps = {
room: Room;
};
import { StateEvent } from '../../../types/matrix/room';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { useRoom } from '../../hooks/useRoom';
-import { useSetSetting } from '../../state/hooks/settings';
+import { useSetSetting, useSetting } from '../../state/hooks/settings';
import { settingsAtom } from '../../state/settings';
import { useSpaceOptionally } from '../../hooks/useSpace';
import { getHomeSearchPath, getSpaceSearchPath, withSearchParam } from '../../pages/pathUtils';
};
const RoomMenu = forwardRef<HTMLDivElement, RoomMenuProps>(({ room, requestClose }, ref) => {
const mx = useMatrixClient();
+ const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
const unread = useRoomUnread(room.roomId, roomToUnreadAtom);
const powerLevels = usePowerLevelsContext();
const { getPowerLevel, canDoAction } = usePowerLevelsAPI(powerLevels);
const canInvite = canDoAction('invite', getPowerLevel(mx.getUserId() ?? ''));
const handleMarkAsRead = () => {
- markAsRead(mx, room.roomId);
+ markAsRead(mx, room.roomId, hideActivity);
requestClose();
};
onReactionToggle: (targetEventId: string, key: string, shortcode?: string) => void;
reply?: ReactNode;
reactions?: ReactNode;
+ hideReadReceipts?: boolean;
};
export const Message = as<'div', MessageProps>(
(
onEditId,
reply,
reactions,
+ hideReadReceipts,
children,
...props
},
</Text>
</MenuItem>
)}
- <MessageReadReceiptItem
- room={room}
- eventId={mEvent.getId() ?? ''}
- onClose={closeMenu}
- />
+ {!hideReadReceipts && (
+ <MessageReadReceiptItem
+ room={room}
+ eventId={mEvent.getId() ?? ''}
+ onClose={closeMenu}
+ />
+ )}
<MessageSourceCodeItem room={room} mEvent={mEvent} onClose={closeMenu} />
<MessageCopyLinkItem room={room} mEvent={mEvent} onClose={closeMenu} />
{canPinEvent && (
highlight: boolean;
canDelete?: boolean;
messageSpacing: MessageSpacing;
+ hideReadReceipts?: boolean;
};
export const Event = as<'div', EventProps>(
- ({ className, room, mEvent, highlight, canDelete, messageSpacing, children, ...props }, ref) => {
+ (
+ {
+ className,
+ room,
+ mEvent,
+ highlight,
+ canDelete,
+ messageSpacing,
+ hideReadReceipts,
+ children,
+ ...props
+ },
+ ref
+ ) => {
const mx = useMatrixClient();
const [hover, setHover] = useState(false);
const { hoverProps } = useHover({ onHoverChange: setHover });
>
<Menu {...props} ref={ref}>
<Box direction="Column" gap="100" className={css.MessageMenuGroup}>
- <MessageReadReceiptItem
- room={room}
- eventId={mEvent.getId() ?? ''}
- onClose={closeMenu}
- />
+ {!hideReadReceipts && (
+ <MessageReadReceiptItem
+ room={room}
+ eventId={mEvent.getId() ?? ''}
+ onClose={closeMenu}
+ />
+ )}
<MessageSourceCodeItem room={room} mEvent={mEvent} onClose={closeMenu} />
<MessageCopyLinkItem room={room} mEvent={mEvent} onClose={closeMenu} />
</Box>
function Editor() {
const [enterForNewline, setEnterForNewline] = useSetting(settingsAtom, 'enterForNewline');
const [isMarkdown, setIsMarkdown] = useSetting(settingsAtom, 'isMarkdown');
+ const [hideActivity, setHideActivity] = useSetting(settingsAtom, 'hideActivity');
return (
<Box direction="Column" gap="100">
after={<Switch variant="Primary" value={isMarkdown} onChange={setIsMarkdown} />}
/>
</SequenceCard>
+ <SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
+ <SettingTile
+ title="Hide Typing & Read Receipts"
+ description="Turn off both typing status and read receipts to keep your activity private."
+ after={<Switch variant="Primary" value={hideActivity} onChange={setHideActivity} />}
+ />
+ </SequenceCard>
</Box>
);
}
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title="Disable Media Auto Load"
- after={<Switch variant="Primary" value={!mediaAutoLoad} onChange={(v) => setMediaAutoLoad(!v)} />}
+ after={
+ <Switch
+ variant="Primary"
+ value={!mediaAutoLoad}
+ onChange={(v) => setMediaAutoLoad(!v)}
+ />
+ }
/>
</SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
import { useRoomsUnread } from '../../../state/hooks/unread';
import { markAsRead } from '../../../../client/action/notifications';
import { stopPropagation } from '../../../utils/keyboard';
+import { useSetting } from '../../../state/hooks/settings';
+import { settingsAtom } from '../../../state/settings';
type DirectMenuProps = {
requestClose: () => void;
};
const DirectMenu = forwardRef<HTMLDivElement, DirectMenuProps>(({ requestClose }, ref) => {
const mx = useMatrixClient();
+ const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
const orphanRooms = useDirectRooms();
const unread = useRoomsUnread(orphanRooms, roomToUnreadAtom);
const handleMarkAsRead = () => {
if (!unread) return;
- orphanRooms.forEach((rId) => markAsRead(mx, rId));
+ orphanRooms.forEach((rId) => markAsRead(mx, rId, hideActivity));
requestClose();
};
import { markAsRead } from '../../../../client/action/notifications';
import { useClosedNavCategoriesAtom } from '../../../state/hooks/closedNavCategories';
import { stopPropagation } from '../../../utils/keyboard';
+import { useSetting } from '../../../state/hooks/settings';
+import { settingsAtom } from '../../../state/settings';
type HomeMenuProps = {
requestClose: () => void;
};
const HomeMenu = forwardRef<HTMLDivElement, HomeMenuProps>(({ requestClose }, ref) => {
const orphanRooms = useHomeRooms();
+ const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
const unread = useRoomsUnread(orphanRooms, roomToUnreadAtom);
const mx = useMatrixClient();
const handleMarkAsRead = () => {
if (!unread) return;
- orphanRooms.forEach((rId) => markAsRead(mx, rId));
+ orphanRooms.forEach((rId) => markAsRead(mx, rId, hideActivity));
requestClose();
};
notifications: INotification[];
mediaAutoLoad?: boolean;
urlPreview?: boolean;
+ hideActivity: boolean;
onOpen: (roomId: string, eventId: string) => void;
};
function RoomNotificationsGroupComp({
notifications,
mediaAutoLoad,
urlPreview,
+ hideActivity,
onOpen,
}: RoomNotificationsGroupProps) {
const mx = useMatrixClient();
onOpen(room.roomId, eventId);
};
const handleMarkAsRead = () => {
- markAsRead(mx, room.roomId);
+ markAsRead(mx, room.roomId, hideActivity);
};
return (
export function Notifications() {
const mx = useMatrixClient();
+ const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
const [mediaAutoLoad] = useSetting(settingsAtom, 'mediaAutoLoad');
const [urlPreview] = useSetting(settingsAtom, 'urlPreview');
const screenSize = useScreenSizeContext();
notifications={group.notifications}
mediaAutoLoad={mediaAutoLoad}
urlPreview={urlPreview}
+ hideActivity={hideActivity}
onOpen={navigateRoom}
/>
</VirtualTile>
import { useDirectRooms } from '../direct/useDirectRooms';
import { markAsRead } from '../../../../client/action/notifications';
import { stopPropagation } from '../../../utils/keyboard';
+import { settingsAtom } from '../../../state/settings';
+import { useSetting } from '../../../state/hooks/settings';
type DirectMenuProps = {
requestClose: () => void;
};
const DirectMenu = forwardRef<HTMLDivElement, DirectMenuProps>(({ requestClose }, ref) => {
const orphanRooms = useDirectRooms();
+ const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
const unread = useRoomsUnread(orphanRooms, roomToUnreadAtom);
const mx = useMatrixClient();
const handleMarkAsRead = () => {
if (!unread) return;
- orphanRooms.forEach((rId) => markAsRead(mx, rId));
+ orphanRooms.forEach((rId) => markAsRead(mx, rId, hideActivity));
requestClose();
};
import { useHomeRooms } from '../home/useHomeRooms';
import { markAsRead } from '../../../../client/action/notifications';
import { stopPropagation } from '../../../utils/keyboard';
+import { useSetting } from '../../../state/hooks/settings';
+import { settingsAtom } from '../../../state/settings';
type HomeMenuProps = {
requestClose: () => void;
};
const HomeMenu = forwardRef<HTMLDivElement, HomeMenuProps>(({ requestClose }, ref) => {
const orphanRooms = useHomeRooms();
+ const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
const unread = useRoomsUnread(orphanRooms, roomToUnreadAtom);
const mx = useMatrixClient();
const handleMarkAsRead = () => {
if (!unread) return;
- orphanRooms.forEach((rId) => markAsRead(mx, rId));
+ orphanRooms.forEach((rId) => markAsRead(mx, rId, hideActivity));
requestClose();
};
import { getViaServers } from '../../../plugins/via-servers';
import { getRoomAvatarUrl } from '../../../utils/room';
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
+import { useSetting } from '../../../state/hooks/settings';
+import { settingsAtom } from '../../../state/settings';
type SpaceMenuProps = {
room: Room;
const SpaceMenu = forwardRef<HTMLDivElement, SpaceMenuProps>(
({ room, requestClose, onUnpin }, ref) => {
const mx = useMatrixClient();
+ const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
const roomToParents = useAtomValue(roomToParentsAtom);
const powerLevels = usePowerLevels(room);
const { getPowerLevel, canDoAction } = usePowerLevelsAPI(powerLevels);
const unread = useRoomsUnread(allChild, roomToUnreadAtom);
const handleMarkAsRead = () => {
- allChild.forEach((childRoomId) => markAsRead(mx, childRoomId));
+ allChild.forEach((childRoomId) => markAsRead(mx, childRoomId, hideActivity));
requestClose();
};
return !target
? undefined
: draggable({
- element: target,
- dragHandle,
- getInitialData: () => ({ item }),
- onDragStart: () => {
- setDragging(true);
- onDragging?.(item);
- },
- onDrop: () => {
- setDragging(false);
- onDragging?.(undefined);
- },
- });
+ element: target,
+ dragHandle,
+ getInitialData: () => ({ item }),
+ onDragStart: () => {
+ setDragging(true);
+ onDragging?.(item);
+ },
+ onDrop: () => {
+ setDragging(false);
+ onDragging?.(undefined);
+ },
+ });
}, [targetRef, dragHandleRef, item, onDragging]);
return dragging;
() =>
folder
? {
- folder,
- spaceId: space.roomId,
- }
+ folder,
+ spaceId: space.roomId,
+ }
: space.roomId,
[folder, space]
);
import { stopPropagation } from '../../../utils/keyboard';
import { getMatrixToRoom } from '../../../plugins/matrix-to';
import { getViaServers } from '../../../plugins/via-servers';
+import { useSetting } from '../../../state/hooks/settings';
+import { settingsAtom } from '../../../state/settings';
type SpaceMenuProps = {
room: Room;
};
const SpaceMenu = forwardRef<HTMLDivElement, SpaceMenuProps>(({ room, requestClose }, ref) => {
const mx = useMatrixClient();
+ const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
const roomToParents = useAtomValue(roomToParentsAtom);
const powerLevels = usePowerLevels(room);
const { getPowerLevel, canDoAction } = usePowerLevelsAPI(powerLevels);
const unread = useRoomsUnread(allChild, roomToUnreadAtom);
const handleMarkAsRead = () => {
- allChild.forEach((childRoomId) => markAsRead(mx, childRoomId));
+ allChild.forEach((childRoomId) => markAsRead(mx, childRoomId, hideActivity));
requestClose();
};
useEffect(() => {
const handleReceipt = (mEvent: MatrixEvent, room: Room) => {
- if (mEvent.getType() === 'm.receipt') {
- const myUserId = mx.getUserId();
- if (!myUserId) return;
- if (room.isSpaceRoom()) return;
- const content = mEvent.getContent<ReceiptContent>();
+ const myUserId = mx.getUserId();
+ if (!myUserId) return;
+ if (room.isSpaceRoom()) return;
+ const content = mEvent.getContent<ReceiptContent>();
- const isMyReceipt = Object.keys(content).find((eventId) =>
- (Object.keys(content[eventId]) as ReceiptType[]).find(
- (receiptType) => content[eventId][receiptType][myUserId]
- )
- );
- if (isMyReceipt) {
- setUnreadAtom({ type: 'DELETE', roomId: room.roomId });
- }
+ const isMyReceipt = Object.keys(content).find((eventId) =>
+ (Object.keys(content[eventId]) as ReceiptType[]).find(
+ (receiptType) => content[eventId][receiptType][myUserId]
+ )
+ );
+ if (isMyReceipt) {
+ setUnreadAtom({ type: 'DELETE', roomId: room.roomId });
}
};
mx.on(RoomEvent.Receipt, handleReceipt);
editorToolbar: boolean;
twitterEmoji: boolean;
pageZoom: number;
+ hideActivity: boolean;
isPeopleDrawer: boolean;
memberSortFilterIndex: number;
editorToolbar: false,
twitterEmoji: false,
pageZoom: 100,
+ hideActivity: false,
isPeopleDrawer: true,
memberSortFilterIndex: 0,
import { atom, useSetAtom } from 'jotai';
import { MatrixClient, RoomMemberEvent, RoomMemberEventHandlerMap } from 'matrix-js-sdk';
import { useEffect } from 'react';
+import { useSetting } from './hooks/settings';
+import { settingsAtom } from './settings';
export const TYPING_TIMEOUT_MS = 5000; // 5 seconds
typingMembersAtom: typeof roomIdToTypingMembersAtom
) => {
const setTypingMembers = useSetAtom(typingMembersAtom);
+ const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
useEffect(() => {
const handleTypingEvent: RoomMemberEventHandlerMap[RoomMemberEvent.Typing] = (
event,
member
) => {
+ if (hideActivity) {
+ return;
+ }
setTypingMembers({
type: member.typing ? 'PUT' : 'DELETE',
roomId: member.roomId,
return () => {
mx.removeListener(RoomMemberEvent.Typing, handleTypingEvent);
};
- }, [mx, setTypingMembers]);
+ }, [mx, setTypingMembers, hideActivity]);
};
-import { MatrixClient } from "matrix-js-sdk";
+import { MatrixClient, ReceiptType } from 'matrix-js-sdk';
-export async function markAsRead(mx: MatrixClient, roomId: string) {
+export async function markAsRead(mx: MatrixClient, roomId: string, privateReceipt: boolean) {
const room = mx.getRoom(roomId);
if (!room) return;
const latestEvent = getLatestValidEvent();
if (latestEvent === null) return;
- await mx.sendReadReceipt(latestEvent);
+ await mx.sendReadReceipt(
+ latestEvent,
+ privateReceipt ? ReceiptType.ReadPrivate : ReceiptType.Read
+ );
}