From d285a45da5a561cfbddb14a6380b1d77c0aab7e4 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 6 Aug 2021 12:28:06 +0100 Subject: [PATCH 01/22] Fix stray tabIndex on AutoHideScrollbar component in FF --- src/components/structures/AutoHideScrollbar.tsx | 4 +++- src/components/structures/LeftPanel.tsx | 3 --- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/components/structures/AutoHideScrollbar.tsx b/src/components/structures/AutoHideScrollbar.tsx index 184d883dda..a60df45770 100644 --- a/src/components/structures/AutoHideScrollbar.tsx +++ b/src/components/structures/AutoHideScrollbar.tsx @@ -61,7 +61,9 @@ export default class AutoHideScrollbar extends React.Component { 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 } ); diff --git a/src/components/structures/LeftPanel.tsx b/src/components/structures/LeftPanel.tsx index 3bd2c68c6c..ff5d15d44d 100644 --- a/src/components/structures/LeftPanel.tsx +++ b/src/components/structures/LeftPanel.tsx @@ -392,9 +392,6 @@ export default class LeftPanel extends React.Component { From 38953452506c1fd8fdc65d130478cdef738f3846 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 6 Aug 2021 12:28:20 +0100 Subject: [PATCH 02/22] Fix disabled state on AccessibleButton not being exposed via ARIA --- src/components/views/elements/AccessibleButton.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/views/elements/AccessibleButton.tsx b/src/components/views/elements/AccessibleButton.tsx index 8bb6341c3d..0ce9a3a030 100644 --- a/src/components/views/elements/AccessibleButton.tsx +++ b/src/components/views/elements/AccessibleButton.tsx @@ -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 = { From 1a1b1738c127bc24e4f8e7036fd0d8e3d6921c48 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 6 Aug 2021 12:28:46 +0100 Subject: [PATCH 03/22] Add aria label to clickable notification badge on space panel --- src/components/views/spaces/SpaceTreeLevel.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/components/views/spaces/SpaceTreeLevel.tsx b/src/components/views/spaces/SpaceTreeLevel.tsx index bb2184853e..0862458d9e 100644 --- a/src/components/views/spaces/SpaceTreeLevel.tsx +++ b/src/components/views/spaces/SpaceTreeLevel.tsx @@ -77,11 +77,17 @@ export const SpaceButton: React.FC = ({ let notifBadge; if (notificationState) { + let ariaLabel = _t("Jump to first unread room."); + if (space.getMyMembership() === "invite") { + ariaLabel = _t("Jump to first invite."); + } + notifBadge =
SpaceStore.instance.setActiveRoomInSpace(space || null)} forceCount={false} notification={notificationState} + aria-label={ariaLabel} />
; } From facc882a1110b002aae4349b966881e86da1ab28 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 6 Aug 2021 12:50:32 +0100 Subject: [PATCH 04/22] i18n and add space null guard for home space --- src/components/views/spaces/SpaceTreeLevel.tsx | 2 +- src/i18n/strings/en_EN.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/views/spaces/SpaceTreeLevel.tsx b/src/components/views/spaces/SpaceTreeLevel.tsx index 0862458d9e..5d2f58fab2 100644 --- a/src/components/views/spaces/SpaceTreeLevel.tsx +++ b/src/components/views/spaces/SpaceTreeLevel.tsx @@ -78,7 +78,7 @@ export const SpaceButton: React.FC = ({ let notifBadge; if (notificationState) { let ariaLabel = _t("Jump to first unread room."); - if (space.getMyMembership() === "invite") { + if (space?.getMyMembership() === "invite") { ariaLabel = _t("Jump to first invite."); } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 9f30776f44..09cb877d33 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1072,6 +1072,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", @@ -1665,8 +1667,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", From 41475a3b948d99cb1955c4e9e4e2bf960fc879f3 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 6 Aug 2021 14:12:55 +0100 Subject: [PATCH 05/22] fix keyboard focus issue around the space hierarchy --- res/css/structures/_SpaceRoomDirectory.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/structures/_SpaceRoomDirectory.scss b/res/css/structures/_SpaceRoomDirectory.scss index cb91aa3c7d..03ff1d5ae5 100644 --- a/res/css/structures/_SpaceRoomDirectory.scss +++ b/res/css/structures/_SpaceRoomDirectory.scss @@ -269,7 +269,7 @@ limitations under the License. } } - &:hover { + &:hover, &:focus-within { background-color: $groupFilterPanel-bg-color; .mx_AccessibleButton { From 54fb24f359bdb9fd7e2f2f76871a9ac52d8f9df2 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 6 Aug 2021 14:21:41 +0100 Subject: [PATCH 06/22] Fix dropdown keyboard accessibility when filter is disabled --- src/components/views/elements/Dropdown.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/views/elements/Dropdown.tsx b/src/components/views/elements/Dropdown.tsx index dddcceb97c..1f3ceabb83 100644 --- a/src/components/views/elements/Dropdown.tsx +++ b/src/components/views/elements/Dropdown.tsx @@ -204,7 +204,7 @@ export default class Dropdown extends React.Component { 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 @@ -320,7 +320,6 @@ export default class Dropdown extends React.Component { type="text" autoFocus={true} className="mx_Dropdown_option" - onKeyDown={this.onInputKeyDown} onChange={this.onInputChange} value={this.state.searchQuery} role="combobox" @@ -358,7 +357,7 @@ export default class Dropdown extends React.Component { // Note the menu sits inside the AccessibleButton div so it's anchored // to the input, but overflows below it. The root contains both. - return
+ return
Date: Fri, 6 Aug 2021 14:21:56 +0100 Subject: [PATCH 07/22] Fix dropdown negative wraparound for keyboard accessibility --- src/components/views/elements/Dropdown.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/elements/Dropdown.tsx b/src/components/views/elements/Dropdown.tsx index 1f3ceabb83..7ad27bc93b 100644 --- a/src/components/views/elements/Dropdown.tsx +++ b/src/components/views/elements/Dropdown.tsx @@ -269,7 +269,7 @@ export default class Dropdown extends React.Component { 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) { From 3fd2c005161c005827ea479c83a85a0f12442346 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 6 Aug 2021 14:46:02 +0100 Subject: [PATCH 08/22] Improve aria labels around spaces avatar uploader --- src/components/views/spaces/SpaceBasicSettings.tsx | 7 ++++++- src/i18n/strings/en_EN.json | 3 ++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/components/views/spaces/SpaceBasicSettings.tsx b/src/components/views/spaces/SpaceBasicSettings.tsx index 9d3696c5a9..4f305edd8b 100644 --- a/src/components/views/spaces/SpaceBasicSettings.tsx +++ b/src/components/views/spaces/SpaceBasicSettings.tsx @@ -65,6 +65,7 @@ export const SpaceAvatar = ({ }} kind="link" className="mx_SpaceBasicSettings_avatar_remove" + aria-label={_t("Delete avatar")} > { _t("Delete") } @@ -72,7 +73,11 @@ export const SpaceAvatar = ({ } else { avatarSection =
avatarUploadRef.current?.click()} /> - avatarUploadRef.current?.click()} kind="link"> + avatarUploadRef.current?.click()} + kind="link" + aria-label={_t("Upload avatar")} + > { _t("Upload") } ; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 09cb877d33..3aab3cb68c 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1014,7 +1014,9 @@ "Your server isn't responding to some requests.": "Your server isn't responding to some requests.", "Decline (%(counter)s)": "Decline (%(counter)s)", "Accept to continue:": "Accept to continue:", + "Delete avatar": "Delete avatar", "Delete": "Delete", + "Upload avatar": "Upload avatar", "Upload": "Upload", "Name": "Name", "Description": "Description", @@ -2718,7 +2720,6 @@ "Everyone": "Everyone", "Your community hasn't got a Long Description, a HTML page to show to community members.
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.
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", From 6fddfe0d59da12421d873312788a3349d44574d9 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 6 Aug 2021 14:48:46 +0100 Subject: [PATCH 09/22] Fix dropdown keyboard selection accessibility --- src/components/views/elements/Dropdown.tsx | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/components/views/elements/Dropdown.tsx b/src/components/views/elements/Dropdown.tsx index 7ad27bc93b..b4f382c9c3 100644 --- a/src/components/views/elements/Dropdown.tsx +++ b/src/components/views/elements/Dropdown.tsx @@ -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 { 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 { 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(); } }; @@ -328,6 +332,7 @@ export default class Dropdown extends React.Component { aria-owns={`${this.props.id}_listbox`} aria-disabled={this.props.disabled} aria-label={this.props.label} + onKeyDown={this.onKeyDown} /> ); } @@ -357,16 +362,17 @@ export default class Dropdown extends React.Component { // Note the menu sits inside the AccessibleButton div so it's anchored // to the input, but overflows below it. The root contains both. - return
+ return
{ currentValue } From 09f20bcda78eb797c8654b11ee6d14c5d1d3e837 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 9 Aug 2021 10:29:55 +0100 Subject: [PATCH 10/22] Make space hierarchy a treeview --- src/accessibility/RovingTabIndex.tsx | 52 ++- .../structures/SpaceRoomDirectory.tsx | 418 +++++++++++------- src/components/views/spaces/SpacePanel.tsx | 1 + .../views/spaces/SpaceTreeLevel.tsx | 2 +- 4 files changed, 289 insertions(+), 184 deletions(-) diff --git a/src/accessibility/RovingTabIndex.tsx b/src/accessibility/RovingTabIndex.tsx index 87f525bdfc..4e2279e09f 100644 --- a/src/accessibility/RovingTabIndex.tsx +++ b/src/accessibility/RovingTabIndex.tsx @@ -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 = ({ children, handleHomeEnd, onKeyDown }) => { +export const RovingTabIndexProvider: React.FC = ({ children, handleHomeEnd, handleUpDown, onKeyDown }) => { const [state, dispatch] = useReducer>(reducer, { activeRef: null, refs: [], @@ -167,21 +168,50 @@ export const RovingTabIndexProvider: React.FC = ({ 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 > 1) { + 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 = ({ children, handleHomeE } else if (onKeyDown) { return onKeyDown(ev, context.state); } - }, [context.state, onKeyDown, handleHomeEnd]); + }, [context.state, onKeyDown, handleHomeEnd, handleUpDown]); return { children({ onKeyDownHandler }) } diff --git a/src/components/structures/SpaceRoomDirectory.tsx b/src/components/structures/SpaceRoomDirectory.tsx index d8cc9593f0..b2d5e787f5 100644 --- a/src/components/structures/SpaceRoomDirectory.tsx +++ b/src/components/structures/SpaceRoomDirectory.tsx @@ -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 } 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 = ({ || (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 = ({ let button; if (joinedRoom) { - button = + button = { _t("View") } ; } else if (onJoinClick) { - button = + button = { _t("Join") } ; } @@ -106,13 +119,13 @@ const Tile: React.FC = ({ let checkbox; if (onToggleClick) { if (hasPermissions) { - checkbox = ; + checkbox = ; } else { checkbox = { ev.stopPropagation(); }} > - + ; } } @@ -185,19 +198,65 @@ const Tile: React.FC = ({ toggleShowChildren(); }} />; + if (showChildren) { - childSection =
+ const onChildrenKeyDown = (e) => { + if (e.key === Key.ARROW_LEFT) { + e.preventDefault(); + e.stopPropagation(); + ref.current?.focus(); + } + }; + + childSection =
{ children }
; } } + const onKeyDown = children ? (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) { + (ref.current?.nextElementSibling?.firstElementChild as HTMLElement)?.focus(); + } else { + toggleShowChildren(); + } + break; + } + + if (handled) { + e.preventDefault(); + e.stopPropagation(); + } + } : undefined; + return <> { content } { childToggle } @@ -414,176 +473,191 @@ export const SpaceHierarchy: React.FC = ({ return

{ _t("Your server does not support showing space hierarchies.") }

; } - 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> = AccessibleButton; - let props = {}; - if (!selectedRelations.length) { - Button = AccessibleTooltipButton; - props = { - tooltip: _t("Select a room below first"), - yOffset: -40, - }; - } - - manageButtons = <> - - - ; - } - - let results; - if (roomsMap.size) { - const hasPermissions = space?.currentState.maySendStateEvent(EventType.SpaceChild, cli.getUserId()); - - results = <> - { - 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 &&
} - ; - } else { - results =
-

{ _t("No results found") }

-
{ _t("You may want to try a different search or check for typos.") }
-
; - } - - content = <> -
- { countsStr } - - { additionalButtons } - { manageButtons } - -
- { error &&
- { error } -
} - - { results } - { children } - - ; - } else { - content = ; - } + }; // TODO loading state/error state - return <> - + return + { ({ 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> = AccessibleButton; + let props = {}; + if (!selectedRelations.length) { + Button = AccessibleTooltipButton; + props = { + tooltip: _t("Select a room below first"), + yOffset: -40, + }; + } + + manageButtons = <> + + + ; + } + + let results; + if (roomsMap.size) { + const hasPermissions = space?.currentState.maySendStateEvent(EventType.SpaceChild, cli.getUserId()); + + results = <> + { + 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 &&
} + ; + } else { + results =
+

{ _t("No results found") }

+
{ _t("You may want to try a different search or check for typos.") }
+
; + } + + content = <> +
+ { countsStr } + + { additionalButtons } + { manageButtons } + +
+ { error &&
+ { error } +
} + + { results } + { children } + + ; + } else { + content = ; + } + + return <> + + + { content } + ; + } } +
; }; interface IProps { diff --git a/src/components/views/spaces/SpacePanel.tsx b/src/components/views/spaces/SpacePanel.tsx index 58e1db4b1d..38d530bee5 100644 --- a/src/components/views/spaces/SpacePanel.tsx +++ b/src/components/views/spaces/SpacePanel.tsx @@ -272,6 +272,7 @@ const SpacePanel = () => {
    { (provided, snapshot) => ( diff --git a/src/components/views/spaces/SpaceTreeLevel.tsx b/src/components/views/spaces/SpaceTreeLevel.tsx index 5d2f58fab2..5b32722504 100644 --- a/src/components/views/spaces/SpaceTreeLevel.tsx +++ b/src/components/views/spaces/SpaceTreeLevel.tsx @@ -328,7 +328,7 @@ const SpaceTreeLevel: React.FC = ({ isNested, parents, }) => { - return
      + return
        { spaces.map(s => { return ( Date: Mon, 9 Aug 2021 14:01:34 +0100 Subject: [PATCH 11/22] iterate spaces treeview stuff --- res/css/structures/_SpaceRoomDirectory.scss | 4 ++ src/accessibility/RovingTabIndex.tsx | 2 +- .../structures/SpaceRoomDirectory.tsx | 72 ++++++++++--------- src/components/views/spaces/SpacePanel.tsx | 18 +++-- .../views/spaces/SpaceTreeLevel.tsx | 7 +- 5 files changed, 57 insertions(+), 46 deletions(-) diff --git a/res/css/structures/_SpaceRoomDirectory.scss b/res/css/structures/_SpaceRoomDirectory.scss index 03ff1d5ae5..88e6a3f494 100644 --- a/res/css/structures/_SpaceRoomDirectory.scss +++ b/res/css/structures/_SpaceRoomDirectory.scss @@ -278,6 +278,10 @@ limitations under the License. } } + li.mx_SpaceRoomDirectory_roomTileWrapper { + list-style: none; + } + .mx_SpaceRoomDirectory_roomTile, .mx_SpaceRoomDirectory_subspace_children { &::before { diff --git a/src/accessibility/RovingTabIndex.tsx b/src/accessibility/RovingTabIndex.tsx index 4e2279e09f..68e10049fd 100644 --- a/src/accessibility/RovingTabIndex.tsx +++ b/src/accessibility/RovingTabIndex.tsx @@ -196,7 +196,7 @@ export const RovingTabIndexProvider: React.FC = ({ children, handleHomeE handled = true; if (context.state.refs.length > 0) { const idx = context.state.refs.indexOf(context.state.activeRef); - if (idx > 1) { + if (idx > 0) { context.state.refs[idx - 1].current.focus(); } } diff --git a/src/components/structures/SpaceRoomDirectory.tsx b/src/components/structures/SpaceRoomDirectory.tsx index b2d5e787f5..bcd1a4252e 100644 --- a/src/components/structures/SpaceRoomDirectory.tsx +++ b/src/components/structures/SpaceRoomDirectory.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { ReactNode, KeyboardEvent, 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"; @@ -185,8 +185,9 @@ const Tile: React.FC = ({
; - 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 =
= ({ { children }
; } + + 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(".mx_SpaceRoomDirectory_roomTile")?.focus(); + } else { + toggleShowChildren(); + } + break; + } + + if (handled) { + e.preventDefault(); + e.stopPropagation(); + } + }; } - const onKeyDown = children ? (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) { - (ref.current?.nextElementSibling?.firstElementChild as HTMLElement)?.focus(); - } else { - toggleShowChildren(); - } - break; - } - - if (handled) { - e.preventDefault(); - e.stopPropagation(); - } - } : undefined; - - return <> + return
  • = ({ inputRef={ref} onFocus={onFocus} tabIndex={isActive ? 0 : -1} - aria-expanded={children ? showChildren : undefined} - role="treeitem" > { content } { childToggle } { childSection } - ; +
  • ; }; export const showRoom = (room: ISpaceSummaryRoom, viaServers?: string[], autoJoin = false) => { diff --git a/src/components/views/spaces/SpacePanel.tsx b/src/components/views/spaces/SpacePanel.tsx index 38d530bee5..4e3455e1fb 100644 --- a/src/components/views/spaces/SpacePanel.tsx +++ b/src/components/views/spaces/SpacePanel.tsx @@ -100,9 +100,12 @@ const HomeButton = ({ selected, isPanelCollapsed }: IHomeButtonProps) => { return SpaceStore.instance.allRoomsInHome; }); - return
  • + return
  • SpaceStore.instance.setActiveSpace(null)} @@ -142,9 +145,12 @@ const CreateSpaceButton = ({ openMenu(); }; - return
  • + return
  • = ({ onClick={onClick} onContextMenu={openMenu} forceHide={!isNarrow || menuDisplayed} - role="treeitem" inputRef={handle} > { children } @@ -290,7 +289,7 @@ export class SpaceItem extends React.PureComponent { /> : null; return ( -
  • +
  • { 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 } From 857bb9db44ee2c29428c8413a866791f88b3d0e2 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 10 Aug 2021 09:46:25 +0100 Subject: [PATCH 12/22] Add some treeview labels --- src/components/structures/SpaceRoomDirectory.tsx | 7 ++++++- src/components/views/spaces/SpacePanel.tsx | 1 + src/i18n/strings/en_EN.json | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/components/structures/SpaceRoomDirectory.tsx b/src/components/structures/SpaceRoomDirectory.tsx index bcd1a4252e..27b70c6841 100644 --- a/src/components/structures/SpaceRoomDirectory.tsx +++ b/src/components/structures/SpaceRoomDirectory.tsx @@ -639,7 +639,12 @@ export const SpaceHierarchy: React.FC = ({ { error &&
    { error }
    } - + { results } { children } diff --git a/src/components/views/spaces/SpacePanel.tsx b/src/components/views/spaces/SpacePanel.tsx index 4e3455e1fb..40016af36f 100644 --- a/src/components/views/spaces/SpacePanel.tsx +++ b/src/components/views/spaces/SpacePanel.tsx @@ -279,6 +279,7 @@ const SpacePanel = () => { className={classNames("mx_SpacePanel", { collapsed: isPanelCollapsed })} onKeyDown={onKeyDownHandler} role="tree" + aria-label={_t("Spaces")} > { (provided, snapshot) => ( diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 3aab3cb68c..6dec6b51ce 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2829,6 +2829,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 create a new room.": "If you can't find the room you're looking for, ask for an invite or create a new room.", "Create room": "Create room", @@ -3110,7 +3111,6 @@ "Page Down": "Page Down", "Esc": "Esc", "Enter": "Enter", - "Space": "Space", "End": "End", "[number]": "[number]" } From c872609ed315784571c85dd38d544dd41b1056b0 Mon Sep 17 00:00:00 2001 From: Dariusz Niemczyk Date: Tue, 10 Aug 2021 18:38:38 +0200 Subject: [PATCH 13/22] Modify encrypted images loading for better UX Adds proper react css transition to encrypted images placeholders, so that we can animate it's entrance and exit properly. In addition, adds simple css animations file to properly handle all of the animations in one place, so that it's much easier to properly handle prefers-reduced-motion media query. --- res/css/_animations.scss | 57 ++++++++++++++++ res/css/_common.scss | 1 + res/css/views/messages/_MImageBody.scss | 11 +++- src/components/views/messages/MImageBody.tsx | 68 +++++++++++++------- 4 files changed, 113 insertions(+), 24 deletions(-) create mode 100644 res/css/_animations.scss diff --git a/res/css/_animations.scss b/res/css/_animations.scss new file mode 100644 index 0000000000..2e6eac66a1 --- /dev/null +++ b/res/css/_animations.scss @@ -0,0 +1,57 @@ +/* +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; + &-active { + opacity: 1; + transition: opacity 300ms ease; + } + } + &-exit { + opacity: 1; + &-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; + } +} diff --git a/res/css/_common.scss b/res/css/_common.scss index 6b4e109b3a..fa925eba5b 100644 --- a/res/css/_common.scss +++ b/res/css/_common.scss @@ -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 diff --git a/res/css/views/messages/_MImageBody.scss b/res/css/views/messages/_MImageBody.scss index a748435cd8..765c74a36d 100644 --- a/res/css/views/messages/_MImageBody.scss +++ b/res/css/views/messages/_MImageBody.scss @@ -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; } } diff --git a/src/components/views/messages/MImageBody.tsx b/src/components/views/messages/MImageBody.tsx index 4bf6b6fe14..5cc94d2617 100644 --- a/src/components/views/messages/MImageBody.tsx +++ b/src/components/views/messages/MImageBody.tsx @@ -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 { // 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 { 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,39 @@ export default class MImageBody extends React.Component { gifLabel =

    GIF

    ; } + 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 = (
    - { showPlaceholder && -
    + - { placeholder } -
    - } +
    + { showPlaceholder &&
    + { placeholder } +
    } +
    + +
    { img } { gifLabel } @@ -413,7 +433,7 @@ export default class MImageBody extends React.Component { // Overidden by MStickerBody protected getPlaceholder(width: number, height: number): JSX.Element { const blurhash = this.props.mxEvent.getContent().info[BLURHASH_FIELD]; - if (blurhash) return ; + if (blurhash) return ; return ( ); @@ -455,10 +475,12 @@ export default class MImageBody extends React.Component { const thumbnail = this.messageContent(contentUrl, thumbUrl, content); const fileBody = this.getFileBody(); - return
    - { thumbnail } - { fileBody } -
    ; + return ( +
    + { thumbnail } + { fileBody } +
    + ); } } From ef3737d1797fec7ea6e9389d1b7afd726380043b Mon Sep 17 00:00:00 2001 From: Dariusz Niemczyk Date: Wed, 11 Aug 2021 10:52:22 +0200 Subject: [PATCH 14/22] Add full class names to animations.scss --- res/css/_animations.scss | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/res/css/_animations.scss b/res/css/_animations.scss index 2e6eac66a1..4d3ad97141 100644 --- a/res/css/_animations.scss +++ b/res/css/_animations.scss @@ -19,21 +19,19 @@ limitations under the License. * know they should not be used anywhere outside of React Transition Groups. */ -.mx_rtg--fade { - &-enter { - opacity: 0; - &-active { - opacity: 1; - transition: opacity 300ms ease; - } - } - &-exit { - opacity: 1; - &-active { - opacity: 0; - transition: opacity 300ms ease; - } - } +.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; } From 723400ace6431c44835f7b39abfd5c11f66f6002 Mon Sep 17 00:00:00 2001 From: Dariusz Niemczyk Date: Wed, 11 Aug 2021 10:56:36 +0200 Subject: [PATCH 15/22] Add a comment for weirdly placed div --- src/components/views/messages/MImageBody.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/views/messages/MImageBody.tsx b/src/components/views/messages/MImageBody.tsx index 5cc94d2617..e7b77b731f 100644 --- a/src/components/views/messages/MImageBody.tsx +++ b/src/components/views/messages/MImageBody.tsx @@ -393,6 +393,7 @@ export default class MImageBody extends React.Component { key={`img-${showPlaceholder}`} timeout={300} > + { /* This weirdly looking div is necessary here, otherwise SwitchTransition fails */ }
    { showPlaceholder &&
    Date: Wed, 11 Aug 2021 18:33:16 +0200 Subject: [PATCH 16/22] Make scrollbar dot transparent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/rooms/_EventTile.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 1c9d8e87d9..56cede0895 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -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; + } } } From cf8ee19e23e494e834b25c82ef7e3c2c3b83c13c Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 11 Aug 2021 19:25:17 +0100 Subject: [PATCH 17/22] Fix Netflify builds from fork PRs Some absolutely horrenous hacks to upload the context as an artifact then download it, unzip it and set the PR number as a variable we can use, because GitHub Actions just doesn't offer any other way of doing this. Maybe we'd be better off going back to Netlify... --- .github/workflows/layered-build.yaml | 12 ++++++++++++ .github/workflows/netflify.yaml | 25 +++++++++++++++++++++++-- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/.github/workflows/layered-build.yaml b/.github/workflows/layered-build.yaml index 7235b4020f..1474338a16 100644 --- a/.github/workflows/layered-build.yaml +++ b/.github/workflows/layered-build.yaml @@ -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}}/context.json', JSON.stringify(context)); + - name: Upload Context + uses: actions/upload-artifact@v2 + with: + name: context.json + path: context.json + # We'll only use this in a triggered job, then we're done with it + retention-days: 1 diff --git a/.github/workflows/netflify.yaml b/.github/workflows/netflify.yaml index ccccfe7e07..007488cae9 100644 --- a/.github/workflows/netflify.yaml +++ b/.github/workflows/netflify.yaml @@ -33,7 +33,28 @@ jobs: }); var fs = require('fs'); fs.writeFileSync('${{github.workspace}}/previewbuild.zip', Buffer.from(download.data)); - - run: unzip previewbuild.zip && rm previewbuild.zip + + var contextArtifact = artifacts.data.artifacts.filter((artifact) => { + return artifact.name == "context.json" + })[0]; + var download = await github.actions.downloadArtifact({ + owner: context.repo.owner, + repo: context.repo.repo, + artifact_id: contextArtifact.id, + archive_format: 'zip', + }); + var fs = require('fs'); + fs.writeFileSync('${{github.workspace}}/context.json.zip', Buffer.from(download.data)); + - name: Extract Artifacts + run: unzip -d webapp previewbuild.zip && rm previewbuild.zip && unzip context.json && rm context.json.zip + - name: 'Read Context' + id: readctx + uses: actions/github-script@v3.1.0 + with: + script: | + var fs = require('fs'); + var ctx = JSON.parse(fs.readFileSync('${{github.workspace}}/context.json')); + console.log(`::set-output name=prnumber::${ctx.payload.pull_request.number}`); - name: Deploy to Netlify id: netlify uses: nwtgck/actions-netlify@v1.2 @@ -51,7 +72,7 @@ jobs: uses: phulsechinmay/rewritable-pr-comment@v0.3.0 with: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - ISSUE_ID: ${{ github.event.workflow_run.pull_requests[0].number }} + ISSUE_ID: ${{ steps.readctx.outputs.prnumber }} 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. From bbdee0d83bf83288559568079c00532344ea5168 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 11 Aug 2021 19:41:37 +0100 Subject: [PATCH 18/22] publish the right directory --- .github/workflows/netflify.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/netflify.yaml b/.github/workflows/netflify.yaml index 007488cae9..444333fdfb 100644 --- a/.github/workflows/netflify.yaml +++ b/.github/workflows/netflify.yaml @@ -59,7 +59,7 @@ jobs: 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 From 1fe5ace8edfe57d82f988e1b088deb3cedb7414d Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 11 Aug 2021 21:10:22 +0100 Subject: [PATCH 19/22] Edit PR Description instead of commenting We could include the magic comments in the PR template so the various automated comments were always in the same order, if we wanted. --- .github/workflows/netflify.yaml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/netflify.yaml b/.github/workflows/netflify.yaml index 444333fdfb..d9ec1f842d 100644 --- a/.github/workflows/netflify.yaml +++ b/.github/workflows/netflify.yaml @@ -68,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: ${{ steps.readctx.outputs.prnumber }} - 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. From 8016b340b00d7a1ca264b82db2adf43f64bfe843 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 11 Aug 2021 21:20:28 +0100 Subject: [PATCH 20/22] Just upload the PR object itself We don't know what secret info might end up in the context --- .github/workflows/layered-build.yaml | 8 ++++---- .github/workflows/netflify.yaml | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/layered-build.yaml b/.github/workflows/layered-build.yaml index 1474338a16..c9d7e89a75 100644 --- a/.github/workflows/layered-build.yaml +++ b/.github/workflows/layered-build.yaml @@ -20,12 +20,12 @@ jobs: with: script: | var fs = require('fs'); - fs.writeFileSync('${{github.workspace}}/context.json', JSON.stringify(context)); - - name: Upload Context + fs.writeFileSync('${{github.workspace}}/pr.json', JSON.stringify(context.payload.pull_request)); + - name: Upload PR Info uses: actions/upload-artifact@v2 with: - name: context.json - path: context.json + name: pr.json + path: pr.json # We'll only use this in a triggered job, then we're done with it retention-days: 1 diff --git a/.github/workflows/netflify.yaml b/.github/workflows/netflify.yaml index 444333fdfb..3cb4543820 100644 --- a/.github/workflows/netflify.yaml +++ b/.github/workflows/netflify.yaml @@ -34,27 +34,27 @@ jobs: var fs = require('fs'); fs.writeFileSync('${{github.workspace}}/previewbuild.zip', Buffer.from(download.data)); - var contextArtifact = artifacts.data.artifacts.filter((artifact) => { - return artifact.name == "context.json" + 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: contextArtifact.id, + artifact_id: prInfoArtifact.id, archive_format: 'zip', }); var fs = require('fs'); - fs.writeFileSync('${{github.workspace}}/context.json.zip', Buffer.from(download.data)); + fs.writeFileSync('${{github.workspace}}/pr.json.zip', Buffer.from(download.data)); - name: Extract Artifacts - run: unzip -d webapp previewbuild.zip && rm previewbuild.zip && unzip context.json && rm context.json.zip - - name: 'Read Context' + 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 ctx = JSON.parse(fs.readFileSync('${{github.workspace}}/context.json')); - console.log(`::set-output name=prnumber::${ctx.payload.pull_request.number}`); + 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 From ccaa69b25b8b6916b9bb33284a1fb11e2bf73434 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 12 Aug 2021 08:53:42 +0200 Subject: [PATCH 21/22] Remove unnecessary code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This somehow doesn't seem to be neccessary anymore, I don't really know why but everything seems to work without it Signed-off-by: Šimon Brandner --- .../views/rooms/BasicMessageComposer.tsx | 24 ------------------- 1 file changed, 24 deletions(-) diff --git a/src/components/views/rooms/BasicMessageComposer.tsx b/src/components/views/rooms/BasicMessageComposer.tsx index c9b7cf60c3..3f98d5d5e4 100644 --- a/src/components/views/rooms/BasicMessageComposer.tsx +++ b/src/components/views/rooms/BasicMessageComposer.tsx @@ -541,7 +541,6 @@ export default class BasicMessageEditor extends React.Component 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 } }; - /** - * 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 { try { await new Promise(resolve => this.setState({ showVisualBell: false }, resolve)); From 9d3569a5778b708ecec146226de0fbfd74912527 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 12 Aug 2021 10:58:56 +0100 Subject: [PATCH 22/22] Iterate PR based on feedback --- res/css/structures/_GroupView.scss | 6 ++- .../_CreateSpaceFromCommunityDialog.scss | 4 ++ src/components/structures/GroupView.js | 38 +++++++++++++++++-- .../CreateSpaceFromCommunityDialog.tsx | 5 ++- src/i18n/strings/en_EN.json | 4 +- 5 files changed, 49 insertions(+), 8 deletions(-) diff --git a/res/css/structures/_GroupView.scss b/res/css/structures/_GroupView.scss index 21137d8a12..fb660f4194 100644 --- a/res/css/structures/_GroupView.scss +++ b/res/css/structures/_GroupView.scss @@ -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; diff --git a/res/css/views/dialogs/_CreateSpaceFromCommunityDialog.scss b/res/css/views/dialogs/_CreateSpaceFromCommunityDialog.scss index 306fc77011..afa722e05e 100644 --- a/res/css/views/dialogs/_CreateSpaceFromCommunityDialog.scss +++ b/res/css/views/dialogs/_CreateSpaceFromCommunityDialog.scss @@ -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 { diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index 26bb1b8ae7..f4f1d50d63 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -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( `

    HTML for your community's page

    @@ -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 here.", {}, { + a: sub => + { sub } + , + }); + } else { + text = _t("Ask the admins of this community to make it into a Space " + + "and keep a look out for the invite.", {}, { + a: sub => + { sub } + , + }); + } + communitiesUpgradeNotice =

    { _t("Communities can now be made into Spaces") }

    { _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.") }

    - +
    ; } diff --git a/src/components/views/dialogs/CreateSpaceFromCommunityDialog.tsx b/src/components/views/dialogs/CreateSpaceFromCommunityDialog.tsx index c9e13d2b61..4fb0994e23 100644 --- a/src/components/views/dialogs/CreateSpaceFromCommunityDialog.tsx +++ b/src/components/views/dialogs/CreateSpaceFromCommunityDialog.tsx @@ -286,8 +286,6 @@ const CreateSpaceFromCommunityDialog: React.FC = ({ matrixClient: cli, g >

    - { _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 = ({ matrixClient: cli, g ? _t("Open space for anyone, best for communities") : _t("Invite only, best for yourself or teams") }

    + { joinRule !== JoinRule.Public && +
    + }
    diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 3b5a748f89..987005a1b3 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2237,7 +2237,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.", @@ -2727,9 +2726,10 @@ "Community Settings": "Community Settings", "Want more than a community? Get your own server": "Want more than a community? Get your own server", "Changes made to your community name and avatar might not be seen by other users for up to 30 minutes.": "Changes made to your community name and avatar might not be seen by other users for up to 30 minutes.", + "You can create a Space from this community here.": "You can create a Space from this community here.", + "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 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:",