Add option to change the size of images/videos in the timeline (#7017)

Co-authored-by: Šimon Brandner <simon.bra.ag@gmail.com>
Co-authored-by: J. Ryan Stinnett <jryans@gmail.com>
Co-authored-by: Timo K <toger5@hotmail.de>
This commit is contained in:
Travis Ralston 2021-11-17 08:19:30 -07:00 committed by GitHub
parent 816136de97
commit 3c06e7f7a0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 293 additions and 45 deletions

View file

@ -32,6 +32,7 @@ $slider-selection-dot-size: 2.4em;
$container-border-width: 8px;
$timeline-image-boarder-radius: 8px;
:root {
font-size: 10px;

View file

@ -252,6 +252,7 @@
@import "./views/settings/_E2eAdvancedPanel.scss";
@import "./views/settings/_EmailAddresses.scss";
@import "./views/settings/_FontScalingPanel.scss";
@import "./views/settings/_ImageSizePanel.scss";
@import "./views/settings/_IntegrationManager.scss";
@import "./views/settings/_JoinRuleSettings.scss";
@import "./views/settings/_LayoutSwitcher.scss";

View file

@ -1,5 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -14,7 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
$timelineImageBorderRadius: 4px;
$timeline-image-boarder-radius: 8px;
.mx_MImageBody_thumbnail--blurhash {
position: absolute;
@ -24,7 +25,7 @@ $timelineImageBorderRadius: 4px;
.mx_MImageBody_thumbnail {
object-fit: contain;
border-radius: $timelineImageBorderRadius;
border-radius: $timeline-image-boarder-radius;
display: flex;
justify-content: center;
@ -32,9 +33,10 @@ $timelineImageBorderRadius: 4px;
height: 100%;
width: 100%;
// this is needed so that the Blurhash can get have rounded corners without beeing the correct size during loading.
overflow: hidden;
.mx_Blurhash > canvas {
animation: mx--anim-pulse 1.75s infinite cubic-bezier(.4, 0, .6, 1);
border-radius: $timelineImageBorderRadius;
}
.mx_no-image-placeholder {

View file

@ -1,5 +1,5 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Copyright 2020 - 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -18,6 +18,6 @@ span.mx_MVideoBody {
video.mx_MVideoBody {
max-width: 100%;
height: auto;
border-radius: 4px;
border-radius: $timeline-image-boarder-radius;
}
}

View file

@ -0,0 +1,47 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_ImageSizePanel {
color: $primary-content;
.mx_ImageSizePanel_radios {
display: flex;
margin-top: 16px; // move away from header a bit
> label {
margin-right: 68px; // keep the boxes separate
cursor: pointer;
}
.mx_ImageSizePanel_size {
background-color: $quinary-content;
mask-repeat: no-repeat;
mask-size: 221px;
mask-position: center;
width: 221px;
height: 148px;
margin-bottom: 14px; // move radio button away from bottom edge a bit
&.mx_ImageSizePanel_sizeDefault {
mask: url("$(res)/img/element-icons/settings/img-size-normal.svg");
}
&.mx_ImageSizePanel_sizeLarge {
mask: url("$(res)/img/element-icons/settings/img-size-large.svg");
}
}
}
}

View file

@ -0,0 +1,15 @@
<svg width="221" height="148" viewBox="0 0 221 148" fill="none" xmlns="http://www.w3.org/2000/svg">
<defs>
<mask id="mountains">
<rect x="0" y="0" width="100%" height="100%" fill="white" />
<path d="M109.918 67.966L80.2282 114.975C79.0024 116.916 80.3971 119.447 82.6927 119.447H142.073C144.369 119.447 145.763 116.916 144.538 114.975L114.847 67.966C113.704 66.1548 111.062 66.1548 109.918 67.966Z" fill="black"/>
<path d="M141.995 82.4526L121.1 114.956C119.853 116.896 121.246 119.447 123.552 119.447H165.342C167.648 119.447 169.041 116.896 167.794 114.956L146.899 82.4526C145.751 80.6677 143.142 80.6677 141.995 82.4526Z" fill="black"/>
<circle cx="161.936" cy="61.1488" r="11.6596" fill="black"/>
</mask>
</defs>
<rect x="0.5" y="0.5" width="220" height="147" rx="7.5" stroke="#E3E8F0"/>
<rect x="57" y="14" width="10" height="10" rx="1.5" fill="#E3E8F0"/>
<rect x="70" y="14" width="60" height="10" rx="1.5" fill="#E3E8F0"/>
<path d="M0 8C0 3.58173 3.58172 0 8 0H45C45.5523 0 46 0.447715 46 1V147C46 147.552 45.5523 148 45 148H8C3.58172 148 0 144.418 0 140V8Z" fill="#E3E8F0"/>
<rect x="57" y="32" width="137" height="102.021" rx="5.82979" fill="#E3E8F0" mask="url(#mountains)"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -0,0 +1,20 @@
<svg width="221" height="148" viewBox="0 0 221 148" fill="none" xmlns="http://www.w3.org/2000/svg">
<defs>
<mask id="mountains">
<rect x="0" y="0" width="100%" height="100%" fill="white" />
<path d="M75.1545 94.3387L64.9688 110.466C64.5483 111.132 65.0268 112 65.8143 112H86.1857C86.9732 112 87.4517 111.132 87.0312 110.466L76.8455 94.3387C76.4531 93.7173 75.5469 93.7173 75.1545 94.3387Z" fill="black"/>
<path d="M86.1588 99.3085L78.9905 110.459C78.5627 111.125 79.0405 112 79.8317 112H94.1683C94.9595 112 95.4373 111.125 95.0095 110.459L87.8412 99.3085C87.4475 98.6962 86.5525 98.6962 86.1588 99.3085Z" fill="black"/>
<circle cx="93" cy="92" r="4" fill="black"/>
</mask>
</defs>
<rect x="0.5" y="0.5" width="220" height="147" rx="7.5" stroke="#E3E8F0"/>
<rect x="57" y="14" width="10" height="10" rx="1.5" fill="#E3E8F0"/>
<rect x="57" y="64" width="10" height="10" rx="1.5" fill="#E3E8F0"/>
<rect x="57" y="125" width="10" height="10" rx="1.5" fill="#E3E8F0"/>
<rect x="70" y="14" width="60" height="10" rx="1.5" fill="#E3E8F0"/>
<rect x="70" y="64" width="60" height="10" rx="1.5" fill="#E3E8F0"/>
<rect x="70" y="125" width="60" height="10" rx="1.5" fill="#E3E8F0"/>
<path d="M57 32.5C57 31.6716 57.6716 31 58.5 31H208.5C209.328 31 210 31.6716 210 32.5V54.5C210 55.3284 209.328 56 208.5 56H58.5C57.6716 56 57 55.3284 57 54.5V32.5Z" fill="#E3E8F0"/>
<path d="M0 8C0 3.58173 3.58172 0 8 0H45C45.5523 0 46 0.447715 46 1V147C46 147.552 45.5523 148 45 148H8C3.58172 148 0 144.418 0 140V8Z" fill="#E3E8F0"/>
<rect x="57" y="82" width="47" height="35" rx="2" fill="#E3E8F0" mask="url(#mountains)" />
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View file

@ -36,7 +36,7 @@ import ResizeNotifier from '../../utils/ResizeNotifier';
import TimelinePanel from "./TimelinePanel";
import Spinner from "../views/elements/Spinner";
import { TileShape } from '../views/rooms/EventTile';
import { Layout } from "../../settings/Layout";
import { Layout } from "../../settings/enums/Layout";
import RoomContext, { TimelineRenderingType } from '../../contexts/RoomContext';
import { logger } from "matrix-js-sdk/src/logger";

View file

@ -27,7 +27,7 @@ import { wantsDateSeparator } from '../../DateUtils';
import { MatrixClientPeg } from '../../MatrixClientPeg';
import SettingsStore from '../../settings/SettingsStore';
import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext";
import { Layout } from "../../settings/Layout";
import { Layout } from "../../settings/enums/Layout";
import { _t } from "../../languageHandler";
import EventTile, { haveTileForEvent, IReadReceiptProps, TileShape } from "../views/rooms/EventTile";
import { hasText } from "../../TextForEvent";

View file

@ -23,7 +23,7 @@ import { replaceableComponent } from "../../utils/replaceableComponent";
import TimelinePanel from "./TimelinePanel";
import Spinner from "../views/elements/Spinner";
import { TileShape } from "../views/rooms/EventTile";
import { Layout } from "../../settings/Layout";
import { Layout } from "../../settings/enums/Layout";
import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext";
import { logger } from "matrix-js-sdk/src/logger";

View file

@ -44,7 +44,7 @@ import RoomViewStore from '../../stores/RoomViewStore';
import RoomScrollStateStore, { ScrollState } from '../../stores/RoomScrollStateStore';
import WidgetEchoStore from '../../stores/WidgetEchoStore';
import SettingsStore from "../../settings/SettingsStore";
import { Layout } from "../../settings/Layout";
import { Layout } from "../../settings/enums/Layout";
import AccessibleButton from "../views/elements/AccessibleButton";
import RightPanelStore from "../../stores/RightPanelStore";
import { haveTileForEvent } from "../views/rooms/EventTile";

View file

@ -27,7 +27,7 @@ import { ContextMenuButton } from '../../accessibility/context_menu/ContextMenuB
import ContextMenu, { ChevronFace, useContextMenu } from './ContextMenu';
import RoomContext, { TimelineRenderingType } from '../../contexts/RoomContext';
import TimelinePanel from './TimelinePanel';
import { Layout } from '../../settings/Layout';
import { Layout } from '../../settings/enums/Layout';
import { useEventEmitter } from '../../hooks/useEventEmitter';
import AccessibleButton from '../views/elements/AccessibleButton';
import { TileShape } from '../views/rooms/EventTile';

View file

@ -27,7 +27,7 @@ import ResizeNotifier from '../../utils/ResizeNotifier';
import { TileShape } from '../views/rooms/EventTile';
import MessageComposer from '../views/rooms/MessageComposer';
import { RoomPermalinkCreator } from '../../utils/permalinks/Permalinks';
import { Layout } from '../../settings/Layout';
import { Layout } from '../../settings/enums/Layout';
import TimelinePanel from './TimelinePanel';
import dis from "../../dispatcher/dispatcher";
import { ActionPayload } from '../../dispatcher/payloads';

View file

@ -25,7 +25,7 @@ import { EventType, RelationType } from 'matrix-js-sdk/src/@types/event';
import { SyncState } from 'matrix-js-sdk/src/sync.api';
import SettingsStore from "../../settings/SettingsStore";
import { Layout } from "../../settings/Layout";
import { Layout } from "../../settings/enums/Layout";
import { _t } from '../../languageHandler';
import { MatrixClientPeg } from "../../MatrixClientPeg";
import RoomContext from "../../contexts/RoomContext";

View file

@ -25,7 +25,7 @@ import { _t } from "../../../languageHandler";
import dis from "../../../dispatcher/dispatcher";
import { useSettingValue, useFeatureEnabled } from "../../../hooks/useSettings";
import { UIFeature } from "../../../settings/UIFeature";
import { Layout } from "../../../settings/Layout";
import { Layout } from "../../../settings/enums/Layout";
import { IDialogProps } from "./IDialogProps";
import BaseDialog from "./BaseDialog";
import { avatarUrlForUser } from "../../../Avatar";

View file

@ -22,7 +22,7 @@ import MemberAvatar from '../avatars/MemberAvatar';
import { _t } from '../../../languageHandler';
import { useStateToggle } from "../../../hooks/useStateToggle";
import AccessibleButton from "./AccessibleButton";
import { Layout } from '../../../settings/Layout';
import { Layout } from '../../../settings/enums/Layout';
interface IProps {
// An array of member events to summarise

View file

@ -22,7 +22,7 @@ import { RoomMember } from 'matrix-js-sdk/src/models/room-member';
import * as Avatar from '../../../Avatar';
import EventTile from '../rooms/EventTile';
import SettingsStore from "../../../settings/SettingsStore";
import { Layout } from "../../../settings/Layout";
import { Layout } from "../../../settings/enums/Layout";
import { UIFeature } from "../../../settings/UIFeature";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import Spinner from './Spinner';

View file

@ -31,7 +31,7 @@ import { Action } from '../../../dispatcher/actions';
import { SetRightPanelPhasePayload } from '../../../dispatcher/payloads/SetRightPanelPhasePayload';
import { jsxJoin } from '../../../utils/ReactUtils';
import { EventType } from 'matrix-js-sdk/src/@types/event';
import { Layout } from '../../../settings/Layout';
import { Layout } from '../../../settings/enums/Layout';
const onPinnedMessagesClick = (): void => {
defaultDispatcher.dispatch<SetRightPanelPhasePayload>({

View file

@ -23,7 +23,7 @@ import dis from '../../../dispatcher/dispatcher';
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
import { makeUserPermalink, RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
import SettingsStore from "../../../settings/SettingsStore";
import { Layout } from "../../../settings/Layout";
import { Layout } from "../../../settings/enums/Layout";
import escapeHtml from "escape-html";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { getUserNameColorClass } from "../../../utils/FormattingUtils";

View file

@ -35,6 +35,7 @@ import classNames from 'classnames';
import { CSSTransition, SwitchTransition } from 'react-transition-group';
import { logger } from "matrix-js-sdk/src/logger";
import { ImageSize, suggestedSize as suggestedImageSize } from "../../../settings/enums/ImageSize";
interface IState {
decryptedUrl?: string;
@ -58,6 +59,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
private unmounted = true;
private image = createRef<HTMLImageElement>();
private timeout?: number;
private sizeWatcher: string;
constructor(props: IBodyProps) {
super(props);
@ -317,12 +319,17 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
}
}, 150);
}
this.sizeWatcher = SettingsStore.watchSetting("Images.size", null, () => {
this.forceUpdate(); // we don't really have a reliable thing to update, so just update the whole thing
});
}
componentWillUnmount() {
this.unmounted = true;
this.context.removeListener('sync', this.onClientSync);
this.clearBlurhashTimeout();
SettingsStore.unwatchSetting(this.sizeWatcher);
}
protected messageContent(
@ -367,11 +374,25 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
infoHeight = this.state.loadedImageDimensions.naturalHeight;
}
// The maximum height of the thumbnail as it is rendered as an <img>
const maxHeight = forcedHeight || Math.min((this.props.maxImageHeight || 600), infoHeight);
// The maximum width of the thumbnail, as dictated by its natural
// maximum height.
const maxWidth = infoWidth * maxHeight / infoHeight;
// The maximum size of the thumbnail as it is rendered as an <img>
// check for any height constraints
const imageSize = SettingsStore.getValue("Images.size") as ImageSize;
const suggestedAndPossibleWidth = Math.min(suggestedImageSize(imageSize).w, infoWidth);
const aspectRatio = infoWidth / infoHeight;
let maxWidth;
let maxHeight;
const maxHeightConstraint = forcedHeight || this.props.maxImageHeight || undefined;
if (maxHeightConstraint && maxHeightConstraint * aspectRatio < suggestedAndPossibleWidth) {
// width is dictated by the maximum height that was defined by the props or the function param `forcedHeight`
maxWidth = maxHeightConstraint * aspectRatio;
// there is no need to check for infoHeight here since this is done with `maxHeightConstraint * aspectRatio < suggestedAndPossibleWidth`
maxHeight = maxHeightConstraint;
} else {
// height is dictated by suggestedWidth (based on the Image.size setting)
maxWidth = suggestedAndPossibleWidth;
maxHeight = suggestedAndPossibleWidth / aspectRatio;
}
let img = null;
let placeholder = null;

View file

@ -28,6 +28,7 @@ import { IBodyProps } from "./IBodyProps";
import MFileBody from "./MFileBody";
import { logger } from "matrix-js-sdk/src/logger";
import { ImageSize, suggestedSize as suggestedVideoSize } from "../../../settings/enums/ImageSize";
interface IState {
decryptedUrl?: string;
@ -42,6 +43,7 @@ interface IState {
@replaceableComponent("views.messages.MVideoBody")
export default class MVideoBody extends React.PureComponent<IBodyProps, IState> {
private videoRef = React.createRef<HTMLVideoElement>();
private sizeWatcher: string;
constructor(props) {
super(props);
@ -57,7 +59,22 @@ export default class MVideoBody extends React.PureComponent<IBodyProps, IState>
};
}
thumbScale(fullWidth: number, fullHeight: number, thumbWidth = 480, thumbHeight = 360) {
private get suggestedDimensions(): { w: number, h: number } {
return suggestedVideoSize(SettingsStore.getValue("Images.size") as ImageSize);
}
private thumbScale(
fullWidth: number,
fullHeight: number,
thumbWidth?: number,
thumbHeight?: number,
): number {
if (!thumbWidth || !thumbHeight) {
const dims = this.suggestedDimensions;
thumbWidth = dims.w;
thumbHeight = dims.h;
}
if (!fullWidth || !fullHeight) {
// Cannot calculate thumbnail height for image: missing w/h in metadata. We can't even
// log this because it's spammy
@ -68,14 +85,8 @@ export default class MVideoBody extends React.PureComponent<IBodyProps, IState>
return 1;
}
const widthMulti = thumbWidth / fullWidth;
const heightMulti = thumbHeight / fullHeight;
if (widthMulti < heightMulti) {
// width is the dominant dimension so scaling will be fixed on that
return widthMulti;
} else {
// height is the dominant dimension so scaling will be fixed on that
return heightMulti;
}
// always scale the videos based on their width.
return widthMulti;
}
private getContentUrl(): string|null {
@ -152,12 +163,16 @@ export default class MVideoBody extends React.PureComponent<IBodyProps, IState>
}
}
async componentDidMount() {
const autoplay = SettingsStore.getValue("autoplayVideo") as boolean;
public async componentDidMount() {
this.sizeWatcher = SettingsStore.watchSetting("Images.size", null, () => {
this.forceUpdate(); // we don't really have a reliable thing to update, so just update the whole thing
});
this.loadBlurhash();
if (this.props.mediaEventHelper.media.isEncrypted && this.state.decryptedUrl === null) {
try {
const autoplay = SettingsStore.getValue("autoplayVideo") as boolean;
const thumbnailUrl = await this.props.mediaEventHelper.thumbnailUrl.value;
if (autoplay) {
logger.log("Preloading video");
@ -189,6 +204,10 @@ export default class MVideoBody extends React.PureComponent<IBodyProps, IState>
}
}
public componentWillUnmount() {
SettingsStore.unwatchSetting(this.sizeWatcher);
}
private videoOnPlay = async () => {
if (this.hasContentUrl() || this.state.fetchingData || this.state.error) {
// We have the file, we are fetching the file, or there is an error.
@ -249,8 +268,9 @@ export default class MVideoBody extends React.PureComponent<IBodyProps, IState>
const contentUrl = this.getContentUrl();
const thumbUrl = this.getThumbUrl();
let height = null;
let width = null;
const defaultDims = this.suggestedDimensions;
let height = defaultDims.h;
let width = defaultDims.w;
let poster = null;
let preload = "metadata";
if (content.info) {

View file

@ -28,7 +28,7 @@ import { _t } from '../../../languageHandler';
import { hasText } from "../../../TextForEvent";
import * as sdk from "../../../index";
import dis from '../../../dispatcher/dispatcher';
import { Layout } from "../../../settings/Layout";
import { Layout } from "../../../settings/enums/Layout";
import { formatTime } from "../../../DateUtils";
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import { ALL_RULE_TYPES } from "../../../mjolnir/BanList";

View file

@ -22,7 +22,7 @@ import SettingsStore from "../../../settings/SettingsStore";
import Slider from "../elements/Slider";
import { FontWatcher } from "../../../settings/watchers/FontWatcher";
import { IValidationResult, IFieldState } from '../elements/Validation';
import { Layout } from "../../../settings/Layout";
import { Layout } from "../../../settings/enums/Layout";
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import { SettingLevel } from "../../../settings/SettingLevel";
import { _t } from "../../../languageHandler";

View file

@ -0,0 +1,79 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from "react";
import SettingsStore from "../../../settings/SettingsStore";
import StyledRadioButton from "../elements/StyledRadioButton";
import { _t } from "../../../languageHandler";
import { SettingLevel } from "../../../settings/SettingLevel";
import { ImageSize } from "../../../settings/enums/ImageSize";
interface IProps {
// none
}
interface IState {
size: ImageSize;
}
export default class ImageSizePanel extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {
size: SettingsStore.getValue("Images.size"),
};
}
private onSizeChange = (ev: React.ChangeEvent<HTMLInputElement>): void => {
const newSize = ev.target.value as ImageSize;
this.setState({ size: newSize });
// noinspection JSIgnoredPromiseFromCall
SettingsStore.setValue("Images.size", null, SettingLevel.ACCOUNT, newSize);
};
public render(): JSX.Element {
return (
<div className="mx_SettingsTab_section mx_ImageSizePanel">
<span className="mx_SettingsTab_subheading">
{ _t("Image size in the timeline") }
</span>
<div className="mx_ImageSizePanel_radios">
<label>
<div className="mx_ImageSizePanel_size mx_ImageSizePanel_sizeDefault" />
<StyledRadioButton
name="image_size"
value={ImageSize.Normal}
checked={this.state.size === ImageSize.Normal}
onChange={this.onSizeChange}
>{ _t("Default") }</StyledRadioButton>
</label>
<label>
<div className="mx_ImageSizePanel_size mx_ImageSizePanel_sizeLarge" />
<StyledRadioButton
name="image_size"
value={ImageSize.Large}
checked={this.state.size === ImageSize.Large}
onChange={this.onSizeChange}
>{ _t("Large") }</StyledRadioButton>
</label>
</div>
</div>
);
}
}

View file

@ -22,7 +22,7 @@ import SettingsStore from "../../../settings/SettingsStore";
import EventTilePreview from "../elements/EventTilePreview";
import StyledRadioButton from "../elements/StyledRadioButton";
import { _t } from "../../../languageHandler";
import { Layout } from "../../../settings/Layout";
import { Layout } from "../../../settings/enums/Layout";
import { SettingLevel } from "../../../settings/SettingLevel";
interface IProps {

View file

@ -25,12 +25,13 @@ import SettingsFlag from '../../../elements/SettingsFlag';
import Field from '../../../elements/Field';
import { SettingLevel } from "../../../../../settings/SettingLevel";
import { UIFeature } from "../../../../../settings/UIFeature";
import { Layout } from "../../../../../settings/Layout";
import { Layout } from "../../../../../settings/enums/Layout";
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
import LayoutSwitcher from "../../LayoutSwitcher";
import FontScalingPanel from '../../FontScalingPanel';
import ThemeChoicePanel from '../../ThemeChoicePanel';
import ImageSizePanel from "../../ImageSizePanel";
interface IProps {
}
@ -188,6 +189,7 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
{ layoutSection }
<FontScalingPanel />
{ this.renderAdvancedSection() }
<ImageSizePanel />
</div>
);
}

View file

@ -17,7 +17,7 @@ limitations under the License.
import { createContext } from "react";
import { IRoomState } from "../components/structures/RoomView";
import { Layout } from "../settings/Layout";
import { Layout } from "../settings/enums/Layout";
export enum TimelineRenderingType {
Room = "Room",

View file

@ -1182,6 +1182,8 @@
"Size must be a number": "Size must be a number",
"Custom font size can only be between %(min)s pt and %(max)s pt": "Custom font size can only be between %(min)s pt and %(max)s pt",
"Use between %(min)s pt and %(max)s pt": "Use between %(min)s pt and %(max)s pt",
"Image size in the timeline": "Image size in the timeline",
"Large": "Large",
"Connecting to integration manager...": "Connecting to integration manager...",
"Cannot connect to integration manager": "Cannot connect to integration manager",
"The integration manager is offline or it cannot reach your homeserver.": "The integration manager is offline or it cannot reach your homeserver.",

View file

@ -37,11 +37,12 @@ import { isMac } from '../Keyboard';
import UIFeatureController from "./controllers/UIFeatureController";
import { UIFeature } from "./UIFeature";
import { OrderedMultiController } from "./controllers/OrderedMultiController";
import { Layout } from "./Layout";
import { Layout } from "./enums/Layout";
import ReducedMotionController from './controllers/ReducedMotionController';
import IncompatibleController from "./controllers/IncompatibleController";
import PseudonymousAnalyticsController from './controllers/PseudonymousAnalyticsController';
import NewLayoutSwitcherController from './controllers/NewLayoutSwitcherController';
import { ImageSize } from "./enums/ImageSize";
import { MetaSpace } from "../stores/spaces";
// These are just a bunch of helper arrays to avoid copy/pasting a bunch of times
@ -737,6 +738,10 @@ export const SETTINGS: {[setting: string]: ISetting} = {
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
default: Layout.Group,
},
"Images.size": {
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
default: ImageSize.Normal,
},
"showChatEffects": {
supportedLevels: LEVELS_ROOM_SETTINGS_WITH_ROOM,
displayName: _td("Show chat effects (animations when receiving e.g. confetti)"),

View file

@ -14,7 +14,7 @@ limitations under the License.
import SettingController from "./SettingController";
import { SettingLevel } from "../SettingLevel";
import SettingsStore from "../SettingsStore";
import { Layout } from "../Layout";
import { Layout } from "../enums/Layout";
export default class NewLayoutSwitcherController extends SettingController {
public onChange(level: SettingLevel, roomId: string, newValue: any) {

View file

@ -0,0 +1,33 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
const SIZE_LARGE = { w: 480, h: 360 };
const SIZE_NORMAL = { w: 324, h: 220 };
export enum ImageSize {
Normal = "normal",
Large = "large",
}
export function suggestedSize(size: ImageSize): { w: number, h: number } {
switch (size) {
case ImageSize.Large:
return SIZE_LARGE;
case ImageSize.Normal:
default:
return SIZE_NORMAL;
}
}

View file

@ -20,7 +20,7 @@ import SettingsHandler from "./SettingsHandler";
import { MatrixClientPeg } from "../../MatrixClientPeg";
import { SettingLevel } from "../SettingLevel";
import { CallbackFn, WatchManager } from "../WatchManager";
import { Layout } from "../Layout";
import { Layout } from "../enums/Layout";
/**
* Gets and sets settings at the "device" level for the current device.

View file

@ -21,7 +21,7 @@ import { mediaFromMxc } from "../../customisations/Media";
import { Room } from "matrix-js-sdk/src/models/room";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { renderToStaticMarkup } from "react-dom/server";
import { Layout } from "../../settings/Layout";
import { Layout } from "../../settings/enums/Layout";
import { shouldFormContinuation } from "../../components/structures/MessagePanel";
import { formatFullDateNoDayNoTime, wantsDateSeparator } from "../../DateUtils";
import { RoomPermalinkCreator } from "../permalinks/Permalinks";

View file

@ -35,7 +35,7 @@ import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
import SpecPermalinkConstructor from "../../../../src/utils/permalinks/SpecPermalinkConstructor";
import defaultDispatcher from "../../../../src/dispatcher/dispatcher";
import DocumentOffset from '../../../../src/editor/offset';
import { Layout } from '../../../../src/settings/Layout';
import { Layout } from '../../../../src/settings/enums/Layout';
jest.mock("../../../../src/stores/RoomViewStore");