Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into t3chguy/fix/18088
Conflicts: src/components/views/spaces/SpacePanel.tsxpull/21833/head
commit
57bf64bd28
|
@ -16,4 +16,16 @@ jobs:
|
|||
path: element-web/webapp
|
||||
# We'll only use this in a triggered job, then we're done with it
|
||||
retention-days: 1
|
||||
- uses: actions/github-script@v3.1.0
|
||||
with:
|
||||
script: |
|
||||
var fs = require('fs');
|
||||
fs.writeFileSync('${{github.workspace}}/pr.json', JSON.stringify(context.payload.pull_request));
|
||||
- name: Upload PR Info
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: pr.json
|
||||
path: pr.json
|
||||
# We'll only use this in a triggered job, then we're done with it
|
||||
retention-days: 1
|
||||
|
||||
|
|
|
@ -33,12 +33,33 @@ jobs:
|
|||
});
|
||||
var fs = require('fs');
|
||||
fs.writeFileSync('${{github.workspace}}/previewbuild.zip', Buffer.from(download.data));
|
||||
- run: unzip previewbuild.zip && rm previewbuild.zip
|
||||
|
||||
var prInfoArtifact = artifacts.data.artifacts.filter((artifact) => {
|
||||
return artifact.name == "pr.json"
|
||||
})[0];
|
||||
var download = await github.actions.downloadArtifact({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
artifact_id: prInfoArtifact.id,
|
||||
archive_format: 'zip',
|
||||
});
|
||||
var fs = require('fs');
|
||||
fs.writeFileSync('${{github.workspace}}/pr.json.zip', Buffer.from(download.data));
|
||||
- name: Extract Artifacts
|
||||
run: unzip -d webapp previewbuild.zip && rm previewbuild.zip && unzip pr.json.zip && rm pr.json.zip
|
||||
- name: 'Read PR Info'
|
||||
id: readctx
|
||||
uses: actions/github-script@v3.1.0
|
||||
with:
|
||||
script: |
|
||||
var fs = require('fs');
|
||||
var pr = JSON.parse(fs.readFileSync('${{github.workspace}}/pr.json'));
|
||||
console.log(`::set-output name=prnumber::${pr.number}`);
|
||||
- name: Deploy to Netlify
|
||||
id: netlify
|
||||
uses: nwtgck/actions-netlify@v1.2
|
||||
with:
|
||||
publish-dir: .
|
||||
publish-dir: webapp
|
||||
deploy-message: "Deploy from GitHub Actions"
|
||||
# These don't work because we're in workflow_run
|
||||
enable-pull-request-comment: false
|
||||
|
@ -47,12 +68,13 @@ jobs:
|
|||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
|
||||
timeout-minutes: 1
|
||||
- name: Comment on PR
|
||||
uses: phulsechinmay/rewritable-pr-comment@v0.3.0
|
||||
with:
|
||||
- name: Edit PR Description
|
||||
uses: velas/pr-description@v1.0.1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
ISSUE_ID: ${{ github.event.workflow_run.pull_requests[0].number }}
|
||||
message: |
|
||||
with:
|
||||
pull-request-number: ${{ steps.readctx.outputs.prnumber }}
|
||||
description-message: |
|
||||
Preview: ${{ steps.netlify.outputs.deploy-url }}
|
||||
⚠️ Do you trust the author of this PR? Maybe this build will steal your keys or give you malware. Exercise caution. Use test accounts.
|
||||
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* React Transition Group animations are prefixed with 'mx_rtg--' so that we
|
||||
* know they should not be used anywhere outside of React Transition Groups.
|
||||
*/
|
||||
|
||||
.mx_rtg--fade-enter {
|
||||
opacity: 0;
|
||||
}
|
||||
.mx_rtg--fade-enter-active {
|
||||
opacity: 1;
|
||||
transition: opacity 300ms ease;
|
||||
}
|
||||
.mx_rtg--fade-exit {
|
||||
opacity: 1;
|
||||
}
|
||||
.mx_rtg--fade-exit-active {
|
||||
opacity: 0;
|
||||
transition: opacity 300ms ease;
|
||||
}
|
||||
|
||||
|
||||
@keyframes mx--anim-pulse {
|
||||
0% { opacity: 1; }
|
||||
50% { opacity: 0.7; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
|
||||
|
||||
@media (prefers-reduced-motion) {
|
||||
@keyframes mx--anim-pulse {
|
||||
// Override all keyframes in reduced-motion
|
||||
}
|
||||
.mx_rtg--fade-enter-active {
|
||||
transition: none;
|
||||
}
|
||||
.mx_rtg--fade-exit-active {
|
||||
transition: none;
|
||||
}
|
||||
}
|
|
@ -18,6 +18,7 @@ limitations under the License.
|
|||
|
||||
@import "./_font-sizes.scss";
|
||||
@import "./_font-weights.scss";
|
||||
@import "./_animations.scss";
|
||||
|
||||
$hover-transition: 0.08s cubic-bezier(.46, .03, .52, .96); // quadratic
|
||||
|
||||
|
|
|
@ -400,7 +400,11 @@ limitations under the License.
|
|||
background-color: $secondary-fg-color;
|
||||
}
|
||||
|
||||
.mx_AccessibleButton {
|
||||
.mx_AccessibleButton_kind_link {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.mx_GroupView_spaceUpgradePrompt_close {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 8px;
|
||||
|
|
|
@ -269,7 +269,7 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&:hover, &:focus-within {
|
||||
background-color: $groupFilterPanel-bg-color;
|
||||
|
||||
.mx_AccessibleButton {
|
||||
|
@ -278,6 +278,10 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
|
||||
li.mx_SpaceRoomDirectory_roomTileWrapper {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.mx_SpaceRoomDirectory_roomTile,
|
||||
.mx_SpaceRoomDirectory_subspace_children {
|
||||
&::before {
|
||||
|
|
|
@ -59,6 +59,10 @@ limitations under the License.
|
|||
.mx_JoinRuleDropdown .mx_Dropdown_menu {
|
||||
width: auto !important; // override fixed width
|
||||
}
|
||||
|
||||
.mx_CreateSpaceFromCommunityDialog_nonPublicSpacer {
|
||||
height: 63px; // balance the height of the missing room alias field to prevent modal bouncing
|
||||
}
|
||||
}
|
||||
|
||||
.mx_CreateSpaceFromCommunityDialog_footer {
|
||||
|
|
|
@ -16,6 +16,12 @@ limitations under the License.
|
|||
|
||||
$timelineImageBorderRadius: 4px;
|
||||
|
||||
.mx_MImageBody_thumbnail--blurhash {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.mx_MImageBody_thumbnail {
|
||||
object-fit: contain;
|
||||
border-radius: $timelineImageBorderRadius;
|
||||
|
@ -23,8 +29,11 @@ $timelineImageBorderRadius: 4px;
|
|||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
> div > canvas {
|
||||
.mx_Blurhash > canvas {
|
||||
animation: mx--anim-pulse 1.75s infinite cubic-bezier(.4, 0, .6, 1);
|
||||
border-radius: $timelineImageBorderRadius;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -489,6 +489,10 @@ $hover-select-border: 4px;
|
|||
// https://github.com/vector-im/vector-web/issues/754
|
||||
overflow-x: overlay;
|
||||
overflow-y: visible;
|
||||
|
||||
&::-webkit-scrollbar-corner {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -150,13 +150,14 @@ const reducer = (state: IState, action: IAction) => {
|
|||
|
||||
interface IProps {
|
||||
handleHomeEnd?: boolean;
|
||||
handleUpDown?: boolean;
|
||||
children(renderProps: {
|
||||
onKeyDownHandler(ev: React.KeyboardEvent);
|
||||
});
|
||||
onKeyDown?(ev: React.KeyboardEvent, state: IState);
|
||||
}
|
||||
|
||||
export const RovingTabIndexProvider: React.FC<IProps> = ({ children, handleHomeEnd, onKeyDown }) => {
|
||||
export const RovingTabIndexProvider: React.FC<IProps> = ({ children, handleHomeEnd, handleUpDown, onKeyDown }) => {
|
||||
const [state, dispatch] = useReducer<Reducer<IState, IAction>>(reducer, {
|
||||
activeRef: null,
|
||||
refs: [],
|
||||
|
@ -167,21 +168,50 @@ export const RovingTabIndexProvider: React.FC<IProps> = ({ children, handleHomeE
|
|||
const onKeyDownHandler = useCallback((ev) => {
|
||||
let handled = false;
|
||||
// Don't interfere with input default keydown behaviour
|
||||
if (handleHomeEnd && ev.target.tagName !== "INPUT" && ev.target.tagName !== "TEXTAREA") {
|
||||
if (ev.target.tagName !== "INPUT" && ev.target.tagName !== "TEXTAREA") {
|
||||
// check if we actually have any items
|
||||
switch (ev.key) {
|
||||
case Key.HOME:
|
||||
handled = true;
|
||||
// move focus to first item
|
||||
if (context.state.refs.length > 0) {
|
||||
context.state.refs[0].current.focus();
|
||||
if (handleHomeEnd) {
|
||||
handled = true;
|
||||
// move focus to first item
|
||||
if (context.state.refs.length > 0) {
|
||||
context.state.refs[0].current.focus();
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case Key.END:
|
||||
handled = true;
|
||||
// move focus to last item
|
||||
if (context.state.refs.length > 0) {
|
||||
context.state.refs[context.state.refs.length - 1].current.focus();
|
||||
if (handleHomeEnd) {
|
||||
handled = true;
|
||||
// move focus to last item
|
||||
if (context.state.refs.length > 0) {
|
||||
context.state.refs[context.state.refs.length - 1].current.focus();
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case Key.ARROW_UP:
|
||||
if (handleUpDown) {
|
||||
handled = true;
|
||||
if (context.state.refs.length > 0) {
|
||||
const idx = context.state.refs.indexOf(context.state.activeRef);
|
||||
if (idx > 0) {
|
||||
context.state.refs[idx - 1].current.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case Key.ARROW_DOWN:
|
||||
if (handleUpDown) {
|
||||
handled = true;
|
||||
if (context.state.refs.length > 0) {
|
||||
const idx = context.state.refs.indexOf(context.state.activeRef);
|
||||
if (idx < context.state.refs.length - 1) {
|
||||
context.state.refs[idx + 1].current.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
@ -193,7 +223,7 @@ export const RovingTabIndexProvider: React.FC<IProps> = ({ children, handleHomeE
|
|||
} else if (onKeyDown) {
|
||||
return onKeyDown(ev, context.state);
|
||||
}
|
||||
}, [context.state, onKeyDown, handleHomeEnd]);
|
||||
}, [context.state, onKeyDown, handleHomeEnd, handleUpDown]);
|
||||
|
||||
return <RovingTabIndexContext.Provider value={context}>
|
||||
{ children({ onKeyDownHandler }) }
|
||||
|
|
|
@ -61,7 +61,9 @@ export default class AutoHideScrollbar extends React.Component<IProps> {
|
|||
style={style}
|
||||
className={["mx_AutoHideScrollbar", className].join(" ")}
|
||||
onWheel={onWheel}
|
||||
tabIndex={tabIndex}
|
||||
// Firefox sometimes makes this element focusable due to
|
||||
// overflow:scroll;, so force it out of tab order by default.
|
||||
tabIndex={tabIndex ?? -1}
|
||||
>
|
||||
{ children }
|
||||
</div>);
|
||||
|
|
|
@ -41,6 +41,9 @@ import RightPanelStore from "../../stores/RightPanelStore";
|
|||
import AutoHideScrollbar from "./AutoHideScrollbar";
|
||||
import { mediaFromMxc } from "../../customisations/Media";
|
||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||
import { createSpaceFromCommunity } from "../../utils/space";
|
||||
import { Action } from "../../dispatcher/actions";
|
||||
import { RightPanelPhases } from "../../stores/RightPanelStorePhases";
|
||||
|
||||
const LONG_DESC_PLACEHOLDER = _td(
|
||||
`<h1>HTML for your community's page</h1>
|
||||
|
@ -815,6 +818,17 @@ export default class GroupView extends React.Component {
|
|||
this.setState({ showUpgradeNotice: false });
|
||||
}
|
||||
|
||||
_onCreateSpaceClick = () => {
|
||||
createSpaceFromCommunity(this._matrixClient, this.props.groupId);
|
||||
};
|
||||
|
||||
_onAdminsLinkClick = () => {
|
||||
dis.dispatch({
|
||||
action: Action.SetRightPanelPhase,
|
||||
phase: RightPanelPhases.GroupMemberList,
|
||||
});
|
||||
};
|
||||
|
||||
_getGroupSection() {
|
||||
const groupSettingsSectionClasses = classnames({
|
||||
"mx_GroupView_group": this.state.editing,
|
||||
|
@ -854,17 +868,35 @@ export default class GroupView extends React.Component {
|
|||
|
||||
let communitiesUpgradeNotice;
|
||||
if (this.state.showUpgradeNotice) {
|
||||
let text;
|
||||
if (this.state.isUserPrivileged) {
|
||||
text = _t("You can create a Space from this community <a>here</a>.", {}, {
|
||||
a: sub => <AccessibleButton onClick={this._onCreateSpaceClick} kind="link">
|
||||
{ sub }
|
||||
</AccessibleButton>,
|
||||
});
|
||||
} else {
|
||||
text = _t("Ask the <a>admins</a> of this community to make it into a Space " +
|
||||
"and keep a look out for the invite.", {}, {
|
||||
a: sub => <AccessibleButton onClick={this._onAdminsLinkClick} kind="link">
|
||||
{ sub }
|
||||
</AccessibleButton>,
|
||||
});
|
||||
}
|
||||
|
||||
communitiesUpgradeNotice = <div className="mx_GroupView_spaceUpgradePrompt">
|
||||
<h2>{ _t("Communities can now be made into Spaces") }</h2>
|
||||
<p>
|
||||
{ _t("Spaces are a new way to make a community, with new features coming.") }
|
||||
|
||||
{ _t("Ask the admins of this community to make it into a Space " +
|
||||
"and keep a look out for the invite.") }
|
||||
{ text }
|
||||
|
||||
{ _t("Communities won't receive further updates.") }
|
||||
</p>
|
||||
<AccessibleButton onClick={this._dismissUpgradeNotice} />
|
||||
<AccessibleButton
|
||||
className="mx_GroupView_spaceUpgradePrompt_close"
|
||||
onClick={this._dismissUpgradeNotice}
|
||||
/>
|
||||
</div>;
|
||||
}
|
||||
|
||||
|
|
|
@ -392,9 +392,6 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
|||
<IndicatorScrollbar
|
||||
className="mx_LeftPanel_breadcrumbsContainer mx_AutoHideScrollbar"
|
||||
verticalScrollsHorizontally={true}
|
||||
// Firefox sometimes makes this element focusable due to
|
||||
// overflow:scroll;, so force it out of tab order.
|
||||
tabIndex={-1}
|
||||
>
|
||||
<RoomBreadcrumbs />
|
||||
</IndicatorScrollbar>
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { ReactNode, useMemo, useState } from "react";
|
||||
import React, { ReactNode, KeyboardEvent, useMemo, useState, KeyboardEventHandler } from "react";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { EventType, RoomType } from "matrix-js-sdk/src/@types/event";
|
||||
import { ISpaceSummaryRoom, ISpaceSummaryEvent } from "matrix-js-sdk/src/@types/spaces";
|
||||
|
@ -46,6 +46,8 @@ import { getDisplayAliasForAliasSet } from "../../Rooms";
|
|||
import { useDispatcher } from "../../hooks/useDispatcher";
|
||||
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||
import { Action } from "../../dispatcher/actions";
|
||||
import { Key } from "../../Keyboard";
|
||||
import { IState, RovingTabIndexProvider, useRovingTabIndex } from "../../accessibility/RovingTabIndex";
|
||||
|
||||
interface IHierarchyProps {
|
||||
space: Room;
|
||||
|
@ -80,6 +82,7 @@ const Tile: React.FC<ITileProps> = ({
|
|||
|| (room.room_type === RoomType.Space ? _t("Unnamed Space") : _t("Unnamed Room"));
|
||||
|
||||
const [showChildren, toggleShowChildren] = useStateToggle(true);
|
||||
const [onFocus, isActive, ref] = useRovingTabIndex();
|
||||
|
||||
const onPreviewClick = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
|
@ -94,11 +97,21 @@ const Tile: React.FC<ITileProps> = ({
|
|||
|
||||
let button;
|
||||
if (joinedRoom) {
|
||||
button = <AccessibleButton onClick={onPreviewClick} kind="primary_outline">
|
||||
button = <AccessibleButton
|
||||
onClick={onPreviewClick}
|
||||
kind="primary_outline"
|
||||
onFocus={onFocus}
|
||||
tabIndex={isActive ? 0 : -1}
|
||||
>
|
||||
{ _t("View") }
|
||||
</AccessibleButton>;
|
||||
} else if (onJoinClick) {
|
||||
button = <AccessibleButton onClick={onJoinClick} kind="primary">
|
||||
button = <AccessibleButton
|
||||
onClick={onJoinClick}
|
||||
kind="primary"
|
||||
onFocus={onFocus}
|
||||
tabIndex={isActive ? 0 : -1}
|
||||
>
|
||||
{ _t("Join") }
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
@ -106,13 +119,13 @@ const Tile: React.FC<ITileProps> = ({
|
|||
let checkbox;
|
||||
if (onToggleClick) {
|
||||
if (hasPermissions) {
|
||||
checkbox = <StyledCheckbox checked={!!selected} onChange={onToggleClick} />;
|
||||
checkbox = <StyledCheckbox checked={!!selected} onChange={onToggleClick} tabIndex={isActive ? 0 : -1} />;
|
||||
} else {
|
||||
checkbox = <TextWithTooltip
|
||||
tooltip={_t("You don't have permission")}
|
||||
onClick={ev => { ev.stopPropagation(); }}
|
||||
>
|
||||
<StyledCheckbox disabled={true} />
|
||||
<StyledCheckbox disabled={true} tabIndex={isActive ? 0 : -1} />
|
||||
</TextWithTooltip>;
|
||||
}
|
||||
}
|
||||
|
@ -172,8 +185,9 @@ const Tile: React.FC<ITileProps> = ({
|
|||
</div>
|
||||
</React.Fragment>;
|
||||
|
||||
let childToggle;
|
||||
let childSection;
|
||||
let childToggle: JSX.Element;
|
||||
let childSection: JSX.Element;
|
||||
let onKeyDown: KeyboardEventHandler;
|
||||
if (children) {
|
||||
// the chevron is purposefully a div rather than a button as it should be ignored for a11y
|
||||
childToggle = <div
|
||||
|
@ -185,25 +199,74 @@ const Tile: React.FC<ITileProps> = ({
|
|||
toggleShowChildren();
|
||||
}}
|
||||
/>;
|
||||
|
||||
if (showChildren) {
|
||||
childSection = <div className="mx_SpaceRoomDirectory_subspace_children">
|
||||
const onChildrenKeyDown = (e) => {
|
||||
if (e.key === Key.ARROW_LEFT) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
ref.current?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
childSection = <div
|
||||
className="mx_SpaceRoomDirectory_subspace_children"
|
||||
onKeyDown={onChildrenKeyDown}
|
||||
role="group"
|
||||
>
|
||||
{ children }
|
||||
</div>;
|
||||
}
|
||||
|
||||
onKeyDown = (e) => {
|
||||
let handled = false;
|
||||
|
||||
switch (e.key) {
|
||||
case Key.ARROW_LEFT:
|
||||
if (showChildren) {
|
||||
handled = true;
|
||||
toggleShowChildren();
|
||||
}
|
||||
break;
|
||||
|
||||
case Key.ARROW_RIGHT:
|
||||
handled = true;
|
||||
if (showChildren) {
|
||||
const childSection = ref.current?.nextElementSibling;
|
||||
childSection?.querySelector<HTMLDivElement>(".mx_SpaceRoomDirectory_roomTile")?.focus();
|
||||
} else {
|
||||
toggleShowChildren();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (handled) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return <>
|
||||
return <li
|
||||
className="mx_SpaceRoomDirectory_roomTileWrapper"
|
||||
role="treeitem"
|
||||
aria-expanded={children ? showChildren : undefined}
|
||||
>
|
||||
<AccessibleButton
|
||||
className={classNames("mx_SpaceRoomDirectory_roomTile", {
|
||||
mx_SpaceRoomDirectory_subspace: room.room_type === RoomType.Space,
|
||||
})}
|
||||
onClick={(hasPermissions && onToggleClick) ? onToggleClick : onPreviewClick}
|
||||
onKeyDown={onKeyDown}
|
||||
inputRef={ref}
|
||||
onFocus={onFocus}
|
||||
tabIndex={isActive ? 0 : -1}
|
||||
>
|
||||
{ content }
|
||||
{ childToggle }
|
||||
</AccessibleButton>
|
||||
{ childSection }
|
||||
</>;
|
||||
</li>;
|
||||
};
|
||||
|
||||
export const showRoom = (room: ISpaceSummaryRoom, viaServers?: string[], autoJoin = false) => {
|
||||
|
@ -414,176 +477,196 @@ export const SpaceHierarchy: React.FC<IHierarchyProps> = ({
|
|||
return <p>{ _t("Your server does not support showing space hierarchies.") }</p>;
|
||||
}
|
||||
|
||||
let content;
|
||||
if (roomsMap) {
|
||||
const numRooms = Array.from(roomsMap.values()).filter(r => !r.room_type).length;
|
||||
const numSpaces = roomsMap.size - numRooms - 1; // -1 at the end to exclude the space we are looking at
|
||||
|
||||
let countsStr;
|
||||
if (numSpaces > 1) {
|
||||
countsStr = _t("%(count)s rooms and %(numSpaces)s spaces", { count: numRooms, numSpaces });
|
||||
} else if (numSpaces > 0) {
|
||||
countsStr = _t("%(count)s rooms and 1 space", { count: numRooms, numSpaces });
|
||||
} else {
|
||||
countsStr = _t("%(count)s rooms", { count: numRooms, numSpaces });
|
||||
const onKeyDown = (ev: KeyboardEvent, state: IState) => {
|
||||
if (ev.key === Key.ARROW_DOWN && ev.currentTarget.classList.contains("mx_SpaceRoomDirectory_search")) {
|
||||
state.refs[0]?.current?.focus();
|
||||
}
|
||||
|
||||
let manageButtons;
|
||||
if (space.getMyMembership() === "join" && space.currentState.maySendStateEvent(EventType.SpaceChild, userId)) {
|
||||
const selectedRelations = Array.from(selected.keys()).flatMap(parentId => {
|
||||
return [...selected.get(parentId).values()].map(childId => [parentId, childId]) as [string, string][];
|
||||
});
|
||||
|
||||
const selectionAllSuggested = selectedRelations.every(([parentId, childId]) => {
|
||||
return parentChildMap.get(parentId)?.get(childId)?.content.suggested;
|
||||
});
|
||||
|
||||
const disabled = !selectedRelations.length || removing || saving;
|
||||
|
||||
let Button: React.ComponentType<React.ComponentProps<typeof AccessibleButton>> = AccessibleButton;
|
||||
let props = {};
|
||||
if (!selectedRelations.length) {
|
||||
Button = AccessibleTooltipButton;
|
||||
props = {
|
||||
tooltip: _t("Select a room below first"),
|
||||
yOffset: -40,
|
||||
};
|
||||
}
|
||||
|
||||
manageButtons = <>
|
||||
<Button
|
||||
{...props}
|
||||
onClick={async () => {
|
||||
setRemoving(true);
|
||||
try {
|
||||
for (const [parentId, childId] of selectedRelations) {
|
||||
await cli.sendStateEvent(parentId, EventType.SpaceChild, {}, childId);
|
||||
parentChildMap.get(parentId).delete(childId);
|
||||
if (parentChildMap.get(parentId).size > 0) {
|
||||
parentChildMap.set(parentId, new Map(parentChildMap.get(parentId)));
|
||||
} else {
|
||||
parentChildMap.delete(parentId);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
setError(_t("Failed to remove some rooms. Try again later"));
|
||||
}
|
||||
setRemoving(false);
|
||||
}}
|
||||
kind="danger_outline"
|
||||
disabled={disabled}
|
||||
>
|
||||
{ removing ? _t("Removing...") : _t("Remove") }
|
||||
</Button>
|
||||
<Button
|
||||
{...props}
|
||||
onClick={async () => {
|
||||
setSaving(true);
|
||||
try {
|
||||
for (const [parentId, childId] of selectedRelations) {
|
||||
const suggested = !selectionAllSuggested;
|
||||
const existingContent = parentChildMap.get(parentId)?.get(childId)?.content;
|
||||
if (!existingContent || existingContent.suggested === suggested) continue;
|
||||
|
||||
const content = {
|
||||
...existingContent,
|
||||
suggested: !selectionAllSuggested,
|
||||
};
|
||||
|
||||
await cli.sendStateEvent(parentId, EventType.SpaceChild, content, childId);
|
||||
|
||||
parentChildMap.get(parentId).get(childId).content = content;
|
||||
parentChildMap.set(parentId, new Map(parentChildMap.get(parentId)));
|
||||
}
|
||||
} catch (e) {
|
||||
setError("Failed to update some suggestions. Try again later");
|
||||
}
|
||||
setSaving(false);
|
||||
setSelected(new Map());
|
||||
}}
|
||||
kind="primary_outline"
|
||||
disabled={disabled}
|
||||
>
|
||||
{ saving
|
||||
? _t("Saving...")
|
||||
: (selectionAllSuggested ? _t("Mark as not suggested") : _t("Mark as suggested"))
|
||||
}
|
||||
</Button>
|
||||
</>;
|
||||
}
|
||||
|
||||
let results;
|
||||
if (roomsMap.size) {
|
||||
const hasPermissions = space?.currentState.maySendStateEvent(EventType.SpaceChild, cli.getUserId());
|
||||
|
||||
results = <>
|
||||
<HierarchyLevel
|
||||
spaceId={space.roomId}
|
||||
rooms={roomsMap}
|
||||
relations={parentChildMap}
|
||||
parents={new Set()}
|
||||
selectedMap={selected}
|
||||
onToggleClick={hasPermissions ? (parentId, childId) => {
|
||||
setError("");
|
||||
if (!selected.has(parentId)) {
|
||||
setSelected(new Map(selected.set(parentId, new Set([childId]))));
|
||||
return;
|
||||
}
|
||||
|
||||
const parentSet = selected.get(parentId);
|
||||
if (!parentSet.has(childId)) {
|
||||
setSelected(new Map(selected.set(parentId, new Set([...parentSet, childId]))));
|
||||
return;
|
||||
}
|
||||
|
||||
parentSet.delete(childId);
|
||||
setSelected(new Map(selected.set(parentId, new Set(parentSet))));
|
||||
} : undefined}
|
||||
onViewRoomClick={(roomId, autoJoin) => {
|
||||
showRoom(roomsMap.get(roomId), Array.from(viaMap.get(roomId) || []), autoJoin);
|
||||
}}
|
||||
/>
|
||||
{ children && <hr /> }
|
||||
</>;
|
||||
} else {
|
||||
results = <div className="mx_SpaceRoomDirectory_noResults">
|
||||
<h3>{ _t("No results found") }</h3>
|
||||
<div>{ _t("You may want to try a different search or check for typos.") }</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
content = <>
|
||||
<div className="mx_SpaceRoomDirectory_listHeader">
|
||||
{ countsStr }
|
||||
<span>
|
||||
{ additionalButtons }
|
||||
{ manageButtons }
|
||||
</span>
|
||||
</div>
|
||||
{ error && <div className="mx_SpaceRoomDirectory_error">
|
||||
{ error }
|
||||
</div> }
|
||||
<AutoHideScrollbar className="mx_SpaceRoomDirectory_list">
|
||||
{ results }
|
||||
{ children }
|
||||
</AutoHideScrollbar>
|
||||
</>;
|
||||
} else {
|
||||
content = <Spinner />;
|
||||
}
|
||||
};
|
||||
|
||||
// TODO loading state/error state
|
||||
return <>
|
||||
<SearchBox
|
||||
className="mx_textinput_icon mx_textinput_search"
|
||||
placeholder={_t("Search names and descriptions")}
|
||||
onSearch={setQuery}
|
||||
autoFocus={true}
|
||||
initialValue={initialText}
|
||||
/>
|
||||
return <RovingTabIndexProvider onKeyDown={onKeyDown} handleHomeEnd handleUpDown>
|
||||
{ ({ onKeyDownHandler }) => {
|
||||
let content;
|
||||
if (roomsMap) {
|
||||
const numRooms = Array.from(roomsMap.values()).filter(r => !r.room_type).length;
|
||||
const numSpaces = roomsMap.size - numRooms - 1; // -1 at the end to exclude the space we are looking at
|
||||
|
||||
{ content }
|
||||
</>;
|
||||
let countsStr;
|
||||
if (numSpaces > 1) {
|
||||
countsStr = _t("%(count)s rooms and %(numSpaces)s spaces", { count: numRooms, numSpaces });
|
||||
} else if (numSpaces > 0) {
|
||||
countsStr = _t("%(count)s rooms and 1 space", { count: numRooms, numSpaces });
|
||||
} else {
|
||||
countsStr = _t("%(count)s rooms", { count: numRooms, numSpaces });
|
||||
}
|
||||
|
||||
let manageButtons;
|
||||
if (space.getMyMembership() === "join" &&
|
||||
space.currentState.maySendStateEvent(EventType.SpaceChild, userId)
|
||||
) {
|
||||
const selectedRelations = Array.from(selected.keys()).flatMap(parentId => {
|
||||
return [
|
||||
...selected.get(parentId).values(),
|
||||
].map(childId => [parentId, childId]) as [string, string][];
|
||||
});
|
||||
|
||||
const selectionAllSuggested = selectedRelations.every(([parentId, childId]) => {
|
||||
return parentChildMap.get(parentId)?.get(childId)?.content.suggested;
|
||||
});
|
||||
|
||||
const disabled = !selectedRelations.length || removing || saving;
|
||||
|
||||
let Button: React.ComponentType<React.ComponentProps<typeof AccessibleButton>> = AccessibleButton;
|
||||
let props = {};
|
||||
if (!selectedRelations.length) {
|
||||
Button = AccessibleTooltipButton;
|
||||
props = {
|
||||
tooltip: _t("Select a room below first"),
|
||||
yOffset: -40,
|
||||
};
|
||||
}
|
||||
|
||||
manageButtons = <>
|
||||
<Button
|
||||
{...props}
|
||||
onClick={async () => {
|
||||
setRemoving(true);
|
||||
try {
|
||||
for (const [parentId, childId] of selectedRelations) {
|
||||
await cli.sendStateEvent(parentId, EventType.SpaceChild, {}, childId);
|
||||
parentChildMap.get(parentId).delete(childId);
|
||||
if (parentChildMap.get(parentId).size > 0) {
|
||||
parentChildMap.set(parentId, new Map(parentChildMap.get(parentId)));
|
||||
} else {
|
||||
parentChildMap.delete(parentId);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
setError(_t("Failed to remove some rooms. Try again later"));
|
||||
}
|
||||
setRemoving(false);
|
||||
}}
|
||||
kind="danger_outline"
|
||||
disabled={disabled}
|
||||
>
|
||||
{ removing ? _t("Removing...") : _t("Remove") }
|
||||
</Button>
|
||||
<Button
|
||||
{...props}
|
||||
onClick={async () => {
|
||||
setSaving(true);
|
||||
try {
|
||||
for (const [parentId, childId] of selectedRelations) {
|
||||
const suggested = !selectionAllSuggested;
|
||||
const existingContent = parentChildMap.get(parentId)?.get(childId)?.content;
|
||||
if (!existingContent || existingContent.suggested === suggested) continue;
|
||||
|
||||
const content = {
|
||||
...existingContent,
|
||||
suggested: !selectionAllSuggested,
|
||||
};
|
||||
|
||||
await cli.sendStateEvent(parentId, EventType.SpaceChild, content, childId);
|
||||
|
||||
parentChildMap.get(parentId).get(childId).content = content;
|
||||
parentChildMap.set(parentId, new Map(parentChildMap.get(parentId)));
|
||||
}
|
||||
} catch (e) {
|
||||
setError("Failed to update some suggestions. Try again later");
|
||||
}
|
||||
setSaving(false);
|
||||
setSelected(new Map());
|
||||
}}
|
||||
kind="primary_outline"
|
||||
disabled={disabled}
|
||||
>
|
||||
{ saving
|
||||
? _t("Saving...")
|
||||
: (selectionAllSuggested ? _t("Mark as not suggested") : _t("Mark as suggested"))
|
||||
}
|
||||
</Button>
|
||||
</>;
|
||||
}
|
||||
|
||||
let results;
|
||||
if (roomsMap.size) {
|
||||
const hasPermissions = space?.currentState.maySendStateEvent(EventType.SpaceChild, cli.getUserId());
|
||||
|
||||
results = <>
|
||||
<HierarchyLevel
|
||||
spaceId={space.roomId}
|
||||
rooms={roomsMap}
|
||||
relations={parentChildMap}
|
||||
parents={new Set()}
|
||||
selectedMap={selected}
|
||||
onToggleClick={hasPermissions ? (parentId, childId) => {
|
||||
setError("");
|
||||
if (!selected.has(parentId)) {
|
||||
setSelected(new Map(selected.set(parentId, new Set([childId]))));
|
||||
return;
|
||||
}
|
||||
|
||||
const parentSet = selected.get(parentId);
|
||||
if (!parentSet.has(childId)) {
|
||||
setSelected(new Map(selected.set(parentId, new Set([...parentSet, childId]))));
|
||||
return;
|
||||
}
|
||||
|
||||
parentSet.delete(childId);
|
||||
setSelected(new Map(selected.set(parentId, new Set(parentSet))));
|
||||
} : undefined}
|
||||
onViewRoomClick={(roomId, autoJoin) => {
|
||||
showRoom(roomsMap.get(roomId), Array.from(viaMap.get(roomId) || []), autoJoin);
|
||||
}}
|
||||
/>
|
||||
{ children && <hr /> }
|
||||
</>;
|
||||
} else {
|
||||
results = <div className="mx_SpaceRoomDirectory_noResults">
|
||||
<h3>{ _t("No results found") }</h3>
|
||||
<div>{ _t("You may want to try a different search or check for typos.") }</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
content = <>
|
||||
<div className="mx_SpaceRoomDirectory_listHeader">
|
||||
{ countsStr }
|
||||
<span>
|
||||
{ additionalButtons }
|
||||
{ manageButtons }
|
||||
</span>
|
||||
</div>
|
||||
{ error && <div className="mx_SpaceRoomDirectory_error">
|
||||
{ error }
|
||||
</div> }
|
||||
<AutoHideScrollbar
|
||||
className="mx_SpaceRoomDirectory_list"
|
||||
onKeyDown={onKeyDownHandler}
|
||||
role="tree"
|
||||
aria-label={_t("Space")}
|
||||
>
|
||||
{ results }
|
||||
{ children }
|
||||
</AutoHideScrollbar>
|
||||
</>;
|
||||
} else {
|
||||
content = <Spinner />;
|
||||
}
|
||||
|
||||
return <>
|
||||
<SearchBox
|
||||
className="mx_SpaceRoomDirectory_search mx_textinput_icon mx_textinput_search"
|
||||
placeholder={_t("Search names and descriptions")}
|
||||
onSearch={setQuery}
|
||||
autoFocus={true}
|
||||
initialValue={initialText}
|
||||
onKeyDown={onKeyDownHandler}
|
||||
/>
|
||||
|
||||
{ content }
|
||||
</>;
|
||||
} }
|
||||
</RovingTabIndexProvider>;
|
||||
};
|
||||
|
||||
interface IProps {
|
||||
|
|
|
@ -286,8 +286,6 @@ const CreateSpaceFromCommunityDialog: React.FC<IProps> = ({ matrixClient: cli, g
|
|||
>
|
||||
<div className="mx_CreateSpaceFromCommunityDialog_content">
|
||||
<p>
|
||||
{ _t("Spaces are the new version of communities - with new features coming.") }
|
||||
|
||||
{ _t("A link to the Space will be put in your community description.") }
|
||||
|
||||
{ _t("All rooms will be added and all community members will be invited.") }
|
||||
|
@ -326,6 +324,9 @@ const CreateSpaceFromCommunityDialog: React.FC<IProps> = ({ matrixClient: cli, g
|
|||
? _t("Open space for anyone, best for communities")
|
||||
: _t("Invite only, best for yourself or teams")
|
||||
}</p>
|
||||
{ joinRule !== JoinRule.Public &&
|
||||
<div className="mx_CreateSpaceFromCommunityDialog_nonPublicSpacer" />
|
||||
}
|
||||
</SpaceCreateForm>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -67,7 +67,9 @@ export default function AccessibleButton({
|
|||
...restProps
|
||||
}: IProps) {
|
||||
const newProps: IAccessibleButtonProps = restProps;
|
||||
if (!disabled) {
|
||||
if (disabled) {
|
||||
newProps["aria-disabled"] = true;
|
||||
} else {
|
||||
newProps.onClick = onClick;
|
||||
// We need to consume enter onKeyDown and space onKeyUp
|
||||
// otherwise we are risking also activating other keyboard focusable elements
|
||||
|
@ -118,7 +120,7 @@ export default function AccessibleButton({
|
|||
);
|
||||
|
||||
// React.createElement expects InputHTMLAttributes
|
||||
return React.createElement(element, restProps, children);
|
||||
return React.createElement(element, newProps, children);
|
||||
}
|
||||
|
||||
AccessibleButton.defaultProps = {
|
||||
|
|
|
@ -18,7 +18,7 @@ limitations under the License.
|
|||
import React, { ChangeEvent, createRef, CSSProperties, ReactElement, ReactNode, Ref } from 'react';
|
||||
import classnames from 'classnames';
|
||||
|
||||
import AccessibleButton from './AccessibleButton';
|
||||
import AccessibleButton, { ButtonEvent } from './AccessibleButton';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { Key } from "../../../Keyboard";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
@ -178,7 +178,7 @@ export default class Dropdown extends React.Component<IProps, IState> {
|
|||
this.ignoreEvent = ev;
|
||||
};
|
||||
|
||||
private onInputClick = (ev: React.MouseEvent) => {
|
||||
private onAccessibleButtonClick = (ev: ButtonEvent) => {
|
||||
if (this.props.disabled) return;
|
||||
|
||||
if (!this.state.expanded) {
|
||||
|
@ -186,6 +186,10 @@ export default class Dropdown extends React.Component<IProps, IState> {
|
|||
expanded: true,
|
||||
});
|
||||
ev.preventDefault();
|
||||
} else if ((ev as React.KeyboardEvent).key === Key.ENTER) {
|
||||
// the accessible button consumes enter onKeyDown for firing onClick, so handle it here
|
||||
this.props.onOptionChange(this.state.highlightedOption);
|
||||
this.close();
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -204,7 +208,7 @@ export default class Dropdown extends React.Component<IProps, IState> {
|
|||
this.props.onOptionChange(dropdownKey);
|
||||
};
|
||||
|
||||
private onInputKeyDown = (e: React.KeyboardEvent) => {
|
||||
private onKeyDown = (e: React.KeyboardEvent) => {
|
||||
let handled = true;
|
||||
|
||||
// These keys don't generate keypress events and so needs to be on keyup
|
||||
|
@ -269,7 +273,7 @@ export default class Dropdown extends React.Component<IProps, IState> {
|
|||
private prevOption(optionKey: string): string {
|
||||
const keys = Object.keys(this.childrenByKey);
|
||||
const index = keys.indexOf(optionKey);
|
||||
return keys[(index - 1) % keys.length];
|
||||
return keys[index <= 0 ? keys.length - 1 : (index - 1) % keys.length];
|
||||
}
|
||||
|
||||
private scrollIntoView(node: Element) {
|
||||
|
@ -320,7 +324,6 @@ export default class Dropdown extends React.Component<IProps, IState> {
|
|||
type="text"
|
||||
autoFocus={true}
|
||||
className="mx_Dropdown_option"
|
||||
onKeyDown={this.onInputKeyDown}
|
||||
onChange={this.onInputChange}
|
||||
value={this.state.searchQuery}
|
||||
role="combobox"
|
||||
|
@ -329,6 +332,7 @@ export default class Dropdown extends React.Component<IProps, IState> {
|
|||
aria-owns={`${this.props.id}_listbox`}
|
||||
aria-disabled={this.props.disabled}
|
||||
aria-label={this.props.label}
|
||||
onKeyDown={this.onKeyDown}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -361,13 +365,14 @@ export default class Dropdown extends React.Component<IProps, IState> {
|
|||
return <div className={classnames(dropdownClasses)} ref={this.collectRoot}>
|
||||
<AccessibleButton
|
||||
className="mx_Dropdown_input mx_no_textinput"
|
||||
onClick={this.onInputClick}
|
||||
onClick={this.onAccessibleButtonClick}
|
||||
aria-haspopup="listbox"
|
||||
aria-expanded={this.state.expanded}
|
||||
disabled={this.props.disabled}
|
||||
inputRef={this.buttonRef}
|
||||
aria-label={this.props.label}
|
||||
aria-describedby={`${this.props.id}_value`}
|
||||
onKeyDown={this.onKeyDown}
|
||||
>
|
||||
{ currentValue }
|
||||
<span className="mx_Dropdown_arrow" />
|
||||
|
|
|
@ -25,12 +25,14 @@ import SettingsStore from "../../../settings/SettingsStore";
|
|||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import InlineSpinner from '../elements/InlineSpinner';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { mediaFromContent } from "../../../customisations/Media";
|
||||
import { Media, mediaFromContent } from "../../../customisations/Media";
|
||||
import { BLURHASH_FIELD } from "../../../ContentMessages";
|
||||
import { IMediaEventContent } from '../../../customisations/models/IMediaEventContent';
|
||||
import ImageView from '../elements/ImageView';
|
||||
import { SyncState } from 'matrix-js-sdk/src/sync.api';
|
||||
import { IBodyProps } from "./IBodyProps";
|
||||
import classNames from 'classnames';
|
||||
import { CSSTransition, SwitchTransition } from 'react-transition-group';
|
||||
|
||||
interface IState {
|
||||
decryptedUrl?: string;
|
||||
|
@ -157,19 +159,21 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
|
|||
// this is only used as a fallback in case content.info.w/h is missing
|
||||
loadedImageDimensions = { naturalWidth, naturalHeight };
|
||||
}
|
||||
|
||||
this.setState({ imgLoaded: true, loadedImageDimensions });
|
||||
};
|
||||
|
||||
protected getContentUrl(): string {
|
||||
const media = mediaFromContent(this.props.mxEvent.getContent());
|
||||
if (media.isEncrypted) {
|
||||
if (this.media.isEncrypted) {
|
||||
return this.state.decryptedUrl;
|
||||
} else {
|
||||
return media.srcHttp;
|
||||
return this.media.srcHttp;
|
||||
}
|
||||
}
|
||||
|
||||
private get media(): Media {
|
||||
return mediaFromContent(this.props.mxEvent.getContent());
|
||||
}
|
||||
|
||||
protected getThumbUrl(): string {
|
||||
// FIXME: we let images grow as wide as you like, rather than capped to 800x600.
|
||||
// So either we need to support custom timeline widths here, or reimpose the cap, otherwise the
|
||||
|
@ -225,7 +229,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
|
|||
info.w > thumbWidth ||
|
||||
info.h > thumbHeight
|
||||
);
|
||||
const isLargeFileSize = info.size > 1*1024*1024; // 1mb
|
||||
const isLargeFileSize = info.size > 1 * 1024 * 1024; // 1mb
|
||||
|
||||
if (isLargeFileSize && isLargerThanThumbnail) {
|
||||
// image is too large physically and bytewise to clutter our timeline so
|
||||
|
@ -374,23 +378,40 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
|
|||
gifLabel = <p className="mx_MImageBody_gifLabel">GIF</p>;
|
||||
}
|
||||
|
||||
const classes = classNames({
|
||||
'mx_MImageBody_thumbnail': true,
|
||||
'mx_MImageBody_thumbnail--blurhash': this.props.mxEvent.getContent().info[BLURHASH_FIELD],
|
||||
});
|
||||
|
||||
// This has incredibly broken types.
|
||||
const C = CSSTransition as any;
|
||||
const thumbnail = (
|
||||
<div className="mx_MImageBody_thumbnail_container" style={{ maxHeight: maxHeight, maxWidth: maxWidth, aspectRatio: `${infoWidth}/${infoHeight}` }}>
|
||||
{ showPlaceholder &&
|
||||
<div
|
||||
className="mx_MImageBody_thumbnail"
|
||||
style={{
|
||||
// Constrain width here so that spinner appears central to the loaded thumbnail
|
||||
maxWidth: `min(100%, ${infoWidth}px)`,
|
||||
}}
|
||||
<SwitchTransition mode="out-in">
|
||||
<C
|
||||
classNames="mx_rtg--fade"
|
||||
key={`img-${showPlaceholder}`}
|
||||
timeout={300}
|
||||
>
|
||||
{ placeholder }
|
||||
</div>
|
||||
}
|
||||
{ /* This weirdly looking div is necessary here, otherwise SwitchTransition fails */ }
|
||||
<div>
|
||||
{ showPlaceholder && <div
|
||||
className={classes}
|
||||
style={{
|
||||
// Constrain width here so that spinner appears central to the loaded thumbnail
|
||||
maxWidth: `min(100%, ${infoWidth}px)`,
|
||||
maxHeight: maxHeight,
|
||||
aspectRatio: `${infoWidth}/${infoHeight}`,
|
||||
}}
|
||||
>
|
||||
{ placeholder }
|
||||
</div> }
|
||||
</div>
|
||||
</C>
|
||||
</SwitchTransition>
|
||||
|
||||
<div style={{
|
||||
display: !showPlaceholder ? undefined : 'none',
|
||||
height: '100%', // Also force to size of a parent to prevent scroll-jumps (see above)
|
||||
height: '100%',
|
||||
}}>
|
||||
{ img }
|
||||
{ gifLabel }
|
||||
|
@ -413,7 +434,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
|
|||
// Overidden by MStickerBody
|
||||
protected getPlaceholder(width: number, height: number): JSX.Element {
|
||||
const blurhash = this.props.mxEvent.getContent().info[BLURHASH_FIELD];
|
||||
if (blurhash) return <Blurhash hash={blurhash} width={width} height={height} />;
|
||||
if (blurhash) return <Blurhash className="mx_Blurhash" hash={blurhash} width={width} height={height} />;
|
||||
return (
|
||||
<InlineSpinner w={32} h={32} />
|
||||
);
|
||||
|
@ -455,10 +476,12 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
|
|||
const thumbnail = this.messageContent(contentUrl, thumbUrl, content);
|
||||
const fileBody = this.getFileBody();
|
||||
|
||||
return <div className="mx_MImageBody">
|
||||
{ thumbnail }
|
||||
{ fileBody }
|
||||
</div>;
|
||||
return (
|
||||
<div className="mx_MImageBody">
|
||||
{ thumbnail }
|
||||
{ fileBody }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -541,7 +541,6 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||
handled = true;
|
||||
} else if (event.key === Key.BACKSPACE || event.key === Key.DELETE) {
|
||||
this.formatBarRef.current.hide();
|
||||
handled = this.fakeDeletion(event.key === Key.BACKSPACE);
|
||||
}
|
||||
|
||||
if (handled) {
|
||||
|
@ -550,29 +549,6 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Because pills have contentEditable="false" there is no event emitted when
|
||||
* the user tries to delete them. Therefore we need to fake what would
|
||||
* normally happen
|
||||
* @param direction in which to delete
|
||||
* @returns handled
|
||||
*/
|
||||
private fakeDeletion(backward: boolean): boolean {
|
||||
const selection = document.getSelection();
|
||||
// Use the default handling for ranges
|
||||
if (selection.type === "Range") return false;
|
||||
|
||||
this.modifiedFlag = true;
|
||||
const { caret, text } = getCaretOffsetAndText(this.editorRef.current, selection);
|
||||
|
||||
// Do the deletion itself
|
||||
if (backward) caret.offset--;
|
||||
const newText = text.slice(0, caret.offset) + text.slice(caret.offset + 1);
|
||||
|
||||
this.props.model.update(newText, backward ? "deleteContentBackward" : "deleteContentForward", caret);
|
||||
return true;
|
||||
}
|
||||
|
||||
private async tabCompleteName(): Promise<void> {
|
||||
try {
|
||||
await new Promise<void>(resolve => this.setState({ showVisualBell: false }, resolve));
|
||||
|
|
|
@ -65,6 +65,7 @@ export const SpaceAvatar = ({
|
|||
}}
|
||||
kind="link"
|
||||
className="mx_SpaceBasicSettings_avatar_remove"
|
||||
aria-label={_t("Delete avatar")}
|
||||
>
|
||||
{ _t("Delete") }
|
||||
</AccessibleButton>
|
||||
|
@ -72,7 +73,11 @@ export const SpaceAvatar = ({
|
|||
} else {
|
||||
avatarSection = <React.Fragment>
|
||||
<div className="mx_SpaceBasicSettings_avatar" onClick={() => avatarUploadRef.current?.click()} />
|
||||
<AccessibleButton onClick={() => avatarUploadRef.current?.click()} kind="link">
|
||||
<AccessibleButton
|
||||
onClick={() => avatarUploadRef.current?.click()}
|
||||
kind="link"
|
||||
aria-label={_t("Upload avatar")}
|
||||
>
|
||||
{ _t("Upload") }
|
||||
</AccessibleButton>
|
||||
</React.Fragment>;
|
||||
|
|
|
@ -100,9 +100,12 @@ const HomeButton = ({ selected, isPanelCollapsed }: IHomeButtonProps) => {
|
|||
return SpaceStore.instance.allRoomsInHome;
|
||||
});
|
||||
|
||||
return <li className={classNames("mx_SpaceItem", {
|
||||
"collapsed": isPanelCollapsed,
|
||||
})}>
|
||||
return <li
|
||||
className={classNames("mx_SpaceItem", {
|
||||
"collapsed": isPanelCollapsed,
|
||||
})}
|
||||
role="treeitem"
|
||||
>
|
||||
<SpaceButton
|
||||
className="mx_SpaceButton_home"
|
||||
onClick={() => SpaceStore.instance.setActiveSpace(null)}
|
||||
|
@ -149,9 +152,12 @@ const CreateSpaceButton = ({
|
|||
betaDot = <div className="mx_BetaDot" />;
|
||||
}
|
||||
|
||||
return <li className={classNames("mx_SpaceItem mx_SpaceItem_new", {
|
||||
"collapsed": isPanelCollapsed,
|
||||
})}>
|
||||
return <li
|
||||
className={classNames("mx_SpaceItem mx_SpaceItem_new", {
|
||||
"collapsed": isPanelCollapsed,
|
||||
})}
|
||||
role="treeitem"
|
||||
>
|
||||
<SpaceButton
|
||||
className={classNames("mx_SpaceButton_new", {
|
||||
mx_SpaceButton_newCancel: menuDisplayed,
|
||||
|
@ -280,6 +286,8 @@ const SpacePanel = () => {
|
|||
<ul
|
||||
className={classNames("mx_SpacePanel", { collapsed: isPanelCollapsed })}
|
||||
onKeyDown={onKeyDownHandler}
|
||||
role="tree"
|
||||
aria-label={_t("Spaces")}
|
||||
>
|
||||
<Droppable droppableId="top-level-spaces">
|
||||
{ (provided, snapshot) => (
|
||||
|
|
|
@ -77,11 +77,17 @@ export const SpaceButton: React.FC<IButtonProps> = ({
|
|||
|
||||
let notifBadge;
|
||||
if (notificationState) {
|
||||
let ariaLabel = _t("Jump to first unread room.");
|
||||
if (space?.getMyMembership() === "invite") {
|
||||
ariaLabel = _t("Jump to first invite.");
|
||||
}
|
||||
|
||||
notifBadge = <div className="mx_SpacePanel_badgeContainer">
|
||||
<NotificationBadge
|
||||
onClick={() => SpaceStore.instance.setActiveRoomInSpace(space || null)}
|
||||
forceCount={false}
|
||||
notification={notificationState}
|
||||
aria-label={ariaLabel}
|
||||
/>
|
||||
</div>;
|
||||
}
|
||||
|
@ -107,7 +113,6 @@ export const SpaceButton: React.FC<IButtonProps> = ({
|
|||
onClick={onClick}
|
||||
onContextMenu={openMenu}
|
||||
forceHide={!isNarrow || menuDisplayed}
|
||||
role="treeitem"
|
||||
inputRef={handle}
|
||||
>
|
||||
{ children }
|
||||
|
@ -284,7 +289,7 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
|
|||
/> : null;
|
||||
|
||||
return (
|
||||
<li {...otherProps} className={itemClasses} ref={innerRef}>
|
||||
<li {...otherProps} className={itemClasses} ref={innerRef} aria-expanded={!collapsed} role="treeitem">
|
||||
<SpaceButton
|
||||
space={space}
|
||||
className={isInvite ? "mx_SpaceButton_invite" : undefined}
|
||||
|
@ -296,9 +301,7 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
|
|||
avatarSize={isNested ? 24 : 32}
|
||||
onClick={this.onClick}
|
||||
onKeyDown={this.onKeyDown}
|
||||
aria-expanded={!collapsed}
|
||||
ContextMenuComponent={this.props.space.getMyMembership() === "join"
|
||||
? SpaceContextMenu : undefined}
|
||||
ContextMenuComponent={this.props.space.getMyMembership() === "join" ? SpaceContextMenu : undefined}
|
||||
>
|
||||
{ toggleCollapseButton }
|
||||
</SpaceButton>
|
||||
|
@ -322,7 +325,7 @@ const SpaceTreeLevel: React.FC<ITreeLevelProps> = ({
|
|||
isNested,
|
||||
parents,
|
||||
}) => {
|
||||
return <ul className="mx_SpaceTreeLevel">
|
||||
return <ul className="mx_SpaceTreeLevel" role="group">
|
||||
{ spaces.map(s => {
|
||||
return (<SpaceItem
|
||||
key={s.roomId}
|
||||
|
|
|
@ -1008,7 +1008,9 @@
|
|||
"Your server isn't responding to some <a>requests</a>.": "Your server isn't responding to some <a>requests</a>.",
|
||||
"Decline (%(counter)s)": "Decline (%(counter)s)",
|
||||
"Accept <policyLink /> to continue:": "Accept <policyLink /> to continue:",
|
||||
"Delete avatar": "Delete avatar",
|
||||
"Delete": "Delete",
|
||||
"Upload avatar": "Upload avatar",
|
||||
"Upload": "Upload",
|
||||
"Name": "Name",
|
||||
"Description": "Description",
|
||||
|
@ -1069,6 +1071,8 @@
|
|||
"Preview Space": "Preview Space",
|
||||
"Allow people to preview your space before they join.": "Allow people to preview your space before they join.",
|
||||
"Recommended for public spaces.": "Recommended for public spaces.",
|
||||
"Jump to first unread room.": "Jump to first unread room.",
|
||||
"Jump to first invite.": "Jump to first invite.",
|
||||
"Expand": "Expand",
|
||||
"Collapse": "Collapse",
|
||||
"Space options": "Space options",
|
||||
|
@ -1670,8 +1674,6 @@
|
|||
"Activity": "Activity",
|
||||
"A-Z": "A-Z",
|
||||
"List options": "List options",
|
||||
"Jump to first unread room.": "Jump to first unread room.",
|
||||
"Jump to first invite.": "Jump to first invite.",
|
||||
"Show %(count)s more|other": "Show %(count)s more",
|
||||
"Show %(count)s more|one": "Show %(count)s more",
|
||||
"Show less": "Show less",
|
||||
|
@ -2232,7 +2234,6 @@
|
|||
"To create a Space from another community, just pick the community in Preferences.": "To create a Space from another community, just pick the community in Preferences.",
|
||||
"Failed to migrate community": "Failed to migrate community",
|
||||
"Create Space from community": "Create Space from community",
|
||||
"Spaces are the new version of communities - with new features coming.": "Spaces are the new version of communities - with new features coming.",
|
||||
"A link to the Space will be put in your community description.": "A link to the Space will be put in your community description.",
|
||||
"All rooms will be added and all community members will be invited.": "All rooms will be added and all community members will be invited.",
|
||||
"Flair won't be available in Spaces for the foreseeable future.": "Flair won't be available in Spaces for the foreseeable future.",
|
||||
|
@ -2722,9 +2723,10 @@
|
|||
"Community Settings": "Community Settings",
|
||||
"Want more than a community? <a>Get your own server</a>": "Want more than a community? <a>Get your own server</a>",
|
||||
"Changes made to your community <bold1>name</bold1> and <bold2>avatar</bold2> might not be seen by other users for up to 30 minutes.": "Changes made to your community <bold1>name</bold1> and <bold2>avatar</bold2> might not be seen by other users for up to 30 minutes.",
|
||||
"You can create a Space from this community <a>here</a>.": "You can create a Space from this community <a>here</a>.",
|
||||
"Ask the <a>admins</a> of this community to make it into a Space and keep a look out for the invite.": "Ask the <a>admins</a> of this community to make it into a Space and keep a look out for the invite.",
|
||||
"Communities can now be made into Spaces": "Communities can now be made into Spaces",
|
||||
"Spaces are a new way to make a community, with new features coming.": "Spaces are a new way to make a community, with new features coming.",
|
||||
"Ask the admins of this community to make it into a Space and keep a look out for the invite.": "Ask the admins of this community to make it into a Space and keep a look out for the invite.",
|
||||
"Communities won't receive further updates.": "Communities won't receive further updates.",
|
||||
"These rooms are displayed to community members on the community page. Community members can join the rooms by clicking on them.": "These rooms are displayed to community members on the community page. Community members can join the rooms by clicking on them.",
|
||||
"Featured Rooms:": "Featured Rooms:",
|
||||
|
@ -2739,7 +2741,6 @@
|
|||
"Everyone": "Everyone",
|
||||
"Your community hasn't got a Long Description, a HTML page to show to community members.<br />Click here to open settings and give it one!": "Your community hasn't got a Long Description, a HTML page to show to community members.<br />Click here to open settings and give it one!",
|
||||
"Long Description (HTML)": "Long Description (HTML)",
|
||||
"Upload avatar": "Upload avatar",
|
||||
"Community %(groupId)s not found": "Community %(groupId)s not found",
|
||||
"This homeserver does not support communities": "This homeserver does not support communities",
|
||||
"Failed to load %(groupId)s": "Failed to load %(groupId)s",
|
||||
|
@ -2852,6 +2853,7 @@
|
|||
"Mark as suggested": "Mark as suggested",
|
||||
"No results found": "No results found",
|
||||
"You may want to try a different search or check for typos.": "You may want to try a different search or check for typos.",
|
||||
"Space": "Space",
|
||||
"Search names and descriptions": "Search names and descriptions",
|
||||
"If you can't find the room you're looking for, ask for an invite or <a>create a new room</a>.": "If you can't find the room you're looking for, ask for an invite or <a>create a new room</a>.",
|
||||
"Create room": "Create room",
|
||||
|
@ -3134,7 +3136,6 @@
|
|||
"Page Down": "Page Down",
|
||||
"Esc": "Esc",
|
||||
"Enter": "Enter",
|
||||
"Space": "Space",
|
||||
"End": "End",
|
||||
"[number]": "[number]"
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue