Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/add-room-banner-support.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
default: minor
---

Add Space banner support per MSC4221. You can now set it from the space settings.
1 change: 0 additions & 1 deletion src/app/components/page/Page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ export const PageNavHeader = as<'header', css.PageNavHeaderVariants>(
<Header
className={classNames(css.PageNavHeader({ outlined }), className)}
variant="Background"
size="600"
{...props}
ref={ref}
/>
Expand Down
198 changes: 197 additions & 1 deletion src/app/features/common-settings/general/RoomProfile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,22 @@ import {
Button,
Chip,
color,
config,
Dialog,
Header,
Icon,
IconButton,
Icons,
Input,
Overlay,
OverlayBackdrop,
OverlayCenter,
Spinner,
Text,
TextArea,
} from 'folds';
import type { FormEventHandler } from 'react';
import { useCallback, useMemo, useState } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useAtomValue } from 'jotai';
import Linkify from 'linkify-react';
import classNames from 'classnames';
Expand Down Expand Up @@ -40,6 +47,12 @@ import { useAlive } from '$hooks/useAlive';
import type { RoomPermissionsAPI } from '$hooks/useRoomPermissions';
import { useSetting } from '$state/hooks/settings';
import { settingsAtom } from '$state/settings';
import { useStateEvent } from '$hooks/useStateEvent';
import type { RoomBannerContent } from '$types/matrix-sdk-events';
import { CustomStateEvent } from '$types/matrix/room';
import { SettingTile } from '$components/setting-tile';
import { stopPropagation } from '$utils/keyboard';
import FocusTrap from 'focus-trap-react';

type RoomProfileEditProps = {
canEditAvatar: boolean;
Expand Down Expand Up @@ -296,6 +309,175 @@ export function RoomProfileEdit({
);
}

export type ProfileProps = {
bannerURI?: string;
};
function RoomBannerEdit({ bannerURI }: Readonly<ProfileProps>) {
const mx = useMatrixClient();
const [alertRemove, setAlertRemove] = useState(false);

const space = useRoom();
const [stagedUrl, setStagedUrl] = useState<string>();
const [isRemoving, setIsRemoving] = useState(false);

const bannerUrl = bannerURI;

useEffect(() => {
if (bannerUrl) {
setStagedUrl(undefined);
}
}, [bannerUrl]);

const [imageFile, setImageFile] = useState<File>();
const imageFileURL = useObjectURL(imageFile);

const uploadAtom = useMemo(() => {
if (imageFile) return createUploadAtom(imageFile);
return undefined;
}, [imageFile]);

const pickFile = useFilePicker(setImageFile, false);

const handlePick = useCallback(() => {
setIsRemoving(false);
setStagedUrl(undefined);
pickFile('image/*');
}, [pickFile]);

const handleRemoveUpload = useCallback(() => {
setImageFile(undefined);
}, []);

const handleUploaded = useCallback(
(upload: UploadSuccess) => {
const { mxc } = upload;

if (imageFileURL) setStagedUrl(imageFileURL);
mx.sendStateEvent(space.roomId, CustomStateEvent.RoomBanner, { url: mxc }, '');
setImageFile(undefined);
},
[mx, imageFileURL, space]
);

const handleRemoveBanner = async () => {
setIsRemoving(true);
setStagedUrl(undefined);
setImageFile(undefined);

mx.sendStateEvent(space.roomId, CustomStateEvent.RoomBanner, { url: '' }, '');

setAlertRemove(false);
};

const previewUrl = isRemoving ? undefined : imageFileURL || stagedUrl || bannerUrl;

return (
<SettingTile title="Banner" focusId="banner">
<Box direction="Column" gap="300" grow="Yes">
<Box
style={{
height: '100px',
width: '100%',
borderRadius: config.radii.R400,
overflow: 'hidden',
backgroundColor: 'var(--sable-surface-container)',
position: 'relative',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
{previewUrl ? (
<img
src={previewUrl}
key={previewUrl}
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
alt="Banner Preview"
/>
) : (
<Box justifyContent="Center" alignItems="Center">
<Text priority="300" size="T200">
No Banner Set
</Text>
</Box>
)}
</Box>

{uploadAtom ? (
<Box gap="200" direction="Column">
<CompactUploadCardRenderer
uploadAtom={uploadAtom}
onRemove={handleRemoveUpload}
onComplete={handleUploaded}
/>
</Box>
) : (
<Box gap="200">
<Button
onClick={handlePick}
size="300"
variant="Secondary"
fill="Soft"
outlined
radii="300"
>
<Text size="B300">{bannerUrl ? 'Change Banner' : 'Upload Banner'}</Text>
</Button>
{bannerUrl && (
<Button
size="300"
variant="Critical"
fill="None"
radii="300"
onClick={() => setAlertRemove(true)}
>
<Text size="B300">Remove</Text>
</Button>
)}
</Box>
)}
</Box>

<Overlay open={alertRemove} backdrop={<OverlayBackdrop />}>
<OverlayCenter>
<FocusTrap
focusTrapOptions={{
initialFocus: false,
onDeactivate: () => setAlertRemove(false),
clickOutsideDeactivates: true,
escapeDeactivates: stopPropagation,
}}
>
<Dialog variant="Surface">
<Header
style={{
padding: `0 ${config.space.S200} 0 ${config.space.S400}`,
borderBottomWidth: config.borderWidth.B300,
}}
variant="Surface"
size="500"
>
<Box grow="Yes">
<Text size="H4">Remove Banner</Text>
</Box>
<IconButton size="300" onClick={() => setAlertRemove(false)} radii="300">
<Icon src={Icons.Cross} />
</IconButton>
</Header>
<Box style={{ padding: config.space.S400 }} direction="Column" gap="400">
<Text priority="400">Are you sure you want to remove profile banner?</Text>
<Button variant="Critical" onClick={handleRemoveBanner}>
<Text size="B400">Remove</Text>
</Button>
</Box>
</Dialog>
</FocusTrap>
</OverlayCenter>
</Overlay>
</SettingTile>
);
}

type RoomProfileProps = {
permissions: RoomPermissionsAPI;
};
Expand Down Expand Up @@ -325,6 +507,10 @@ export function RoomProfile({ permissions }: RoomProfileProps) {

const handleCloseEdit = useCallback(() => setEdit(false), []);

const bannerState = useStateEvent(room, CustomStateEvent.RoomBanner);
const bannerMXC = bannerState?.getContent<RoomBannerContent>()?.url;
const bannerURI = mxcUrlToHttp(mx, bannerMXC ?? '', true);

return (
<Box direction="Column" gap="100">
<Text size="L400">Profile</Text>
Expand Down Expand Up @@ -393,6 +579,16 @@ export function RoomProfile({ permissions }: RoomProfileProps) {
</Box>
)}
</SequenceCard>
{room.isSpaceRoom() && (
<SequenceCard
className={SequenceCardStyle}
variant="SurfaceVariant"
direction="Column"
gap="400"
>
<RoomBannerEdit bannerURI={bannerURI ?? undefined} />
</SequenceCard>
)}
</Box>
);
}
13 changes: 13 additions & 0 deletions src/app/features/settings/cosmetics/Themes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@ function ThemeVisualPreferences() {
const [linkPreviewMaxHeightInput, setLinkPreviewMaxHeightInput] = useState(
linkPreviewImageMaxHeight.toString()
);
const [showRoomBanners, setShowRoomBanners] = useSetting(settingsAtom, 'showRoomBanners');

const handleIncomingDefaultHeightChange: ChangeEventHandler<HTMLInputElement> = (evt) => {
const val = evt.target.value;
Expand Down Expand Up @@ -345,6 +346,14 @@ function ThemeVisualPreferences() {
/>
</SequenceCard>

<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title="Display Room banners"
focusId="display-room-banners"
after={<Switch variant="Primary" value={showRoomBanners} onChange={setShowRoomBanners} />}
/>
</SequenceCard>

<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title="Incoming inline images default height"
Expand Down Expand Up @@ -618,6 +627,7 @@ function SidebarWidth({ sidebarSelector }: { sidebarSelector: string }) {
settingsAtom,
'widgetSidebarWidth'
);
const [roomBannerHeight, setRoomBannerHeight] = useSetting(settingsAtom, 'roomBannerHeight');

// Yandere style code but it works and is as straight forward as can be :shrug:
const getCurValue = useMemo(() => {
Expand All @@ -627,6 +637,7 @@ function SidebarWidth({ sidebarSelector }: { sidebarSelector: string }) {
if (sidebarSelector === 'threadRootHeight') return threadRootHeight;
if (sidebarSelector === 'vcmsgSidebarWidth') return vcmsgSidebarWidth;
if (sidebarSelector === 'widgetSidebarWidth') return widgetSidebarWidth;
if (sidebarSelector === 'roomBannerHeight') return roomBannerHeight;
return undefined;
}, [
sidebarSelector,
Expand All @@ -636,6 +647,7 @@ function SidebarWidth({ sidebarSelector }: { sidebarSelector: string }) {
threadRootHeight,
vcmsgSidebarWidth,
widgetSidebarWidth,
roomBannerHeight,
]);
const [curValue, setCurValue] = useState(getCurValue);
const setValue = (value: number) => {
Expand All @@ -645,6 +657,7 @@ function SidebarWidth({ sidebarSelector }: { sidebarSelector: string }) {
if (sidebarSelector === 'threadRootHeight') setThreadRootHeight(value);
if (sidebarSelector === 'vcmsgSidebarWidth') setvcmsgSidebarWidth(value);
if (sidebarSelector === 'widgetSidebarWidth') setWidgetSidebarWidth(value);
if (sidebarSelector === 'roomBannerHeight') setRoomBannerHeight(value);
};

useEffect(() => {
Expand Down
1 change: 1 addition & 0 deletions src/app/features/settings/settingsLink.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ const settingsLinkFocusIdsBySection: Record<SettingsSectionId, readonly string[]
'customize-dm-cards',
'custom-profile-cards',
'dark-theme',
'display-room-banners',
'jumbo-emoji-size',
'light-theme',
'manual-theme',
Expand Down
4 changes: 4 additions & 0 deletions src/app/hooks/usePanelSizes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ export const usePanelSizeItems = (): PanelSizetItem[] =>
layout: 'widgetSidebarWidth',
name: 'Widget Panel Width',
},
{
layout: 'roomBannerHeight',
name: 'Room Banner Height',
},
],
[]
);
Loading
Loading