Merge pull request #5975 from matrix-org/t3chguy/fix/16891

Add retry mechanism and progress bar to add existing to space dialog
This commit is contained in:
Michael Telatynski 2021-05-05 16:32:31 +01:00 committed by GitHub
commit 7cae566011
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 181 additions and 68 deletions

View file

@ -101,7 +101,7 @@ limitations under the License.
.mx_BaseAvatar { .mx_BaseAvatar {
display: inline-flex; display: inline-flex;
margin: 5px 16px 5px 5px; margin: auto 16px auto 5px;
vertical-align: middle; vertical-align: middle;
} }
@ -160,31 +160,32 @@ limitations under the License.
} }
} }
.mx_AddExistingToSpaceDialog_errorText {
font-weight: $font-semi-bold;
font-size: $font-12px;
line-height: $font-15px;
color: $notice-primary-color;
margin-bottom: 28px;
}
.mx_AddExistingToSpace { .mx_AddExistingToSpace {
display: contents; display: contents;
} }
.mx_AddExistingToSpaceDialog_footer { .mx_AddExistingToSpaceDialog_footer {
display: flex; display: flex;
margin-top: 32px; margin-top: 20px;
> span { > span {
flex-grow: 1; flex-grow: 1;
font-size: $font-14px; font-size: $font-12px;
line-height: $font-15px; line-height: $font-15px;
font-weight: $font-semi-bold; color: $secondary-fg-color;
.mx_AccessibleButton { .mx_ProgressBar {
font-size: inherit; height: 8px;
display: inline-block; width: 100%;
@mixin ProgressBarBorderRadius 8px;
}
.mx_AddExistingToSpaceDialog_progressText {
margin-top: 8px;
font-size: $font-15px;
line-height: $font-24px;
color: $primary-fg-color;
} }
> * { > * {
@ -192,8 +193,54 @@ limitations under the License.
} }
} }
.mx_AddExistingToSpaceDialog_error {
padding-left: 12px;
> img {
align-self: center;
}
.mx_AddExistingToSpaceDialog_errorHeading {
font-weight: $font-semi-bold;
font-size: $font-15px;
line-height: $font-18px;
color: $notice-primary-color;
}
.mx_AddExistingToSpaceDialog_errorCaption {
margin-top: 4px;
font-size: $font-12px;
line-height: $font-15px;
color: $primary-fg-color;
}
}
.mx_AccessibleButton { .mx_AccessibleButton {
display: inline-block; display: inline-block;
align-self: center;
}
.mx_AccessibleButton_kind_primary {
padding: 8px 36px;
}
.mx_AddExistingToSpaceDialog_retryButton {
margin-left: 12px;
padding-left: 24px;
position: relative;
&::before {
content: '';
position: absolute;
background-color: $primary-fg-color;
mask-repeat: no-repeat;
mask-position: center;
mask-size: contain;
mask-image: url('$(res)/img/element-icons/retry.svg');
width: 18px;
height: 18px;
left: 0;
}
} }
.mx_AccessibleButton_kind_link { .mx_AccessibleButton_kind_link {

View file

@ -21,7 +21,7 @@ progress.mx_ProgressBar {
appearance: none; appearance: none;
border: none; border: none;
@mixin ProgressBarBorderRadius "6px"; @mixin ProgressBarBorderRadius 6px;
@mixin ProgressBarColour $progressbar-fg-color; @mixin ProgressBarColour $progressbar-fg-color;
@mixin ProgressBarBgColour $progressbar-bg-color; @mixin ProgressBarBgColour $progressbar-bg-color;
::-webkit-progress-value { ::-webkit-progress-value {

View file

@ -52,7 +52,7 @@ import {useStateToggle} from "../../hooks/useStateToggle";
import SpaceStore from "../../stores/SpaceStore"; import SpaceStore from "../../stores/SpaceStore";
import FacePile from "../views/elements/FacePile"; import FacePile from "../views/elements/FacePile";
import {AddExistingToSpace} from "../views/dialogs/AddExistingToSpaceDialog"; import {AddExistingToSpace} from "../views/dialogs/AddExistingToSpaceDialog";
import {allSettled} from "../../utils/promise"; import {sleep} from "../../utils/promise";
import {calculateRoomVia} from "../../utils/permalinks/Permalinks"; import {calculateRoomVia} from "../../utils/permalinks/Permalinks";
interface IProps { interface IProps {
@ -389,15 +389,24 @@ const SpaceAddExistingRooms = ({ space, onFinished }) => {
let buttonLabel = _t("Skip for now"); let buttonLabel = _t("Skip for now");
if (selectedToAdd.size > 0) { if (selectedToAdd.size > 0) {
onClick = async () => { onClick = async () => {
// TODO rate limiting
setBusy(true); setBusy(true);
for (const room of selectedToAdd) {
const via = calculateRoomVia(room);
try { try {
await allSettled(Array.from(selectedToAdd).map((room) => await SpaceStore.instance.addRoomToSpace(space, room.roomId, via).catch(async e => {
SpaceStore.instance.addRoomToSpace(space, room.roomId, calculateRoomVia(room)))); if (e.errcode === "M_LIMIT_EXCEEDED") {
onFinished(true); await sleep(e.data.retry_after_ms);
return SpaceStore.instance.addRoomToSpace(space, room.roomId, via); // retry
}
throw e;
});
} catch (e) { } catch (e) {
console.error("Failed to add rooms to space", e); console.error("Failed to add rooms to space", e);
setError(_t("Failed to add rooms to space")); setError(_t("Failed to add rooms to space"));
break;
}
} }
setBusy(false); setBusy(false);
}; };

View file

@ -29,12 +29,13 @@ import RoomAvatar from "../avatars/RoomAvatar";
import {getDisplayAliasForRoom} from "../../../Rooms"; import {getDisplayAliasForRoom} from "../../../Rooms";
import AccessibleButton from "../elements/AccessibleButton"; import AccessibleButton from "../elements/AccessibleButton";
import AutoHideScrollbar from "../../structures/AutoHideScrollbar"; import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
import {allSettled} from "../../../utils/promise"; import {sleep} from "../../../utils/promise";
import DMRoomMap from "../../../utils/DMRoomMap"; import DMRoomMap from "../../../utils/DMRoomMap";
import {calculateRoomVia} from "../../../utils/permalinks/Permalinks"; import {calculateRoomVia} from "../../../utils/permalinks/Permalinks";
import StyledCheckbox from "../elements/StyledCheckbox"; import StyledCheckbox from "../elements/StyledCheckbox";
import MatrixClientContext from "../../../contexts/MatrixClientContext"; import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {sortRooms} from "../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm"; import {sortRooms} from "../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm";
import ProgressBar from "../elements/ProgressBar";
interface IProps extends IDialogProps { interface IProps extends IDialogProps {
matrixClient: MatrixClient; matrixClient: MatrixClient;
@ -46,7 +47,11 @@ const Entry = ({ room, checked, onChange }) => {
return <label className="mx_AddExistingToSpace_entry"> return <label className="mx_AddExistingToSpace_entry">
<RoomAvatar room={room} height={32} width={32} /> <RoomAvatar room={room} height={32} width={32} />
<span className="mx_AddExistingToSpace_entry_name">{ room.name }</span> <span className="mx_AddExistingToSpace_entry_name">{ room.name }</span>
<StyledCheckbox onChange={(e) => onChange(e.target.checked)} checked={checked} /> <StyledCheckbox
onChange={onChange ? (e) => onChange(e.target.checked) : null}
checked={checked}
disabled={!onChange}
/>
</label>; </label>;
}; };
@ -104,9 +109,9 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({ space,
key={room.roomId} key={room.roomId}
room={room} room={room}
checked={selected.has(room)} checked={selected.has(room)}
onChange={(checked) => { onChange={onChange ? (checked) => {
onChange(checked, room); onChange(checked, room);
}} } : null}
/>; />;
}) } }) }
</div> </div>
@ -120,9 +125,9 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({ space,
key={space.roomId} key={space.roomId}
room={space} room={space}
checked={selected.has(space)} checked={selected.has(space)}
onChange={(checked) => { onChange={onChange ? (checked) => {
onChange(checked, space); onChange(checked, space);
}} } : null}
/>; />;
}) } }) }
</div> </div>
@ -136,9 +141,9 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({ space,
key={room.roomId} key={room.roomId}
room={room} room={room}
checked={selected.has(room)} checked={selected.has(room)}
onChange={(checked) => { onChange={onChange ? (checked) => {
onChange(checked, room); onChange(checked, room);
}} } : null}
/>; />;
}) } }) }
</div> </div>
@ -156,8 +161,8 @@ const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space,
const existingSubspaces = SpaceStore.instance.getChildSpaces(space.roomId); const existingSubspaces = SpaceStore.instance.getChildSpaces(space.roomId);
const [selectedToAdd, setSelectedToAdd] = useState(new Set<Room>()); const [selectedToAdd, setSelectedToAdd] = useState(new Set<Room>());
const [busy, setBusy] = useState(false); const [progress, setProgress] = useState<number>(null);
const [error, setError] = useState(""); const [error, setError] = useState<Error>(null);
let spaceOptionSection; let spaceOptionSection;
if (existingSubspaces.length > 0) { if (existingSubspaces.length > 0) {
@ -197,6 +202,82 @@ const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space,
</div> </div>
</React.Fragment>; </React.Fragment>;
const addRooms = async () => {
setError(null);
setProgress(0);
let error;
for (const room of selectedToAdd) {
const via = calculateRoomVia(room);
try {
await SpaceStore.instance.addRoomToSpace(space, room.roomId, via).catch(async e => {
if (e.errcode === "M_LIMIT_EXCEEDED") {
await sleep(e.data.retry_after_ms);
return SpaceStore.instance.addRoomToSpace(space, room.roomId, via); // retry
}
throw e;
});
setProgress(i => i + 1);
} catch (e) {
console.error("Failed to add rooms to space", e);
setError(error = e);
break;
}
}
if (!error) {
onFinished(true);
}
};
const busy = progress !== null;
let footer;
if (error) {
footer = <>
<img
src={require("../../../../res/img/element-icons/warning-badge.svg")}
height="24"
width="24"
alt=""
/>
<span className="mx_AddExistingToSpaceDialog_error">
<div className="mx_AddExistingToSpaceDialog_errorHeading">{ _t("Not all selected were added") }</div>
<div className="mx_AddExistingToSpaceDialog_errorCaption">{ _t("Try again") }</div>
</span>
<AccessibleButton className="mx_AddExistingToSpaceDialog_retryButton" onClick={addRooms}>
{ _t("Retry") }
</AccessibleButton>
</>;
} else if (busy) {
footer = <span>
<ProgressBar value={progress} max={selectedToAdd.size} />
<div className="mx_AddExistingToSpaceDialog_progressText">
{ _t("Adding rooms... (%(progress)s out of %(count)s)", {
count: selectedToAdd.size,
progress,
}) }
</div>
</span>;
} else {
footer = <>
<span>
<div>{ _t("Want to add a new room instead?") }</div>
<AccessibleButton onClick={() => onCreateRoomClick(cli, space)} kind="link">
{ _t("Create a new room") }
</AccessibleButton>
</span>
<AccessibleButton kind="primary" disabled={selectedToAdd.size < 1} onClick={addRooms}>
{ _t("Add") }
</AccessibleButton>
</>;
}
return <BaseDialog return <BaseDialog
title={title} title={title}
className="mx_AddExistingToSpaceDialog" className="mx_AddExistingToSpaceDialog"
@ -204,50 +285,23 @@ const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space,
onFinished={onFinished} onFinished={onFinished}
fixedWidth={false} fixedWidth={false}
> >
{ error && <div className="mx_AddExistingToSpaceDialog_errorText">{ error }</div> }
<MatrixClientContext.Provider value={cli}> <MatrixClientContext.Provider value={cli}>
<AddExistingToSpace <AddExistingToSpace
space={space} space={space}
selected={selectedToAdd} selected={selectedToAdd}
onChange={(checked, room) => { onChange={!busy && !error ? (checked, room) => {
if (checked) { if (checked) {
selectedToAdd.add(room); selectedToAdd.add(room);
} else { } else {
selectedToAdd.delete(room); selectedToAdd.delete(room);
} }
setSelectedToAdd(new Set(selectedToAdd)); setSelectedToAdd(new Set(selectedToAdd));
}} } : null}
/> />
</MatrixClientContext.Provider> </MatrixClientContext.Provider>
<div className="mx_AddExistingToSpaceDialog_footer"> <div className="mx_AddExistingToSpaceDialog_footer">
<span> { footer }
<div>{ _t("Don't want to add an existing room?") }</div>
<AccessibleButton onClick={() => onCreateRoomClick(cli, space)} kind="link">
{ _t("Create a new room") }
</AccessibleButton>
</span>
<AccessibleButton
kind="primary"
disabled={busy || selectedToAdd.size < 1}
onClick={async () => {
// TODO rate limiting
setBusy(true);
try {
await allSettled(Array.from(selectedToAdd).map((room) =>
SpaceStore.instance.addRoomToSpace(space, room.roomId, calculateRoomVia(room))));
onFinished(true);
} catch (e) {
console.error("Failed to add rooms to space", e);
setError(_t("Failed to add rooms to space"));
}
setBusy(false);
}}
>
{ busy ? _t("Adding...") : _t("Add") }
</AccessibleButton>
</div> </div>
</BaseDialog>; </BaseDialog>;
}; };

View file

@ -2034,10 +2034,11 @@
"Direct Messages": "Direct Messages", "Direct Messages": "Direct Messages",
"Space selection": "Space selection", "Space selection": "Space selection",
"Add existing rooms": "Add existing rooms", "Add existing rooms": "Add existing rooms",
"Don't want to add an existing room?": "Don't want to add an existing room?", "Not all selected were added": "Not all selected were added",
"Adding rooms... (%(progress)s out of %(count)s)|other": "Adding rooms... (%(progress)s out of %(count)s)",
"Adding rooms... (%(progress)s out of %(count)s)|one": "Adding room...",
"Want to add a new room instead?": "Want to add a new room instead?",
"Create a new room": "Create a new room", "Create a new room": "Create a new room",
"Failed to add rooms to space": "Failed to add rooms to space",
"Adding...": "Adding...",
"Matrix ID": "Matrix ID", "Matrix ID": "Matrix ID",
"Matrix Room ID": "Matrix Room ID", "Matrix Room ID": "Matrix Room ID",
"email address": "email address", "email address": "email address",
@ -2675,6 +2676,8 @@
"Failed to create initial space rooms": "Failed to create initial space rooms", "Failed to create initial space rooms": "Failed to create initial space rooms",
"Skip for now": "Skip for now", "Skip for now": "Skip for now",
"Creating rooms...": "Creating rooms...", "Creating rooms...": "Creating rooms...",
"Failed to add rooms to space": "Failed to add rooms to space",
"Adding...": "Adding...",
"What do you want to organise?": "What do you want to organise?", "What do you want to organise?": "What do you want to organise?",
"Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.": "Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.", "Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.": "Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.",
"Share %(name)s": "Share %(name)s", "Share %(name)s": "Share %(name)s",