Merge branch 'develop' into travis/files/visual

This commit is contained in:
Travis Ralston 2021-05-05 13:47:33 -06:00
commit 60e3a8ac77
20 changed files with 324 additions and 122 deletions

View file

@ -28,7 +28,7 @@ Platform Targets:
* WebRTC features (VoIP and Video calling) are only available in Chrome & Firefox. * WebRTC features (VoIP and Video calling) are only available in Chrome & Firefox.
* Mobile Web is not currently a target platform - instead please use the native * Mobile Web is not currently a target platform - instead please use the native
iOS (https://github.com/matrix-org/matrix-ios-kit) and Android iOS (https://github.com/matrix-org/matrix-ios-kit) and Android
(https://github.com/matrix-org/matrix-android-sdk) SDKs. (https://github.com/matrix-org/matrix-android-sdk2) SDKs.
All code lands on the `develop` branch - `master` is only used for stable releases. All code lands on the `develop` branch - `master` is only used for stable releases.
**Please file PRs against `develop`!!** **Please file PRs against `develop`!!**

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

@ -39,14 +39,14 @@ limitations under the License.
width: 14px; // w&h are size of icon width: 14px; // w&h are size of icon
height: 18px; height: 18px;
vertical-align: middle; vertical-align: middle;
margin-right: 7px; // distance from left edge of waveform container (container has some margin too) margin-right: 11px; // distance from left edge of waveform container (container has some margin too)
background-color: $voice-record-icon-color; background-color: $voice-record-icon-color;
mask-repeat: no-repeat; mask-repeat: no-repeat;
mask-size: contain; mask-size: contain;
mask-image: url('$(res)/img/element-icons/trashcan.svg'); mask-image: url('$(res)/img/element-icons/trashcan.svg');
} }
.mx_VoiceMessagePrimaryContainer { .mx_VoiceRecordComposerTile_recording.mx_VoiceMessagePrimaryContainer {
// Note: remaining class properties are in the PlayerContainer CSS. // Note: remaining class properties are in the PlayerContainer CSS.
margin: 6px; // force the composer area to put a gutter around us margin: 6px; // force the composer area to put a gutter around us
@ -55,7 +55,9 @@ limitations under the License.
position: relative; // important for the live circle position: relative; // important for the live circle
&.mx_VoiceRecordComposerTile_recording { &.mx_VoiceRecordComposerTile_recording {
padding-left: 16px; // +10px for the live circle, +6px for regular padding // We are putting the circle in this padding, so we need +10px from the regular
// padding on the left side.
padding-left: 22px;
&::before { &::before {
animation: recording-pulse 2s infinite; animation: recording-pulse 2s infinite;
@ -65,8 +67,8 @@ limitations under the License.
width: 10px; width: 10px;
height: 10px; height: 10px;
position: absolute; position: absolute;
left: 8px; left: 12px; // 12px from the left edge for container padding
top: 16px; // vertically center top: 18px; // vertically center (middle align with clock)
border-radius: 10px; border-radius: 10px;
} }
} }

View file

@ -22,3 +22,34 @@ limitations under the License.
.mx_HelpUserSettingsTab span.mx_AccessibleButton { .mx_HelpUserSettingsTab span.mx_AccessibleButton {
word-break: break-word; word-break: break-word;
} }
.mx_HelpUserSettingsTab code {
word-break: break-all;
user-select: all;
}
.mx_HelpUserSettingsTab_accessToken {
display: flex;
justify-content: space-between;
border-radius: 5px;
border: solid 1px $light-fg-color;
margin-bottom: 10px;
margin-top: 10px;
padding: 10px;
}
.mx_HelpUserSettingsTab_accessToken_copy {
flex-shrink: 0;
cursor: pointer;
margin-left: 20px;
display: inherit;
}
.mx_HelpUserSettingsTab_accessToken_copy > div {
mask-image: url($copy-button-url);
background-color: $message-action-bar-fg-color;
margin-left: 5px;
width: 20px;
height: 20px;
background-repeat: no-repeat;
}

View file

@ -19,8 +19,9 @@ limitations under the License.
// Container for live recording and playback controls // Container for live recording and playback controls
.mx_VoiceMessagePrimaryContainer { .mx_VoiceMessagePrimaryContainer {
padding: 6px; // makes us 4px taller than the send/stop button // 7px top and bottom for visual design. 12px left & right, but the waveform (right)
padding-right: 5px; // there's 1px from the waveform itself, so account for that // has a 1px padding on it that we want to account for.
padding: 7px 12px 7px 11px;
background-color: $voice-record-waveform-bg-color; background-color: $voice-record-waveform-bg-color;
border-radius: 12px; border-radius: 12px;
@ -30,11 +31,9 @@ limitations under the License.
color: $voice-record-waveform-fg-color; color: $voice-record-waveform-fg-color;
font-size: $font-14px; font-size: $font-14px;
line-height: $font-24px;
.mx_Waveform { .mx_Waveform {
// We want the bars to be 2px shorter than the play/pause button in the waveform control
height: 28px; // default is 30px, so we're subtracting the 2px border off the bars
.mx_Waveform_bar { .mx_Waveform_bar {
background-color: $voice-record-waveform-incomplete-fg-color; background-color: $voice-record-waveform-incomplete-fg-color;
@ -47,8 +46,8 @@ limitations under the License.
} }
.mx_Clock { .mx_Clock {
padding-right: 4px; // isolate from waveform width: 42px; // we're not using a monospace font, so fake it
padding-left: 8px; // isolate from live circle padding-right: 6px; // with the fixed width this ends up as a visual 8px most of the time, as intended.
width: 40px; // we're not using a monospace font, so fake it padding-left: 8px; // isolate from recording circle / play control
} }
} }

View file

@ -9,6 +9,7 @@ $header-panel-text-primary-color: #B9BEC6;
$header-panel-text-secondary-color: #c8c8cd; $header-panel-text-secondary-color: #c8c8cd;
$text-primary-color: #ffffff; $text-primary-color: #ffffff;
$text-secondary-color: #B9BEC6; $text-secondary-color: #B9BEC6;
$quaternary-fg-color: #6F7882;
$search-bg-color: #181b21; $search-bg-color: #181b21;
$search-placeholder-color: #61708b; $search-placeholder-color: #61708b;
$room-highlight-color: #343a46; $room-highlight-color: #343a46;
@ -42,13 +43,13 @@ $preview-bar-bg-color: $header-panel-bg-color;
$groupFilterPanel-bg-color: rgba(38, 39, 43, 0.82); $groupFilterPanel-bg-color: rgba(38, 39, 43, 0.82);
$inverted-bg-color: $base-color; $inverted-bg-color: $base-color;
$voice-record-stop-border-color: #6F7882; // "Quarterly" $voice-record-stop-border-color: $quaternary-fg-color;
$voice-record-waveform-bg-color: #394049; // "Dark Tile" $voice-record-waveform-bg-color: #394049; // "Dark Tile"
$voice-record-waveform-fg-color: $tertiary-fg-color; $voice-record-waveform-fg-color: $secondary-fg-color;
$voice-record-waveform-incomplete-fg-color: #5b646d; $voice-record-waveform-incomplete-fg-color: $quaternary-fg-color;
$voice-record-icon-color: $tertiary-fg-color; $voice-record-icon-color: $quaternary-fg-color;
$voice-playback-button-bg-color: $tertiary-fg-color; $voice-playback-button-bg-color: $tertiary-fg-color;
$voice-playback-button-fg-color: $bg-color; $voice-playback-button-fg-color: #21262C; // "Separator"
// used by AddressSelector // used by AddressSelector
$selected-color: $room-highlight-color; $selected-color: $room-highlight-color;

View file

@ -127,11 +127,11 @@ $groupFilterPanel-divider-color: $roomlist-header-color;
// See non-legacy dark for variable information // See non-legacy dark for variable information
$voice-record-stop-border-color: #6F7882; $voice-record-stop-border-color: #6F7882;
$voice-record-waveform-bg-color: #394049; $voice-record-waveform-bg-color: #394049;
$voice-record-waveform-fg-color: $tertiary-fg-color; $voice-record-waveform-fg-color: $secondary-fg-color;
$voice-record-waveform-incomplete-fg-color: #5b646d; $voice-record-waveform-incomplete-fg-color: #6F7882;
$voice-record-icon-color: $tertiary-fg-color; $voice-record-icon-color: #6F7882;
$voice-playback-button-bg-color: $tertiary-fg-color; $voice-playback-button-bg-color: $tertiary-fg-color;
$voice-playback-button-fg-color: $bg-color; $voice-playback-button-fg-color: #21262C;
$roomtile-preview-color: #9e9e9e; $roomtile-preview-color: #9e9e9e;
$roomtile-default-badge-bg-color: #61708b; $roomtile-default-badge-bg-color: #61708b;

View file

@ -192,15 +192,15 @@ $roomsublist-skeleton-ui-bg: linear-gradient(180deg, #ffffff 0%, #ffffff00 100%)
$groupFilterPanel-divider-color: $roomlist-header-color; $groupFilterPanel-divider-color: $roomlist-header-color;
// See non-legacy _light for variable information // See non-legacy _light for variable information
$voice-record-stop-border-color: #E3E8F0;
$voice-record-stop-symbol-color: #ff4b55; $voice-record-stop-symbol-color: #ff4b55;
$voice-record-waveform-bg-color: #E3E8F0;
$voice-record-waveform-fg-color: $muted-fg-color;
$voice-record-waveform-incomplete-fg-color: #C1C6CD;
$voice-record-live-circle-color: #ff4b55; $voice-record-live-circle-color: #ff4b55;
$voice-record-icon-color: $muted-fg-color; $voice-record-stop-border-color: #E3E8F0;
$voice-record-waveform-bg-color: #E3E8F0;
$voice-record-waveform-fg-color: $secondary-fg-color;
$voice-record-waveform-incomplete-fg-color: #C1C6CD;
$voice-record-icon-color: $tertiary-fg-color;
$voice-playback-button-bg-color: $primary-bg-color; $voice-playback-button-bg-color: $primary-bg-color;
$voice-playback-button-fg-color: $muted-fg-color; $voice-playback-button-fg-color: $secondary-fg-color;
$fileinfo-bg-color: $voice-record-waveform-bg-color; $fileinfo-bg-color: $voice-record-waveform-bg-color;
$fileinfo-fg-color: $secondary-fg-color; $fileinfo-fg-color: $secondary-fg-color;

View file

@ -21,6 +21,7 @@ $notice-primary-bg-color: rgba(255, 75, 85, 0.16);
$primary-fg-color: #2e2f32; $primary-fg-color: #2e2f32;
$secondary-fg-color: #737D8C; $secondary-fg-color: #737D8C;
$tertiary-fg-color: #8D99A5; $tertiary-fg-color: #8D99A5;
$quaternary-fg-color: #C1C6CD;
$header-panel-bg-color: #f3f8fd; $header-panel-bg-color: #f3f8fd;
// typical text (dark-on-white in light skin) // typical text (dark-on-white in light skin)
@ -182,15 +183,18 @@ $roomsublist-skeleton-ui-bg: linear-gradient(180deg, #ffffff 0%, #ffffff00 100%)
$groupFilterPanel-divider-color: $roomlist-header-color; $groupFilterPanel-divider-color: $roomlist-header-color;
$voice-record-stop-border-color: #E3E8F0; // These two don't change between themes. They are the $warning-color, but we don't
$voice-record-stop-symbol-color: #ff4b55; // $warning-color, but without letting people change it in themes // want custom themes to affect them by accident.
$voice-record-waveform-bg-color: #E3E8F0; $voice-record-stop-symbol-color: #ff4b55;
$voice-record-waveform-fg-color: $muted-fg-color; $voice-record-live-circle-color: #ff4b55;
$voice-record-waveform-incomplete-fg-color: #C1C6CD;
$voice-record-live-circle-color: #ff4b55; // $warning-color, but without letting people change it in themes $voice-record-stop-border-color: #E3E8F0; // "Separator"
$voice-record-icon-color: $muted-fg-color; $voice-record-waveform-bg-color: #E3E8F0; // "Separator"
$voice-record-waveform-fg-color: $secondary-fg-color;
$voice-record-waveform-incomplete-fg-color: $quaternary-fg-color;
$voice-record-icon-color: $tertiary-fg-color;
$voice-playback-button-bg-color: $primary-bg-color; $voice-playback-button-bg-color: $primary-bg-color;
$voice-playback-button-fg-color: $muted-fg-color; $voice-playback-button-fg-color: $secondary-fg-color;
$roomtile-preview-color: $secondary-fg-color; $roomtile-preview-color: $secondary-fg-color;
$roomtile-default-badge-bg-color: #61708b; $roomtile-default-badge-bg-color: #61708b;

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);
try {
await allSettled(Array.from(selectedToAdd).map((room) => for (const room of selectedToAdd) {
SpaceStore.instance.addRoomToSpace(space, room.roomId, calculateRoomVia(room)))); const via = calculateRoomVia(room);
onFinished(true); try {
} catch (e) { await SpaceStore.instance.addRoomToSpace(space, room.roomId, via).catch(async e => {
console.error("Failed to add rooms to space", e); if (e.errcode === "M_LIMIT_EXCEEDED") {
setError(_t("Failed to add rooms to space")); await sleep(e.data.retry_after_ms);
return SpaceStore.instance.addRoomToSpace(space, room.roomId, via); // retry
}
throw e;
});
} catch (e) {
console.error("Failed to add rooms to space", e);
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

@ -114,6 +114,8 @@ export default class ImageView extends React.Component<IProps, IState> {
componentWillUnmount() { componentWillUnmount() {
this.focusLock.current.removeEventListener('wheel', this.onWheel); this.focusLock.current.removeEventListener('wheel', this.onWheel);
window.removeEventListener("resize", this.calculateZoom);
this.image.current.removeEventListener("load", this.calculateZoom);
} }
private calculateZoom = () => { private calculateZoom = () => {

View file

@ -18,6 +18,7 @@ import React from 'react';
import {_t, getCurrentLanguage} from "../../../../../languageHandler"; import {_t, getCurrentLanguage} from "../../../../../languageHandler";
import {MatrixClientPeg} from "../../../../../MatrixClientPeg"; import {MatrixClientPeg} from "../../../../../MatrixClientPeg";
import AccessibleButton from "../../../elements/AccessibleButton"; import AccessibleButton from "../../../elements/AccessibleButton";
import AccessibleTooltipButton from '../../../elements/AccessibleTooltipButton';
import SdkConfig from "../../../../../SdkConfig"; import SdkConfig from "../../../../../SdkConfig";
import createRoom from "../../../../../createRoom"; import createRoom from "../../../../../createRoom";
import Modal from "../../../../../Modal"; import Modal from "../../../../../Modal";
@ -26,6 +27,9 @@ import PlatformPeg from "../../../../../PlatformPeg";
import * as KeyboardShortcuts from "../../../../../accessibility/KeyboardShortcuts"; import * as KeyboardShortcuts from "../../../../../accessibility/KeyboardShortcuts";
import UpdateCheckButton from "../../UpdateCheckButton"; import UpdateCheckButton from "../../UpdateCheckButton";
import { replaceableComponent } from "../../../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../../../utils/replaceableComponent";
import { copyPlaintext } from "../../../../../utils/strings";
import * as ContextMenu from "../../../../structures/ContextMenu";
import { toRightOf } from "../../../../structures/ContextMenu";
interface IProps { interface IProps {
closeSettingsFn: () => {}; closeSettingsFn: () => {};
@ -38,6 +42,8 @@ interface IState {
@replaceableComponent("views.settings.tabs.user.HelpUserSettingsTab") @replaceableComponent("views.settings.tabs.user.HelpUserSettingsTab")
export default class HelpUserSettingsTab extends React.Component<IProps, IState> { export default class HelpUserSettingsTab extends React.Component<IProps, IState> {
protected closeCopiedTooltip: () => void;
constructor(props) { constructor(props) {
super(props); super(props);
@ -56,6 +62,12 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
}); });
} }
componentWillUnmount() {
// if the Copied tooltip is open then get rid of it, there are ways to close the modal which wouldn't close
// the tooltip otherwise, such as pressing Escape
if (this.closeCopiedTooltip) this.closeCopiedTooltip();
}
private onClearCacheAndReload = (e) => { private onClearCacheAndReload = (e) => {
if (!PlatformPeg.get()) return; if (!PlatformPeg.get()) return;
@ -153,6 +165,20 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
); );
} }
onAccessTokenCopyClick = async (e) => {
e.preventDefault();
const target = e.target; // copy target before we go async and React throws it away
const successful = await copyPlaintext(MatrixClientPeg.get().getAccessToken());
const buttonRect = target.getBoundingClientRect();
const GenericTextContextMenu = sdk.getComponent('context_menus.GenericTextContextMenu');
const {close} = ContextMenu.createMenu(GenericTextContextMenu, {
...toRightOf(buttonRect, 2),
message: successful ? _t('Copied!') : _t('Failed to copy'),
});
this.closeCopiedTooltip = target.onmouseleave = close;
}
render() { render() {
const brand = SdkConfig.get().brand; const brand = SdkConfig.get().brand;
@ -269,12 +295,20 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
<div className='mx_SettingsTab_subsectionText'> <div className='mx_SettingsTab_subsectionText'>
{_t("Homeserver is")} <code>{MatrixClientPeg.get().getHomeserverUrl()}</code><br /> {_t("Homeserver is")} <code>{MatrixClientPeg.get().getHomeserverUrl()}</code><br />
{_t("Identity Server is")} <code>{MatrixClientPeg.get().getIdentityServerUrl()}</code><br /> {_t("Identity Server is")} <code>{MatrixClientPeg.get().getIdentityServerUrl()}</code><br />
{_t("Access Token:") + ' '} <br />
<AccessibleButton element="span" onClick={this.showSpoiler} <details>
data-spoiler={MatrixClientPeg.get().getAccessToken()} <summary>{_t("Access Token")}</summary><br />
> <b>{_t("Your access token gives full access to your account."
&lt;{ _t("click to reveal") }&gt; + " Do not share it with anyone." )}</b>
</AccessibleButton> <div className="mx_HelpUserSettingsTab_accessToken">
<code>{MatrixClientPeg.get().getAccessToken()}</code>
<AccessibleTooltipButton
title={_t("Copy")}
onClick={this.onAccessTokenCopyClick}
className="mx_HelpUserSettingsTab_accessToken_copy"
/>
</div>
</details><br />
<div className='mx_HelpUserSettingsTab_debugButton'> <div className='mx_HelpUserSettingsTab_debugButton'>
<AccessibleButton onClick={this.onClearCacheAndReload} kind='danger'> <AccessibleButton onClick={this.onClearCacheAndReload} kind='danger'>
{_t("Clear cache and reload")} {_t("Clear cache and reload")}

View file

@ -15,12 +15,11 @@ limitations under the License.
*/ */
import React from "react"; import React from "react";
import {IRecordingUpdate, VoiceRecording} from "../../../voice/VoiceRecording"; import {IRecordingUpdate, RECORDING_PLAYBACK_SAMPLES, VoiceRecording} from "../../../voice/VoiceRecording";
import {replaceableComponent} from "../../../utils/replaceableComponent"; import {replaceableComponent} from "../../../utils/replaceableComponent";
import {arrayFastResample, arraySeed} from "../../../utils/arrays"; import {arrayFastResample, arraySeed} from "../../../utils/arrays";
import {percentageOf} from "../../../utils/numbers"; import {percentageOf} from "../../../utils/numbers";
import Waveform from "./Waveform"; import Waveform from "./Waveform";
import {PLAYBACK_WAVEFORM_SAMPLES} from "../../../voice/Playback";
interface IProps { interface IProps {
recorder: VoiceRecording; recorder: VoiceRecording;
@ -38,14 +37,14 @@ export default class LiveRecordingWaveform extends React.PureComponent<IProps, I
public constructor(props) { public constructor(props) {
super(props); super(props);
this.state = {heights: arraySeed(0, PLAYBACK_WAVEFORM_SAMPLES)}; this.state = {heights: arraySeed(0, RECORDING_PLAYBACK_SAMPLES)};
this.props.recorder.liveData.onUpdate(this.onRecordingUpdate); this.props.recorder.liveData.onUpdate(this.onRecordingUpdate);
} }
private onRecordingUpdate = (update: IRecordingUpdate) => { private onRecordingUpdate = (update: IRecordingUpdate) => {
// The waveform and the downsample target are pretty close, so we should be fine to // The waveform and the downsample target are pretty close, so we should be fine to
// do this, despite the docs on arrayFastResample. // do this, despite the docs on arrayFastResample.
const bars = arrayFastResample(Array.from(update.waveform), PLAYBACK_WAVEFORM_SAMPLES); const bars = arrayFastResample(Array.from(update.waveform), RECORDING_PLAYBACK_SAMPLES);
this.setState({ this.setState({
// The incoming data is between zero and one, but typically even screaming into a // The incoming data is between zero and one, but typically even screaming into a
// microphone won't send you over 0.6, so we artificially adjust the gain for the // microphone won't send you over 0.6, so we artificially adjust the gain for the

View file

@ -833,7 +833,7 @@
"Match system theme": "Match system theme", "Match system theme": "Match system theme",
"Use a system font": "Use a system font", "Use a system font": "Use a system font",
"System font name": "System font name", "System font name": "System font name",
"Allow Peer-to-Peer for 1:1 calls": "Allow Peer-to-Peer for 1:1 calls", "Allow Peer-to-Peer for 1:1 calls (if you enable this, the other party might be able to see your IP address)": "Allow Peer-to-Peer for 1:1 calls (if you enable this, the other party might be able to see your IP address)",
"Send analytics data": "Send analytics data", "Send analytics data": "Send analytics data",
"Never send encrypted messages to unverified sessions from this session": "Never send encrypted messages to unverified sessions from this session", "Never send encrypted messages to unverified sessions from this session": "Never send encrypted messages to unverified sessions from this session",
"Never send encrypted messages to unverified sessions in this room from this session": "Never send encrypted messages to unverified sessions in this room from this session", "Never send encrypted messages to unverified sessions in this room from this session": "Never send encrypted messages to unverified sessions in this room from this session",
@ -1252,8 +1252,9 @@
"olm version:": "olm version:", "olm version:": "olm version:",
"Homeserver is": "Homeserver is", "Homeserver is": "Homeserver is",
"Identity Server is": "Identity Server is", "Identity Server is": "Identity Server is",
"Access Token:": "Access Token:", "Access Token": "Access Token",
"click to reveal": "click to reveal", "Your access token gives full access to your account. Do not share it with anyone.": "Your access token gives full access to your account. Do not share it with anyone.",
"Copy": "Copy",
"Clear cache and reload": "Clear cache and reload", "Clear cache and reload": "Clear cache and reload",
"Labs": "Labs", "Labs": "Labs",
"Customise your experience with experimental labs features. <a>Learn more</a>.": "Customise your experience with experimental labs features. <a>Learn more</a>.", "Customise your experience with experimental labs features. <a>Learn more</a>.": "Customise your experience with experimental labs features. <a>Learn more</a>.",
@ -2033,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",
@ -2347,7 +2349,6 @@
"Share Community": "Share Community", "Share Community": "Share Community",
"Share Room Message": "Share Room Message", "Share Room Message": "Share Room Message",
"Link to selected message": "Link to selected message", "Link to selected message": "Link to selected message",
"Copy": "Copy",
"Command Help": "Command Help", "Command Help": "Command Help",
"Failed to save space settings.": "Failed to save space settings.", "Failed to save space settings.": "Failed to save space settings.",
"Space settings": "Space settings", "Space settings": "Space settings",
@ -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",

View file

@ -438,7 +438,10 @@ export const SETTINGS: {[setting: string]: ISetting} = {
}, },
"webRtcAllowPeerToPeer": { "webRtcAllowPeerToPeer": {
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG, supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG,
displayName: _td('Allow Peer-to-Peer for 1:1 calls'), displayName: _td(
"Allow Peer-to-Peer for 1:1 calls " +
"(if you enable this, the other party might be able to see your IP address)",
),
default: true, default: true,
invertedSettingName: 'webRtcForceTURN', invertedSettingName: 'webRtcForceTURN',
}, },

View file

@ -413,7 +413,19 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
this.spaceFilteredRooms.forEach((roomIds, s) => { this.spaceFilteredRooms.forEach((roomIds, s) => {
// Update NotificationStates // Update NotificationStates
this.getNotificationState(s)?.setRooms(visibleRooms.filter(room => roomIds.has(room.roomId))); this.getNotificationState(s)?.setRooms(visibleRooms.filter(room => {
if (roomIds.has(room.roomId)) {
// Don't aggregate notifications for DMs except in the Home Space
if (s !== HOME_SPACE) {
return !DMRoomMap.shared().getUserIdForRoomId(room.roomId)
|| RoomListStore.instance.getTagsForRoom(room).includes(DefaultTagID.Favourite);
}
return true;
}
return false;
}));
}); });
}, 100, {trailing: true, leading: true}); }, 100, {trailing: true, leading: true});

View file

@ -29,7 +29,7 @@ export enum PlaybackState {
Playing = "playing", // active progress through timeline Playing = "playing", // active progress through timeline
} }
export const PLAYBACK_WAVEFORM_SAMPLES = 35; export const PLAYBACK_WAVEFORM_SAMPLES = 39;
const DEFAULT_WAVEFORM = arraySeed(0, PLAYBACK_WAVEFORM_SAMPLES); const DEFAULT_WAVEFORM = arraySeed(0, PLAYBACK_WAVEFORM_SAMPLES);
export class Playback extends EventEmitter implements IDestroyable { export class Playback extends EventEmitter implements IDestroyable {

View file

@ -33,6 +33,8 @@ const BITRATE = 24000; // 24kbps is pretty high quality for our use case in opus
const TARGET_MAX_LENGTH = 120; // 2 minutes in seconds. Somewhat arbitrary, though longer == larger files. const TARGET_MAX_LENGTH = 120; // 2 minutes in seconds. Somewhat arbitrary, though longer == larger files.
const TARGET_WARN_TIME_LEFT = 10; // 10 seconds, also somewhat arbitrary. const TARGET_WARN_TIME_LEFT = 10; // 10 seconds, also somewhat arbitrary.
export const RECORDING_PLAYBACK_SAMPLES = 44;
export interface IRecordingUpdate { export interface IRecordingUpdate {
waveform: number[]; // floating points between 0 (low) and 1 (high). waveform: number[]; // floating points between 0 (low) and 1 (high).
timeSeconds: number; // float timeSeconds: number; // float