Merge branch 'develop' into revert-6752-revert-6682-gsouquet/compact-composer-18533

pull/21833/head
Germain Souquet 2021-09-09 13:30:35 +01:00
commit 07ebd85f7e
26 changed files with 302 additions and 130 deletions

24
.github/workflows/typecheck.yaml vendored Normal file
View File

@ -0,0 +1,24 @@
name: Type Check
on:
pull_request:
branches: [develop]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: c-hive/gha-yarn-cache@v2
- name: Install Deps
run: "./scripts/ci/install-deps.sh --ignore-scripts"
- name: Typecheck
run: "yarn run lint:types"
- name: Switch js-sdk to release mode
run: |
scripts/ci/js-sdk-to-release.js
cd node_modules/matrix-js-sdk
yarn install
yarn run build:compile
yarn run build:types
- name: Typecheck (release mode)
run: "yarn run lint:types"

View File

@ -151,7 +151,7 @@
"@typescript-eslint/eslint-plugin": "^4.17.0", "@typescript-eslint/eslint-plugin": "^4.17.0",
"@typescript-eslint/parser": "^4.17.0", "@typescript-eslint/parser": "^4.17.0",
"@wojtekmaj/enzyme-adapter-react-17": "^0.6.1", "@wojtekmaj/enzyme-adapter-react-17": "^0.6.1",
"allchange": "^1.0.2", "allchange": "^1.0.3",
"babel-jest": "^26.6.3", "babel-jest": "^26.6.3",
"chokidar": "^3.5.1", "chokidar": "^3.5.1",
"concurrently": "^5.3.0", "concurrently": "^5.3.0",

17
scripts/ci/js-sdk-to-release.js Executable file
View File

@ -0,0 +1,17 @@
#!/usr/bin/env node
const fsProm = require('fs/promises');
const PKGJSON = 'node_modules/matrix-js-sdk/package.json';
async function main() {
const pkgJson = JSON.parse(await fsProm.readFile(PKGJSON, 'utf8'));
for (const field of ['main', 'typings']) {
if (pkgJson["matrix_lib_"+field] !== undefined) {
pkgJson[field] = pkgJson["matrix_lib_"+field];
}
}
await fsProm.writeFile(PKGJSON, JSON.stringify(pkgJson, null, 2));
}
main();

View File

@ -1,21 +0,0 @@
#!/bin/sh
# This changes the js-sdk into 'release mode', that is:
# * The entry point for the library is the babel-compiled lib/index.js rather than src/index.ts
# * There's a 'typings' entry referencing the types output by tsc
# We do this so we can test that each PR still builds / type checks correctly when built
# against the released js-sdk, because if you do things like `import { User } from 'matrix-js-sdk';`
# rather than `import { User } from 'matrix-js-sdk/src/models/user';` it will work fine with the
# js-sdk in development mode but then break at release time.
# We can't use the last release of the js-sdk though: it might not be up to date enough.
cd node_modules/matrix-js-sdk
for i in main typings
do
lib_value=$(jq -r ".matrix_lib_$i" package.json)
if [ "$lib_value" != "null" ]; then
jq ".$i = .matrix_lib_$i" package.json > package.json.new && mv package.json.new package.json
fi
done
yarn run build:compile
yarn run build:types

View File

@ -14,7 +14,9 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk"; import { MatrixClient } from "matrix-js-sdk/src/client";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { Room } from "matrix-js-sdk/src/models/room";
import { Playback, PlaybackState } from "./Playback"; import { Playback, PlaybackState } from "./Playback";
import { UPDATE_EVENT } from "../stores/AsyncStore"; import { UPDATE_EVENT } from "../stores/AsyncStore";
import { MatrixClientPeg } from "../MatrixClientPeg"; import { MatrixClientPeg } from "../MatrixClientPeg";

View File

@ -105,8 +105,12 @@ export default class CallEventGrouper extends EventEmitter {
return ![...this.events].some((event) => event.sender?.userId === MatrixClientPeg.get().getUserId()); return ![...this.events].some((event) => event.sender?.userId === MatrixClientPeg.get().getUserId());
} }
private get callId(): string { private get callId(): string | undefined {
return [...this.events][0].getContent().call_id; return [...this.events][0]?.getContent()?.call_id;
}
private get roomId(): string | undefined {
return [...this.events][0]?.getRoomId();
} }
private onSilencedCallsChanged = () => { private onSilencedCallsChanged = () => {
@ -119,18 +123,24 @@ export default class CallEventGrouper extends EventEmitter {
}; };
public answerCall = () => { public answerCall = () => {
this.call?.answer(); defaultDispatcher.dispatch({
action: 'answer',
room_id: this.roomId,
});
}; };
public rejectCall = () => { public rejectCall = () => {
this.call?.reject(); defaultDispatcher.dispatch({
action: 'reject',
room_id: this.roomId,
});
}; };
public callBack = () => { public callBack = () => {
defaultDispatcher.dispatch({ defaultDispatcher.dispatch({
action: 'place_call', action: 'place_call',
type: this.isVoice ? CallType.Voice : CallType.Video, type: this.isVoice ? CallType.Voice : CallType.Video,
room_id: [...this.events][0]?.getRoomId(), room_id: this.roomId,
}); });
}; };

View File

@ -399,7 +399,9 @@ export default class LeftPanel extends React.Component<IProps, IState> {
mx_LeftPanel_exploreButton_space: !!this.state.activeSpace, mx_LeftPanel_exploreButton_space: !!this.state.activeSpace,
})} })}
onClick={this.onExplore} onClick={this.onExplore}
title={_t("Explore rooms")} title={this.state.activeSpace
? _t("Explore %(spaceName)s", { spaceName: this.state.activeSpace.name })
: _t("Explore rooms")}
/> />
</div> </div>
); );

View File

@ -271,7 +271,7 @@ export default class RightPanel extends React.Component<IProps, IState> {
case RightPanelPhases.EncryptionPanel: case RightPanelPhases.EncryptionPanel:
panel = <UserInfo panel = <UserInfo
user={this.state.member} user={this.state.member}
room={this.state.phase === RightPanelPhases.SpaceMemberInfo ? this.state.space : this.props.room} room={this.context.getRoom(this.state.member.roomId) ?? this.props.room}
key={roomId || this.state.member.userId} key={roomId || this.state.member.userId}
onClose={this.onClose} onClose={this.onClose}
phase={this.state.phase} phase={this.state.phase}

View File

@ -1848,6 +1848,19 @@ export default class RoomView extends React.Component<IProps, IState> {
/>; />;
} }
const statusBarAreaClass = classNames("mx_RoomView_statusArea", {
"mx_RoomView_statusArea_expanded": isStatusAreaExpanded,
});
// if statusBar does not exist then statusBarArea is blank and takes up unnecessary space on the screen
// show statusBarArea only if statusBar is present
const statusBarArea = statusBar && <div className={statusBarAreaClass}>
<div className="mx_RoomView_statusAreaBox">
<div className="mx_RoomView_statusAreaBox_line" />
{ statusBar }
</div>
</div>;
const roomVersionRecommendation = this.state.upgradeRecommendation; const roomVersionRecommendation = this.state.upgradeRecommendation;
const showRoomUpgradeBar = ( const showRoomUpgradeBar = (
roomVersionRecommendation && roomVersionRecommendation &&
@ -2045,10 +2058,6 @@ export default class RoomView extends React.Component<IProps, IState> {
/>); />);
} }
const statusBarAreaClass = classNames("mx_RoomView_statusArea", {
"mx_RoomView_statusArea_expanded": isStatusAreaExpanded,
});
const showRightPanel = this.state.room && this.state.showRightPanel; const showRightPanel = this.state.room && this.state.showRightPanel;
const rightPanel = showRightPanel const rightPanel = showRightPanel
? <RightPanel ? <RightPanel
@ -2098,12 +2107,7 @@ export default class RoomView extends React.Component<IProps, IState> {
{ messagePanel } { messagePanel }
{ searchResultsPanel } { searchResultsPanel }
</div> </div>
<div className={statusBarAreaClass}> { statusBarArea }
<div className="mx_RoomView_statusAreaBox">
<div className="mx_RoomView_statusAreaBox_line" />
{ statusBar }
</div>
</div>
{ previewBar } { previewBar }
{ messageComposer } { messageComposer }
</div> </div>

View File

@ -168,7 +168,7 @@ const SpaceContextMenu = ({ space, onFinished, ...props }: IProps) => {
defaultDispatcher.dispatch<SetRightPanelPhasePayload>({ defaultDispatcher.dispatch<SetRightPanelPhasePayload>({
action: Action.SetRightPanelPhase, action: Action.SetRightPanelPhase,
phase: RightPanelPhases.SpaceMemberList, phase: RightPanelPhases.SpaceMemberList,
refireParams: { space: space }, refireParams: { space },
}); });
onFinished(); onFinished();
}; };

View File

@ -79,7 +79,10 @@ export default class RoomSettingsDialog extends React.Component<IProps> {
ROOM_SECURITY_TAB, ROOM_SECURITY_TAB,
_td("Security & Privacy"), _td("Security & Privacy"),
"mx_RoomSettingsDialog_securityIcon", "mx_RoomSettingsDialog_securityIcon",
<SecurityRoomSettingsTab roomId={this.props.roomId} />, <SecurityRoomSettingsTab
roomId={this.props.roomId}
closeSettingsFn={() => this.props.onFinished(true)}
/>,
)); ));
tabs.push(new Tab( tabs.push(new Tab(
ROOM_ROLES_TAB, ROOM_ROLES_TAB,

View File

@ -16,26 +16,38 @@ limitations under the License.
import React, { forwardRef, useContext } from 'react'; import React, { forwardRef, useContext } from 'react';
import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { IRoomEncryption } from "matrix-js-sdk/src/crypto/RoomList";
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import { MatrixClientPeg } from '../../../MatrixClientPeg'; import { MatrixClientPeg } from '../../../MatrixClientPeg';
import EventTileBubble from "./EventTileBubble"; import EventTileBubble from "./EventTileBubble";
import MatrixClientContext from "../../../contexts/MatrixClientContext"; import MatrixClientContext from "../../../contexts/MatrixClientContext";
import DMRoomMap from "../../../utils/DMRoomMap"; import DMRoomMap from "../../../utils/DMRoomMap";
import { objectHasDiff } from "../../../utils/objects";
interface IProps { interface IProps {
mxEvent: MatrixEvent; mxEvent: MatrixEvent;
} }
const ALGORITHM = "m.megolm.v1.aes-sha2";
const EncryptionEvent = forwardRef<HTMLDivElement, IProps>(({ mxEvent }, ref) => { const EncryptionEvent = forwardRef<HTMLDivElement, IProps>(({ mxEvent }, ref) => {
const cli = useContext(MatrixClientContext); const cli = useContext(MatrixClientContext);
const roomId = mxEvent.getRoomId(); const roomId = mxEvent.getRoomId();
const isRoomEncrypted = MatrixClientPeg.get().isRoomEncrypted(roomId); const isRoomEncrypted = MatrixClientPeg.get().isRoomEncrypted(roomId);
if (mxEvent.getContent().algorithm === 'm.megolm.v1.aes-sha2' && isRoomEncrypted) { const prevContent = mxEvent.getPrevContent() as IRoomEncryption;
const content = mxEvent.getContent<IRoomEncryption>();
// if no change happened then skip rendering this, a shallow check is enough as all known fields are top-level.
if (!objectHasDiff(prevContent, content)) return null; // nop
if (content.algorithm === ALGORITHM && isRoomEncrypted) {
let subtitle: string; let subtitle: string;
const dmPartner = DMRoomMap.shared().getUserIdForRoomId(roomId); const dmPartner = DMRoomMap.shared().getUserIdForRoomId(roomId);
if (dmPartner) { if (prevContent.algorithm === ALGORITHM) {
subtitle = _t("Some encryption parameters have been changed.");
} else if (dmPartner) {
const displayName = cli?.getRoom(roomId)?.getMember(dmPartner)?.rawDisplayName || dmPartner; const displayName = cli?.getRoom(roomId)?.getMember(dmPartner)?.rawDisplayName || dmPartner;
subtitle = _t("Messages here are end-to-end encrypted. " + subtitle = _t("Messages here are end-to-end encrypted. " +
"Verify %(displayName)s in their profile - tap on their avatar.", { displayName }); "Verify %(displayName)s in their profile - tap on their avatar.", { displayName });
@ -49,7 +61,9 @@ const EncryptionEvent = forwardRef<HTMLDivElement, IProps>(({ mxEvent }, ref) =>
title={_t("Encryption enabled")} title={_t("Encryption enabled")}
subtitle={subtitle} subtitle={subtitle}
/>; />;
} else if (isRoomEncrypted) { }
if (isRoomEncrypted) {
return <EventTileBubble return <EventTileBubble
className="mx_cryptoEvent mx_cryptoEvent_icon" className="mx_cryptoEvent mx_cryptoEvent_icon"
title={_t("Encryption enabled")} title={_t("Encryption enabled")}

View File

@ -57,7 +57,7 @@ const EncryptionPanel: React.FC<IProps> = (props: IProps) => {
// state to show a spinner immediately after clicking "start verification", // state to show a spinner immediately after clicking "start verification",
// before we have a request // before we have a request
const [isRequesting, setRequesting] = useState(false); const [isRequesting, setRequesting] = useState(false);
const [phase, setPhase] = useState(request && request.phase); const [phase, setPhase] = useState(request?.phase);
useEffect(() => { useEffect(() => {
setRequest(verificationRequest); setRequest(verificationRequest);
if (verificationRequest) { if (verificationRequest) {

View File

@ -1278,7 +1278,9 @@ const BasicUserInfo: React.FC<{
// hide the Roles section for DMs as it doesn't make sense there // hide the Roles section for DMs as it doesn't make sense there
if (!DMRoomMap.shared().getUserIdForRoomId((member as RoomMember).roomId)) { if (!DMRoomMap.shared().getUserIdForRoomId((member as RoomMember).roomId)) {
memberDetails = <div className="mx_UserInfo_container"> memberDetails = <div className="mx_UserInfo_container">
<h3>{ _t("Role") }</h3> <h3>{ _t("Role in <RoomName/>", {}, {
RoomName: () => <b>{ room.name }</b>,
}) }</h3>
<PowerLevelSection <PowerLevelSection
powerLevels={powerLevels} powerLevels={powerLevels}
user={member as RoomMember} user={member as RoomMember}
@ -1573,11 +1575,12 @@ const UserInfo: React.FC<IProps> = ({
// We have no previousPhase for when viewing a UserInfo from a Group or without a Room at this time // We have no previousPhase for when viewing a UserInfo from a Group or without a Room at this time
if (room && phase === RightPanelPhases.EncryptionPanel) { if (room && phase === RightPanelPhases.EncryptionPanel) {
previousPhase = RightPanelPhases.RoomMemberInfo; previousPhase = RightPanelPhases.RoomMemberInfo;
refireParams = { member: member }; refireParams = { member };
} else if (room?.isSpaceRoom() && SpaceStore.spacesEnabled) {
previousPhase = previousPhase = RightPanelPhases.SpaceMemberList;
refireParams = { space: room };
} else if (room) { } else if (room) {
previousPhase = previousPhase = SpaceStore.spacesEnabled && room.isSpaceRoom() previousPhase = RightPanelPhases.RoomMemberList;
? RightPanelPhases.SpaceMemberList
: RightPanelPhases.RoomMemberList;
} }
const onEncryptionPanelClose = () => { const onEncryptionPanelClose = () => {

View File

@ -29,43 +29,27 @@ import VerificationQRCode from "../elements/crypto/VerificationQRCode";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import SdkConfig from "../../../SdkConfig"; import SdkConfig from "../../../SdkConfig";
import E2EIcon from "../rooms/E2EIcon"; import E2EIcon from "../rooms/E2EIcon";
import { import { Phase } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
PHASE_READY,
PHASE_DONE,
PHASE_STARTED,
PHASE_CANCELLED,
} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
import Spinner from "../elements/Spinner"; import Spinner from "../elements/Spinner";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import AccessibleButton from "../elements/AccessibleButton"; import AccessibleButton from "../elements/AccessibleButton";
import VerificationShowSas from "../verification/VerificationShowSas"; import VerificationShowSas from "../verification/VerificationShowSas";
// XXX: Should be defined in matrix-js-sdk
enum VerificationPhase {
PHASE_UNSENT,
PHASE_REQUESTED,
PHASE_READY,
PHASE_DONE,
PHASE_STARTED,
PHASE_CANCELLED,
}
interface IProps { interface IProps {
layout: string; layout: string;
request: VerificationRequest; request: VerificationRequest;
member: RoomMember | User; member: RoomMember | User;
phase: VerificationPhase; phase: Phase;
onClose: () => void; onClose: () => void;
isRoomEncrypted: boolean; isRoomEncrypted: boolean;
inDialog: boolean; inDialog: boolean;
key: number;
} }
interface IState { interface IState {
sasEvent?: SAS; sasEvent?: SAS["sasEvent"];
emojiButtonClicked?: boolean; emojiButtonClicked?: boolean;
reciprocateButtonClicked?: boolean; reciprocateButtonClicked?: boolean;
reciprocateQREvent?: ReciprocateQRCode; reciprocateQREvent?: ReciprocateQRCode["reciprocateQREvent"];
} }
@replaceableComponent("views.right_panel.VerificationPanel") @replaceableComponent("views.right_panel.VerificationPanel")
@ -321,9 +305,9 @@ export default class VerificationPanel extends React.PureComponent<IProps, IStat
const displayName = (member as User).displayName || (member as RoomMember).name || member.userId; const displayName = (member as User).displayName || (member as RoomMember).name || member.userId;
switch (phase) { switch (phase) {
case PHASE_READY: case Phase.Ready:
return this.renderQRPhase(); return this.renderQRPhase();
case PHASE_STARTED: case Phase.Started:
switch (request.chosenMethod) { switch (request.chosenMethod) {
case verificationMethods.RECIPROCATE_QR_CODE: case verificationMethods.RECIPROCATE_QR_CODE:
return this.renderQRReciprocatePhase(); return this.renderQRReciprocatePhase();
@ -346,9 +330,9 @@ export default class VerificationPanel extends React.PureComponent<IProps, IStat
default: default:
return null; return null;
} }
case PHASE_DONE: case Phase.Done:
return this.renderVerifiedPhase(); return this.renderVerifiedPhase();
case PHASE_CANCELLED: case Phase.Cancelled:
return this.renderCancelledPhase(); return this.renderCancelledPhase();
} }
console.error("VerificationPanel unhandled phase:", phase); console.error("VerificationPanel unhandled phase:", phase);
@ -375,7 +359,8 @@ export default class VerificationPanel extends React.PureComponent<IProps, IStat
private updateVerifierState = () => { private updateVerifierState = () => {
const { request } = this.props; const { request } = this.props;
const { sasEvent, reciprocateQREvent } = request.verifier; const sasEvent = (request.verifier as SAS).sasEvent;
const reciprocateQREvent = (request.verifier as ReciprocateQRCode).reciprocateQREvent;
request.verifier.off('show_sas', this.updateVerifierState); request.verifier.off('show_sas', this.updateVerifierState);
request.verifier.off('show_reciprocate_qr', this.updateVerifierState); request.verifier.off('show_reciprocate_qr', this.updateVerifierState);
this.setState({ sasEvent, reciprocateQREvent }); this.setState({ sasEvent, reciprocateQREvent });
@ -402,7 +387,8 @@ export default class VerificationPanel extends React.PureComponent<IProps, IStat
const { request } = this.props; const { request } = this.props;
request.on("change", this.onRequestChange); request.on("change", this.onRequestChange);
if (request.verifier) { if (request.verifier) {
const { sasEvent, reciprocateQREvent } = request.verifier; const sasEvent = (request.verifier as SAS).sasEvent;
const reciprocateQREvent = (request.verifier as ReciprocateQRCode).reciprocateQREvent;
this.setState({ sasEvent, reciprocateQREvent }); this.setState({ sasEvent, reciprocateQREvent });
} }
this.onRequestChange(); this.onRequestChange();

View File

@ -48,6 +48,7 @@ import SpaceStore, { ISuggestedRoom, SUGGESTED_ROOMS } from "../../../stores/Spa
import { showAddExistingRooms, showCreateNewRoom, showSpaceInvite } from "../../../utils/space"; import { showAddExistingRooms, showCreateNewRoom, showSpaceInvite } from "../../../utils/space";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import RoomAvatar from "../avatars/RoomAvatar"; import RoomAvatar from "../avatars/RoomAvatar";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
interface IProps { interface IProps {
onKeyDown: (ev: React.KeyboardEvent) => void; onKeyDown: (ev: React.KeyboardEvent) => void;
@ -522,20 +523,23 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
} else if ( } else if (
this.props.activeSpace?.canInvite(userId) || this.props.activeSpace?.getMyMembership() === "join" this.props.activeSpace?.canInvite(userId) || this.props.activeSpace?.getMyMembership() === "join"
) { ) {
const spaceName = this.props.activeSpace.name;
explorePrompt = <div className="mx_RoomList_explorePrompt"> explorePrompt = <div className="mx_RoomList_explorePrompt">
<div>{ _t("Quick actions") }</div> <div>{ _t("Quick actions") }</div>
{ this.props.activeSpace.canInvite(userId) && <AccessibleButton { this.props.activeSpace.canInvite(userId) && <AccessibleTooltipButton
className="mx_RoomList_explorePrompt_spaceInvite" className="mx_RoomList_explorePrompt_spaceInvite"
onClick={this.onSpaceInviteClick} onClick={this.onSpaceInviteClick}
title={_t("Invite to %(spaceName)s", { spaceName })}
> >
{ _t("Invite people") } { _t("Invite people") }
</AccessibleButton> } </AccessibleTooltipButton> }
{ this.props.activeSpace.getMyMembership() === "join" && <AccessibleButton { this.props.activeSpace.getMyMembership() === "join" && <AccessibleTooltipButton
className="mx_RoomList_explorePrompt_spaceExplore" className="mx_RoomList_explorePrompt_spaceExplore"
onClick={this.onExplore} onClick={this.onExplore}
title={_t("Explore %(spaceName)s", { spaceName })}
> >
{ _t("Explore rooms") } { _t("Explore rooms") }
</AccessibleButton> } </AccessibleTooltipButton> }
</div>; </div>;
} else if (Object.values(this.state.sublists).some(list => list.length > 0)) { } else if (Object.values(this.state.sublists).some(list => list.length > 0)) {
const unfilteredLists = RoomListStore.instance.unfilteredLists; const unfilteredLists = RoomListStore.instance.unfilteredLists;

View File

@ -39,9 +39,12 @@ import { arrayHasDiff } from "../../../../../utils/arrays";
import SettingsFlag from '../../../elements/SettingsFlag'; import SettingsFlag from '../../../elements/SettingsFlag';
import createRoom, { IOpts } from '../../../../../createRoom'; import createRoom, { IOpts } from '../../../../../createRoom';
import CreateRoomDialog from '../../../dialogs/CreateRoomDialog'; import CreateRoomDialog from '../../../dialogs/CreateRoomDialog';
import dis from "../../../../../dispatcher/dispatcher";
import { ROOM_SECURITY_TAB } from "../../../dialogs/RoomSettingsDialog";
interface IProps { interface IProps {
roomId: string; roomId: string;
closeSettingsFn: () => void;
} }
interface IState { interface IState {
@ -220,9 +223,20 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
targetVersion, targetVersion,
description: _t("This upgrade will allow members of selected spaces " + description: _t("This upgrade will allow members of selected spaces " +
"access to this room without an invite."), "access to this room without an invite."),
onFinished: (resp) => { onFinished: async (resp) => {
if (!resp?.continue) return; if (!resp?.continue) return;
upgradeRoom(room, targetVersion, resp.invite); const roomId = await upgradeRoom(room, targetVersion, resp.invite, true, true, true);
this.props.closeSettingsFn();
// switch to the new room in the background
dis.dispatch({
action: "view_room",
room_id: roomId,
});
// open new settings on this tab
dis.dispatch({
action: "open_room_settings",
initial_tab_id: ROOM_SECURITY_TAB,
});
}, },
}); });
return; return;

View File

@ -195,12 +195,10 @@ const InnerSpacePanel = React.memo<IInnerSpacePanelProps>(({ children, isPanelCo
{ (provided, snapshot) => ( { (provided, snapshot) => (
<SpaceItem <SpaceItem
{...provided.draggableProps} {...provided.draggableProps}
{...provided.dragHandleProps} dragHandleProps={provided.dragHandleProps}
key={s.roomId} key={s.roomId}
innerRef={provided.innerRef} innerRef={provided.innerRef}
className={snapshot.isDragging className={snapshot.isDragging ? "mx_SpaceItem_dragging" : undefined}
? "mx_SpaceItem_dragging"
: undefined}
space={s} space={s}
activeSpaces={activeSpaces} activeSpaces={activeSpaces}
isPanelCollapsed={isPanelCollapsed} isPanelCollapsed={isPanelCollapsed}
@ -223,6 +221,8 @@ const SpacePanel = () => {
}, []); }, []);
const onKeyDown = (ev: React.KeyboardEvent) => { const onKeyDown = (ev: React.KeyboardEvent) => {
if (ev.defaultPrevented) return;
let handled = true; let handled = true;
switch (ev.key) { switch (ev.key) {

View File

@ -29,7 +29,6 @@ import RoomAvatar from "../avatars/RoomAvatar";
import SpaceStore from "../../../stores/SpaceStore"; import SpaceStore from "../../../stores/SpaceStore";
import SpaceTreeLevelLayoutStore from "../../../stores/SpaceTreeLevelLayoutStore"; import SpaceTreeLevelLayoutStore from "../../../stores/SpaceTreeLevelLayoutStore";
import NotificationBadge from "../rooms/NotificationBadge"; import NotificationBadge from "../rooms/NotificationBadge";
import { RovingAccessibleTooltipButton } from "../../../accessibility/roving/RovingAccessibleTooltipButton";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import { ContextMenuTooltipButton } from "../../../accessibility/context_menu/ContextMenuTooltipButton"; import { ContextMenuTooltipButton } from "../../../accessibility/context_menu/ContextMenuTooltipButton";
import { toRightOf, useContextMenu } from "../../structures/ContextMenu"; import { toRightOf, useContextMenu } from "../../structures/ContextMenu";
@ -40,8 +39,11 @@ import { NotificationColor } from "../../../stores/notifications/NotificationCol
import { getKeyBindingsManager, RoomListAction } from "../../../KeyBindingsManager"; import { getKeyBindingsManager, RoomListAction } from "../../../KeyBindingsManager";
import { NotificationState } from "../../../stores/notifications/NotificationState"; import { NotificationState } from "../../../stores/notifications/NotificationState";
import SpaceContextMenu from "../context_menus/SpaceContextMenu"; import SpaceContextMenu from "../context_menus/SpaceContextMenu";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import { DraggableProvidedDragHandleProps } from "react-beautiful-dnd";
import { useRovingTabIndex } from "../../../accessibility/RovingTabIndex";
interface IButtonProps extends Omit<ComponentProps<typeof RovingAccessibleTooltipButton>, "title"> { interface IButtonProps extends Omit<ComponentProps<typeof AccessibleTooltipButton>, "title"> {
space?: Room; space?: Room;
className?: string; className?: string;
selected?: boolean; selected?: boolean;
@ -68,7 +70,9 @@ export const SpaceButton: React.FC<IButtonProps> = ({
ContextMenuComponent, ContextMenuComponent,
...props ...props
}) => { }) => {
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu<HTMLElement>(); const [menuDisplayed, ref, openMenu, closeMenu] = useContextMenu<HTMLElement>();
const [onFocus, isActive, handle] = useRovingTabIndex(ref);
const tabIndex = isActive ? 0 : -1;
let avatar = <div className="mx_SpaceButton_avatarPlaceholder"><div className="mx_SpaceButton_icon" /></div>; let avatar = <div className="mx_SpaceButton_avatarPlaceholder"><div className="mx_SpaceButton_icon" /></div>;
if (space) { if (space) {
@ -88,6 +92,7 @@ export const SpaceButton: React.FC<IButtonProps> = ({
forceCount={false} forceCount={false}
notification={notificationState} notification={notificationState}
aria-label={ariaLabel} aria-label={ariaLabel}
tabIndex={tabIndex}
/> />
</div>; </div>;
} }
@ -102,7 +107,7 @@ export const SpaceButton: React.FC<IButtonProps> = ({
} }
return ( return (
<RovingAccessibleTooltipButton <AccessibleTooltipButton
{...props} {...props}
className={classNames("mx_SpaceButton", className, { className={classNames("mx_SpaceButton", className, {
mx_SpaceButton_active: selected, mx_SpaceButton_active: selected,
@ -114,6 +119,8 @@ export const SpaceButton: React.FC<IButtonProps> = ({
onContextMenu={openMenu} onContextMenu={openMenu}
forceHide={!isNarrow || menuDisplayed} forceHide={!isNarrow || menuDisplayed}
inputRef={handle} inputRef={handle}
tabIndex={tabIndex}
onFocus={onFocus}
> >
{ children } { children }
<div className="mx_SpaceButton_selectionWrapper"> <div className="mx_SpaceButton_selectionWrapper">
@ -130,7 +137,7 @@ export const SpaceButton: React.FC<IButtonProps> = ({
{ contextMenu } { contextMenu }
</div> </div>
</RovingAccessibleTooltipButton> </AccessibleTooltipButton>
); );
}; };
@ -142,6 +149,7 @@ interface IItemProps extends InputHTMLAttributes<HTMLLIElement> {
onExpand?: Function; onExpand?: Function;
parents?: Set<string>; parents?: Set<string>;
innerRef?: LegacyRef<HTMLLIElement>; innerRef?: LegacyRef<HTMLLIElement>;
dragHandleProps?: DraggableProvidedDragHandleProps;
} }
interface IItemState { interface IItemState {
@ -270,8 +278,10 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
? StaticNotificationState.forSymbol("!", NotificationColor.Red) ? StaticNotificationState.forSymbol("!", NotificationColor.Red)
: SpaceStore.instance.getNotificationState(space.roomId); : SpaceStore.instance.getNotificationState(space.roomId);
const hasChildren = this.state.childSpaces?.length;
let childItems; let childItems;
if (this.state.childSpaces?.length && !collapsed) { if (hasChildren && !collapsed) {
childItems = <SpaceTreeLevel childItems = <SpaceTreeLevel
spaces={this.state.childSpaces} spaces={this.state.childSpaces}
activeSpaces={activeSpaces} activeSpaces={activeSpaces}
@ -280,7 +290,7 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
/>; />;
} }
const toggleCollapseButton = this.state.childSpaces?.length ? const toggleCollapseButton = hasChildren ?
<AccessibleButton <AccessibleButton
className="mx_SpaceButton_toggleCollapse" className="mx_SpaceButton_toggleCollapse"
onClick={this.toggleCollapse} onClick={this.toggleCollapse}
@ -288,9 +298,19 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
aria-label={collapsed ? _t("Expand") : _t("Collapse")} aria-label={collapsed ? _t("Expand") : _t("Collapse")}
/> : null; /> : null;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { tabIndex, ...dragHandleProps } = this.props.dragHandleProps || {};
return ( return (
<li {...otherProps} className={itemClasses} ref={innerRef} aria-expanded={!collapsed} role="treeitem"> <li
{...otherProps}
className={itemClasses}
ref={innerRef}
aria-expanded={hasChildren ? !collapsed : undefined}
role="treeitem"
>
<SpaceButton <SpaceButton
{...dragHandleProps}
space={space} space={space}
className={isInvite ? "mx_SpaceButton_invite" : undefined} className={isInvite ? "mx_SpaceButton_invite" : undefined}
selected={activeSpaces.includes(space)} selected={activeSpaces.includes(space)}

View File

@ -15,7 +15,7 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import { SAS } from "matrix-js-sdk/src/crypto/verification/SAS"; import { IGeneratedSas } from "matrix-js-sdk/src/crypto/verification/SAS";
import { DeviceInfo } from "matrix-js-sdk/src//crypto/deviceinfo"; import { DeviceInfo } from "matrix-js-sdk/src//crypto/deviceinfo";
import { _t, _td } from '../../../languageHandler'; import { _t, _td } from '../../../languageHandler';
import { PendingActionSpinner } from "../right_panel/EncryptionInfo"; import { PendingActionSpinner } from "../right_panel/EncryptionInfo";
@ -30,7 +30,7 @@ interface IProps {
device?: DeviceInfo; device?: DeviceInfo;
onDone: () => void; onDone: () => void;
onCancel: () => void; onCancel: () => void;
sas: SAS.sas; sas: IGeneratedSas;
isSelf?: boolean; isSelf?: boolean;
inDialog?: boolean; // whether this component is being shown in a dialog and to use DialogButtons inDialog?: boolean; // whether this component is being shown in a dialog and to use DialogButtons
} }

View File

@ -1650,6 +1650,7 @@
"Start a new chat": "Start a new chat", "Start a new chat": "Start a new chat",
"Explore all public rooms": "Explore all public rooms", "Explore all public rooms": "Explore all public rooms",
"Quick actions": "Quick actions", "Quick actions": "Quick actions",
"Explore %(spaceName)s": "Explore %(spaceName)s",
"Use the + to make a new room or explore existing ones below": "Use the + to make a new room or explore existing ones below", "Use the + to make a new room or explore existing ones below": "Use the + to make a new room or explore existing ones below",
"%(count)s results in all spaces|other": "%(count)s results in all spaces", "%(count)s results in all spaces|other": "%(count)s results in all spaces",
"%(count)s results in all spaces|one": "%(count)s result in all spaces", "%(count)s results in all spaces|one": "%(count)s result in all spaces",
@ -1871,7 +1872,7 @@
"Deactivating this user will log them out and prevent them from logging back in. Additionally, they will leave all the rooms they are in. This action cannot be reversed. Are you sure you want to deactivate this user?": "Deactivating this user will log them out and prevent them from logging back in. Additionally, they will leave all the rooms they are in. This action cannot be reversed. Are you sure you want to deactivate this user?", "Deactivating this user will log them out and prevent them from logging back in. Additionally, they will leave all the rooms they are in. This action cannot be reversed. Are you sure you want to deactivate this user?": "Deactivating this user will log them out and prevent them from logging back in. Additionally, they will leave all the rooms they are in. This action cannot be reversed. Are you sure you want to deactivate this user?",
"Deactivate user": "Deactivate user", "Deactivate user": "Deactivate user",
"Failed to deactivate user": "Failed to deactivate user", "Failed to deactivate user": "Failed to deactivate user",
"Role": "Role", "Role in <RoomName/>": "Role in <RoomName/>",
"This client does not support end-to-end encryption.": "This client does not support end-to-end encryption.", "This client does not support end-to-end encryption.": "This client does not support end-to-end encryption.",
"Edit devices": "Edit devices", "Edit devices": "Edit devices",
"Security": "Security", "Security": "Security",
@ -1927,6 +1928,7 @@
"Decrypting": "Decrypting", "Decrypting": "Decrypting",
"Download": "Download", "Download": "Download",
"View Source": "View Source", "View Source": "View Source",
"Some encryption parameters have been changed.": "Some encryption parameters have been changed.",
"Messages here are end-to-end encrypted. Verify %(displayName)s in their profile - tap on their avatar.": "Messages here are end-to-end encrypted. Verify %(displayName)s in their profile - tap on their avatar.", "Messages here are end-to-end encrypted. Verify %(displayName)s in their profile - tap on their avatar.": "Messages here are end-to-end encrypted. Verify %(displayName)s in their profile - tap on their avatar.",
"Messages in this room are end-to-end encrypted. When people join, you can verify them in their profile, just tap on their avatar.": "Messages in this room are end-to-end encrypted. When people join, you can verify them in their profile, just tap on their avatar.", "Messages in this room are end-to-end encrypted. When people join, you can verify them in their profile, just tap on their avatar.": "Messages in this room are end-to-end encrypted. When people join, you can verify them in their profile, just tap on their avatar.",
"Encryption enabled": "Encryption enabled", "Encryption enabled": "Encryption enabled",

View File

@ -257,7 +257,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
"go to that room's Security & Privacy settings.") }</p> "go to that room's Security & Privacy settings.") }</p>
{ /* Reuses classes from TabbedView for simplicity, non-interactive */ } { /* Reuses classes from TabbedView for simplicity, non-interactive */ }
<div style={{ width: "190px" }}> <div className="mx_TabbedView_tabsOnLeft" style={{ width: "190px", position: "relative" }}>
<div className="mx_TabbedView_tabLabel"> <div className="mx_TabbedView_tabLabel">
<span className="mx_TabbedView_maskedIcon mx_RoomSettingsDialog_settingsIcon" /> <span className="mx_TabbedView_maskedIcon mx_RoomSettingsDialog_settingsIcon" />
<span className="mx_TabbedView_tabLabel_text">{ _t("General") }</span> <span className="mx_TabbedView_tabLabel_text">{ _t("General") }</span>
@ -366,16 +366,22 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
} }
public getParents(roomId: string, canonicalOnly = false): Room[] { public getParents(roomId: string, canonicalOnly = false): Room[] {
const userId = this.matrixClient?.getUserId();
const room = this.matrixClient?.getRoom(roomId); const room = this.matrixClient?.getRoom(roomId);
return room?.currentState.getStateEvents(EventType.SpaceParent) return room?.currentState.getStateEvents(EventType.SpaceParent)
.filter(ev => { .map(ev => {
const content = ev.getContent(); const content = ev.getContent();
if (!content?.via?.length) return false; if (Array.isArray(content?.via) && (!canonicalOnly || content?.canonical)) {
// TODO apply permissions check to verify that the parent mapping is valid const parent = this.matrixClient.getRoom(ev.getStateKey());
if (canonicalOnly && !content?.canonical) return false; // only respect the relationship if the sender has sufficient permissions in the parent to set
return true; // child relations, as per MSC1772.
// https://github.com/matrix-org/matrix-doc/blob/main/proposals/1772-groups-as-rooms.md#relationship-between-rooms-and-spaces
if (parent?.currentState.maySendStateEvent(EventType.SpaceChild, userId)) {
return parent;
}
}
// else implicit undefined which causes this element to be filtered out
}) })
.map(ev => this.matrixClient.getRoom(ev.getStateKey()))
.filter(Boolean) || []; .filter(Boolean) || [];
} }
@ -530,6 +536,14 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
}); });
} }
const hiddenChildren = new EnhancedMap<string, Set<string>>();
visibleRooms.forEach(room => {
if (room.getMyMembership() !== "join") return;
this.getParents(room.roomId).forEach(parent => {
hiddenChildren.getOrCreate(parent.roomId, new Set()).add(room.roomId);
});
});
this.rootSpaces.forEach(s => { this.rootSpaces.forEach(s => {
// traverse each space tree in DFS to build up the supersets as you go up, // traverse each space tree in DFS to build up the supersets as you go up,
// reusing results from like subtrees. // reusing results from like subtrees.
@ -559,6 +573,9 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
roomIds.add(roomId); roomIds.add(roomId);
}); });
}); });
hiddenChildren.get(spaceId)?.forEach(roomId => {
roomIds.add(roomId);
});
this.spaceFilteredRooms.set(spaceId, roomIds); this.spaceFilteredRooms.set(spaceId, roomIds);
return roomIds; return roomIds;
}; };
@ -690,6 +707,12 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
} }
this.emit(room.roomId); this.emit(room.roomId);
break; break;
case EventType.RoomPowerLevels:
if (room.isSpaceRoom()) {
this.onRoomsUpdate();
}
break;
} }
}; };

View File

@ -44,7 +44,7 @@ import { containsEmoji } from "../../effects/utils";
import dis from "../../dispatcher/dispatcher"; import dis from "../../dispatcher/dispatcher";
import { tryTransformPermalinkToLocalHref } from "../../utils/permalinks/Permalinks"; import { tryTransformPermalinkToLocalHref } from "../../utils/permalinks/Permalinks";
import { IEvent, MatrixEvent } from "matrix-js-sdk/src/models/event"; import { IEvent, MatrixEvent } from "matrix-js-sdk/src/models/event";
import { Room } from "matrix-js-sdk"; import { Room } from "matrix-js-sdk/src/models/room";
// TODO: Purge this from the universe // TODO: Purge this from the universe

View File

@ -22,6 +22,7 @@ import Modal from "../Modal";
import { _t } from "../languageHandler"; import { _t } from "../languageHandler";
import ErrorDialog from "../components/views/dialogs/ErrorDialog"; import ErrorDialog from "../components/views/dialogs/ErrorDialog";
import SpaceStore from "../stores/SpaceStore"; import SpaceStore from "../stores/SpaceStore";
import Spinner from "../components/views/elements/Spinner";
export async function upgradeRoom( export async function upgradeRoom(
room: Room, room: Room,
@ -29,8 +30,10 @@ export async function upgradeRoom(
inviteUsers = false, inviteUsers = false,
handleError = true, handleError = true,
updateSpaces = true, updateSpaces = true,
awaitRoom = false,
): Promise<string> { ): Promise<string> {
const cli = room.client; const cli = room.client;
const spinnerModal = Modal.createDialog(Spinner, null, "mx_Dialog_spinner");
let newRoomId: string; let newRoomId: string;
try { try {
@ -46,27 +49,36 @@ export async function upgradeRoom(
throw e; throw e;
} }
// We have to wait for the js-sdk to give us the room back so if (awaitRoom || inviteUsers) {
// we can more effectively abuse the MultiInviter behaviour await new Promise<void>(resolve => {
// which heavily relies on the Room object being available. // already have the room
if (inviteUsers) { if (room.client.getRoom(newRoomId)) {
const checkForUpgradeFn = async (newRoom: Room): Promise<void> => { resolve();
// The upgradePromise should be done by the time we await it here. return;
if (newRoom.roomId !== newRoomId) return;
const toInvite = [
...room.getMembersWithMembership("join"),
...room.getMembersWithMembership("invite"),
].map(m => m.userId).filter(m => m !== cli.getUserId());
if (toInvite.length > 0) {
// Errors are handled internally to this function
await inviteUsersToRoom(newRoomId, toInvite);
} }
cli.removeListener('Room', checkForUpgradeFn); // We have to wait for the js-sdk to give us the room back so
}; // we can more effectively abuse the MultiInviter behaviour
cli.on('Room', checkForUpgradeFn); // which heavily relies on the Room object being available.
const checkForRoomFn = (newRoom: Room) => {
if (newRoom.roomId !== newRoomId) return;
resolve();
cli.off("Room", checkForRoomFn);
};
cli.on("Room", checkForRoomFn);
});
}
if (inviteUsers) {
const toInvite = [
...room.getMembersWithMembership("join"),
...room.getMembersWithMembership("invite"),
].map(m => m.userId).filter(m => m !== cli.getUserId());
if (toInvite.length > 0) {
// Errors are handled internally to this function
await inviteUsersToRoom(newRoomId, toInvite);
}
} }
if (updateSpaces) { if (updateSpaces) {
@ -89,5 +101,6 @@ export async function upgradeRoom(
} }
} }
spinnerModal.close();
return newRoomId; return newRoomId;
} }

View File

@ -276,10 +276,12 @@ describe("SpaceStore", () => {
describe("test fixture 1", () => { describe("test fixture 1", () => {
beforeEach(async () => { beforeEach(async () => {
[fav1, fav2, fav3, dm1, dm2, dm3, orphan1, orphan2, invite1, invite2, room1].forEach(mkRoom); [fav1, fav2, fav3, dm1, dm2, dm3, orphan1, orphan2, invite1, invite2, room1, room2, room3]
.forEach(mkRoom);
mkSpace(space1, [fav1, room1]); mkSpace(space1, [fav1, room1]);
mkSpace(space2, [fav1, fav2, fav3, room1]); mkSpace(space2, [fav1, fav2, fav3, room1]);
mkSpace(space3, [invite2]); mkSpace(space3, [invite2]);
// client.getRoom.mockImplementation(roomId => rooms.find(room => room.roomId === roomId));
[fav1, fav2, fav3].forEach(roomId => { [fav1, fav2, fav3].forEach(roomId => {
client.getRoom(roomId).tags = { client.getRoom(roomId).tags = {
@ -329,6 +331,48 @@ describe("SpaceStore", () => {
]); ]);
// dmPartner3 is not in any common spaces with you // dmPartner3 is not in any common spaces with you
// room 2 claims to be a child of space2 and is so via a valid m.space.parent
const cliRoom2 = client.getRoom(room2);
cliRoom2.currentState.getStateEvents.mockImplementation(testUtils.mockStateEventImplementation([
mkEvent({
event: true,
type: EventType.SpaceParent,
room: room2,
user: client.getUserId(),
skey: space2,
content: { via: [], canonical: true },
ts: Date.now(),
}),
]));
const cliSpace2 = client.getRoom(space2);
cliSpace2.currentState.maySendStateEvent.mockImplementation((evType: string, userId: string) => {
if (evType === EventType.SpaceChild) {
return userId === client.getUserId();
}
return true;
});
// room 3 claims to be a child of space3 but is not due to invalid m.space.parent (permissions)
const cliRoom3 = client.getRoom(room3);
cliRoom3.currentState.getStateEvents.mockImplementation(testUtils.mockStateEventImplementation([
mkEvent({
event: true,
type: EventType.SpaceParent,
room: room3,
user: client.getUserId(),
skey: space3,
content: { via: [], canonical: true },
ts: Date.now(),
}),
]));
const cliSpace3 = client.getRoom(space3);
cliSpace3.currentState.maySendStateEvent.mockImplementation((evType: string, userId: string) => {
if (evType === EventType.SpaceChild) {
return false;
}
return true;
});
await run(); await run();
}); });
@ -445,6 +489,14 @@ describe("SpaceStore", () => {
expect(store.getNotificationState(space2).rooms.map(r => r.roomId).includes(room1)).toBeTruthy(); expect(store.getNotificationState(space2).rooms.map(r => r.roomId).includes(room1)).toBeTruthy();
expect(store.getNotificationState(space3).rooms.map(r => r.roomId).includes(room1)).toBeFalsy(); expect(store.getNotificationState(space3).rooms.map(r => r.roomId).includes(room1)).toBeFalsy();
}); });
it("honours m.space.parent if sender has permission in parent space", () => {
expect(store.getSpaceFilteredRoomIds(client.getRoom(space2)).has(room2)).toBeTruthy();
});
it("does not honour m.space.parent if sender does not have permission in parent space", () => {
expect(store.getSpaceFilteredRoomIds(client.getRoom(space3)).has(room3)).toBeFalsy();
});
}); });
}); });

View File

@ -2036,10 +2036,10 @@ ajv@^8.0.1:
require-from-string "^2.0.2" require-from-string "^2.0.2"
uri-js "^4.2.2" uri-js "^4.2.2"
allchange@^1.0.2: allchange@^1.0.3:
version "1.0.2" version "1.0.3"
resolved "https://registry.yarnpkg.com/allchange/-/allchange-1.0.2.tgz#86b9190e12b7ede4f230ae763cbd504c48fd907b" resolved "https://registry.yarnpkg.com/allchange/-/allchange-1.0.3.tgz#f8814ddfbcfe39a01bf4570778ee7e6d9ff0ebb3"
integrity sha512-qJv1t2yvBThkes8g/dPMt8CGu+04U+q5QjCJn2Ngp92edZU8DJBfKGyGXo7w1iV48LVuQKQDfMsdIWhP7zHdlQ== integrity sha512-UZkfz5SkNEMFQFLr8vZcXHaph2EbJxmkVNF5Nt6D9RIa5pmAar7oAMfNdda714jg7IQijvaFty5PYazXLgd5WA==
dependencies: dependencies:
"@actions/core" "^1.4.0" "@actions/core" "^1.4.0"
"@actions/github" "^5.0.0" "@actions/github" "^5.0.0"