Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into t3chguy/fix/17021
Conflicts: src/components/structures/SpaceRoomView.tsx src/i18n/strings/en_EN.jsonpull/21833/head
commit
a8c4aabb52
|
@ -86,7 +86,7 @@ limitations under the License.
|
|||
color: $primary-fg-color;
|
||||
|
||||
.mx_AccessibleButton {
|
||||
padding: 2px 8px;
|
||||
padding: 4px 12px;
|
||||
font-weight: normal;
|
||||
|
||||
& + .mx_AccessibleButton {
|
||||
|
@ -94,6 +94,11 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
|
||||
.mx_AccessibleButton_kind_danger_outline,
|
||||
.mx_AccessibleButton_kind_primary_outline {
|
||||
padding: 3px 12px; // to account for the 1px border
|
||||
}
|
||||
|
||||
> span {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
@ -246,11 +251,17 @@ limitations under the License.
|
|||
grid-row: 1/3;
|
||||
|
||||
.mx_AccessibleButton {
|
||||
padding: 8px 18px;
|
||||
line-height: $font-24px;
|
||||
padding: 4px 16px;
|
||||
display: inline-block;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.mx_AccessibleButton_kind_danger_outline,
|
||||
.mx_AccessibleButton_kind_primary_outline {
|
||||
padding: 3px 16px; // to account for the 1px border
|
||||
}
|
||||
|
||||
.mx_Checkbox {
|
||||
display: inline-flex;
|
||||
vertical-align: middle;
|
||||
|
|
|
@ -276,7 +276,8 @@ $SpaceRoomViewInnerWidth: 428px;
|
|||
|
||||
.mx_SpaceRoomView_landing_inviteButton {
|
||||
position: relative;
|
||||
padding-left: 40px;
|
||||
padding: 4px 18px 4px 40px;
|
||||
line-height: $font-24px;
|
||||
height: min-content;
|
||||
|
||||
&::before {
|
||||
|
@ -292,6 +293,27 @@ $SpaceRoomViewInnerWidth: 428px;
|
|||
mask-image: url('$(res)/img/element-icons/room/invite.svg');
|
||||
}
|
||||
}
|
||||
|
||||
.mx_SpaceRoomView_landing_settingsButton {
|
||||
position: relative;
|
||||
margin-left: 16px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
background: $tertiary-fg-color;
|
||||
mask-position: center;
|
||||
mask-size: contain;
|
||||
mask-repeat: no-repeat;
|
||||
mask-image: url('$(res)/img/element-icons/settings.svg');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_SpaceRoomView_landing_topic {
|
||||
|
@ -306,80 +328,6 @@ $SpaceRoomViewInnerWidth: 428px;
|
|||
background-color: $groupFilterPanel-bg-color;
|
||||
}
|
||||
|
||||
.mx_SpaceRoomView_landing_adminButtons {
|
||||
margin-top: 24px;
|
||||
|
||||
.mx_AccessibleButton {
|
||||
position: relative;
|
||||
width: 160px;
|
||||
height: 124px;
|
||||
box-sizing: border-box;
|
||||
padding: 72px 16px 0;
|
||||
border-radius: 12px;
|
||||
border: 1px solid $input-border-color;
|
||||
margin-right: 28px;
|
||||
margin-bottom: 20px;
|
||||
font-size: $font-14px;
|
||||
display: inline-block;
|
||||
vertical-align: bottom;
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(141, 151, 165, 0.1);
|
||||
}
|
||||
|
||||
&::before, &::after {
|
||||
position: absolute;
|
||||
content: "";
|
||||
left: 16px;
|
||||
top: 16px;
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
&::after {
|
||||
mask-position: center;
|
||||
mask-size: 30px;
|
||||
mask-repeat: no-repeat;
|
||||
background: #ffffff; // white icon fill
|
||||
}
|
||||
|
||||
&.mx_SpaceRoomView_landing_addButton {
|
||||
&::before {
|
||||
background-color: #ac3ba8;
|
||||
}
|
||||
|
||||
&::after {
|
||||
mask-image: url('$(res)/img/element-icons/roomlist/explore.svg');
|
||||
}
|
||||
}
|
||||
|
||||
&.mx_SpaceRoomView_landing_createButton {
|
||||
&::before {
|
||||
background-color: #368bd6;
|
||||
}
|
||||
|
||||
&::after {
|
||||
mask-image: url('$(res)/img/element-icons/roomlist/explore.svg');
|
||||
}
|
||||
}
|
||||
|
||||
&.mx_SpaceRoomView_landing_settingsButton {
|
||||
&::before {
|
||||
background-color: #5c56f5;
|
||||
}
|
||||
|
||||
&::after {
|
||||
mask-image: url('$(res)/img/element-icons/settings.svg');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_SearchBox {
|
||||
margin: 0 0 20px;
|
||||
}
|
||||
|
|
|
@ -76,12 +76,16 @@ limitations under the License.
|
|||
border: 1px solid $button-danger-bg-color;
|
||||
}
|
||||
|
||||
.mx_AccessibleButton_kind_danger.mx_AccessibleButton_disabled,
|
||||
.mx_AccessibleButton_kind_danger_outline.mx_AccessibleButton_disabled {
|
||||
.mx_AccessibleButton_kind_danger.mx_AccessibleButton_disabled {
|
||||
color: $button-danger-disabled-fg-color;
|
||||
background-color: $button-danger-disabled-bg-color;
|
||||
}
|
||||
|
||||
.mx_AccessibleButton_kind_danger_outline.mx_AccessibleButton_disabled {
|
||||
color: $button-danger-disabled-bg-color;
|
||||
border-color: $button-danger-disabled-bg-color;
|
||||
}
|
||||
|
||||
.mx_AccessibleButton_hasKind.mx_AccessibleButton_kind_danger_sm {
|
||||
padding: 5px 12px;
|
||||
color: $button-danger-fg-color;
|
||||
|
|
|
@ -61,9 +61,9 @@ limitations under the License.
|
|||
|
||||
.mx_MFileBody_info {
|
||||
background-color: $message-body-panel-bg-color;
|
||||
border-radius: 4px;
|
||||
width: 270px;
|
||||
padding: 8px;
|
||||
border-radius: 12px;
|
||||
width: 243px; // same width as a playable voice message, accounting for padding
|
||||
padding: 6px 12px;
|
||||
color: $message-body-panel-fg-color;
|
||||
|
||||
.mx_MFileBody_info_icon {
|
||||
|
@ -82,7 +82,7 @@ limitations under the License.
|
|||
mask-position: center;
|
||||
mask-size: cover;
|
||||
mask-image: url('$(res)/img/element-icons/room/composer/attach.svg');
|
||||
background-color: $message-body-panel-fg-color;
|
||||
background-color: $message-body-panel-icon-fg-color;
|
||||
width: 13px;
|
||||
height: 15px;
|
||||
|
||||
|
|
|
@ -65,14 +65,17 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
|
||||
.mx_CallView_voice {
|
||||
.mx_CallView_content {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.mx_CallView_voice {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
background-color: $inverted-bg-color;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.mx_CallView_voice_avatarsContainer {
|
||||
|
@ -109,9 +112,7 @@ limitations under the License.
|
|||
.mx_CallView_video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
z-index: 30;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
|
|
@ -14,21 +14,37 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
.mx_VideoFeed_voice {
|
||||
// We don't want to collide with the call controls that have 52px of height
|
||||
padding-bottom: 52px;
|
||||
background-color: $inverted-bg-color;
|
||||
}
|
||||
|
||||
|
||||
.mx_VideoFeed_remote {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #000;
|
||||
z-index: 50;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
&.mx_VideoFeed_video {
|
||||
background-color: #000;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_VideoFeed_local {
|
||||
width: 25%;
|
||||
height: 25%;
|
||||
max-width: 25%;
|
||||
max-height: 25%;
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 10px;
|
||||
z-index: 100;
|
||||
border-radius: 4px;
|
||||
|
||||
&.mx_VideoFeed_video {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_VideoFeed_mirror {
|
||||
|
|
|
@ -43,14 +43,6 @@ $preview-bar-bg-color: $header-panel-bg-color;
|
|||
$groupFilterPanel-bg-color: rgba(38, 39, 43, 0.82);
|
||||
$inverted-bg-color: $base-color;
|
||||
|
||||
$voice-record-stop-border-color: $quaternary-fg-color;
|
||||
$voice-record-waveform-bg-color: #394049; // "Dark Tile"
|
||||
$voice-record-waveform-fg-color: $secondary-fg-color;
|
||||
$voice-record-waveform-incomplete-fg-color: $quaternary-fg-color;
|
||||
$voice-record-icon-color: $quaternary-fg-color;
|
||||
$voice-playback-button-bg-color: $tertiary-fg-color;
|
||||
$voice-playback-button-fg-color: #21262C; // "Separator"
|
||||
|
||||
// used by AddressSelector
|
||||
$selected-color: $room-highlight-color;
|
||||
|
||||
|
@ -214,9 +206,18 @@ $breadcrumb-placeholder-bg-color: #272c35;
|
|||
|
||||
$user-tile-hover-bg-color: $header-panel-bg-color;
|
||||
|
||||
$message-body-panel-bg-color: #21262c82;
|
||||
$message-body-panel-icon-bg-color: #8e99a4;
|
||||
$message-body-panel-fg-color: $primary-fg-color;
|
||||
$message-body-panel-fg-color: $secondary-fg-color;
|
||||
$message-body-panel-bg-color: #394049; // "Dark Tile"
|
||||
$message-body-panel-icon-fg-color: #21262C; // "Separator"
|
||||
$message-body-panel-icon-bg-color: $tertiary-fg-color;
|
||||
|
||||
$voice-record-stop-border-color: $quaternary-fg-color;
|
||||
$voice-record-waveform-bg-color: $message-body-panel-bg-color;
|
||||
$voice-record-waveform-fg-color: $message-body-panel-fg-color;
|
||||
$voice-record-waveform-incomplete-fg-color: $quaternary-fg-color;
|
||||
$voice-record-icon-color: $quaternary-fg-color;
|
||||
$voice-playback-button-bg-color: $message-body-panel-icon-bg-color;
|
||||
$voice-playback-button-fg-color: $message-body-panel-icon-fg-color;
|
||||
|
||||
// Appearance tab colors
|
||||
$appearance-tab-border-color: $room-highlight-color;
|
||||
|
|
|
@ -124,15 +124,6 @@ $roomsublist-skeleton-ui-bg: linear-gradient(180deg, #3e444c 0%, #3e444c00 100%)
|
|||
|
||||
$groupFilterPanel-divider-color: $roomlist-header-color;
|
||||
|
||||
// See non-legacy dark for variable information
|
||||
$voice-record-stop-border-color: #6F7882;
|
||||
$voice-record-waveform-bg-color: #394049;
|
||||
$voice-record-waveform-fg-color: $secondary-fg-color;
|
||||
$voice-record-waveform-incomplete-fg-color: #6F7882;
|
||||
$voice-record-icon-color: #6F7882;
|
||||
$voice-playback-button-bg-color: $tertiary-fg-color;
|
||||
$voice-playback-button-fg-color: #21262C;
|
||||
|
||||
$roomtile-preview-color: #9e9e9e;
|
||||
$roomtile-default-badge-bg-color: #61708b;
|
||||
$roomtile-selected-bg-color: #1A1D23;
|
||||
|
@ -209,9 +200,19 @@ $breadcrumb-placeholder-bg-color: #272c35;
|
|||
|
||||
$user-tile-hover-bg-color: $header-panel-bg-color;
|
||||
|
||||
$message-body-panel-bg-color: #21262c82;
|
||||
$message-body-panel-icon-bg-color: #8e99a4;
|
||||
$message-body-panel-fg-color: $primary-fg-color;
|
||||
$message-body-panel-fg-color: $secondary-fg-color;
|
||||
$message-body-panel-bg-color: #394049;
|
||||
$message-body-panel-icon-fg-color: $primary-bg-color;
|
||||
$message-body-panel-icon-bg-color: $secondary-fg-color;
|
||||
|
||||
// See non-legacy dark for variable information
|
||||
$voice-record-stop-border-color: #6F7882;
|
||||
$voice-record-waveform-bg-color: $message-body-panel-bg-color;
|
||||
$voice-record-waveform-fg-color: $message-body-panel-fg-color;
|
||||
$voice-record-waveform-incomplete-fg-color: #6F7882;
|
||||
$voice-record-icon-color: #6F7882;
|
||||
$voice-playback-button-bg-color: $tertiary-fg-color;
|
||||
$voice-playback-button-fg-color: #21262C;
|
||||
|
||||
// Appearance tab colors
|
||||
$appearance-tab-border-color: $room-highlight-color;
|
||||
|
|
|
@ -191,17 +191,6 @@ $roomsublist-skeleton-ui-bg: linear-gradient(180deg, #ffffff 0%, #ffffff00 100%)
|
|||
|
||||
$groupFilterPanel-divider-color: $roomlist-header-color;
|
||||
|
||||
// See non-legacy _light for variable information
|
||||
$voice-record-stop-symbol-color: #ff4b55;
|
||||
$voice-record-live-circle-color: #ff4b55;
|
||||
$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-fg-color: $secondary-fg-color;
|
||||
|
||||
$roomtile-preview-color: #9e9e9e;
|
||||
$roomtile-default-badge-bg-color: #61708b;
|
||||
$roomtile-selected-bg-color: #fff;
|
||||
|
@ -334,9 +323,21 @@ $breadcrumb-placeholder-bg-color: #e8eef5;
|
|||
|
||||
$user-tile-hover-bg-color: $header-panel-bg-color;
|
||||
|
||||
$message-body-panel-bg-color: #e3e8f082;
|
||||
$message-body-panel-icon-bg-color: #ffffff;
|
||||
$message-body-panel-fg-color: $muted-fg-color;
|
||||
$message-body-panel-fg-color: $secondary-fg-color;
|
||||
$message-body-panel-bg-color: #E3E8F0;
|
||||
$message-body-panel-icon-fg-color: $secondary-fg-color;
|
||||
$message-body-panel-icon-bg-color: $primary-bg-color;
|
||||
|
||||
// See non-legacy _light for variable information
|
||||
$voice-record-stop-symbol-color: #ff4b55;
|
||||
$voice-record-live-circle-color: #ff4b55;
|
||||
$voice-record-stop-border-color: #E3E8F0;
|
||||
$voice-record-waveform-bg-color: $message-body-panel-bg-color;
|
||||
$voice-record-waveform-fg-color: $message-body-panel-fg-color;
|
||||
$voice-record-waveform-incomplete-fg-color: #C1C6CD;
|
||||
$voice-record-icon-color: $tertiary-fg-color;
|
||||
$voice-playback-button-bg-color: $message-body-panel-icon-bg-color;
|
||||
$voice-playback-button-fg-color: $message-body-panel-icon-fg-color;
|
||||
|
||||
// FontSlider colors
|
||||
$appearance-tab-border-color: $input-darker-bg-color;
|
||||
|
|
|
@ -183,19 +183,6 @@ $roomsublist-skeleton-ui-bg: linear-gradient(180deg, #ffffff 0%, #ffffff00 100%)
|
|||
|
||||
$groupFilterPanel-divider-color: $roomlist-header-color;
|
||||
|
||||
// These two don't change between themes. They are the $warning-color, but we don't
|
||||
// want custom themes to affect them by accident.
|
||||
$voice-record-stop-symbol-color: #ff4b55;
|
||||
$voice-record-live-circle-color: #ff4b55;
|
||||
|
||||
$voice-record-stop-border-color: #E3E8F0; // "Separator"
|
||||
$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-fg-color: $secondary-fg-color;
|
||||
|
||||
$roomtile-preview-color: $secondary-fg-color;
|
||||
$roomtile-default-badge-bg-color: #61708b;
|
||||
$roomtile-selected-bg-color: #FFF;
|
||||
|
@ -335,9 +322,23 @@ $breadcrumb-placeholder-bg-color: #e8eef5;
|
|||
|
||||
$user-tile-hover-bg-color: $header-panel-bg-color;
|
||||
|
||||
$message-body-panel-bg-color: #e3e8f082;
|
||||
$message-body-panel-icon-bg-color: #ffffff;
|
||||
$message-body-panel-fg-color: $muted-fg-color;
|
||||
$message-body-panel-fg-color: $secondary-fg-color;
|
||||
$message-body-panel-bg-color: #E3E8F0; // "Separator"
|
||||
$message-body-panel-icon-fg-color: $secondary-fg-color;
|
||||
$message-body-panel-icon-bg-color: $primary-bg-color;
|
||||
|
||||
// These two don't change between themes. They are the $warning-color, but we don't
|
||||
// want custom themes to affect them by accident.
|
||||
$voice-record-stop-symbol-color: #ff4b55;
|
||||
$voice-record-live-circle-color: #ff4b55;
|
||||
|
||||
$voice-record-stop-border-color: #E3E8F0; // "Separator"
|
||||
$voice-record-waveform-bg-color: $message-body-panel-bg-color;
|
||||
$voice-record-waveform-fg-color: $message-body-panel-fg-color;
|
||||
$voice-record-waveform-incomplete-fg-color: $quaternary-fg-color;
|
||||
$voice-record-icon-color: $tertiary-fg-color;
|
||||
$voice-playback-button-bg-color: $message-body-panel-icon-bg-color;
|
||||
$voice-playback-button-fg-color: $message-body-panel-icon-fg-color;
|
||||
|
||||
// FontSlider colors
|
||||
$appearance-tab-border-color: $input-darker-bg-color;
|
||||
|
|
|
@ -118,6 +118,16 @@ declare global {
|
|||
|
||||
interface HTMLAudioElement {
|
||||
type?: string;
|
||||
// sinkId & setSinkId are experimental and typescript doesn't know about them
|
||||
sinkId: string;
|
||||
setSinkId(outputId: string);
|
||||
}
|
||||
|
||||
interface HTMLVideoElement {
|
||||
type?: string;
|
||||
// sinkId & setSinkId are experimental and typescript doesn't know about them
|
||||
sinkId: string;
|
||||
setSinkId(outputId: string);
|
||||
}
|
||||
|
||||
interface Element {
|
||||
|
|
|
@ -20,6 +20,7 @@ import {Room} from "matrix-js-sdk/src/models/room";
|
|||
|
||||
import DMRoomMap from './utils/DMRoomMap';
|
||||
import {mediaFromMxc} from "./customisations/Media";
|
||||
import SettingsStore from "./settings/SettingsStore";
|
||||
|
||||
export type ResizeMethod = "crop" | "scale";
|
||||
|
||||
|
@ -143,7 +144,7 @@ export function avatarUrlForRoom(room: Room, width: number, height: number, resi
|
|||
}
|
||||
|
||||
// space rooms cannot be DMs so skip the rest
|
||||
if (room.isSpaceRoom()) return null;
|
||||
if (SettingsStore.getValue("feature_spaces") && room.isSpaceRoom()) return null;
|
||||
|
||||
let otherMember = null;
|
||||
const otherUserId = DMRoomMap.shared().getUserIdForRoomId(room.roomId);
|
||||
|
|
|
@ -85,6 +85,7 @@ import { Action } from './dispatcher/actions';
|
|||
import VoipUserMapper from './VoipUserMapper';
|
||||
import { addManagedHybridWidget, isManagedHybridWidgetEnabled } from './widgets/ManagedHybrid';
|
||||
import { randomUppercaseString, randomLowercaseString } from "matrix-js-sdk/src/randomstring";
|
||||
import EventEmitter from 'events';
|
||||
import SdkConfig from './SdkConfig';
|
||||
import { ensureDMExists, findDMForUser } from './createRoom';
|
||||
|
||||
|
@ -138,22 +139,12 @@ export enum PlaceCallType {
|
|||
ScreenSharing = 'screensharing',
|
||||
}
|
||||
|
||||
function getRemoteAudioElement(): HTMLAudioElement {
|
||||
// this needs to be somewhere at the top of the DOM which
|
||||
// always exists to avoid audio interruptions.
|
||||
// Might as well just use DOM.
|
||||
const remoteAudioElement = document.getElementById("remoteAudio") as HTMLAudioElement;
|
||||
if (!remoteAudioElement) {
|
||||
console.error(
|
||||
"Failed to find remoteAudio element - cannot play audio!" +
|
||||
"You need to add an <audio/> to the DOM.",
|
||||
);
|
||||
return null;
|
||||
}
|
||||
return remoteAudioElement;
|
||||
export enum CallHandlerEvent {
|
||||
CallsChanged = "calls_changed",
|
||||
CallChangeRoom = "call_change_room",
|
||||
}
|
||||
|
||||
export default class CallHandler {
|
||||
export default class CallHandler extends EventEmitter {
|
||||
private calls = new Map<string, MatrixCall>(); // roomId -> call
|
||||
// Calls started as an attended transfer, ie. with the intention of transferring another
|
||||
// call with a different party to this one.
|
||||
|
@ -514,6 +505,7 @@ export default class CallHandler {
|
|||
}
|
||||
|
||||
this.calls.set(mappedRoomId, newCall);
|
||||
this.emit(CallHandlerEvent.CallsChanged, this.calls);
|
||||
this.setCallListeners(newCall);
|
||||
this.setCallState(newCall, newCall.state);
|
||||
});
|
||||
|
@ -546,10 +538,7 @@ export default class CallHandler {
|
|||
this.removeCallForRoom(mappedRoomId);
|
||||
mappedRoomId = newMappedRoomId;
|
||||
this.calls.set(mappedRoomId, call);
|
||||
dis.dispatch({
|
||||
action: Action.CallChangeRoom,
|
||||
call,
|
||||
});
|
||||
this.emit(CallHandlerEvent.CallChangeRoom, call);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -598,11 +587,6 @@ export default class CallHandler {
|
|||
}
|
||||
}
|
||||
|
||||
private setCallAudioElement(call: MatrixCall) {
|
||||
const audioElement = getRemoteAudioElement();
|
||||
if (audioElement) call.setRemoteAudioElement(audioElement);
|
||||
}
|
||||
|
||||
private setCallState(call: MatrixCall, status: CallState) {
|
||||
const mappedRoomId = CallHandler.sharedInstance().roomIdForCall(call);
|
||||
|
||||
|
@ -619,6 +603,7 @@ export default class CallHandler {
|
|||
|
||||
private removeCallForRoom(roomId: string) {
|
||||
this.calls.delete(roomId);
|
||||
this.emit(CallHandlerEvent.CallsChanged, this.calls);
|
||||
}
|
||||
|
||||
private showICEFallbackPrompt() {
|
||||
|
@ -679,11 +664,7 @@ export default class CallHandler {
|
|||
}, null, true);
|
||||
}
|
||||
|
||||
private async placeCall(
|
||||
roomId: string, type: PlaceCallType,
|
||||
localElement: HTMLVideoElement, remoteElement: HTMLVideoElement,
|
||||
transferee: MatrixCall,
|
||||
) {
|
||||
private async placeCall(roomId: string, type: PlaceCallType, transferee: MatrixCall) {
|
||||
Analytics.trackEvent('voip', 'placeCall', 'type', type);
|
||||
CountlyAnalytics.instance.trackStartCall(roomId, type === PlaceCallType.Video, false);
|
||||
|
||||
|
@ -695,22 +676,19 @@ export default class CallHandler {
|
|||
const call = MatrixClientPeg.get().createCall(mappedRoomId);
|
||||
|
||||
this.calls.set(roomId, call);
|
||||
this.emit(CallHandlerEvent.CallsChanged, this.calls);
|
||||
if (transferee) {
|
||||
this.transferees[call.callId] = transferee;
|
||||
}
|
||||
|
||||
this.setCallListeners(call);
|
||||
this.setCallAudioElement(call);
|
||||
|
||||
this.setActiveCallRoomId(roomId);
|
||||
|
||||
if (type === PlaceCallType.Voice) {
|
||||
call.placeVoiceCall();
|
||||
} else if (type === 'video') {
|
||||
call.placeVideoCall(
|
||||
remoteElement,
|
||||
localElement,
|
||||
);
|
||||
call.placeVideoCall();
|
||||
} else if (type === PlaceCallType.ScreenSharing) {
|
||||
const screenCapErrorString = PlatformPeg.get().screenCaptureErrorString();
|
||||
if (screenCapErrorString) {
|
||||
|
@ -724,13 +702,12 @@ export default class CallHandler {
|
|||
}
|
||||
|
||||
call.placeScreenSharingCall(
|
||||
remoteElement,
|
||||
localElement,
|
||||
async (): Promise<DesktopCapturerSource> => {
|
||||
const {finished} = Modal.createDialog(DesktopCapturerSourcePicker);
|
||||
const [source] = await finished;
|
||||
return source;
|
||||
});
|
||||
},
|
||||
);
|
||||
} else {
|
||||
console.error("Unknown conf call type: " + type);
|
||||
}
|
||||
|
@ -787,17 +764,12 @@ export default class CallHandler {
|
|||
} else if (members.length === 2) {
|
||||
console.info(`Place ${payload.type} call in ${payload.room_id}`);
|
||||
|
||||
this.placeCall(
|
||||
payload.room_id, payload.type, payload.local_element, payload.remote_element,
|
||||
payload.transferee,
|
||||
);
|
||||
this.placeCall(payload.room_id, payload.type, payload.transferee);
|
||||
} else { // > 2
|
||||
dis.dispatch({
|
||||
action: "place_conference_call",
|
||||
room_id: payload.room_id,
|
||||
type: payload.type,
|
||||
remote_element: payload.remote_element,
|
||||
local_element: payload.local_element,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -833,6 +805,7 @@ export default class CallHandler {
|
|||
|
||||
Analytics.trackEvent('voip', 'receiveCall', 'type', call.type);
|
||||
this.calls.set(mappedRoomId, call)
|
||||
this.emit(CallHandlerEvent.CallsChanged, this.calls);
|
||||
this.setCallListeners(call);
|
||||
|
||||
// get ready to send encrypted events in the room, so if the user does answer
|
||||
|
@ -875,7 +848,6 @@ export default class CallHandler {
|
|||
|
||||
const call = this.calls.get(payload.room_id);
|
||||
call.answer();
|
||||
this.setCallAudioElement(call);
|
||||
this.setActiveCallRoomId(payload.room_id);
|
||||
CountlyAnalytics.instance.trackJoinCall(payload.room_id, call.type === CallType.Video, false);
|
||||
dis.dispatch({
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
import SettingsStore from "./settings/SettingsStore";
|
||||
import {SettingLevel} from "./settings/SettingLevel";
|
||||
import {setMatrixCallAudioInput, setMatrixCallAudioOutput, setMatrixCallVideoInput} from "matrix-js-sdk/src/matrix";
|
||||
import {setMatrixCallAudioInput, setMatrixCallVideoInput} from "matrix-js-sdk/src/matrix";
|
||||
|
||||
export default {
|
||||
hasAnyLabeledDevices: async function() {
|
||||
|
@ -50,18 +50,15 @@ export default {
|
|||
},
|
||||
|
||||
loadDevices: function() {
|
||||
const audioOutDeviceId = SettingsStore.getValue("webrtc_audiooutput");
|
||||
const audioDeviceId = SettingsStore.getValue("webrtc_audioinput");
|
||||
const videoDeviceId = SettingsStore.getValue("webrtc_videoinput");
|
||||
|
||||
setMatrixCallAudioOutput(audioOutDeviceId);
|
||||
setMatrixCallAudioInput(audioDeviceId);
|
||||
setMatrixCallVideoInput(videoDeviceId);
|
||||
},
|
||||
|
||||
setAudioOutput: function(deviceId) {
|
||||
SettingsStore.setValue("webrtc_audiooutput", null, SettingLevel.DEVICE, deviceId);
|
||||
setMatrixCallAudioOutput(deviceId);
|
||||
},
|
||||
|
||||
setAudioInput: function(deviceId) {
|
||||
|
|
|
@ -59,6 +59,9 @@ import { getKeyBindingsManager, NavigationAction, RoomAction } from '../../KeyBi
|
|||
import { IOpts } from "../../createRoom";
|
||||
import SpacePanel from "../views/spaces/SpacePanel";
|
||||
import {replaceableComponent} from "../../utils/replaceableComponent";
|
||||
import CallHandler, { CallHandlerEvent } from '../../CallHandler';
|
||||
import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
||||
import AudioFeedArrayForCall from '../views/voip/AudioFeedArrayForCall';
|
||||
|
||||
// We need to fetch each pinned message individually (if we don't already have it)
|
||||
// so each pinned message may trigger a request. Limit the number per room for sanity.
|
||||
|
@ -119,6 +122,7 @@ interface IState {
|
|||
usageLimitEventContent?: IUsageLimit;
|
||||
usageLimitEventTs?: number;
|
||||
useCompactLayout: boolean;
|
||||
activeCalls: Array<MatrixCall>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -160,6 +164,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||
// use compact timeline view
|
||||
useCompactLayout: SettingsStore.getValue('useCompactLayout'),
|
||||
usageLimitDismissed: false,
|
||||
activeCalls: [],
|
||||
};
|
||||
|
||||
// stash the MatrixClient in case we log out before we are unmounted
|
||||
|
@ -175,6 +180,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||
|
||||
componentDidMount() {
|
||||
document.addEventListener('keydown', this._onNativeKeyDown, false);
|
||||
CallHandler.sharedInstance().addListener(CallHandlerEvent.CallsChanged, this.onCallsChanged);
|
||||
|
||||
this._updateServerNoticeEvents();
|
||||
|
||||
|
@ -199,6 +205,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener('keydown', this._onNativeKeyDown, false);
|
||||
CallHandler.sharedInstance().removeListener(CallHandlerEvent.CallsChanged, this.onCallsChanged);
|
||||
this._matrixClient.removeListener("accountData", this.onAccountData);
|
||||
this._matrixClient.removeListener("sync", this.onSync);
|
||||
this._matrixClient.removeListener("RoomState.events", this.onRoomStateEvents);
|
||||
|
@ -206,6 +213,12 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||
this.resizer.detach();
|
||||
}
|
||||
|
||||
private onCallsChanged = () => {
|
||||
this.setState({
|
||||
activeCalls: CallHandler.sharedInstance().getAllActiveCalls(),
|
||||
});
|
||||
};
|
||||
|
||||
// Child components assume that the client peg will not be null, so give them some
|
||||
// sort of assurance here by only allowing a re-render if the client is truthy.
|
||||
//
|
||||
|
@ -661,6 +674,12 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||
bodyClasses += ' mx_MatrixChat_useCompactLayout';
|
||||
}
|
||||
|
||||
const audioFeedArraysForCalls = this.state.activeCalls.map((call) => {
|
||||
return (
|
||||
<AudioFeedArrayForCall call={call} key={call.callId} />
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<MatrixClientContext.Provider value={this._matrixClient}>
|
||||
<div
|
||||
|
@ -685,6 +704,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||
<CallContainer />
|
||||
<NonUrgentToastContainer />
|
||||
<HostSignupContainer />
|
||||
{audioFeedArraysForCalls}
|
||||
</MatrixClientContext.Provider>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1096,7 +1096,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
|
||||
private leaveRoomWarnings(roomId: string) {
|
||||
const roomToLeave = MatrixClientPeg.get().getRoom(roomId);
|
||||
const isSpace = roomToLeave?.isSpaceRoom();
|
||||
const isSpace = SettingsStore.getValue("feature_spaces") && roomToLeave?.isSpaceRoom();
|
||||
// Show a warning if there are additional complications.
|
||||
const warnings = [];
|
||||
|
||||
|
@ -1135,7 +1135,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
const roomToLeave = MatrixClientPeg.get().getRoom(roomId);
|
||||
const warnings = this.leaveRoomWarnings(roomId);
|
||||
|
||||
const isSpace = roomToLeave?.isSpaceRoom();
|
||||
const isSpace = SettingsStore.getValue("feature_spaces") && roomToLeave?.isSpaceRoom();
|
||||
Modal.createTrackedDialog(isSpace ? "Leave space" : "Leave room", '', QuestionDialog, {
|
||||
title: isSpace ? _t("Leave space") : _t("Leave room"),
|
||||
description: (
|
||||
|
|
|
@ -35,6 +35,7 @@ import {Action} from "../../dispatcher/actions";
|
|||
import RoomSummaryCard from "../views/right_panel/RoomSummaryCard";
|
||||
import WidgetCard from "../views/right_panel/WidgetCard";
|
||||
import {replaceableComponent} from "../../utils/replaceableComponent";
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
|
||||
@replaceableComponent("structures.RightPanel")
|
||||
export default class RightPanel extends React.Component {
|
||||
|
@ -85,7 +86,9 @@ export default class RightPanel extends React.Component {
|
|||
return RightPanelPhases.GroupMemberList;
|
||||
}
|
||||
return rps.groupPanelPhase;
|
||||
} else if (this.props.room?.isSpaceRoom() && !RIGHT_PANEL_SPACE_PHASES.includes(rps.roomPanelPhase)) {
|
||||
} else if (SettingsStore.getValue("feature_spaces") && this.props.room?.isSpaceRoom()
|
||||
&& !RIGHT_PANEL_SPACE_PHASES.includes(rps.roomPanelPhase)
|
||||
) {
|
||||
return RightPanelPhases.SpaceMemberList;
|
||||
} else if (userForPanel) {
|
||||
// XXX FIXME AAAAAARGH: What is going on with this class!? It takes some of its state
|
||||
|
|
|
@ -1750,7 +1750,10 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
const myMembership = this.state.room.getMyMembership();
|
||||
if (myMembership === "invite" && !this.state.room.isSpaceRoom()) { // SpaceRoomView handles invites itself
|
||||
if (myMembership === "invite"
|
||||
// SpaceRoomView handles invites itself
|
||||
&& (!SettingsStore.getValue("feature_spaces") || !this.state.room.isSpaceRoom())
|
||||
) {
|
||||
if (this.state.joining || this.state.rejecting) {
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
|
@ -1892,7 +1895,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
room={this.state.room}
|
||||
/>
|
||||
);
|
||||
if (!this.state.canPeek && !this.state.room?.isSpaceRoom()) {
|
||||
if (!this.state.canPeek && (!SettingsStore.getValue("feature_spaces") || !this.state.room?.isSpaceRoom())) {
|
||||
return (
|
||||
<div className="mx_RoomView">
|
||||
{ previewBar }
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {useMemo, useState} from "react";
|
||||
import React, {ReactNode, useMemo, useState} from "react";
|
||||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
import {MatrixClient} from "matrix-js-sdk/src/client";
|
||||
import {EventType, RoomType} from "matrix-js-sdk/src/@types/event";
|
||||
|
@ -24,7 +24,7 @@ import {sortBy} from "lodash";
|
|||
import {MatrixClientPeg} from "../../MatrixClientPeg";
|
||||
import dis from "../../dispatcher/dispatcher";
|
||||
import {_t} from "../../languageHandler";
|
||||
import AccessibleButton from "../views/elements/AccessibleButton";
|
||||
import AccessibleButton, {ButtonEvent} from "../views/elements/AccessibleButton";
|
||||
import BaseDialog from "../views/dialogs/BaseDialog";
|
||||
import Spinner from "../views/elements/Spinner";
|
||||
import SearchBox from "./SearchBox";
|
||||
|
@ -40,11 +40,13 @@ import InfoTooltip from "../views/elements/InfoTooltip";
|
|||
import TextWithTooltip from "../views/elements/TextWithTooltip";
|
||||
import {useStateToggle} from "../../hooks/useStateToggle";
|
||||
import {getOrder} from "../../stores/SpaceStore";
|
||||
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
|
||||
|
||||
interface IHierarchyProps {
|
||||
space: Room;
|
||||
initialText?: string;
|
||||
refreshToken?: any;
|
||||
additionalButtons?: ReactNode;
|
||||
showRoom(room: ISpaceSummaryRoom, viaServers?: string[], autoJoin?: boolean): void;
|
||||
}
|
||||
|
||||
|
@ -107,8 +109,16 @@ const Tile: React.FC<ITileProps> = ({
|
|||
const cliRoom = cli.getRoom(room.room_id);
|
||||
const myMembership = cliRoom?.getMyMembership();
|
||||
|
||||
const onPreviewClick = () => onViewRoomClick(false);
|
||||
const onJoinClick = () => onViewRoomClick(true);
|
||||
const onPreviewClick = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
onViewRoomClick(false);
|
||||
}
|
||||
const onJoinClick = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
onViewRoomClick(true);
|
||||
}
|
||||
|
||||
let button;
|
||||
if (myMembership === "join") {
|
||||
|
@ -355,6 +365,7 @@ export const SpaceHierarchy: React.FC<IHierarchyProps> = ({
|
|||
initialText = "",
|
||||
showRoom,
|
||||
refreshToken,
|
||||
additionalButtons,
|
||||
children,
|
||||
}) => {
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
@ -420,78 +431,83 @@ export const SpaceHierarchy: React.FC<IHierarchyProps> = ({
|
|||
countsStr = _t("%(count)s rooms", { count: numRooms, numSpaces });
|
||||
}
|
||||
|
||||
let editSection;
|
||||
let manageButtons;
|
||||
if (space.getMyMembership() === "join" && space.currentState.maySendStateEvent(EventType.SpaceChild, userId)) {
|
||||
const selectedRelations = Array.from(selected.keys()).flatMap(parentId => {
|
||||
return [...selected.get(parentId).values()].map(childId => [parentId, childId]) as [string, string][];
|
||||
});
|
||||
|
||||
let buttons;
|
||||
if (selectedRelations.length) {
|
||||
const selectionAllSuggested = selectedRelations.every(([parentId, childId]) => {
|
||||
return parentChildMap.get(parentId)?.get(childId)?.content.suggested;
|
||||
});
|
||||
const selectionAllSuggested = selectedRelations.every(([parentId, childId]) => {
|
||||
return parentChildMap.get(parentId)?.get(childId)?.content.suggested;
|
||||
});
|
||||
|
||||
const disabled = removing || saving;
|
||||
const disabled = !selectedRelations.length || removing || saving;
|
||||
|
||||
buttons = <>
|
||||
<AccessibleButton
|
||||
onClick={async () => {
|
||||
setRemoving(true);
|
||||
try {
|
||||
for (const [parentId, childId] of selectedRelations) {
|
||||
await cli.sendStateEvent(parentId, EventType.SpaceChild, {}, childId);
|
||||
parentChildMap.get(parentId).get(childId).content = {};
|
||||
parentChildMap.set(parentId, new Map(parentChildMap.get(parentId)));
|
||||
}
|
||||
} catch (e) {
|
||||
setError(_t("Failed to remove some rooms. Try again later"));
|
||||
}
|
||||
setRemoving(false);
|
||||
}}
|
||||
kind="danger_outline"
|
||||
disabled={disabled}
|
||||
>
|
||||
{ removing ? _t("Removing...") : _t("Remove") }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
onClick={async () => {
|
||||
setSaving(true);
|
||||
try {
|
||||
for (const [parentId, childId] of selectedRelations) {
|
||||
const suggested = !selectionAllSuggested;
|
||||
const existingContent = parentChildMap.get(parentId)?.get(childId)?.content;
|
||||
if (!existingContent || existingContent.suggested === suggested) continue;
|
||||
|
||||
const content = {
|
||||
...existingContent,
|
||||
suggested: !selectionAllSuggested,
|
||||
};
|
||||
|
||||
await cli.sendStateEvent(parentId, EventType.SpaceChild, content, childId);
|
||||
|
||||
parentChildMap.get(parentId).get(childId).content = content;
|
||||
parentChildMap.set(parentId, new Map(parentChildMap.get(parentId)));
|
||||
}
|
||||
} catch (e) {
|
||||
setError("Failed to update some suggestions. Try again later");
|
||||
}
|
||||
setSaving(false);
|
||||
}}
|
||||
kind="primary_outline"
|
||||
disabled={disabled}
|
||||
>
|
||||
{ saving
|
||||
? _t("Saving...")
|
||||
: (selectionAllSuggested ? _t("Mark as not suggested") : _t("Mark as suggested"))
|
||||
}
|
||||
</AccessibleButton>
|
||||
</>;
|
||||
let Button: React.ComponentType<React.ComponentProps<typeof AccessibleButton>> = AccessibleButton;
|
||||
let props = {};
|
||||
if (!selectedRelations.length) {
|
||||
Button = AccessibleTooltipButton;
|
||||
props = {
|
||||
tooltip: _t("Select a room below first"),
|
||||
yOffset: -40,
|
||||
};
|
||||
}
|
||||
|
||||
editSection = <span>
|
||||
{ buttons }
|
||||
</span>;
|
||||
manageButtons = <>
|
||||
<Button
|
||||
{...props}
|
||||
onClick={async () => {
|
||||
setRemoving(true);
|
||||
try {
|
||||
for (const [parentId, childId] of selectedRelations) {
|
||||
await cli.sendStateEvent(parentId, EventType.SpaceChild, {}, childId);
|
||||
parentChildMap.get(parentId).get(childId).content = {};
|
||||
parentChildMap.set(parentId, new Map(parentChildMap.get(parentId)));
|
||||
}
|
||||
} catch (e) {
|
||||
setError(_t("Failed to remove some rooms. Try again later"));
|
||||
}
|
||||
setRemoving(false);
|
||||
}}
|
||||
kind="danger_outline"
|
||||
disabled={disabled}
|
||||
>
|
||||
{ removing ? _t("Removing...") : _t("Remove") }
|
||||
</Button>
|
||||
<Button
|
||||
{...props}
|
||||
onClick={async () => {
|
||||
setSaving(true);
|
||||
try {
|
||||
for (const [parentId, childId] of selectedRelations) {
|
||||
const suggested = !selectionAllSuggested;
|
||||
const existingContent = parentChildMap.get(parentId)?.get(childId)?.content;
|
||||
if (!existingContent || existingContent.suggested === suggested) continue;
|
||||
|
||||
const content = {
|
||||
...existingContent,
|
||||
suggested: !selectionAllSuggested,
|
||||
};
|
||||
|
||||
await cli.sendStateEvent(parentId, EventType.SpaceChild, content, childId);
|
||||
|
||||
parentChildMap.get(parentId).get(childId).content = content;
|
||||
parentChildMap.set(parentId, new Map(parentChildMap.get(parentId)));
|
||||
}
|
||||
} catch (e) {
|
||||
setError("Failed to update some suggestions. Try again later");
|
||||
}
|
||||
setSaving(false);
|
||||
}}
|
||||
kind="primary_outline"
|
||||
disabled={disabled}
|
||||
>
|
||||
{ saving
|
||||
? _t("Saving...")
|
||||
: (selectionAllSuggested ? _t("Mark as not suggested") : _t("Mark as suggested"))
|
||||
}
|
||||
</Button>
|
||||
</>;
|
||||
}
|
||||
|
||||
let results;
|
||||
|
@ -537,7 +553,10 @@ export const SpaceHierarchy: React.FC<IHierarchyProps> = ({
|
|||
content = <>
|
||||
<div className="mx_SpaceRoomDirectory_listHeader">
|
||||
{ countsStr }
|
||||
{ editSection }
|
||||
<span>
|
||||
{ additionalButtons }
|
||||
{ manageButtons }
|
||||
</span>
|
||||
</div>
|
||||
{ error && <div className="mx_SpaceRoomDirectory_error">
|
||||
{ error }
|
||||
|
|
|
@ -54,6 +54,12 @@ import FacePile from "../views/elements/FacePile";
|
|||
import {AddExistingToSpace} from "../views/dialogs/AddExistingToSpaceDialog";
|
||||
import {sleep} from "../../utils/promise";
|
||||
import {calculateRoomVia} from "../../utils/permalinks/Permalinks";
|
||||
import {ChevronFace, ContextMenuButton, useContextMenu} from "./ContextMenu";
|
||||
import IconizedContextMenu, {
|
||||
IconizedContextMenuOption,
|
||||
IconizedContextMenuOptionList,
|
||||
} from "../views/context_menus/IconizedContextMenu";
|
||||
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
|
||||
import {BetaPill} from "../views/beta/BetaCard";
|
||||
import {USER_LABS_TAB} from "../views/dialogs/UserSettingsDialog";
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
|
@ -262,6 +268,67 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
|
|||
</div>;
|
||||
};
|
||||
|
||||
const SpaceLandingAddButton = ({ space, onNewRoomAdded }) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu();
|
||||
|
||||
let contextMenu;
|
||||
if (menuDisplayed) {
|
||||
const rect = handle.current.getBoundingClientRect();
|
||||
contextMenu = <IconizedContextMenu
|
||||
left={rect.left + window.pageXOffset + 0}
|
||||
top={rect.bottom + window.pageYOffset + 8}
|
||||
chevronFace={ChevronFace.None}
|
||||
onFinished={closeMenu}
|
||||
className="mx_RoomTile_contextMenu"
|
||||
compact
|
||||
>
|
||||
<IconizedContextMenuOptionList first>
|
||||
<IconizedContextMenuOption
|
||||
label={_t("Create new room")}
|
||||
iconClassName="mx_RoomList_iconPlus"
|
||||
onClick={async (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
closeMenu();
|
||||
|
||||
if (await showCreateNewRoom(cli, space)) {
|
||||
onNewRoomAdded();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<IconizedContextMenuOption
|
||||
label={_t("Add existing room")}
|
||||
iconClassName="mx_RoomList_iconHash"
|
||||
onClick={async (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
closeMenu();
|
||||
|
||||
const [added] = await showAddExistingRooms(cli, space);
|
||||
if (added) {
|
||||
onNewRoomAdded();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</IconizedContextMenuOptionList>
|
||||
</IconizedContextMenu>;
|
||||
}
|
||||
|
||||
return <>
|
||||
<ContextMenuButton
|
||||
kind="primary"
|
||||
inputRef={handle}
|
||||
onClick={openMenu}
|
||||
isExpanded={menuDisplayed}
|
||||
label={_t("Add")}
|
||||
>
|
||||
{ _t("Add") }
|
||||
</ContextMenuButton>
|
||||
{ contextMenu }
|
||||
</>;
|
||||
};
|
||||
|
||||
const SpaceLanding = ({ space }) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
const myMembership = useMyRoomMembership(space);
|
||||
|
@ -286,32 +353,20 @@ const SpaceLanding = ({ space }) => {
|
|||
|
||||
const [refreshToken, forceUpdate] = useStateToggle(false);
|
||||
|
||||
let addRoomButtons;
|
||||
let addRoomButton;
|
||||
if (canAddRooms) {
|
||||
addRoomButtons = <React.Fragment>
|
||||
<AccessibleButton className="mx_SpaceRoomView_landing_addButton" onClick={async () => {
|
||||
const [added] = await showAddExistingRooms(cli, space);
|
||||
if (added) {
|
||||
forceUpdate();
|
||||
}
|
||||
}}>
|
||||
{ _t("Add existing rooms & spaces") }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton className="mx_SpaceRoomView_landing_createButton" onClick={() => {
|
||||
showCreateNewRoom(cli, space);
|
||||
}}>
|
||||
{ _t("Create a new room") }
|
||||
</AccessibleButton>
|
||||
</React.Fragment>;
|
||||
addRoomButton = <SpaceLandingAddButton space={space} onNewRoomAdded={forceUpdate} />;
|
||||
}
|
||||
|
||||
let settingsButton;
|
||||
if (shouldShowSpaceSettings(cli, space)) {
|
||||
settingsButton = <AccessibleButton className="mx_SpaceRoomView_landing_settingsButton" onClick={() => {
|
||||
showSpaceSettings(cli, space);
|
||||
}}>
|
||||
{ _t("Settings") }
|
||||
</AccessibleButton>;
|
||||
settingsButton = <AccessibleTooltipButton
|
||||
className="mx_SpaceRoomView_landing_settingsButton"
|
||||
onClick={() => {
|
||||
showSpaceSettings(cli, space);
|
||||
}}
|
||||
title={_t("Settings")}
|
||||
/>;
|
||||
}
|
||||
|
||||
const onMembersClick = () => {
|
||||
|
@ -338,17 +393,19 @@ const SpaceLanding = ({ space }) => {
|
|||
<SpaceInfo space={space} />
|
||||
<FacePile room={space} onlyKnownUsers={false} numShown={7} onClick={onMembersClick} />
|
||||
{ inviteButton }
|
||||
{ settingsButton }
|
||||
</div>
|
||||
<div className="mx_SpaceRoomView_landing_topic">
|
||||
<RoomTopic room={space} />
|
||||
</div>
|
||||
<hr />
|
||||
<div className="mx_SpaceRoomView_landing_adminButtons">
|
||||
{ addRoomButtons }
|
||||
{ settingsButton }
|
||||
</div>
|
||||
|
||||
<SpaceHierarchy space={space} showRoom={showRoom} refreshToken={refreshToken} />
|
||||
<SpaceHierarchy
|
||||
space={space}
|
||||
showRoom={showRoom}
|
||||
refreshToken={refreshToken}
|
||||
additionalButtons={addRoomButton}
|
||||
/>
|
||||
</div>;
|
||||
};
|
||||
|
||||
|
|
|
@ -1312,7 +1312,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
goButtonFn = this._startDm;
|
||||
} else if (this.props.kind === KIND_INVITE) {
|
||||
const room = MatrixClientPeg.get()?.getRoom(this.props.roomId);
|
||||
const isSpace = room?.isSpaceRoom();
|
||||
const isSpace = SettingsStore.getValue("feature_spaces") && room?.isSpaceRoom();
|
||||
title = isSpace
|
||||
? _t("Invite to %(spaceName)s", {
|
||||
spaceName: room.name || _t("Unnamed Space"),
|
||||
|
|
|
@ -440,7 +440,7 @@ const UserOptionsSection: React.FC<{
|
|||
);
|
||||
};
|
||||
|
||||
const warnSelfDemote = async (isSpace) => {
|
||||
const warnSelfDemote = async (isSpace: boolean) => {
|
||||
const {finished} = Modal.createTrackedDialog('Demoting Self', '', QuestionDialog, {
|
||||
title: _t("Demote yourself?"),
|
||||
description:
|
||||
|
@ -727,7 +727,7 @@ const MuteToggleButton: React.FC<IBaseRoomProps> = ({member, room, powerLevels,
|
|||
// if muting self, warn as it may be irreversible
|
||||
if (target === cli.getUserId()) {
|
||||
try {
|
||||
if (!(await warnSelfDemote(room?.isSpaceRoom()))) return;
|
||||
if (!(await warnSelfDemote(SettingsStore.getValue("feature_spaces") && room?.isSpaceRoom()))) return;
|
||||
} catch (e) {
|
||||
console.error("Failed to warn about self demotion: ", e);
|
||||
return;
|
||||
|
@ -816,7 +816,7 @@ const RoomAdminToolsContainer: React.FC<IBaseRoomProps> = ({
|
|||
if (canAffectUser && me.powerLevel >= kickPowerLevel) {
|
||||
kickButton = <RoomKickButton member={member} startUpdating={startUpdating} stopUpdating={stopUpdating} />;
|
||||
}
|
||||
if (me.powerLevel >= redactPowerLevel && !room.isSpaceRoom()) {
|
||||
if (me.powerLevel >= redactPowerLevel && (!SettingsStore.getValue("feature_spaces") || !room.isSpaceRoom())) {
|
||||
redactButton = (
|
||||
<RedactMessagesButton member={member} startUpdating={startUpdating} stopUpdating={stopUpdating} />
|
||||
);
|
||||
|
@ -1095,7 +1095,7 @@ const PowerLevelEditor: React.FC<{
|
|||
} else if (myUserId === target) {
|
||||
// If we are changing our own PL it can only ever be decreasing, which we cannot reverse.
|
||||
try {
|
||||
if (!(await warnSelfDemote(room?.isSpaceRoom()))) return;
|
||||
if (!(await warnSelfDemote(SettingsStore.getValue("feature_spaces") && room?.isSpaceRoom()))) return;
|
||||
} catch (e) {
|
||||
console.error("Failed to warn about self demotion: ", e);
|
||||
}
|
||||
|
@ -1325,10 +1325,10 @@ const BasicUserInfo: React.FC<{
|
|||
if (!isRoomEncrypted) {
|
||||
if (!cryptoEnabled) {
|
||||
text = _t("This client does not support end-to-end encryption.");
|
||||
} else if (room && !room.isSpaceRoom()) {
|
||||
} else if (room && (!SettingsStore.getValue("feature_spaces") || !room.isSpaceRoom())) {
|
||||
text = _t("Messages in this room are not end-to-end encrypted.");
|
||||
}
|
||||
} else if (!room.isSpaceRoom()) {
|
||||
} else if (!SettingsStore.getValue("feature_spaces") || !room.isSpaceRoom()) {
|
||||
text = _t("Messages in this room are end-to-end encrypted.");
|
||||
}
|
||||
|
||||
|
@ -1405,7 +1405,7 @@ const BasicUserInfo: React.FC<{
|
|||
canInvite={roomPermissions.canInvite}
|
||||
isIgnored={isIgnored}
|
||||
member={member}
|
||||
isSpace={room?.isSpaceRoom()}
|
||||
isSpace={SettingsStore.getValue("feature_spaces") && room?.isSpaceRoom()}
|
||||
/>
|
||||
|
||||
{ adminToolsContainer }
|
||||
|
@ -1567,7 +1567,7 @@ const UserInfo: React.FC<Props> = ({
|
|||
previousPhase = RightPanelPhases.RoomMemberInfo;
|
||||
refireParams = {member: member};
|
||||
} else if (room) {
|
||||
previousPhase = previousPhase = room.isSpaceRoom()
|
||||
previousPhase = previousPhase = SettingsStore.getValue("feature_spaces") && room.isSpaceRoom()
|
||||
? RightPanelPhases.SpaceMemberList
|
||||
: RightPanelPhases.RoomMemberList;
|
||||
}
|
||||
|
@ -1616,7 +1616,7 @@ const UserInfo: React.FC<Props> = ({
|
|||
}
|
||||
|
||||
let scopeHeader;
|
||||
if (room?.isSpaceRoom()) {
|
||||
if (SettingsStore.getValue("feature_spaces") && room?.isSpaceRoom()) {
|
||||
scopeHeader = <div className="mx_RightPanel_scopeHeader">
|
||||
<RoomAvatar room={room} height={32} width={32} />
|
||||
<RoomName room={room} />
|
||||
|
|
|
@ -30,6 +30,7 @@ import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
|
|||
import RoomAvatar from "../avatars/RoomAvatar";
|
||||
import RoomName from "../elements/RoomName";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
const INITIAL_LOAD_NUM_MEMBERS = 30;
|
||||
const INITIAL_LOAD_NUM_INVITED = 5;
|
||||
|
@ -460,7 +461,7 @@ export default class MemberList extends React.Component {
|
|||
const chat = CommunityPrototypeStore.instance.getSelectedCommunityGeneralChat();
|
||||
if (chat && chat.roomId === this.props.roomId) {
|
||||
inviteButtonText = _t("Invite to this community");
|
||||
} else if (room.isSpaceRoom()) {
|
||||
} else if (SettingsStore.getValue("feature_spaces") && room.isSpaceRoom()) {
|
||||
inviteButtonText = _t("Invite to this space");
|
||||
}
|
||||
|
||||
|
@ -492,7 +493,7 @@ export default class MemberList extends React.Component {
|
|||
let previousPhase = RightPanelPhases.RoomSummary;
|
||||
// We have no previousPhase for when viewing a MemberList from a Space
|
||||
let scopeHeader;
|
||||
if (room?.isSpaceRoom()) {
|
||||
if (SettingsStore.getValue("feature_spaces") && room?.isSpaceRoom()) {
|
||||
previousPhase = undefined;
|
||||
scopeHeader = <div className="mx_RightPanel_scopeHeader">
|
||||
<RoomAvatar room={room} height={32} width={32} />
|
||||
|
|
|
@ -26,6 +26,7 @@ import {isValid3pidInvite} from "../../../RoomInvite";
|
|||
import RoomAvatar from "../avatars/RoomAvatar";
|
||||
import RoomName from "../elements/RoomName";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
interface IProps {
|
||||
event: MatrixEvent;
|
||||
|
@ -135,7 +136,7 @@ export default class ThirdPartyMemberInfo extends React.Component<IProps, IState
|
|||
}
|
||||
|
||||
let scopeHeader;
|
||||
if (this.room.isSpaceRoom()) {
|
||||
if (SettingsStore.getValue("feature_spaces") && this.room.isSpaceRoom()) {
|
||||
scopeHeader = <div className="mx_RightPanel_scopeHeader">
|
||||
<RoomAvatar room={this.room} height={32} width={32} />
|
||||
<RoomName room={this.room} />
|
||||
|
|
|
@ -0,0 +1,97 @@
|
|||
/*
|
||||
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {createRef} from 'react';
|
||||
import { CallFeed, CallFeedEvent } from 'matrix-js-sdk/src/webrtc/callFeed';
|
||||
import { logger } from 'matrix-js-sdk/src/logger';
|
||||
import CallMediaHandler from "../../../CallMediaHandler";
|
||||
|
||||
interface IProps {
|
||||
feed: CallFeed,
|
||||
}
|
||||
|
||||
export default class AudioFeed extends React.Component<IProps> {
|
||||
private element = createRef<HTMLAudioElement>();
|
||||
|
||||
componentDidMount() {
|
||||
this.props.feed.addListener(CallFeedEvent.NewStream, this.onNewStream);
|
||||
this.playMedia();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.feed.removeListener(CallFeedEvent.NewStream, this.onNewStream);
|
||||
this.stopMedia();
|
||||
}
|
||||
|
||||
private playMedia() {
|
||||
const element = this.element.current;
|
||||
const audioOutput = CallMediaHandler.getAudioOutput();
|
||||
|
||||
if (audioOutput) {
|
||||
try {
|
||||
// This seems quite unreliable in Chrome, although I haven't yet managed to make a jsfiddle where
|
||||
// it fails.
|
||||
// It seems reliable if you set the sink ID after setting the srcObject and then set the sink ID
|
||||
// back to the default after the call is over - Dave
|
||||
element.setSinkId(audioOutput);
|
||||
} catch (e) {
|
||||
console.error("Couldn't set requested audio output device: using default", e);
|
||||
logger.warn("Couldn't set requested audio output device: using default", e);
|
||||
}
|
||||
}
|
||||
|
||||
element.muted = false;
|
||||
element.srcObject = this.props.feed.stream;
|
||||
element.autoplay = true;
|
||||
|
||||
try {
|
||||
// A note on calling methods on media elements:
|
||||
// We used to have queues per media element to serialise all calls on those elements.
|
||||
// The reason given for this was that load() and play() were racing. However, we now
|
||||
// never call load() explicitly so this seems unnecessary. However, serialising every
|
||||
// operation was causing bugs where video would not resume because some play command
|
||||
// had got stuck and all media operations were queued up behind it. If necessary, we
|
||||
// should serialise the ones that need to be serialised but then be able to interrupt
|
||||
// them with another load() which will cancel the pending one, but since we don't call
|
||||
// load() explicitly, it shouldn't be a problem. - Dave
|
||||
element.play()
|
||||
} catch (e) {
|
||||
logger.info("Failed to play media element with feed", this.props.feed, e);
|
||||
}
|
||||
}
|
||||
|
||||
private stopMedia() {
|
||||
const element = this.element.current;
|
||||
|
||||
element.pause();
|
||||
element.src = null;
|
||||
|
||||
// As per comment in componentDidMount, setting the sink ID back to the
|
||||
// default once the call is over makes setSinkId work reliably. - Dave
|
||||
// Since we are not using the same element anymore, the above doesn't
|
||||
// seem to be necessary - Šimon
|
||||
}
|
||||
|
||||
private onNewStream = () => {
|
||||
this.playMedia();
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<audio ref={this.element} />
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import AudioFeed from "./AudioFeed"
|
||||
import { CallEvent, MatrixCall } from "matrix-js-sdk/src/webrtc/call";
|
||||
import { CallFeed } from "matrix-js-sdk/src/webrtc/callFeed";
|
||||
|
||||
interface IProps {
|
||||
call: MatrixCall;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
feeds: Array<CallFeed>;
|
||||
}
|
||||
|
||||
export default class AudioFeedArrayForCall extends React.Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
feeds: [],
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.call.addListener(CallEvent.FeedsChanged, this.onFeedsChanged);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.call.removeListener(CallEvent.FeedsChanged, this.onFeedsChanged);
|
||||
}
|
||||
|
||||
onFeedsChanged = () => {
|
||||
this.setState({
|
||||
feeds: this.props.call.getRemoteFeeds(),
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return this.state.feeds.map((feed, i) => {
|
||||
return (
|
||||
<AudioFeed feed={feed} key={i} />
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -19,7 +19,7 @@ import React from 'react';
|
|||
|
||||
import CallView from "./CallView";
|
||||
import RoomViewStore from '../../../stores/RoomViewStore';
|
||||
import CallHandler from '../../../CallHandler';
|
||||
import CallHandler, { CallHandlerEvent } from '../../../CallHandler';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import { ActionPayload } from '../../../dispatcher/payloads';
|
||||
import PersistentApp from "../elements/PersistentApp";
|
||||
|
@ -27,7 +27,6 @@ import SettingsStore from "../../../settings/SettingsStore";
|
|||
import { CallEvent, CallState, MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import { Action } from '../../../dispatcher/actions';
|
||||
|
||||
const SHOW_CALL_IN_STATES = [
|
||||
CallState.Connected,
|
||||
|
@ -110,12 +109,14 @@ export default class CallPreview extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
public componentDidMount() {
|
||||
CallHandler.sharedInstance().addListener(CallHandlerEvent.CallChangeRoom, this.updateCalls);
|
||||
this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate);
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
MatrixClientPeg.get().on(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold);
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
CallHandler.sharedInstance().removeListener(CallHandlerEvent.CallChangeRoom, this.updateCalls);
|
||||
MatrixClientPeg.get().removeListener(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold);
|
||||
if (this.roomStoreToken) {
|
||||
this.roomStoreToken.remove();
|
||||
|
@ -143,21 +144,24 @@ export default class CallPreview extends React.Component<IProps, IState> {
|
|||
switch (payload.action) {
|
||||
// listen for call state changes to prod the render method, which
|
||||
// may hide the global CallView if the call it is tracking is dead
|
||||
case Action.CallChangeRoom:
|
||||
case 'call_state': {
|
||||
const [primaryCall, secondaryCalls] = getPrimarySecondaryCalls(
|
||||
CallHandler.sharedInstance().getAllActiveCallsNotInRoom(this.state.roomId),
|
||||
);
|
||||
|
||||
this.setState({
|
||||
primaryCall: primaryCall,
|
||||
secondaryCall: secondaryCalls[0],
|
||||
});
|
||||
this.updateCalls();
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private updateCalls = () => {
|
||||
const [primaryCall, secondaryCalls] = getPrimarySecondaryCalls(
|
||||
CallHandler.sharedInstance().getAllActiveCallsNotInRoom(this.state.roomId),
|
||||
);
|
||||
|
||||
this.setState({
|
||||
primaryCall: primaryCall,
|
||||
secondaryCall: secondaryCalls[0],
|
||||
});
|
||||
};
|
||||
|
||||
private onCallRemoteHold = () => {
|
||||
const [primaryCall, secondaryCalls] = getPrimarySecondaryCalls(
|
||||
CallHandler.sharedInstance().getAllActiveCallsNotInRoom(this.state.roomId),
|
||||
|
|
|
@ -20,10 +20,9 @@ import dis from '../../../dispatcher/dispatcher';
|
|||
import CallHandler from '../../../CallHandler';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
import VideoFeed, { VideoFeedType } from "./VideoFeed";
|
||||
import VideoFeed from './VideoFeed';
|
||||
import RoomAvatar from "../avatars/RoomAvatar";
|
||||
import { CallState, CallType, MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
||||
import { CallEvent } from 'matrix-js-sdk/src/webrtc/call';
|
||||
import { CallState, CallType, MatrixCall, CallEvent } from 'matrix-js-sdk/src/webrtc/call';
|
||||
import classNames from 'classnames';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import {isOnlyCtrlOrCmdKeyEvent, Key} from '../../../Keyboard';
|
||||
|
@ -31,6 +30,7 @@ import {alwaysAboveLeftOf, alwaysAboveRightOf, ChevronFace, ContextMenuButton} f
|
|||
import CallContextMenu from '../context_menus/CallContextMenu';
|
||||
import { avatarUrlForMember } from '../../../Avatar';
|
||||
import DialpadContextMenu from '../context_menus/DialpadContextMenu';
|
||||
import { CallFeed } from 'matrix-js-sdk/src/webrtc/callFeed';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
|
||||
interface IProps {
|
||||
|
@ -40,11 +40,11 @@ interface IProps {
|
|||
// Another ongoing call to display information about
|
||||
secondaryCall?: MatrixCall,
|
||||
|
||||
// a callback which is called when the content in the callview changes
|
||||
// a callback which is called when the content in the CallView changes
|
||||
// in a way that is likely to cause a resize.
|
||||
onResize?: any;
|
||||
|
||||
// Whether this call view is for picture-in-pictue mode
|
||||
// Whether this call view is for picture-in-picture mode
|
||||
// otherwise, it's the larger call view when viewing the room the call is in.
|
||||
// This is sort of a proxy for a number of things but we currently have no
|
||||
// need to control those things separately, so this is simpler.
|
||||
|
@ -60,6 +60,7 @@ interface IState {
|
|||
controlsVisible: boolean,
|
||||
showMoreMenu: boolean,
|
||||
showDialpad: boolean,
|
||||
feeds: CallFeed[],
|
||||
}
|
||||
|
||||
function getFullScreenElement() {
|
||||
|
@ -115,6 +116,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
controlsVisible: true,
|
||||
showMoreMenu: false,
|
||||
showDialpad: false,
|
||||
feeds: this.props.call.getFeeds(),
|
||||
}
|
||||
|
||||
this.updateCallListeners(null, this.props.call);
|
||||
|
@ -172,11 +174,13 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
oldCall.removeListener(CallEvent.State, this.onCallState);
|
||||
oldCall.removeListener(CallEvent.LocalHoldUnhold, this.onCallLocalHoldUnhold);
|
||||
oldCall.removeListener(CallEvent.RemoteHoldUnhold, this.onCallRemoteHoldUnhold);
|
||||
oldCall.removeListener(CallEvent.FeedsChanged, this.onFeedsChanged);
|
||||
}
|
||||
if (newCall) {
|
||||
newCall.on(CallEvent.State, this.onCallState);
|
||||
newCall.on(CallEvent.LocalHoldUnhold, this.onCallLocalHoldUnhold);
|
||||
newCall.on(CallEvent.RemoteHoldUnhold, this.onCallRemoteHoldUnhold);
|
||||
newCall.on(CallEvent.FeedsChanged, this.onFeedsChanged);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -186,6 +190,10 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
});
|
||||
};
|
||||
|
||||
private onFeedsChanged = (newFeeds: Array<CallFeed>) => {
|
||||
this.setState({feeds: newFeeds});
|
||||
};
|
||||
|
||||
private onCallLocalHoldUnhold = () => {
|
||||
this.setState({
|
||||
isLocalOnHold: this.props.call.isLocalOnHold(),
|
||||
|
@ -304,7 +312,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
// we register global shortcuts here, they *must not conflict* with local shortcuts elsewhere or both will fire
|
||||
// Note that this assumes we always have a callview on screen at any given time
|
||||
// Note that this assumes we always have a CallView on screen at any given time
|
||||
// CallHandler would probably be a better place for this
|
||||
private onNativeKeyDown = ev => {
|
||||
let handled = false;
|
||||
|
@ -474,6 +482,8 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
{contextMenuButton}
|
||||
</div>;
|
||||
|
||||
const avatarSize = this.props.pipMode ? 76 : 160;
|
||||
|
||||
// The 'content' for the call, ie. the videos for a video call and profile picture
|
||||
// for voice calls (fills the bg)
|
||||
let contentView: React.ReactNode;
|
||||
|
@ -524,41 +534,85 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
</div>;
|
||||
}
|
||||
|
||||
if (this.props.call.type === CallType.Video) {
|
||||
let localVideoFeed = null;
|
||||
let onHoldBackground = null;
|
||||
const backgroundStyle: CSSProperties = {};
|
||||
const containerClasses = classNames({
|
||||
mx_CallView_video: true,
|
||||
mx_CallView_video_hold: isOnHold,
|
||||
});
|
||||
if (isOnHold) {
|
||||
// This is a bit messy. I can't see a reason to have two onHold/transfer screens
|
||||
if (isOnHold || transfereeCall) {
|
||||
if (this.props.call.type === CallType.Video) {
|
||||
const containerClasses = classNames({
|
||||
mx_CallView_content: true,
|
||||
mx_CallView_video: true,
|
||||
mx_CallView_video_hold: isOnHold,
|
||||
});
|
||||
let onHoldBackground = null;
|
||||
const backgroundStyle: CSSProperties = {};
|
||||
const backgroundAvatarUrl = avatarUrlForMember(
|
||||
// is it worth getting the size of the div to pass here?
|
||||
// is it worth getting the size of the div to pass here?
|
||||
this.props.call.getOpponentMember(), 1024, 1024, 'crop',
|
||||
);
|
||||
backgroundStyle.backgroundImage = 'url(' + backgroundAvatarUrl + ')';
|
||||
onHoldBackground = <div className="mx_CallView_video_holdBackground" style={backgroundStyle} />;
|
||||
}
|
||||
if (!this.state.vidMuted) {
|
||||
localVideoFeed = <VideoFeed type={VideoFeedType.Local} call={this.props.call} />;
|
||||
}
|
||||
|
||||
contentView = <div className={containerClasses} ref={this.contentRef} onMouseMove={this.onMouseMove}>
|
||||
{onHoldBackground}
|
||||
<VideoFeed type={VideoFeedType.Remote} call={this.props.call} onResize={this.props.onResize} />
|
||||
{localVideoFeed}
|
||||
{holdTransferContent}
|
||||
{callControls}
|
||||
</div>;
|
||||
} else {
|
||||
const avatarSize = this.props.pipMode ? 76 : 160;
|
||||
contentView = (
|
||||
<div className={containerClasses} ref={this.contentRef} onMouseMove={this.onMouseMove}>
|
||||
{onHoldBackground}
|
||||
{holdTransferContent}
|
||||
{callControls}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
const classes = classNames({
|
||||
mx_CallView_content: true,
|
||||
mx_CallView_voice: true,
|
||||
mx_CallView_voice_hold: isOnHold,
|
||||
});
|
||||
|
||||
contentView =(
|
||||
<div className={classes} onMouseMove={this.onMouseMove}>
|
||||
<div className="mx_CallView_voice_avatarsContainer">
|
||||
<div
|
||||
className="mx_CallView_voice_avatarContainer"
|
||||
style={{width: avatarSize, height: avatarSize}}
|
||||
>
|
||||
<RoomAvatar
|
||||
room={callRoom}
|
||||
height={avatarSize}
|
||||
width={avatarSize}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{holdTransferContent}
|
||||
{callControls}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
} else if (this.props.call.noIncomingFeeds()) {
|
||||
// Here we're reusing the css classes from voice on hold, because
|
||||
// I am lazy. If this gets merged, the CallView might be subject
|
||||
// to change anyway - I might take an axe to this file in order to
|
||||
// try to get other things working
|
||||
const classes = classNames({
|
||||
mx_CallView_content: true,
|
||||
mx_CallView_voice: true,
|
||||
mx_CallView_voice_hold: isOnHold,
|
||||
});
|
||||
|
||||
const feeds = this.props.call.getLocalFeeds().map((feed, i) => {
|
||||
// Here we check to hide local audio feeds to achieve the same UI/UX
|
||||
// as before. But once again this might be subject to change
|
||||
if (feed.isVideoMuted()) return;
|
||||
return (
|
||||
<VideoFeed
|
||||
key={i}
|
||||
feed={feed}
|
||||
call={this.props.call}
|
||||
pipMode={this.props.pipMode}
|
||||
onResize={this.props.onResize}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
// Saying "Connecting" here isn't really true, but the best thing
|
||||
// I can come up with, but this might be subject to change as well
|
||||
contentView = <div className={classes} onMouseMove={this.onMouseMove}>
|
||||
{feeds}
|
||||
<div className="mx_CallView_voice_avatarsContainer">
|
||||
<div className="mx_CallView_voice_avatarContainer" style={{width: avatarSize, height: avatarSize}}>
|
||||
<RoomAvatar
|
||||
|
@ -568,7 +622,35 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
{holdTransferContent}
|
||||
<div className="mx_CallView_holdTransferContent">{_t("Connecting")}</div>
|
||||
{callControls}
|
||||
</div>;
|
||||
} else {
|
||||
const containerClasses = classNames({
|
||||
mx_CallView_content: true,
|
||||
mx_CallView_video: true,
|
||||
});
|
||||
|
||||
// TODO: Later the CallView should probably be reworked to support
|
||||
// any number of feeds but now we can always expect there to be two
|
||||
// feeds. This is because the js-sdk ignores any new incoming streams
|
||||
const feeds = this.state.feeds.map((feed, i) => {
|
||||
// Here we check to hide local audio feeds to achieve the same UI/UX
|
||||
// as before. But once again this might be subject to change
|
||||
if (feed.isVideoMuted() && feed.isLocal()) return;
|
||||
return (
|
||||
<VideoFeed
|
||||
key={i}
|
||||
feed={feed}
|
||||
call={this.props.call}
|
||||
pipMode={this.props.pipMode}
|
||||
onResize={this.props.onResize}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
contentView = <div className={containerClasses} ref={this.contentRef} onMouseMove={this.onMouseMove}>
|
||||
{feeds}
|
||||
{callControls}
|
||||
</div>;
|
||||
}
|
||||
|
|
|
@ -16,13 +16,12 @@ limitations under the License.
|
|||
|
||||
import { CallState, MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
||||
import React from 'react';
|
||||
import CallHandler from '../../../CallHandler';
|
||||
import CallHandler, { CallHandlerEvent } from '../../../CallHandler';
|
||||
import CallView from './CallView';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import {Resizable} from "re-resizable";
|
||||
import ResizeNotifier from "../../../utils/ResizeNotifier";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import { Action } from '../../../dispatcher/actions';
|
||||
|
||||
interface IProps {
|
||||
// What room we should display the call for
|
||||
|
@ -55,25 +54,30 @@ export default class CallViewForRoom extends React.Component<IProps, IState> {
|
|||
|
||||
public componentDidMount() {
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
CallHandler.sharedInstance().addListener(CallHandlerEvent.CallChangeRoom, this.updateCall);
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
dis.unregister(this.dispatcherRef);
|
||||
CallHandler.sharedInstance().removeListener(CallHandlerEvent.CallChangeRoom, this.updateCall);
|
||||
}
|
||||
|
||||
private onAction = (payload) => {
|
||||
switch (payload.action) {
|
||||
case Action.CallChangeRoom:
|
||||
case 'call_state': {
|
||||
const newCall = this.getCall();
|
||||
if (newCall !== this.state.call) {
|
||||
this.setState({call: newCall});
|
||||
}
|
||||
this.updateCall();
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private updateCall = () => {
|
||||
const newCall = this.getCall();
|
||||
if (newCall !== this.state.call) {
|
||||
this.setState({call: newCall});
|
||||
}
|
||||
};
|
||||
|
||||
private getCall(): MatrixCall {
|
||||
const call = CallHandler.sharedInstance().getCallForRoom(this.props.roomId);
|
||||
|
||||
|
|
|
@ -18,52 +18,102 @@ import classnames from 'classnames';
|
|||
import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
||||
import React, {createRef} from 'react';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { CallFeed, CallFeedEvent } from 'matrix-js-sdk/src/webrtc/callFeed';
|
||||
import { logger } from 'matrix-js-sdk/src/logger';
|
||||
import MemberAvatar from "../avatars/MemberAvatar"
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
|
||||
export enum VideoFeedType {
|
||||
Local,
|
||||
Remote,
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
call: MatrixCall,
|
||||
|
||||
type: VideoFeedType,
|
||||
feed: CallFeed,
|
||||
|
||||
// Whether this call view is for picture-in-picture mode
|
||||
// otherwise, it's the larger call view when viewing the room the call is in.
|
||||
// This is sort of a proxy for a number of things but we currently have no
|
||||
// need to control those things separately, so this is simpler.
|
||||
pipMode?: boolean;
|
||||
|
||||
// a callback which is called when the video element is resized
|
||||
// due to a change in video metadata
|
||||
onResize?: (e: Event) => void,
|
||||
}
|
||||
|
||||
@replaceableComponent("views.voip.VideoFeed")
|
||||
export default class VideoFeed extends React.Component<IProps> {
|
||||
private vid = createRef<HTMLVideoElement>();
|
||||
interface IState {
|
||||
audioMuted: boolean;
|
||||
videoMuted: boolean;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.vid.current.addEventListener('resize', this.onResize);
|
||||
this.setVideoElement();
|
||||
@replaceableComponent("views.voip.VideoFeed")
|
||||
export default class VideoFeed extends React.Component<IProps, IState> {
|
||||
private element = createRef<HTMLVideoElement>();
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
audioMuted: this.props.feed.isAudioMuted(),
|
||||
videoMuted: this.props.feed.isVideoMuted(),
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.call !== prevProps.call) {
|
||||
this.setVideoElement();
|
||||
}
|
||||
componentDidMount() {
|
||||
this.props.feed.addListener(CallFeedEvent.NewStream, this.onNewStream);
|
||||
this.playMedia();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.vid.current.removeEventListener('resize', this.onResize);
|
||||
this.props.feed.removeListener(CallFeedEvent.NewStream, this.onNewStream);
|
||||
this.element.current?.removeEventListener('resize', this.onResize);
|
||||
this.stopMedia();
|
||||
}
|
||||
|
||||
private setVideoElement() {
|
||||
if (this.props.type === VideoFeedType.Local) {
|
||||
this.props.call.setLocalVideoElement(this.vid.current);
|
||||
} else {
|
||||
this.props.call.setRemoteVideoElement(this.vid.current);
|
||||
private playMedia() {
|
||||
const element = this.element.current;
|
||||
if (!element) return;
|
||||
// We play audio in AudioFeed, not here
|
||||
element.muted = true;
|
||||
element.srcObject = this.props.feed.stream;
|
||||
element.autoplay = true;
|
||||
try {
|
||||
// A note on calling methods on media elements:
|
||||
// We used to have queues per media element to serialise all calls on those elements.
|
||||
// The reason given for this was that load() and play() were racing. However, we now
|
||||
// never call load() explicitly so this seems unnecessary. However, serialising every
|
||||
// operation was causing bugs where video would not resume because some play command
|
||||
// had got stuck and all media operations were queued up behind it. If necessary, we
|
||||
// should serialise the ones that need to be serialised but then be able to interrupt
|
||||
// them with another load() which will cancel the pending one, but since we don't call
|
||||
// load() explicitly, it shouldn't be a problem. - Dave
|
||||
element.play()
|
||||
} catch (e) {
|
||||
logger.info("Failed to play media element with feed", this.props.feed, e);
|
||||
}
|
||||
}
|
||||
|
||||
onResize = (e) => {
|
||||
if (this.props.onResize) {
|
||||
private stopMedia() {
|
||||
const element = this.element.current;
|
||||
if (!element) return;
|
||||
|
||||
element.pause();
|
||||
element.src = null;
|
||||
|
||||
// As per comment in componentDidMount, setting the sink ID back to the
|
||||
// default once the call is over makes setSinkId work reliably. - Dave
|
||||
// Since we are not using the same element anymore, the above doesn't
|
||||
// seem to be necessary - Šimon
|
||||
}
|
||||
|
||||
private onNewStream = () => {
|
||||
this.setState({
|
||||
audioMuted: this.props.feed.isAudioMuted(),
|
||||
videoMuted: this.props.feed.isVideoMuted(),
|
||||
});
|
||||
this.playMedia();
|
||||
};
|
||||
|
||||
private onResize = (e) => {
|
||||
if (this.props.onResize && !this.props.feed.isLocal()) {
|
||||
this.props.onResize(e);
|
||||
}
|
||||
};
|
||||
|
@ -71,14 +121,33 @@ export default class VideoFeed extends React.Component<IProps> {
|
|||
render() {
|
||||
const videoClasses = {
|
||||
mx_VideoFeed: true,
|
||||
mx_VideoFeed_local: this.props.type === VideoFeedType.Local,
|
||||
mx_VideoFeed_remote: this.props.type === VideoFeedType.Remote,
|
||||
mx_VideoFeed_local: this.props.feed.isLocal(),
|
||||
mx_VideoFeed_remote: !this.props.feed.isLocal(),
|
||||
mx_VideoFeed_voice: this.state.videoMuted,
|
||||
mx_VideoFeed_video: !this.state.videoMuted,
|
||||
mx_VideoFeed_mirror: (
|
||||
this.props.type === VideoFeedType.Local &&
|
||||
this.props.feed.isLocal() &&
|
||||
SettingsStore.getValue('VideoView.flipVideoHorizontally')
|
||||
),
|
||||
};
|
||||
|
||||
return <video className={classnames(videoClasses)} ref={this.vid} />;
|
||||
if (this.state.videoMuted) {
|
||||
const member = this.props.feed.getMember();
|
||||
const avatarSize = this.props.pipMode ? 76 : 160;
|
||||
|
||||
return (
|
||||
<div className={classnames(videoClasses)} >
|
||||
<MemberAvatar
|
||||
member={member}
|
||||
height={avatarSize}
|
||||
width={avatarSize}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<video className={classnames(videoClasses)} ref={this.element} />
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -114,9 +114,6 @@ export enum Action {
|
|||
*/
|
||||
VirtualRoomSupportUpdated = "virtual_room_support_updated",
|
||||
|
||||
// Probably would be better to have a VoIP states in a store and have the store emit changes
|
||||
CallChangeRoom = "call_change_room",
|
||||
|
||||
/**
|
||||
* Fired when an upload has started. Should be used with UploadStartedPayload.
|
||||
*/
|
||||
|
|
|
@ -785,13 +785,6 @@
|
|||
"%(senderName)s: %(stickerName)s": "%(senderName)s: %(stickerName)s",
|
||||
"Change notification settings": "Change notification settings",
|
||||
"Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags. Requires compatible homeserver for some features.": "Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags. Requires compatible homeserver for some features.",
|
||||
"Spaces": "Spaces",
|
||||
"Spaces are new ways to group rooms and people.": "Spaces are new ways to group rooms and people.",
|
||||
"%(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "%(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.",
|
||||
"Beta available for web, desktop and Android. Thank you for trying the beta.": "Beta available for web, desktop and Android. Thank you for trying the beta.",
|
||||
"%(brand)s will reload with Spaces enabled. Communities and custom tags will be hidden.": "%(brand)s will reload with Spaces enabled. Communities and custom tags will be hidden.",
|
||||
"You can leave the beta any time from settings or tapping on a beta badge, like the one above.": "You can leave the beta any time from settings or tapping on a beta badge, like the one above.",
|
||||
"Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.": "Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.",
|
||||
"Show options to enable 'Do not disturb' mode": "Show options to enable 'Do not disturb' mode",
|
||||
"Send and receive voice messages (in development)": "Send and receive voice messages (in development)",
|
||||
"Render LaTeX maths in messages": "Render LaTeX maths in messages",
|
||||
|
@ -892,6 +885,7 @@
|
|||
"You held the call <a>Switch</a>": "You held the call <a>Switch</a>",
|
||||
"You held the call <a>Resume</a>": "You held the call <a>Resume</a>",
|
||||
"%(peerName)s held the call": "%(peerName)s held the call",
|
||||
"Connecting": "Connecting",
|
||||
"Video Call": "Video Call",
|
||||
"Voice Call": "Voice Call",
|
||||
"Fill Screen": "Fill Screen",
|
||||
|
@ -1264,7 +1258,7 @@
|
|||
"Copy": "Copy",
|
||||
"Clear cache and reload": "Clear cache and reload",
|
||||
"Labs": "Labs",
|
||||
"Feeling experimental? Labs are the best way to get things early, test out new features and help shape them before they actually launch. <a>Learn more</a>.": "Feeling experimental? Labs are the best way to get things early, test out new features and help shape them before they actually launch. <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>.",
|
||||
"Ignored/Blocked": "Ignored/Blocked",
|
||||
"Error adding ignored user/server": "Error adding ignored user/server",
|
||||
"Something went wrong. Please try again or view your console for hints.": "Something went wrong. Please try again or view your console for hints.",
|
||||
|
@ -2037,6 +2031,7 @@
|
|||
"%(networkName)s rooms": "%(networkName)s rooms",
|
||||
"Matrix rooms": "Matrix rooms",
|
||||
"Filter your rooms and spaces": "Filter your rooms and spaces",
|
||||
"Spaces": "Spaces",
|
||||
"Direct Messages": "Direct Messages",
|
||||
"Space selection": "Space selection",
|
||||
"Add existing rooms": "Add existing rooms",
|
||||
|
@ -2469,11 +2464,6 @@
|
|||
"Revoke permissions": "Revoke permissions",
|
||||
"Move left": "Move left",
|
||||
"Move right": "Move right",
|
||||
"Spaces is a beta feature": "Spaces is a beta feature",
|
||||
"Tap for more info": "Tap for more info",
|
||||
"Beta": "Beta",
|
||||
"Leave the beta": "Leave the beta",
|
||||
"Join the beta": "Join the beta",
|
||||
"Avatar": "Avatar",
|
||||
"This room is public": "This room is public",
|
||||
"Away": "Away",
|
||||
|
@ -2607,7 +2597,6 @@
|
|||
"Error whilst fetching joined communities": "Error whilst fetching joined communities",
|
||||
"Create a new community": "Create a new community",
|
||||
"Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.": "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.",
|
||||
"Communities are changing to Spaces": "Communities are changing to Spaces",
|
||||
"You’re all caught up": "You’re all caught up",
|
||||
"You have no visible notifications.": "You have no visible notifications.",
|
||||
"%(brand)s failed to get the protocol list from the homeserver. The homeserver may be too old to support third party networks.": "%(brand)s failed to get the protocol list from the homeserver. The homeserver may be too old to support third party networks.",
|
||||
|
@ -2668,6 +2657,7 @@
|
|||
"%(count)s rooms and %(numSpaces)s spaces|one": "%(count)s room and %(numSpaces)s spaces",
|
||||
"%(count)s rooms and 1 space|other": "%(count)s rooms and 1 space",
|
||||
"%(count)s rooms and 1 space|one": "%(count)s room and 1 space",
|
||||
"Select a room below first": "Select a room below first",
|
||||
"Failed to remove some rooms. Try again later": "Failed to remove some rooms. Try again later",
|
||||
"Removing...": "Removing...",
|
||||
"Mark as not suggested": "Mark as not suggested",
|
||||
|
@ -2680,9 +2670,6 @@
|
|||
"Public space": "Public space",
|
||||
"Private space": "Private space",
|
||||
"<inviter/> invites you": "<inviter/> invites you",
|
||||
"To view %(spaceName)s, turn on the <a>Spaces beta</a>": "To view %(spaceName)s, turn on the <a>Spaces beta</a>",
|
||||
"To join %(spaceName)s, turn on the <a>Spaces beta</a>": "To join %(spaceName)s, turn on the <a>Spaces beta</a>",
|
||||
"Add existing rooms & spaces": "Add existing rooms & spaces",
|
||||
"Welcome to <name/>": "Welcome to <name/>",
|
||||
"Random": "Random",
|
||||
"Support": "Support",
|
||||
|
@ -2707,7 +2694,6 @@
|
|||
"Inviting...": "Inviting...",
|
||||
"Invite your teammates": "Invite your teammates",
|
||||
"Make sure the right people have access. You can invite more later.": "Make sure the right people have access. You can invite more later.",
|
||||
"<b>This is an experimental feature.</b> For now, new users receiving an invite will have to open the invite on <link/> to actually join.": "<b>This is an experimental feature.</b> For now, new users receiving an invite will have to open the invite on <link/> to actually join.",
|
||||
"Invite by username": "Invite by username",
|
||||
"What are some things you want to discuss in %(spaceName)s?": "What are some things you want to discuss in %(spaceName)s?",
|
||||
"Let's create a room for each of them.": "Let's create a room for each of them.",
|
||||
|
|
|
@ -122,7 +122,7 @@ export class BreadcrumbsStore extends AsyncStoreWithClient<IState> {
|
|||
}
|
||||
|
||||
private async appendRoom(room: Room) {
|
||||
if (room.isSpaceRoom() && SettingsStore.getValue("feature_spaces")) return; // hide space rooms
|
||||
if (SettingsStore.getValue("feature_spaces") && room.isSpaceRoom()) return; // hide space rooms
|
||||
let updated = false;
|
||||
const rooms = (this.state.rooms || []).slice(); // cheap clone
|
||||
|
||||
|
|
|
@ -199,8 +199,10 @@ export class Algorithm extends EventEmitter {
|
|||
}
|
||||
|
||||
private async doUpdateStickyRoom(val: Room) {
|
||||
// no-op sticky rooms for spaces - they're effectively virtual rooms
|
||||
if (val?.isSpaceRoom() && val.getMyMembership() !== "invite") val = null;
|
||||
if (SettingsStore.getValue("feature_spaces") && val?.isSpaceRoom() && val.getMyMembership() !== "invite") {
|
||||
// no-op sticky rooms for spaces - they're effectively virtual rooms
|
||||
val = null;
|
||||
}
|
||||
|
||||
// Note throughout: We need async so we can wait for handleRoomUpdate() to do its thing,
|
||||
// otherwise we risk duplicating rooms.
|
||||
|
|
|
@ -50,7 +50,7 @@ export class VisibilityProvider {
|
|||
}
|
||||
|
||||
// hide space rooms as they'll be shown in the SpacePanel
|
||||
if (room.isSpaceRoom() && SettingsStore.getValue("feature_spaces")) {
|
||||
if (SettingsStore.getValue("feature_spaces") && room.isSpaceRoom()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
@ -83,6 +83,7 @@ export const showCreateNewRoom = async (cli: MatrixClient, space: Room) => {
|
|||
if (shouldCreate) {
|
||||
await createRoom(opts);
|
||||
}
|
||||
return shouldCreate;
|
||||
};
|
||||
|
||||
export const showSpaceInvite = (space: Room, initialText = "") => {
|
||||
|
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
|||
|
||||
import './skinned-sdk';
|
||||
|
||||
import CallHandler, { PlaceCallType } from '../src/CallHandler';
|
||||
import CallHandler, { PlaceCallType, CallHandlerEvent } from '../src/CallHandler';
|
||||
import { stubClient, mkStubRoom } from './test-utils';
|
||||
import { MatrixClientPeg } from '../src/MatrixClientPeg';
|
||||
import dis from '../src/dispatcher/dispatcher';
|
||||
|
@ -172,11 +172,9 @@ describe('CallHandler', () => {
|
|||
|
||||
let callRoomChangeEventCount = 0;
|
||||
const roomChangePromise = new Promise<void>(resolve => {
|
||||
dispatchHandle = dis.register(payload => {
|
||||
if (payload.action === Action.CallChangeRoom) {
|
||||
++callRoomChangeEventCount;
|
||||
resolve();
|
||||
}
|
||||
callHandler.addListener(CallHandlerEvent.CallChangeRoom, () => {
|
||||
++callRoomChangeEventCount;
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -201,7 +199,7 @@ describe('CallHandler', () => {
|
|||
fakeCall.emit(CallEvent.AssertedIdentityChanged);
|
||||
|
||||
await roomChangePromise;
|
||||
dis.unregister(dispatchHandle);
|
||||
callHandler.removeAllListeners();
|
||||
|
||||
// If everything's gone well, we should have seen only one room change
|
||||
// event and the call should now be in user 3's room.
|
||||
|
|
|
@ -435,9 +435,9 @@ jsprim@^1.2.2:
|
|||
verror "1.10.0"
|
||||
|
||||
lodash@^4.15.0, lodash@^4.17.11:
|
||||
version "4.17.19"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b"
|
||||
integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==
|
||||
version "4.17.21"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
||||
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
||||
|
||||
mime-db@~1.38.0:
|
||||
version "1.38.0"
|
||||
|
|
12
yarn.lock
12
yarn.lock
|
@ -5580,9 +5580,9 @@ lodash.sortby@^4.7.0:
|
|||
integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=
|
||||
|
||||
lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.2.1:
|
||||
version "4.17.20"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
|
||||
integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==
|
||||
version "4.17.21"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
||||
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
||||
|
||||
log-symbols@^4.0.0:
|
||||
version "4.0.0"
|
||||
|
@ -8070,9 +8070,9 @@ typescript@^4.1.3:
|
|||
integrity sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg==
|
||||
|
||||
ua-parser-js@^0.7.18:
|
||||
version "0.7.23"
|
||||
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.23.tgz#704d67f951e13195fbcd3d78818577f5bc1d547b"
|
||||
integrity sha512-m4hvMLxgGHXG3O3fQVAyyAQpZzDOvwnhOTjYz5Xmr7r/+LpkNy3vJXdVRWgd1TkAb7NGROZuSy96CrlNVjA7KA==
|
||||
version "0.7.28"
|
||||
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.28.tgz#8ba04e653f35ce210239c64661685bf9121dec31"
|
||||
integrity sha512-6Gurc1n//gjp9eQNXjD9O3M/sMwVtN5S8Lv9bvOYBfKfDNiIIhqiyi01vMBO45u4zkDE420w/e0se7Vs+sIg+g==
|
||||
|
||||
unhomoglyph@^1.0.6:
|
||||
version "1.0.6"
|
||||
|
|
Loading…
Reference in New Issue