diff --git a/src/app/features/create-post/CreatePost.tsx b/src/app/features/create-post/CreatePost.tsx index 7b6c43c0..396cc9a6 100644 --- a/src/app/features/create-post/CreatePost.tsx +++ b/src/app/features/create-post/CreatePost.tsx @@ -6,7 +6,7 @@ import React, { useState, } from 'react'; import { useAtomValue } from 'jotai'; -import { IContent, MatrixError, MsgType, Room } from 'matrix-js-sdk'; +import { IContent, JoinRule, MatrixError, MsgType, Room } from 'matrix-js-sdk'; import FocusTrap from 'focus-trap-react'; import { isKeyHotkey } from 'is-hotkey'; import { @@ -55,7 +55,7 @@ import { roomUploadAtomFamily, } from '../../state/room/roomInputDrafts'; import { UploadCardRenderer } from '../../components/upload-card'; -import { useFeedRooms } from '../feed'; +import { useSpacesWithPostsRoom } from '../feed'; import { getAudioMsgContent, getFileMsgContent, @@ -85,24 +85,32 @@ export function CreatePostForm({ defaultRoomId, onCreate }: CreatePostFormProps) const alive = useAlive(); const editor = useEditor(); const useAuthentication = useMediaAuthentication(); - const feedRoomIds = useFeedRooms(); + const spacesWithPostsRoom = useSpacesWithPostsRoom(); const isComposing = useComposingCheck(); const [isMarkdown] = useSetting(settingsAtom, 'isMarkdown'); const [globalToolbar] = useSetting(settingsAtom, 'editorToolbar'); const [toolbar, setToolbar] = useState(globalToolbar); - const feedRooms = useMemo( + const spaces = useMemo( () => - feedRoomIds.map((roomId) => mx.getRoom(roomId)).filter((room): room is Room => room !== null), - [mx, feedRoomIds] + spacesWithPostsRoom + .map(({ spaceId }) => mx.getRoom(spaceId)) + .filter((room): room is Room => room !== null), + [mx, spacesWithPostsRoom] ); - const [roomId, setRoomId] = useState( - (defaultRoomId && feedRoomIds.includes(defaultRoomId) ? defaultRoomId : undefined) ?? - feedRoomIds[0] + const [spaceId, setSpaceId] = useState( + (defaultRoomId && + spacesWithPostsRoom.find( + (space) => space.spaceId === defaultRoomId || space.postsRoomId === defaultRoomId + )?.spaceId) ?? + spacesWithPostsRoom[0]?.spaceId ); - const selectedRoom = roomId ? mx.getRoom(roomId) : undefined; + const selectedSpace = spaceId ? mx.getRoom(spaceId) : undefined; + const postsRoomId = spacesWithPostsRoom.find((space) => space.spaceId === spaceId)?.postsRoomId; + const selectedRoom = postsRoomId ? mx.getRoom(postsRoomId) : undefined; + const isPublicRoom = selectedRoom?.getJoinRule() === JoinRule.Public; const [menuAnchor, setMenuAnchor] = useState(); const [selectedFiles, setSelectedFiles] = useState([]); @@ -162,7 +170,7 @@ export function CreatePostForm({ defaultRoomId, onCreate }: CreatePostFormProps) const sendAttachments = useCallback( async (successUploads: UploadSuccess[], caption?: { body: string; formattedBody?: string }) => { - if (!roomId) return; + if (!postsRoomId) return; const contentsPromises = successUploads.map(async (upload) => { const fileItem = selectedFiles.find((f) => f.file === upload.file); if (!fileItem) throw new Error('Broken upload'); @@ -184,19 +192,21 @@ export function CreatePostForm({ defaultRoomId, onCreate }: CreatePostFormProps) } } await Promise.all( - contents.map((content) => mx.sendMessage(roomId, { ...content, 'm.post': true } as any)) + contents.map((content) => + mx.sendMessage(postsRoomId, { ...content, 'm.post': true } as any) + ) ); successUploads.forEach((upload) => roomUploadAtomFamily.remove(upload.file)); setSelectedFiles((items) => items.filter((item) => !successUploads.some((u) => u.file === item.file)) ); }, - [mx, roomId, selectedFiles] + [mx, postsRoomId, selectedFiles] ); const [createState, create] = useAsyncCallback( useCallback(async () => { - if (!roomId) throw new Error('Select a room to share to'); + if (!postsRoomId) throw new Error('Select a space to share to'); const plainText = toPlainText(editor.children, isMarkdown).trim(); const hasText = plainText !== ''; @@ -230,7 +240,7 @@ export function CreatePostForm({ defaultRoomId, onCreate }: CreatePostFormProps) } if (hasText && !mergeCaptionIntoAttachment) { - const mentionData = getMentions(mx, roomId, editor); + const mentionData = getMentions(mx, postsRoomId, editor); const mMentions = getMentionContent(Array.from(mentionData.users), mentionData.room); const content: IContent = { @@ -244,12 +254,21 @@ export function CreatePostForm({ defaultRoomId, onCreate }: CreatePostFormProps) content.formatted_body = customHtml; } - await mx.sendMessage(roomId, content as any); + await mx.sendMessage(postsRoomId, content as any); } resetEditor(editor); resetEditorHistory(editor); - }, [mx, roomId, editor, isMarkdown, selectedFiles, uploads, uploadsPending, sendAttachments]) + }, [ + mx, + postsRoomId, + editor, + isMarkdown, + selectedFiles, + uploads, + uploadsPending, + sendAttachments, + ]) ); const loading = createState.status === AsyncStatus.Loading; const error = createState.status === AsyncStatus.Error ? createState.error : undefined; @@ -279,8 +298,8 @@ export function CreatePostForm({ defaultRoomId, onCreate }: CreatePostFormProps) setMenuAnchor(evt.currentTarget.getBoundingClientRect()); }; - const handleRoomSelect = (id: string) => { - setRoomId(id); + const handleSpaceSelect = (id: string) => { + setSpaceId(id); setMenuAnchor(undefined); }; @@ -307,13 +326,13 @@ export function CreatePostForm({ defaultRoomId, onCreate }: CreatePostFormProps) type="button" variant="SurfaceVariant" radii="400" - before={selectedRoom ? renderRoomAvatar(selectedRoom) : undefined} + before={selectedSpace ? renderRoomAvatar(selectedSpace) : undefined} after={} onClick={handleOpenMenu} disabled={loading} > - {selectedRoom ? selectedRoom.name : 'Select a room'} + {selectedSpace ? selectedSpace.name : 'Select a space'} - {feedRooms.length === 0 && ( + {spaces.length === 0 && ( - No rooms available + No spaces available )} - {feedRooms.map((room) => ( + {spaces.map((space) => ( handleRoomSelect(room.roomId)} + variant={space.roomId === spaceId ? 'Success' : 'Surface'} + aria-pressed={space.roomId === spaceId} + before={renderRoomAvatar(space)} + onClick={() => handleSpaceSelect(space.roomId)} > - {room.name} + {space.name} ))} @@ -358,6 +377,13 @@ export function CreatePostForm({ defaultRoomId, onCreate }: CreatePostFormProps) } /> + {selectedRoom && ( + + {isPublicRoom + ? 'This will be shared publicly' + : 'This will only be visible to members of this room'} + + )} Content @@ -388,7 +414,7 @@ export function CreatePostForm({ defaultRoomId, onCreate }: CreatePostFormProps) size="300" radii="300" onClick={() => pickFile('*')} - disabled={!roomId} + disabled={!postsRoomId} aria-label="Attach media or files" > @@ -439,7 +465,7 @@ export function CreatePostForm({ defaultRoomId, onCreate }: CreatePostFormProps) size="500" variant="Primary" radii="400" - disabled={loading || !roomId || uploadsPending} + disabled={loading || !postsRoomId || uploadsPending} before={loading && } onClick={submit} > diff --git a/src/app/features/feed/useFeedRooms.ts b/src/app/features/feed/useFeedRooms.ts index 3b8ef1f6..4cc2314a 100644 --- a/src/app/features/feed/useFeedRooms.ts +++ b/src/app/features/feed/useFeedRooms.ts @@ -1,10 +1,11 @@ -import { useCallback } from 'react'; +import { useCallback, useMemo } from 'react'; import { useAtomValue } from 'jotai'; import { RoomSelector, useRecursiveChildRoomScopeFactory, useSelectedRooms, useSpaceChildren, + useSpaces, } from '../../state/hooks/roomList'; import { allRoomsAtom } from '../../state/room-list/roomList'; import { mDirectAtom } from '../../state/mDirectList'; @@ -29,3 +30,39 @@ export const useSpaceFeedRooms = (spaceId: string): string[] => { useRecursiveChildRoomScopeFactory(mx, mDirects, roomToParents) ); }; + +const POSTS_ROOM_SUFFIX = '-posts'; + +export type SpaceWithPostsRoom = { + spaceId: string; + postsRoomId: string; +}; + +/** + * Spaces that contain at least one room whose name ends with "-posts", + * paired with the id of the first such room (the post destination for that space). + */ +export const useSpacesWithPostsRoom = (): SpaceWithPostsRoom[] => { + const mx = useMatrixClient(); + const allRooms = useAtomValue(allRoomsAtom); + const mDirects = useAtomValue(mDirectAtom); + const roomToParents = useAtomValue(roomToParentsAtom); + const spaceIds = useSpaces(mx, allRoomsAtom); + const childRoomSelectorFactory = useRecursiveChildRoomScopeFactory(mx, mDirects, roomToParents); + + return useMemo( + () => + spaceIds.reduce((spacesWithPostsRoom, spaceId) => { + const childRoomSelector = childRoomSelectorFactory(spaceId); + const postsRoomId = allRooms.find( + (roomId) => + childRoomSelector(roomId) && mx.getRoom(roomId)?.name.endsWith(POSTS_ROOM_SUFFIX) + ); + if (postsRoomId) { + spacesWithPostsRoom.push({ spaceId, postsRoomId }); + } + return spacesWithPostsRoom; + }, []), + [mx, allRooms, spaceIds, childRoomSelectorFactory] + ); +};