mirror of https://github.com/vector-im/riot-web
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
|
path: element-web/webapp
|
||||||
# We'll only use this in a triggered job, then we're done with it
|
# We'll only use this in a triggered job, then we're done with it
|
||||||
retention-days: 1
|
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');
|
var fs = require('fs');
|
||||||
fs.writeFileSync('${{github.workspace}}/previewbuild.zip', Buffer.from(download.data));
|
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
|
- name: Deploy to Netlify
|
||||||
id: netlify
|
id: netlify
|
||||||
uses: nwtgck/actions-netlify@v1.2
|
uses: nwtgck/actions-netlify@v1.2
|
||||||
with:
|
with:
|
||||||
publish-dir: .
|
publish-dir: webapp
|
||||||
deploy-message: "Deploy from GitHub Actions"
|
deploy-message: "Deploy from GitHub Actions"
|
||||||
# These don't work because we're in workflow_run
|
# These don't work because we're in workflow_run
|
||||||
enable-pull-request-comment: false
|
enable-pull-request-comment: false
|
||||||
|
@ -47,12 +68,13 @@ jobs:
|
||||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||||
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
|
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
|
||||||
timeout-minutes: 1
|
timeout-minutes: 1
|
||||||
- name: Comment on PR
|
- name: Edit PR Description
|
||||||
uses: phulsechinmay/rewritable-pr-comment@v0.3.0
|
uses: velas/pr-description@v1.0.1
|
||||||
with:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
ISSUE_ID: ${{ github.event.workflow_run.pull_requests[0].number }}
|
with:
|
||||||
message: |
|
pull-request-number: ${{ steps.readctx.outputs.prnumber }}
|
||||||
|
description-message: |
|
||||||
Preview: ${{ steps.netlify.outputs.deploy-url }}
|
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.
|
⚠️ 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-sizes.scss";
|
||||||
@import "./_font-weights.scss";
|
@import "./_font-weights.scss";
|
||||||
|
@import "./_animations.scss";
|
||||||
|
|
||||||
$hover-transition: 0.08s cubic-bezier(.46, .03, .52, .96); // quadratic
|
$hover-transition: 0.08s cubic-bezier(.46, .03, .52, .96); // quadratic
|
||||||
|
|
||||||
|
|
|
@ -400,7 +400,11 @@ limitations under the License.
|
||||||
background-color: $secondary-fg-color;
|
background-color: $secondary-fg-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_AccessibleButton {
|
.mx_AccessibleButton_kind_link {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_GroupView_spaceUpgradePrompt_close {
|
||||||
width: 16px;
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
|
|
@ -269,7 +269,7 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover, &:focus-within {
|
||||||
background-color: $groupFilterPanel-bg-color;
|
background-color: $groupFilterPanel-bg-color;
|
||||||
|
|
||||||
.mx_AccessibleButton {
|
.mx_AccessibleButton {
|
||||||
|
@ -278,6 +278,10 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
li.mx_SpaceRoomDirectory_roomTileWrapper {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
.mx_SpaceRoomDirectory_roomTile,
|
.mx_SpaceRoomDirectory_roomTile,
|
||||||
.mx_SpaceRoomDirectory_subspace_children {
|
.mx_SpaceRoomDirectory_subspace_children {
|
||||||
&::before {
|
&::before {
|
||||||
|
|
|
@ -59,6 +59,10 @@ limitations under the License.
|
||||||
.mx_JoinRuleDropdown .mx_Dropdown_menu {
|
.mx_JoinRuleDropdown .mx_Dropdown_menu {
|
||||||
width: auto !important; // override fixed width
|
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 {
|
.mx_CreateSpaceFromCommunityDialog_footer {
|
||||||
|
|
|
@ -16,6 +16,12 @@ limitations under the License.
|
||||||
|
|
||||||
$timelineImageBorderRadius: 4px;
|
$timelineImageBorderRadius: 4px;
|
||||||
|
|
||||||
|
.mx_MImageBody_thumbnail--blurhash {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.mx_MImageBody_thumbnail {
|
.mx_MImageBody_thumbnail {
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
border-radius: $timelineImageBorderRadius;
|
border-radius: $timelineImageBorderRadius;
|
||||||
|
@ -23,8 +29,11 @@ $timelineImageBorderRadius: 4px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: 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;
|
border-radius: $timelineImageBorderRadius;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -489,6 +489,10 @@ $hover-select-border: 4px;
|
||||||
// https://github.com/vector-im/vector-web/issues/754
|
// https://github.com/vector-im/vector-web/issues/754
|
||||||
overflow-x: overlay;
|
overflow-x: overlay;
|
||||||
overflow-y: visible;
|
overflow-y: visible;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-corner {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -150,13 +150,14 @@ const reducer = (state: IState, action: IAction) => {
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
handleHomeEnd?: boolean;
|
handleHomeEnd?: boolean;
|
||||||
|
handleUpDown?: boolean;
|
||||||
children(renderProps: {
|
children(renderProps: {
|
||||||
onKeyDownHandler(ev: React.KeyboardEvent);
|
onKeyDownHandler(ev: React.KeyboardEvent);
|
||||||
});
|
});
|
||||||
onKeyDown?(ev: React.KeyboardEvent, state: IState);
|
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, {
|
const [state, dispatch] = useReducer<Reducer<IState, IAction>>(reducer, {
|
||||||
activeRef: null,
|
activeRef: null,
|
||||||
refs: [],
|
refs: [],
|
||||||
|
@ -167,21 +168,50 @@ export const RovingTabIndexProvider: React.FC<IProps> = ({ children, handleHomeE
|
||||||
const onKeyDownHandler = useCallback((ev) => {
|
const onKeyDownHandler = useCallback((ev) => {
|
||||||
let handled = false;
|
let handled = false;
|
||||||
// Don't interfere with input default keydown behaviour
|
// 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
|
// check if we actually have any items
|
||||||
switch (ev.key) {
|
switch (ev.key) {
|
||||||
case Key.HOME:
|
case Key.HOME:
|
||||||
handled = true;
|
if (handleHomeEnd) {
|
||||||
// move focus to first item
|
handled = true;
|
||||||
if (context.state.refs.length > 0) {
|
// move focus to first item
|
||||||
context.state.refs[0].current.focus();
|
if (context.state.refs.length > 0) {
|
||||||
|
context.state.refs[0].current.focus();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Key.END:
|
case Key.END:
|
||||||
handled = true;
|
if (handleHomeEnd) {
|
||||||
// move focus to last item
|
handled = true;
|
||||||
if (context.state.refs.length > 0) {
|
// move focus to last item
|
||||||
context.state.refs[context.state.refs.length - 1].current.focus();
|
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;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -193,7 +223,7 @@ export const RovingTabIndexProvider: React.FC<IProps> = ({ children, handleHomeE
|
||||||
} else if (onKeyDown) {
|
} else if (onKeyDown) {
|
||||||
return onKeyDown(ev, context.state);
|
return onKeyDown(ev, context.state);
|
||||||
}
|
}
|
||||||
}, [context.state, onKeyDown, handleHomeEnd]);
|
}, [context.state, onKeyDown, handleHomeEnd, handleUpDown]);
|
||||||
|
|
||||||
return <RovingTabIndexContext.Provider value={context}>
|
return <RovingTabIndexContext.Provider value={context}>
|
||||||
{ children({ onKeyDownHandler }) }
|
{ children({ onKeyDownHandler }) }
|
||||||
|
|
|
@ -61,7 +61,9 @@ export default class AutoHideScrollbar extends React.Component<IProps> {
|
||||||
style={style}
|
style={style}
|
||||||
className={["mx_AutoHideScrollbar", className].join(" ")}
|
className={["mx_AutoHideScrollbar", className].join(" ")}
|
||||||
onWheel={onWheel}
|
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 }
|
{ children }
|
||||||
</div>);
|
</div>);
|
||||||
|
|
|
@ -41,6 +41,9 @@ import RightPanelStore from "../../stores/RightPanelStore";
|
||||||
import AutoHideScrollbar from "./AutoHideScrollbar";
|
import AutoHideScrollbar from "./AutoHideScrollbar";
|
||||||
import { mediaFromMxc } from "../../customisations/Media";
|
import { mediaFromMxc } from "../../customisations/Media";
|
||||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
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(
|
const LONG_DESC_PLACEHOLDER = _td(
|
||||||
`<h1>HTML for your community's page</h1>
|
`<h1>HTML for your community's page</h1>
|
||||||
|
@ -815,6 +818,17 @@ export default class GroupView extends React.Component {
|
||||||
this.setState({ showUpgradeNotice: false });
|
this.setState({ showUpgradeNotice: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onCreateSpaceClick = () => {
|
||||||
|
createSpaceFromCommunity(this._matrixClient, this.props.groupId);
|
||||||
|
};
|
||||||
|
|
||||||
|
_onAdminsLinkClick = () => {
|
||||||
|
dis.dispatch({
|
||||||
|
action: Action.SetRightPanelPhase,
|
||||||
|
phase: RightPanelPhases.GroupMemberList,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
_getGroupSection() {
|
_getGroupSection() {
|
||||||
const groupSettingsSectionClasses = classnames({
|
const groupSettingsSectionClasses = classnames({
|
||||||
"mx_GroupView_group": this.state.editing,
|
"mx_GroupView_group": this.state.editing,
|
||||||
|
@ -854,17 +868,35 @@ export default class GroupView extends React.Component {
|
||||||
|
|
||||||
let communitiesUpgradeNotice;
|
let communitiesUpgradeNotice;
|
||||||
if (this.state.showUpgradeNotice) {
|
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">
|
communitiesUpgradeNotice = <div className="mx_GroupView_spaceUpgradePrompt">
|
||||||
<h2>{ _t("Communities can now be made into Spaces") }</h2>
|
<h2>{ _t("Communities can now be made into Spaces") }</h2>
|
||||||
<p>
|
<p>
|
||||||
{ _t("Spaces are a new way to make a community, with new features coming.") }
|
{ _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 " +
|
{ text }
|
||||||
"and keep a look out for the invite.") }
|
|
||||||
|
|
||||||
{ _t("Communities won't receive further updates.") }
|
{ _t("Communities won't receive further updates.") }
|
||||||
</p>
|
</p>
|
||||||
<AccessibleButton onClick={this._dismissUpgradeNotice} />
|
<AccessibleButton
|
||||||
|
className="mx_GroupView_spaceUpgradePrompt_close"
|
||||||
|
onClick={this._dismissUpgradeNotice}
|
||||||
|
/>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -392,9 +392,6 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||||
<IndicatorScrollbar
|
<IndicatorScrollbar
|
||||||
className="mx_LeftPanel_breadcrumbsContainer mx_AutoHideScrollbar"
|
className="mx_LeftPanel_breadcrumbsContainer mx_AutoHideScrollbar"
|
||||||
verticalScrollsHorizontally={true}
|
verticalScrollsHorizontally={true}
|
||||||
// Firefox sometimes makes this element focusable due to
|
|
||||||
// overflow:scroll;, so force it out of tab order.
|
|
||||||
tabIndex={-1}
|
|
||||||
>
|
>
|
||||||
<RoomBreadcrumbs />
|
<RoomBreadcrumbs />
|
||||||
</IndicatorScrollbar>
|
</IndicatorScrollbar>
|
||||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
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 { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import { EventType, RoomType } from "matrix-js-sdk/src/@types/event";
|
import { EventType, RoomType } from "matrix-js-sdk/src/@types/event";
|
||||||
import { ISpaceSummaryRoom, ISpaceSummaryEvent } from "matrix-js-sdk/src/@types/spaces";
|
import { ISpaceSummaryRoom, ISpaceSummaryEvent } from "matrix-js-sdk/src/@types/spaces";
|
||||||
|
@ -46,6 +46,8 @@ import { getDisplayAliasForAliasSet } from "../../Rooms";
|
||||||
import { useDispatcher } from "../../hooks/useDispatcher";
|
import { useDispatcher } from "../../hooks/useDispatcher";
|
||||||
import defaultDispatcher from "../../dispatcher/dispatcher";
|
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||||
import { Action } from "../../dispatcher/actions";
|
import { Action } from "../../dispatcher/actions";
|
||||||
|
import { Key } from "../../Keyboard";
|
||||||
|
import { IState, RovingTabIndexProvider, useRovingTabIndex } from "../../accessibility/RovingTabIndex";
|
||||||
|
|
||||||
interface IHierarchyProps {
|
interface IHierarchyProps {
|
||||||
space: Room;
|
space: Room;
|
||||||
|
@ -80,6 +82,7 @@ const Tile: React.FC<ITileProps> = ({
|
||||||
|| (room.room_type === RoomType.Space ? _t("Unnamed Space") : _t("Unnamed Room"));
|
|| (room.room_type === RoomType.Space ? _t("Unnamed Space") : _t("Unnamed Room"));
|
||||||
|
|
||||||
const [showChildren, toggleShowChildren] = useStateToggle(true);
|
const [showChildren, toggleShowChildren] = useStateToggle(true);
|
||||||
|
const [onFocus, isActive, ref] = useRovingTabIndex();
|
||||||
|
|
||||||
const onPreviewClick = (ev: ButtonEvent) => {
|
const onPreviewClick = (ev: ButtonEvent) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
|
@ -94,11 +97,21 @@ const Tile: React.FC<ITileProps> = ({
|
||||||
|
|
||||||
let button;
|
let button;
|
||||||
if (joinedRoom) {
|
if (joinedRoom) {
|
||||||
button = <AccessibleButton onClick={onPreviewClick} kind="primary_outline">
|
button = <AccessibleButton
|
||||||
|
onClick={onPreviewClick}
|
||||||
|
kind="primary_outline"
|
||||||
|
onFocus={onFocus}
|
||||||
|
tabIndex={isActive ? 0 : -1}
|
||||||
|
>
|
||||||
{ _t("View") }
|
{ _t("View") }
|
||||||
</AccessibleButton>;
|
</AccessibleButton>;
|
||||||
} else if (onJoinClick) {
|
} else if (onJoinClick) {
|
||||||
button = <AccessibleButton onClick={onJoinClick} kind="primary">
|
button = <AccessibleButton
|
||||||
|
onClick={onJoinClick}
|
||||||
|
kind="primary"
|
||||||
|
onFocus={onFocus}
|
||||||
|
tabIndex={isActive ? 0 : -1}
|
||||||
|
>
|
||||||
{ _t("Join") }
|
{ _t("Join") }
|
||||||
</AccessibleButton>;
|
</AccessibleButton>;
|
||||||
}
|
}
|
||||||
|
@ -106,13 +119,13 @@ const Tile: React.FC<ITileProps> = ({
|
||||||
let checkbox;
|
let checkbox;
|
||||||
if (onToggleClick) {
|
if (onToggleClick) {
|
||||||
if (hasPermissions) {
|
if (hasPermissions) {
|
||||||
checkbox = <StyledCheckbox checked={!!selected} onChange={onToggleClick} />;
|
checkbox = <StyledCheckbox checked={!!selected} onChange={onToggleClick} tabIndex={isActive ? 0 : -1} />;
|
||||||
} else {
|
} else {
|
||||||
checkbox = <TextWithTooltip
|
checkbox = <TextWithTooltip
|
||||||
tooltip={_t("You don't have permission")}
|
tooltip={_t("You don't have permission")}
|
||||||
onClick={ev => { ev.stopPropagation(); }}
|
onClick={ev => { ev.stopPropagation(); }}
|
||||||
>
|
>
|
||||||
<StyledCheckbox disabled={true} />
|
<StyledCheckbox disabled={true} tabIndex={isActive ? 0 : -1} />
|
||||||
</TextWithTooltip>;
|
</TextWithTooltip>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -172,8 +185,9 @@ const Tile: React.FC<ITileProps> = ({
|
||||||
</div>
|
</div>
|
||||||
</React.Fragment>;
|
</React.Fragment>;
|
||||||
|
|
||||||
let childToggle;
|
let childToggle: JSX.Element;
|
||||||
let childSection;
|
let childSection: JSX.Element;
|
||||||
|
let onKeyDown: KeyboardEventHandler;
|
||||||
if (children) {
|
if (children) {
|
||||||
// the chevron is purposefully a div rather than a button as it should be ignored for a11y
|
// the chevron is purposefully a div rather than a button as it should be ignored for a11y
|
||||||
childToggle = <div
|
childToggle = <div
|
||||||
|
@ -185,25 +199,74 @@ const Tile: React.FC<ITileProps> = ({
|
||||||
toggleShowChildren();
|
toggleShowChildren();
|
||||||
}}
|
}}
|
||||||
/>;
|
/>;
|
||||||
|
|
||||||
if (showChildren) {
|
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 }
|
{ children }
|
||||||
</div>;
|
</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
|
<AccessibleButton
|
||||||
className={classNames("mx_SpaceRoomDirectory_roomTile", {
|
className={classNames("mx_SpaceRoomDirectory_roomTile", {
|
||||||
mx_SpaceRoomDirectory_subspace: room.room_type === RoomType.Space,
|
mx_SpaceRoomDirectory_subspace: room.room_type === RoomType.Space,
|
||||||
})}
|
})}
|
||||||
onClick={(hasPermissions && onToggleClick) ? onToggleClick : onPreviewClick}
|
onClick={(hasPermissions && onToggleClick) ? onToggleClick : onPreviewClick}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
inputRef={ref}
|
||||||
|
onFocus={onFocus}
|
||||||
|
tabIndex={isActive ? 0 : -1}
|
||||||
>
|
>
|
||||||
{ content }
|
{ content }
|
||||||
{ childToggle }
|
{ childToggle }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
{ childSection }
|
{ childSection }
|
||||||
</>;
|
</li>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const showRoom = (room: ISpaceSummaryRoom, viaServers?: string[], autoJoin = false) => {
|
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>;
|
return <p>{ _t("Your server does not support showing space hierarchies.") }</p>;
|
||||||
}
|
}
|
||||||
|
|
||||||
let content;
|
const onKeyDown = (ev: KeyboardEvent, state: IState) => {
|
||||||
if (roomsMap) {
|
if (ev.key === Key.ARROW_DOWN && ev.currentTarget.classList.contains("mx_SpaceRoomDirectory_search")) {
|
||||||
const numRooms = Array.from(roomsMap.values()).filter(r => !r.room_type).length;
|
state.refs[0]?.current?.focus();
|
||||||
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 });
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
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
|
// TODO loading state/error state
|
||||||
return <>
|
return <RovingTabIndexProvider onKeyDown={onKeyDown} handleHomeEnd handleUpDown>
|
||||||
<SearchBox
|
{ ({ onKeyDownHandler }) => {
|
||||||
className="mx_textinput_icon mx_textinput_search"
|
let content;
|
||||||
placeholder={_t("Search names and descriptions")}
|
if (roomsMap) {
|
||||||
onSearch={setQuery}
|
const numRooms = Array.from(roomsMap.values()).filter(r => !r.room_type).length;
|
||||||
autoFocus={true}
|
const numSpaces = roomsMap.size - numRooms - 1; // -1 at the end to exclude the space we are looking at
|
||||||
initialValue={initialText}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{ 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 {
|
interface IProps {
|
||||||
|
|
|
@ -286,8 +286,6 @@ const CreateSpaceFromCommunityDialog: React.FC<IProps> = ({ matrixClient: cli, g
|
||||||
>
|
>
|
||||||
<div className="mx_CreateSpaceFromCommunityDialog_content">
|
<div className="mx_CreateSpaceFromCommunityDialog_content">
|
||||||
<p>
|
<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("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.") }
|
{ _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("Open space for anyone, best for communities")
|
||||||
: _t("Invite only, best for yourself or teams")
|
: _t("Invite only, best for yourself or teams")
|
||||||
}</p>
|
}</p>
|
||||||
|
{ joinRule !== JoinRule.Public &&
|
||||||
|
<div className="mx_CreateSpaceFromCommunityDialog_nonPublicSpacer" />
|
||||||
|
}
|
||||||
</SpaceCreateForm>
|
</SpaceCreateForm>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -67,7 +67,9 @@ export default function AccessibleButton({
|
||||||
...restProps
|
...restProps
|
||||||
}: IProps) {
|
}: IProps) {
|
||||||
const newProps: IAccessibleButtonProps = restProps;
|
const newProps: IAccessibleButtonProps = restProps;
|
||||||
if (!disabled) {
|
if (disabled) {
|
||||||
|
newProps["aria-disabled"] = true;
|
||||||
|
} else {
|
||||||
newProps.onClick = onClick;
|
newProps.onClick = onClick;
|
||||||
// We need to consume enter onKeyDown and space onKeyUp
|
// We need to consume enter onKeyDown and space onKeyUp
|
||||||
// otherwise we are risking also activating other keyboard focusable elements
|
// otherwise we are risking also activating other keyboard focusable elements
|
||||||
|
@ -118,7 +120,7 @@ export default function AccessibleButton({
|
||||||
);
|
);
|
||||||
|
|
||||||
// React.createElement expects InputHTMLAttributes
|
// React.createElement expects InputHTMLAttributes
|
||||||
return React.createElement(element, restProps, children);
|
return React.createElement(element, newProps, children);
|
||||||
}
|
}
|
||||||
|
|
||||||
AccessibleButton.defaultProps = {
|
AccessibleButton.defaultProps = {
|
||||||
|
|
|
@ -18,7 +18,7 @@ limitations under the License.
|
||||||
import React, { ChangeEvent, createRef, CSSProperties, ReactElement, ReactNode, Ref } from 'react';
|
import React, { ChangeEvent, createRef, CSSProperties, ReactElement, ReactNode, Ref } from 'react';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
|
|
||||||
import AccessibleButton from './AccessibleButton';
|
import AccessibleButton, { ButtonEvent } from './AccessibleButton';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import { Key } from "../../../Keyboard";
|
import { Key } from "../../../Keyboard";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
@ -178,7 +178,7 @@ export default class Dropdown extends React.Component<IProps, IState> {
|
||||||
this.ignoreEvent = ev;
|
this.ignoreEvent = ev;
|
||||||
};
|
};
|
||||||
|
|
||||||
private onInputClick = (ev: React.MouseEvent) => {
|
private onAccessibleButtonClick = (ev: ButtonEvent) => {
|
||||||
if (this.props.disabled) return;
|
if (this.props.disabled) return;
|
||||||
|
|
||||||
if (!this.state.expanded) {
|
if (!this.state.expanded) {
|
||||||
|
@ -186,6 +186,10 @@ export default class Dropdown extends React.Component<IProps, IState> {
|
||||||
expanded: true,
|
expanded: true,
|
||||||
});
|
});
|
||||||
ev.preventDefault();
|
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);
|
this.props.onOptionChange(dropdownKey);
|
||||||
};
|
};
|
||||||
|
|
||||||
private onInputKeyDown = (e: React.KeyboardEvent) => {
|
private onKeyDown = (e: React.KeyboardEvent) => {
|
||||||
let handled = true;
|
let handled = true;
|
||||||
|
|
||||||
// These keys don't generate keypress events and so needs to be on keyup
|
// 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 {
|
private prevOption(optionKey: string): string {
|
||||||
const keys = Object.keys(this.childrenByKey);
|
const keys = Object.keys(this.childrenByKey);
|
||||||
const index = keys.indexOf(optionKey);
|
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) {
|
private scrollIntoView(node: Element) {
|
||||||
|
@ -320,7 +324,6 @@ export default class Dropdown extends React.Component<IProps, IState> {
|
||||||
type="text"
|
type="text"
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
className="mx_Dropdown_option"
|
className="mx_Dropdown_option"
|
||||||
onKeyDown={this.onInputKeyDown}
|
|
||||||
onChange={this.onInputChange}
|
onChange={this.onInputChange}
|
||||||
value={this.state.searchQuery}
|
value={this.state.searchQuery}
|
||||||
role="combobox"
|
role="combobox"
|
||||||
|
@ -329,6 +332,7 @@ export default class Dropdown extends React.Component<IProps, IState> {
|
||||||
aria-owns={`${this.props.id}_listbox`}
|
aria-owns={`${this.props.id}_listbox`}
|
||||||
aria-disabled={this.props.disabled}
|
aria-disabled={this.props.disabled}
|
||||||
aria-label={this.props.label}
|
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}>
|
return <div className={classnames(dropdownClasses)} ref={this.collectRoot}>
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
className="mx_Dropdown_input mx_no_textinput"
|
className="mx_Dropdown_input mx_no_textinput"
|
||||||
onClick={this.onInputClick}
|
onClick={this.onAccessibleButtonClick}
|
||||||
aria-haspopup="listbox"
|
aria-haspopup="listbox"
|
||||||
aria-expanded={this.state.expanded}
|
aria-expanded={this.state.expanded}
|
||||||
disabled={this.props.disabled}
|
disabled={this.props.disabled}
|
||||||
inputRef={this.buttonRef}
|
inputRef={this.buttonRef}
|
||||||
aria-label={this.props.label}
|
aria-label={this.props.label}
|
||||||
aria-describedby={`${this.props.id}_value`}
|
aria-describedby={`${this.props.id}_value`}
|
||||||
|
onKeyDown={this.onKeyDown}
|
||||||
>
|
>
|
||||||
{ currentValue }
|
{ currentValue }
|
||||||
<span className="mx_Dropdown_arrow" />
|
<span className="mx_Dropdown_arrow" />
|
||||||
|
|
|
@ -25,12 +25,14 @@ import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
import InlineSpinner from '../elements/InlineSpinner';
|
import InlineSpinner from '../elements/InlineSpinner';
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import { mediaFromContent } from "../../../customisations/Media";
|
import { Media, mediaFromContent } from "../../../customisations/Media";
|
||||||
import { BLURHASH_FIELD } from "../../../ContentMessages";
|
import { BLURHASH_FIELD } from "../../../ContentMessages";
|
||||||
import { IMediaEventContent } from '../../../customisations/models/IMediaEventContent';
|
import { IMediaEventContent } from '../../../customisations/models/IMediaEventContent';
|
||||||
import ImageView from '../elements/ImageView';
|
import ImageView from '../elements/ImageView';
|
||||||
import { SyncState } from 'matrix-js-sdk/src/sync.api';
|
import { SyncState } from 'matrix-js-sdk/src/sync.api';
|
||||||
import { IBodyProps } from "./IBodyProps";
|
import { IBodyProps } from "./IBodyProps";
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { CSSTransition, SwitchTransition } from 'react-transition-group';
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
decryptedUrl?: string;
|
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
|
// this is only used as a fallback in case content.info.w/h is missing
|
||||||
loadedImageDimensions = { naturalWidth, naturalHeight };
|
loadedImageDimensions = { naturalWidth, naturalHeight };
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({ imgLoaded: true, loadedImageDimensions });
|
this.setState({ imgLoaded: true, loadedImageDimensions });
|
||||||
};
|
};
|
||||||
|
|
||||||
protected getContentUrl(): string {
|
protected getContentUrl(): string {
|
||||||
const media = mediaFromContent(this.props.mxEvent.getContent());
|
if (this.media.isEncrypted) {
|
||||||
if (media.isEncrypted) {
|
|
||||||
return this.state.decryptedUrl;
|
return this.state.decryptedUrl;
|
||||||
} else {
|
} else {
|
||||||
return media.srcHttp;
|
return this.media.srcHttp;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private get media(): Media {
|
||||||
|
return mediaFromContent(this.props.mxEvent.getContent());
|
||||||
|
}
|
||||||
|
|
||||||
protected getThumbUrl(): string {
|
protected getThumbUrl(): string {
|
||||||
// FIXME: we let images grow as wide as you like, rather than capped to 800x600.
|
// 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
|
// 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.w > thumbWidth ||
|
||||||
info.h > thumbHeight
|
info.h > thumbHeight
|
||||||
);
|
);
|
||||||
const isLargeFileSize = info.size > 1*1024*1024; // 1mb
|
const isLargeFileSize = info.size > 1 * 1024 * 1024; // 1mb
|
||||||
|
|
||||||
if (isLargeFileSize && isLargerThanThumbnail) {
|
if (isLargeFileSize && isLargerThanThumbnail) {
|
||||||
// image is too large physically and bytewise to clutter our timeline so
|
// 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>;
|
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 = (
|
const thumbnail = (
|
||||||
<div className="mx_MImageBody_thumbnail_container" style={{ maxHeight: maxHeight, maxWidth: maxWidth, aspectRatio: `${infoWidth}/${infoHeight}` }}>
|
<div className="mx_MImageBody_thumbnail_container" style={{ maxHeight: maxHeight, maxWidth: maxWidth, aspectRatio: `${infoWidth}/${infoHeight}` }}>
|
||||||
{ showPlaceholder &&
|
<SwitchTransition mode="out-in">
|
||||||
<div
|
<C
|
||||||
className="mx_MImageBody_thumbnail"
|
classNames="mx_rtg--fade"
|
||||||
style={{
|
key={`img-${showPlaceholder}`}
|
||||||
// Constrain width here so that spinner appears central to the loaded thumbnail
|
timeout={300}
|
||||||
maxWidth: `min(100%, ${infoWidth}px)`,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{ placeholder }
|
{ /* This weirdly looking div is necessary here, otherwise SwitchTransition fails */ }
|
||||||
</div>
|
<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={{
|
<div style={{
|
||||||
display: !showPlaceholder ? undefined : 'none',
|
height: '100%',
|
||||||
height: '100%', // Also force to size of a parent to prevent scroll-jumps (see above)
|
|
||||||
}}>
|
}}>
|
||||||
{ img }
|
{ img }
|
||||||
{ gifLabel }
|
{ gifLabel }
|
||||||
|
@ -413,7 +434,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
|
||||||
// Overidden by MStickerBody
|
// Overidden by MStickerBody
|
||||||
protected getPlaceholder(width: number, height: number): JSX.Element {
|
protected getPlaceholder(width: number, height: number): JSX.Element {
|
||||||
const blurhash = this.props.mxEvent.getContent().info[BLURHASH_FIELD];
|
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 (
|
return (
|
||||||
<InlineSpinner w={32} h={32} />
|
<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 thumbnail = this.messageContent(contentUrl, thumbUrl, content);
|
||||||
const fileBody = this.getFileBody();
|
const fileBody = this.getFileBody();
|
||||||
|
|
||||||
return <div className="mx_MImageBody">
|
return (
|
||||||
{ thumbnail }
|
<div className="mx_MImageBody">
|
||||||
{ fileBody }
|
{ thumbnail }
|
||||||
</div>;
|
{ fileBody }
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -541,7 +541,6 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
||||||
handled = true;
|
handled = true;
|
||||||
} else if (event.key === Key.BACKSPACE || event.key === Key.DELETE) {
|
} else if (event.key === Key.BACKSPACE || event.key === Key.DELETE) {
|
||||||
this.formatBarRef.current.hide();
|
this.formatBarRef.current.hide();
|
||||||
handled = this.fakeDeletion(event.key === Key.BACKSPACE);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (handled) {
|
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> {
|
private async tabCompleteName(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await new Promise<void>(resolve => this.setState({ showVisualBell: false }, resolve));
|
await new Promise<void>(resolve => this.setState({ showVisualBell: false }, resolve));
|
||||||
|
|
|
@ -65,6 +65,7 @@ export const SpaceAvatar = ({
|
||||||
}}
|
}}
|
||||||
kind="link"
|
kind="link"
|
||||||
className="mx_SpaceBasicSettings_avatar_remove"
|
className="mx_SpaceBasicSettings_avatar_remove"
|
||||||
|
aria-label={_t("Delete avatar")}
|
||||||
>
|
>
|
||||||
{ _t("Delete") }
|
{ _t("Delete") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
|
@ -72,7 +73,11 @@ export const SpaceAvatar = ({
|
||||||
} else {
|
} else {
|
||||||
avatarSection = <React.Fragment>
|
avatarSection = <React.Fragment>
|
||||||
<div className="mx_SpaceBasicSettings_avatar" onClick={() => avatarUploadRef.current?.click()} />
|
<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") }
|
{ _t("Upload") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
</React.Fragment>;
|
</React.Fragment>;
|
||||||
|
|
|
@ -100,9 +100,12 @@ const HomeButton = ({ selected, isPanelCollapsed }: IHomeButtonProps) => {
|
||||||
return SpaceStore.instance.allRoomsInHome;
|
return SpaceStore.instance.allRoomsInHome;
|
||||||
});
|
});
|
||||||
|
|
||||||
return <li className={classNames("mx_SpaceItem", {
|
return <li
|
||||||
"collapsed": isPanelCollapsed,
|
className={classNames("mx_SpaceItem", {
|
||||||
})}>
|
"collapsed": isPanelCollapsed,
|
||||||
|
})}
|
||||||
|
role="treeitem"
|
||||||
|
>
|
||||||
<SpaceButton
|
<SpaceButton
|
||||||
className="mx_SpaceButton_home"
|
className="mx_SpaceButton_home"
|
||||||
onClick={() => SpaceStore.instance.setActiveSpace(null)}
|
onClick={() => SpaceStore.instance.setActiveSpace(null)}
|
||||||
|
@ -149,9 +152,12 @@ const CreateSpaceButton = ({
|
||||||
betaDot = <div className="mx_BetaDot" />;
|
betaDot = <div className="mx_BetaDot" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <li className={classNames("mx_SpaceItem mx_SpaceItem_new", {
|
return <li
|
||||||
"collapsed": isPanelCollapsed,
|
className={classNames("mx_SpaceItem mx_SpaceItem_new", {
|
||||||
})}>
|
"collapsed": isPanelCollapsed,
|
||||||
|
})}
|
||||||
|
role="treeitem"
|
||||||
|
>
|
||||||
<SpaceButton
|
<SpaceButton
|
||||||
className={classNames("mx_SpaceButton_new", {
|
className={classNames("mx_SpaceButton_new", {
|
||||||
mx_SpaceButton_newCancel: menuDisplayed,
|
mx_SpaceButton_newCancel: menuDisplayed,
|
||||||
|
@ -280,6 +286,8 @@ const SpacePanel = () => {
|
||||||
<ul
|
<ul
|
||||||
className={classNames("mx_SpacePanel", { collapsed: isPanelCollapsed })}
|
className={classNames("mx_SpacePanel", { collapsed: isPanelCollapsed })}
|
||||||
onKeyDown={onKeyDownHandler}
|
onKeyDown={onKeyDownHandler}
|
||||||
|
role="tree"
|
||||||
|
aria-label={_t("Spaces")}
|
||||||
>
|
>
|
||||||
<Droppable droppableId="top-level-spaces">
|
<Droppable droppableId="top-level-spaces">
|
||||||
{ (provided, snapshot) => (
|
{ (provided, snapshot) => (
|
||||||
|
|
|
@ -77,11 +77,17 @@ export const SpaceButton: React.FC<IButtonProps> = ({
|
||||||
|
|
||||||
let notifBadge;
|
let notifBadge;
|
||||||
if (notificationState) {
|
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">
|
notifBadge = <div className="mx_SpacePanel_badgeContainer">
|
||||||
<NotificationBadge
|
<NotificationBadge
|
||||||
onClick={() => SpaceStore.instance.setActiveRoomInSpace(space || null)}
|
onClick={() => SpaceStore.instance.setActiveRoomInSpace(space || null)}
|
||||||
forceCount={false}
|
forceCount={false}
|
||||||
notification={notificationState}
|
notification={notificationState}
|
||||||
|
aria-label={ariaLabel}
|
||||||
/>
|
/>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
@ -107,7 +113,6 @@ export const SpaceButton: React.FC<IButtonProps> = ({
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
onContextMenu={openMenu}
|
onContextMenu={openMenu}
|
||||||
forceHide={!isNarrow || menuDisplayed}
|
forceHide={!isNarrow || menuDisplayed}
|
||||||
role="treeitem"
|
|
||||||
inputRef={handle}
|
inputRef={handle}
|
||||||
>
|
>
|
||||||
{ children }
|
{ children }
|
||||||
|
@ -284,7 +289,7 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
|
||||||
/> : null;
|
/> : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li {...otherProps} className={itemClasses} ref={innerRef}>
|
<li {...otherProps} className={itemClasses} ref={innerRef} aria-expanded={!collapsed} role="treeitem">
|
||||||
<SpaceButton
|
<SpaceButton
|
||||||
space={space}
|
space={space}
|
||||||
className={isInvite ? "mx_SpaceButton_invite" : undefined}
|
className={isInvite ? "mx_SpaceButton_invite" : undefined}
|
||||||
|
@ -296,9 +301,7 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
|
||||||
avatarSize={isNested ? 24 : 32}
|
avatarSize={isNested ? 24 : 32}
|
||||||
onClick={this.onClick}
|
onClick={this.onClick}
|
||||||
onKeyDown={this.onKeyDown}
|
onKeyDown={this.onKeyDown}
|
||||||
aria-expanded={!collapsed}
|
ContextMenuComponent={this.props.space.getMyMembership() === "join" ? SpaceContextMenu : undefined}
|
||||||
ContextMenuComponent={this.props.space.getMyMembership() === "join"
|
|
||||||
? SpaceContextMenu : undefined}
|
|
||||||
>
|
>
|
||||||
{ toggleCollapseButton }
|
{ toggleCollapseButton }
|
||||||
</SpaceButton>
|
</SpaceButton>
|
||||||
|
@ -322,7 +325,7 @@ const SpaceTreeLevel: React.FC<ITreeLevelProps> = ({
|
||||||
isNested,
|
isNested,
|
||||||
parents,
|
parents,
|
||||||
}) => {
|
}) => {
|
||||||
return <ul className="mx_SpaceTreeLevel">
|
return <ul className="mx_SpaceTreeLevel" role="group">
|
||||||
{ spaces.map(s => {
|
{ spaces.map(s => {
|
||||||
return (<SpaceItem
|
return (<SpaceItem
|
||||||
key={s.roomId}
|
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>.",
|
"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)",
|
"Decline (%(counter)s)": "Decline (%(counter)s)",
|
||||||
"Accept <policyLink /> to continue:": "Accept <policyLink /> to continue:",
|
"Accept <policyLink /> to continue:": "Accept <policyLink /> to continue:",
|
||||||
|
"Delete avatar": "Delete avatar",
|
||||||
"Delete": "Delete",
|
"Delete": "Delete",
|
||||||
|
"Upload avatar": "Upload avatar",
|
||||||
"Upload": "Upload",
|
"Upload": "Upload",
|
||||||
"Name": "Name",
|
"Name": "Name",
|
||||||
"Description": "Description",
|
"Description": "Description",
|
||||||
|
@ -1069,6 +1071,8 @@
|
||||||
"Preview Space": "Preview Space",
|
"Preview Space": "Preview Space",
|
||||||
"Allow people to preview your space before they join.": "Allow people to preview your space before they join.",
|
"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.",
|
"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",
|
"Expand": "Expand",
|
||||||
"Collapse": "Collapse",
|
"Collapse": "Collapse",
|
||||||
"Space options": "Space options",
|
"Space options": "Space options",
|
||||||
|
@ -1670,8 +1674,6 @@
|
||||||
"Activity": "Activity",
|
"Activity": "Activity",
|
||||||
"A-Z": "A-Z",
|
"A-Z": "A-Z",
|
||||||
"List options": "List options",
|
"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|other": "Show %(count)s more",
|
||||||
"Show %(count)s more|one": "Show %(count)s more",
|
"Show %(count)s more|one": "Show %(count)s more",
|
||||||
"Show less": "Show less",
|
"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.",
|
"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",
|
"Failed to migrate community": "Failed to migrate community",
|
||||||
"Create Space from community": "Create Space from 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.",
|
"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.",
|
"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.",
|
"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",
|
"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>",
|
"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.",
|
"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",
|
"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.",
|
"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.",
|
"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.",
|
"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:",
|
"Featured Rooms:": "Featured Rooms:",
|
||||||
|
@ -2739,7 +2741,6 @@
|
||||||
"Everyone": "Everyone",
|
"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!",
|
"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)",
|
"Long Description (HTML)": "Long Description (HTML)",
|
||||||
"Upload avatar": "Upload avatar",
|
|
||||||
"Community %(groupId)s not found": "Community %(groupId)s not found",
|
"Community %(groupId)s not found": "Community %(groupId)s not found",
|
||||||
"This homeserver does not support communities": "This homeserver does not support communities",
|
"This homeserver does not support communities": "This homeserver does not support communities",
|
||||||
"Failed to load %(groupId)s": "Failed to load %(groupId)s",
|
"Failed to load %(groupId)s": "Failed to load %(groupId)s",
|
||||||
|
@ -2852,6 +2853,7 @@
|
||||||
"Mark as suggested": "Mark as suggested",
|
"Mark as suggested": "Mark as suggested",
|
||||||
"No results found": "No results found",
|
"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.",
|
"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",
|
"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>.",
|
"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",
|
"Create room": "Create room",
|
||||||
|
@ -3134,7 +3136,6 @@
|
||||||
"Page Down": "Page Down",
|
"Page Down": "Page Down",
|
||||||
"Esc": "Esc",
|
"Esc": "Esc",
|
||||||
"Enter": "Enter",
|
"Enter": "Enter",
|
||||||
"Space": "Space",
|
|
||||||
"End": "End",
|
"End": "End",
|
||||||
"[number]": "[number]"
|
"[number]": "[number]"
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue