Improve UI/UX in calls (#7791)
							parent
							
								
									5cdc8fb3fd
								
							
						
					
					
						commit
						a5b795c934
					
				| 
						 | 
				
			
			@ -1,7 +1,7 @@
 | 
			
		|||
/*
 | 
			
		||||
Copyright 2015, 2016 OpenMarket Ltd
 | 
			
		||||
Copyright 2020 - 2021 The Matrix.org Foundation C.I.C.
 | 
			
		||||
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
 | 
			
		||||
Copyright 2021 - 2022 Š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.
 | 
			
		||||
| 
						 | 
				
			
			@ -27,7 +27,7 @@ limitations under the License.
 | 
			
		|||
    position: absolute;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
    bottom: 24px;
 | 
			
		||||
    bottom: 32px;
 | 
			
		||||
    opacity: 1;
 | 
			
		||||
    transition: opacity 0.5s;
 | 
			
		||||
    z-index: 200; // To be above _all_ feeds
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,7 @@
 | 
			
		|||
/*
 | 
			
		||||
Copyright 2015, 2016 OpenMarket Ltd
 | 
			
		||||
Copyright 2020 The Matrix.org Foundation C.I.C.
 | 
			
		||||
Copyright 2020 - 2021 The Matrix.org Foundation C.I.C.
 | 
			
		||||
Copyright 2021 - 2022 Š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.
 | 
			
		||||
| 
						 | 
				
			
			@ -22,202 +23,176 @@ limitations under the License.
 | 
			
		|||
    padding-right: 8px;
 | 
			
		||||
    // XXX: PiPContainer sets pointer-events: none - should probably be set back in a better place
 | 
			
		||||
    pointer-events: initial;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mx_CallView_large {
 | 
			
		||||
    padding-bottom: 10px;
 | 
			
		||||
    margin: $container-gap-width;
 | 
			
		||||
    // The left side gap is fully handled by this margin. To prohibit bleeding on webkit browser.
 | 
			
		||||
    margin-right: calc($container-gap-width / 2);
 | 
			
		||||
    margin-bottom: 10px;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    flex: 1;
 | 
			
		||||
    .mx_CallView_toast {
 | 
			
		||||
        position: absolute;
 | 
			
		||||
        top: 74px;
 | 
			
		||||
 | 
			
		||||
        padding: 4px 8px;
 | 
			
		||||
 | 
			
		||||
        border-radius: 4px;
 | 
			
		||||
        z-index: 50;
 | 
			
		||||
 | 
			
		||||
        // Same on both themes
 | 
			
		||||
        color: white;
 | 
			
		||||
        background-color: #17191c;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .mx_CallView_content_wrapper {
 | 
			
		||||
        display: flex;
 | 
			
		||||
        justify-content: center;
 | 
			
		||||
 | 
			
		||||
        width: 100%;
 | 
			
		||||
        height: 100%;
 | 
			
		||||
 | 
			
		||||
        overflow: hidden;
 | 
			
		||||
 | 
			
		||||
        .mx_CallView_content {
 | 
			
		||||
            position: relative;
 | 
			
		||||
 | 
			
		||||
            display: flex;
 | 
			
		||||
            flex-direction: column;
 | 
			
		||||
            justify-content: center;
 | 
			
		||||
            align-items: center;
 | 
			
		||||
 | 
			
		||||
            flex: 1;
 | 
			
		||||
            overflow: hidden;
 | 
			
		||||
 | 
			
		||||
            border-radius: 10px;
 | 
			
		||||
 | 
			
		||||
            padding: 10px;
 | 
			
		||||
            padding-right: calc(20% + 20px); // Space for the sidebar
 | 
			
		||||
 | 
			
		||||
            background-color: $call-view-content-background;
 | 
			
		||||
 | 
			
		||||
            .mx_CallView_status {
 | 
			
		||||
                z-index: 50;
 | 
			
		||||
                color: $accent-fg-color;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            .mx_CallView_avatarsContainer {
 | 
			
		||||
                display: flex;
 | 
			
		||||
                flex-direction: row;
 | 
			
		||||
                align-items: center;
 | 
			
		||||
                justify-content: center;
 | 
			
		||||
 | 
			
		||||
                div {
 | 
			
		||||
                    margin-left: 12px;
 | 
			
		||||
                    margin-right: 12px;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            .mx_CallView_holdBackground {
 | 
			
		||||
                position: absolute;
 | 
			
		||||
                left: 0;
 | 
			
		||||
                right: 0;
 | 
			
		||||
 | 
			
		||||
                width: 100%;
 | 
			
		||||
                height: 100%;
 | 
			
		||||
 | 
			
		||||
                background-repeat: no-repeat;
 | 
			
		||||
                background-size: cover;
 | 
			
		||||
                background-position: center;
 | 
			
		||||
                filter: blur(20px);
 | 
			
		||||
 | 
			
		||||
                &::after {
 | 
			
		||||
                    content: "";
 | 
			
		||||
                    display: block;
 | 
			
		||||
                    position: absolute;
 | 
			
		||||
                    width: 100%;
 | 
			
		||||
                    height: 100%;
 | 
			
		||||
                    left: 0;
 | 
			
		||||
                    right: 0;
 | 
			
		||||
                    background-color: rgba(0, 0, 0, 0.6);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            &.mx_CallView_content_hold .mx_CallView_status {
 | 
			
		||||
                font-weight: bold;
 | 
			
		||||
                text-align: center;
 | 
			
		||||
 | 
			
		||||
                &::before {
 | 
			
		||||
                    display: block;
 | 
			
		||||
                    margin-left: auto;
 | 
			
		||||
                    margin-right: auto;
 | 
			
		||||
                    content: "";
 | 
			
		||||
                    width: 40px;
 | 
			
		||||
                    height: 40px;
 | 
			
		||||
                    background-image: url("$(res)/img/voip/paused.svg");
 | 
			
		||||
                    background-position: center;
 | 
			
		||||
                    background-size: cover;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                .mx_CallView_pip &::before {
 | 
			
		||||
                    width: 30px;
 | 
			
		||||
                    height: 30px;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                .mx_AccessibleButton_hasKind {
 | 
			
		||||
                    padding: 0px;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &:not(.mx_CallView_sidebar) .mx_CallView_content {
 | 
			
		||||
        padding: 0;
 | 
			
		||||
        width: 100%;
 | 
			
		||||
        height: 100%;
 | 
			
		||||
 | 
			
		||||
        .mx_VideoFeed_primary {
 | 
			
		||||
            aspect-ratio: unset;
 | 
			
		||||
            border: 0;
 | 
			
		||||
 | 
			
		||||
            width: 100%;
 | 
			
		||||
            height: 100%;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &.mx_CallView_pip {
 | 
			
		||||
        width: 320px;
 | 
			
		||||
        padding-bottom: 8px;
 | 
			
		||||
 | 
			
		||||
        border-radius: 8px;
 | 
			
		||||
 | 
			
		||||
        background-color: $system;
 | 
			
		||||
        box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.2);
 | 
			
		||||
 | 
			
		||||
        .mx_CallViewButtons {
 | 
			
		||||
            bottom: 13px;
 | 
			
		||||
 | 
			
		||||
            .mx_CallViewButtons_button {
 | 
			
		||||
                width: 34px;
 | 
			
		||||
                height: 34px;
 | 
			
		||||
 | 
			
		||||
                &::before {
 | 
			
		||||
                    width: 22px;
 | 
			
		||||
                    height: 22px;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .mx_CallView_content {
 | 
			
		||||
            min-height: 180px;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &.mx_CallView_large {
 | 
			
		||||
        display: flex;
 | 
			
		||||
        flex-direction: column;
 | 
			
		||||
        align-items: center;
 | 
			
		||||
 | 
			
		||||
    .mx_CallView_voice {
 | 
			
		||||
        flex: 1;
 | 
			
		||||
 | 
			
		||||
        padding-bottom: 10px;
 | 
			
		||||
 | 
			
		||||
        margin: $container-gap-width;
 | 
			
		||||
        // The left side gap is fully handled by this margin. To prohibit bleeding on webkit browser.
 | 
			
		||||
        margin-right: calc($container-gap-width / 2);
 | 
			
		||||
        margin-bottom: 10px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &.mx_CallView_belowWidget {
 | 
			
		||||
        margin-top: 0;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mx_CallView_pip {
 | 
			
		||||
    width: 320px;
 | 
			
		||||
    padding-bottom: 8px;
 | 
			
		||||
    background-color: $system;
 | 
			
		||||
    box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.2);
 | 
			
		||||
    border-radius: 8px;
 | 
			
		||||
 | 
			
		||||
    .mx_CallView_video_hold,
 | 
			
		||||
    .mx_CallView_voice {
 | 
			
		||||
        height: 180px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .mx_CallViewButtons {
 | 
			
		||||
        bottom: 13px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .mx_CallViewButtons_button {
 | 
			
		||||
        width: 34px;
 | 
			
		||||
        height: 34px;
 | 
			
		||||
 | 
			
		||||
        &::before {
 | 
			
		||||
            width: 22px;
 | 
			
		||||
            height: 22px;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .mx_CallView_holdTransferContent {
 | 
			
		||||
        padding-top: 10px;
 | 
			
		||||
        padding-bottom: 25px;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mx_CallView_content {
 | 
			
		||||
    position: relative;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
    border-radius: 8px;
 | 
			
		||||
 | 
			
		||||
    > .mx_VideoFeed {
 | 
			
		||||
        width: 100%;
 | 
			
		||||
        height: 100%;
 | 
			
		||||
 | 
			
		||||
        &.mx_VideoFeed_voice {
 | 
			
		||||
            display: flex;
 | 
			
		||||
            justify-content: center;
 | 
			
		||||
            align-items: center;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .mx_VideoFeed_video {
 | 
			
		||||
            height: 100%;
 | 
			
		||||
            background-color: #000;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .mx_VideoFeed_mic {
 | 
			
		||||
            left: 10px;
 | 
			
		||||
            bottom: 10px;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mx_CallView_voice {
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    background-color: $inverted-bg-color;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mx_CallView_voice_avatarsContainer {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-direction: row;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
    div {
 | 
			
		||||
        margin-left: 12px;
 | 
			
		||||
        margin-right: 12px;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mx_CallView_voice .mx_CallView_holdTransferContent {
 | 
			
		||||
    // This masks the avatar image so when it's blurred, the edge is still crisp
 | 
			
		||||
    .mx_CallView_voice_avatarContainer {
 | 
			
		||||
        border-radius: 2000px;
 | 
			
		||||
        overflow: hidden;
 | 
			
		||||
        position: relative;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mx_CallView_holdTransferContent {
 | 
			
		||||
    height: 20px;
 | 
			
		||||
    padding-top: 20px;
 | 
			
		||||
    padding-bottom: 15px;
 | 
			
		||||
    color: $accent-fg-color;
 | 
			
		||||
    user-select: none;
 | 
			
		||||
 | 
			
		||||
    .mx_AccessibleButton_hasKind {
 | 
			
		||||
        padding: 0px;
 | 
			
		||||
        font-weight: bold;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mx_CallView_video {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    height: 100%;
 | 
			
		||||
    z-index: 30;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mx_CallView_video_hold {
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
 | 
			
		||||
    // we keep these around in the DOM: it saved wiring them up again when the call
 | 
			
		||||
    // is resumed and keeps the container the right size
 | 
			
		||||
    .mx_VideoFeed {
 | 
			
		||||
        visibility: hidden;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mx_CallView_video_holdBackground {
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    height: 100%;
 | 
			
		||||
    left: 0;
 | 
			
		||||
    right: 0;
 | 
			
		||||
    background-repeat: no-repeat;
 | 
			
		||||
    background-size: cover;
 | 
			
		||||
    background-position: center;
 | 
			
		||||
    filter: blur(20px);
 | 
			
		||||
    &::after {
 | 
			
		||||
        content: "";
 | 
			
		||||
        display: block;
 | 
			
		||||
        position: absolute;
 | 
			
		||||
        width: 100%;
 | 
			
		||||
        height: 100%;
 | 
			
		||||
        left: 0;
 | 
			
		||||
        right: 0;
 | 
			
		||||
        background-color: rgba(0, 0, 0, 0.6);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mx_CallView_video .mx_CallView_holdTransferContent {
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    top: 50%;
 | 
			
		||||
    left: 50%;
 | 
			
		||||
    transform: translate(-50%, -50%);
 | 
			
		||||
    font-weight: bold;
 | 
			
		||||
    color: $accent-fg-color;
 | 
			
		||||
    text-align: center;
 | 
			
		||||
 | 
			
		||||
    &::before {
 | 
			
		||||
        display: block;
 | 
			
		||||
        margin-left: auto;
 | 
			
		||||
        margin-right: auto;
 | 
			
		||||
        content: "";
 | 
			
		||||
        width: 40px;
 | 
			
		||||
        height: 40px;
 | 
			
		||||
        background-image: url("$(res)/img/voip/paused.svg");
 | 
			
		||||
        background-position: center;
 | 
			
		||||
        background-size: cover;
 | 
			
		||||
    }
 | 
			
		||||
    .mx_CallView_pip &::before {
 | 
			
		||||
        width: 30px;
 | 
			
		||||
        height: 30px;
 | 
			
		||||
    }
 | 
			
		||||
    .mx_AccessibleButton_hasKind {
 | 
			
		||||
        padding: 0px;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mx_CallView_presenting {
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    margin-top: 18px;
 | 
			
		||||
    padding: 4px 8px;
 | 
			
		||||
    border-radius: 4px;
 | 
			
		||||
 | 
			
		||||
    // Same on both themes
 | 
			
		||||
    color: white;
 | 
			
		||||
    background-color: #17191c;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,6 @@
 | 
			
		|||
/*
 | 
			
		||||
Copyright 2021 The Matrix.org Foundation C.I.C.
 | 
			
		||||
Copyright 2021 - 2022 Š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.
 | 
			
		||||
| 
						 | 
				
			
			@ -19,8 +20,9 @@ limitations under the License.
 | 
			
		|||
    display: flex;
 | 
			
		||||
    flex-direction: row;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    justify-content: left;
 | 
			
		||||
    justify-content: space-between;
 | 
			
		||||
    flex-shrink: 0;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
 | 
			
		||||
    &.mx_CallViewHeader_pip {
 | 
			
		||||
        cursor: pointer;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,5 @@
 | 
			
		|||
/*
 | 
			
		||||
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
 | 
			
		||||
Copyright 2021 - 2022 Š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.
 | 
			
		||||
| 
						 | 
				
			
			@ -16,18 +16,15 @@ limitations under the License.
 | 
			
		|||
 | 
			
		||||
.mx_CallViewSidebar {
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    right: 16px;
 | 
			
		||||
    bottom: 16px;
 | 
			
		||||
    z-index: 100; // To be above the primary feed
 | 
			
		||||
    right: 10px;
 | 
			
		||||
 | 
			
		||||
    width: 20%;
 | 
			
		||||
    height: 100%;
 | 
			
		||||
    overflow: auto;
 | 
			
		||||
 | 
			
		||||
    height: calc(100% - 32px); // Subtract the top and bottom padding
 | 
			
		||||
    width: 20%;
 | 
			
		||||
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-direction: column-reverse;
 | 
			
		||||
    justify-content: flex-start;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
    align-items: flex-end;
 | 
			
		||||
    gap: 12px;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -42,15 +39,6 @@ limitations under the License.
 | 
			
		|||
 | 
			
		||||
            background-color: $video-feed-secondary-background;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .mx_VideoFeed_video {
 | 
			
		||||
            border-radius: 4px;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .mx_VideoFeed_mic {
 | 
			
		||||
            left: 6px;
 | 
			
		||||
            bottom: 6px;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &.mx_CallViewSidebar_pipMode {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,6 @@
 | 
			
		|||
/*
 | 
			
		||||
Copyright 2015, 2016, 2020 The Matrix.org Foundation C.I.C.
 | 
			
		||||
Copyright 2015, 2016, 2020, 2021 The Matrix.org Foundation C.I.C.
 | 
			
		||||
Copyright 2021 - 2022 Š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.
 | 
			
		||||
| 
						 | 
				
			
			@ -20,15 +21,32 @@ limitations under the License.
 | 
			
		|||
    box-sizing: border-box;
 | 
			
		||||
    border: transparent 2px solid;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    border-radius: 4px;
 | 
			
		||||
 | 
			
		||||
    &.mx_VideoFeed_secondary {
 | 
			
		||||
        position: absolute;
 | 
			
		||||
        right: 24px;
 | 
			
		||||
        bottom: 72px;
 | 
			
		||||
        width: 20%;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &.mx_VideoFeed_voice {
 | 
			
		||||
        background-color: $inverted-bg-color;
 | 
			
		||||
        aspect-ratio: 16 / 9;
 | 
			
		||||
 | 
			
		||||
        display: flex;
 | 
			
		||||
        justify-content: center;
 | 
			
		||||
        align-items: center;
 | 
			
		||||
 | 
			
		||||
        &:not(.mx_VideoFeed_primary) {
 | 
			
		||||
            aspect-ratio: 16 / 9;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .mx_VideoFeed_video {
 | 
			
		||||
        height: 100%;
 | 
			
		||||
        width: 100%;
 | 
			
		||||
        background-color: transparent;
 | 
			
		||||
        border-radius: 4px;
 | 
			
		||||
        background-color: #000000;
 | 
			
		||||
 | 
			
		||||
        &.mx_VideoFeed_video_mirror {
 | 
			
		||||
            transform: scale(-1, 1);
 | 
			
		||||
| 
						 | 
				
			
			@ -37,6 +55,8 @@ limitations under the License.
 | 
			
		|||
 | 
			
		||||
    .mx_VideoFeed_mic {
 | 
			
		||||
        position: absolute;
 | 
			
		||||
        left: 6px;
 | 
			
		||||
        bottom: 6px;
 | 
			
		||||
        display: flex;
 | 
			
		||||
        align-items: center;
 | 
			
		||||
        justify-content: center;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -185,6 +185,7 @@ $call-view-button-on-foreground: $primary-content;
 | 
			
		|||
$call-view-button-on-background: $system;
 | 
			
		||||
$call-view-button-off-foreground: $system;
 | 
			
		||||
$call-view-button-off-background: $primary-content;
 | 
			
		||||
$call-view-content-background: $quinary-content;
 | 
			
		||||
 | 
			
		||||
$video-feed-secondary-background: $system;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -117,6 +117,7 @@ $call-view-button-on-foreground: $primary-content;
 | 
			
		|||
$call-view-button-on-background: $system;
 | 
			
		||||
$call-view-button-off-foreground: $system;
 | 
			
		||||
$call-view-button-off-background: $primary-content;
 | 
			
		||||
$call-view-content-background: $quinary-content;
 | 
			
		||||
 | 
			
		||||
$video-feed-secondary-background: $system;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -175,6 +175,7 @@ $call-view-button-on-foreground: $secondary-content;
 | 
			
		|||
$call-view-button-on-background: $background;
 | 
			
		||||
$call-view-button-off-foreground: $background;
 | 
			
		||||
$call-view-button-off-background: $secondary-content;
 | 
			
		||||
$call-view-content-background: #21262C;
 | 
			
		||||
 | 
			
		||||
$video-feed-secondary-background: #394049; // XXX: Color from dark theme
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -277,6 +277,7 @@ $call-view-button-on-foreground: $secondary-content;
 | 
			
		|||
$call-view-button-on-background: $background;
 | 
			
		||||
$call-view-button-off-foreground: $background;
 | 
			
		||||
$call-view-button-off-background: $secondary-content;
 | 
			
		||||
$call-view-content-background: #21262C;
 | 
			
		||||
 | 
			
		||||
$video-feed-secondary-background: #394049; // XXX: Color from dark theme
 | 
			
		||||
$voipcall-plinth-color: $system;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,7 +1,7 @@
 | 
			
		|||
/*
 | 
			
		||||
Copyright 2015, 2016 OpenMarket Ltd
 | 
			
		||||
Copyright 2019 - 2021 The Matrix.org Foundation C.I.C.
 | 
			
		||||
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
 | 
			
		||||
Copyright 2021 - 2022 Š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.
 | 
			
		||||
| 
						 | 
				
			
			@ -16,7 +16,7 @@ See the License for the specific language governing permissions and
 | 
			
		|||
limitations under the License.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
import React, { createRef, CSSProperties } from 'react';
 | 
			
		||||
import React, { createRef } from 'react';
 | 
			
		||||
import { CallEvent, CallState, MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
 | 
			
		||||
import classNames from 'classnames';
 | 
			
		||||
import { CallFeed } from 'matrix-js-sdk/src/webrtc/callFeed';
 | 
			
		||||
| 
						 | 
				
			
			@ -36,6 +36,7 @@ import CallViewSidebar from './CallViewSidebar';
 | 
			
		|||
import CallViewHeader from './CallView/CallViewHeader';
 | 
			
		||||
import CallViewButtons from "./CallView/CallViewButtons";
 | 
			
		||||
import PlatformPeg from "../../../PlatformPeg";
 | 
			
		||||
import { ActionPayload } from "../../../dispatcher/payloads";
 | 
			
		||||
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
 | 
			
		||||
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -69,8 +70,9 @@ interface IState {
 | 
			
		|||
    vidMuted: boolean;
 | 
			
		||||
    screensharing: boolean;
 | 
			
		||||
    callState: CallState;
 | 
			
		||||
    primaryFeed: CallFeed;
 | 
			
		||||
    secondaryFeeds: Array<CallFeed>;
 | 
			
		||||
    primaryFeed?: CallFeed;
 | 
			
		||||
    secondaryFeed?: CallFeed;
 | 
			
		||||
    sidebarFeeds: Array<CallFeed>;
 | 
			
		||||
    sidebarShown: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -104,13 +106,13 @@ function exitFullscreen() {
 | 
			
		|||
 | 
			
		||||
export default class CallView extends React.Component<IProps, IState> {
 | 
			
		||||
    private dispatcherRef: string;
 | 
			
		||||
    private contentRef = createRef<HTMLDivElement>();
 | 
			
		||||
    private contentWrapperRef = createRef<HTMLDivElement>();
 | 
			
		||||
    private buttonsRef = createRef<CallViewButtons>();
 | 
			
		||||
 | 
			
		||||
    constructor(props: IProps) {
 | 
			
		||||
        super(props);
 | 
			
		||||
 | 
			
		||||
        const { primary, secondary } = CallView.getOrderedFeeds(this.props.call.getFeeds());
 | 
			
		||||
        const { primary, secondary, sidebar } = CallView.getOrderedFeeds(this.props.call.getFeeds());
 | 
			
		||||
 | 
			
		||||
        this.state = {
 | 
			
		||||
            isLocalOnHold: this.props.call.isLocalOnHold(),
 | 
			
		||||
| 
						 | 
				
			
			@ -120,19 +122,20 @@ export default class CallView extends React.Component<IProps, IState> {
 | 
			
		|||
            screensharing: this.props.call.isScreensharing(),
 | 
			
		||||
            callState: this.props.call.state,
 | 
			
		||||
            primaryFeed: primary,
 | 
			
		||||
            secondaryFeeds: secondary,
 | 
			
		||||
            secondaryFeed: secondary,
 | 
			
		||||
            sidebarFeeds: sidebar,
 | 
			
		||||
            sidebarShown: true,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        this.updateCallListeners(null, this.props.call);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public componentDidMount() {
 | 
			
		||||
    public componentDidMount(): void {
 | 
			
		||||
        this.dispatcherRef = dis.register(this.onAction);
 | 
			
		||||
        document.addEventListener('keydown', this.onNativeKeyDown);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public componentWillUnmount() {
 | 
			
		||||
    public componentWillUnmount(): void {
 | 
			
		||||
        if (getFullScreenElement()) {
 | 
			
		||||
            exitFullscreen();
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			@ -143,11 +146,12 @@ export default class CallView extends React.Component<IProps, IState> {
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    static getDerivedStateFromProps(props: IProps): Partial<IState> {
 | 
			
		||||
        const { primary, secondary } = CallView.getOrderedFeeds(props.call.getFeeds());
 | 
			
		||||
        const { primary, secondary, sidebar } = CallView.getOrderedFeeds(props.call.getFeeds());
 | 
			
		||||
 | 
			
		||||
        return {
 | 
			
		||||
            primaryFeed: primary,
 | 
			
		||||
            secondaryFeeds: secondary,
 | 
			
		||||
            secondaryFeed: secondary,
 | 
			
		||||
            sidebarFeeds: sidebar,
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -165,14 +169,14 @@ export default class CallView extends React.Component<IProps, IState> {
 | 
			
		|||
        this.updateCallListeners(null, this.props.call);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private onAction = (payload) => {
 | 
			
		||||
    private onAction = (payload: ActionPayload): void => {
 | 
			
		||||
        switch (payload.action) {
 | 
			
		||||
            case 'video_fullscreen': {
 | 
			
		||||
                if (!this.contentRef.current) {
 | 
			
		||||
                if (!this.contentWrapperRef.current) {
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
                if (payload.fullscreen) {
 | 
			
		||||
                    requestFullscreen(this.contentRef.current);
 | 
			
		||||
                    requestFullscreen(this.contentWrapperRef.current);
 | 
			
		||||
                } else if (getFullScreenElement()) {
 | 
			
		||||
                    exitFullscreen();
 | 
			
		||||
                }
 | 
			
		||||
| 
						 | 
				
			
			@ -181,7 +185,7 @@ export default class CallView extends React.Component<IProps, IState> {
 | 
			
		|||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    private updateCallListeners(oldCall: MatrixCall, newCall: MatrixCall) {
 | 
			
		||||
    private updateCallListeners(oldCall: MatrixCall, newCall: MatrixCall): void {
 | 
			
		||||
        if (oldCall === newCall) return;
 | 
			
		||||
 | 
			
		||||
        if (oldCall) {
 | 
			
		||||
| 
						 | 
				
			
			@ -198,29 +202,30 @@ export default class CallView extends React.Component<IProps, IState> {
 | 
			
		|||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private onCallState = (state) => {
 | 
			
		||||
    private onCallState = (state: CallState): void => {
 | 
			
		||||
        this.setState({
 | 
			
		||||
            callState: state,
 | 
			
		||||
        });
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    private onFeedsChanged = (newFeeds: Array<CallFeed>) => {
 | 
			
		||||
        const { primary, secondary } = CallView.getOrderedFeeds(newFeeds);
 | 
			
		||||
    private onFeedsChanged = (newFeeds: Array<CallFeed>): void => {
 | 
			
		||||
        const { primary, secondary, sidebar } = CallView.getOrderedFeeds(newFeeds);
 | 
			
		||||
        this.setState({
 | 
			
		||||
            primaryFeed: primary,
 | 
			
		||||
            secondaryFeeds: secondary,
 | 
			
		||||
            secondaryFeed: secondary,
 | 
			
		||||
            sidebarFeeds: sidebar,
 | 
			
		||||
            micMuted: this.props.call.isMicrophoneMuted(),
 | 
			
		||||
            vidMuted: this.props.call.isLocalVideoMuted(),
 | 
			
		||||
        });
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    private onCallLocalHoldUnhold = () => {
 | 
			
		||||
    private onCallLocalHoldUnhold = (): void => {
 | 
			
		||||
        this.setState({
 | 
			
		||||
            isLocalOnHold: this.props.call.isLocalOnHold(),
 | 
			
		||||
        });
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    private onCallRemoteHoldUnhold = () => {
 | 
			
		||||
    private onCallRemoteHoldUnhold = (): void => {
 | 
			
		||||
        this.setState({
 | 
			
		||||
            isRemoteOnHold: this.props.call.isRemoteOnHold(),
 | 
			
		||||
            // update both here because isLocalOnHold changes when we hold the call too
 | 
			
		||||
| 
						 | 
				
			
			@ -228,12 +233,22 @@ export default class CallView extends React.Component<IProps, IState> {
 | 
			
		|||
        });
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    private onMouseMove = () => {
 | 
			
		||||
    private onMouseMove = (): void => {
 | 
			
		||||
        this.buttonsRef.current?.showControls();
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    static getOrderedFeeds(feeds: Array<CallFeed>): { primary: CallFeed, secondary: Array<CallFeed> } {
 | 
			
		||||
        let primary;
 | 
			
		||||
    static getOrderedFeeds(
 | 
			
		||||
        feeds: Array<CallFeed>,
 | 
			
		||||
    ): { primary?: CallFeed, secondary?: CallFeed, sidebar: Array<CallFeed> } {
 | 
			
		||||
        if (feeds.length <= 2) {
 | 
			
		||||
            return {
 | 
			
		||||
                primary: feeds.find((feed) => !feed.isLocal()),
 | 
			
		||||
                secondary: feeds.find((feed) => feed.isLocal()),
 | 
			
		||||
                sidebar: [],
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let primary: CallFeed;
 | 
			
		||||
 | 
			
		||||
        // Try to use a screensharing as primary, a remote one if possible
 | 
			
		||||
        const screensharingFeeds = feeds.filter((feed) => feed.purpose === SDPStreamMetadataPurpose.Screenshare);
 | 
			
		||||
| 
						 | 
				
			
			@ -243,16 +258,16 @@ export default class CallView extends React.Component<IProps, IState> {
 | 
			
		|||
            primary = feeds.find((feed) => !feed.isLocal());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const secondary = [...feeds];
 | 
			
		||||
        const sidebar = [...feeds];
 | 
			
		||||
        // Remove the primary feed from the array
 | 
			
		||||
        if (primary) secondary.splice(secondary.indexOf(primary), 1);
 | 
			
		||||
        secondary.sort((a, b) => {
 | 
			
		||||
        if (primary) sidebar.splice(sidebar.indexOf(primary), 1);
 | 
			
		||||
        sidebar.sort((a, b) => {
 | 
			
		||||
            if (a.isLocal() && !b.isLocal()) return -1;
 | 
			
		||||
            if (!a.isLocal() && b.isLocal()) return 1;
 | 
			
		||||
            return 0;
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return { primary, secondary };
 | 
			
		||||
        return { primary, sidebar };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private onMicMuteClick = async (): Promise<void> => {
 | 
			
		||||
| 
						 | 
				
			
			@ -336,7 +351,7 @@ export default class CallView extends React.Component<IProps, IState> {
 | 
			
		|||
 | 
			
		||||
    private renderCallControls(): JSX.Element {
 | 
			
		||||
        const { call, pipMode } = this.props;
 | 
			
		||||
        const { primaryFeed, callState, micMuted, vidMuted, screensharing, sidebarShown } = this.state;
 | 
			
		||||
        const { callState, micMuted, vidMuted, screensharing, sidebarShown, secondaryFeed, sidebarFeeds } = this.state;
 | 
			
		||||
 | 
			
		||||
        // If SDPStreamMetadata isn't supported don't show video mute button in voice calls
 | 
			
		||||
        const vidMuteButtonShown = call.opponentSupportsSDPStreamMetadata() || call.hasLocalUserMediaVideoTrack;
 | 
			
		||||
| 
						 | 
				
			
			@ -348,13 +363,8 @@ export default class CallView extends React.Component<IProps, IState> {
 | 
			
		|||
            (call.opponentSupportsSDPStreamMetadata() || call.hasLocalUserMediaVideoTrack) &&
 | 
			
		||||
            call.state === CallState.Connected
 | 
			
		||||
        );
 | 
			
		||||
        // To show the sidebar we need secondary feeds, if we don't have them,
 | 
			
		||||
        // we can hide this button. If we are in PiP, sidebar is also hidden, so
 | 
			
		||||
        // we can hide the button too
 | 
			
		||||
        const sidebarButtonShown = (
 | 
			
		||||
            primaryFeed?.purpose === SDPStreamMetadataPurpose.Screenshare ||
 | 
			
		||||
            call.isScreensharing()
 | 
			
		||||
        );
 | 
			
		||||
        // Show the sidebar button only if there is something to hide/show
 | 
			
		||||
        const sidebarButtonShown = (secondaryFeed && !secondaryFeed.isVideoMuted()) || sidebarFeeds.length > 0;
 | 
			
		||||
        // The dial pad & 'more' button actions are only relevant in a connected call
 | 
			
		||||
        const contextMenuButtonShown = callState === CallState.Connected;
 | 
			
		||||
        const dialpadButtonShown = (
 | 
			
		||||
| 
						 | 
				
			
			@ -391,158 +401,126 @@ export default class CallView extends React.Component<IProps, IState> {
 | 
			
		|||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public render() {
 | 
			
		||||
        const client = MatrixClientPeg.get();
 | 
			
		||||
        const callRoomId = CallHandler.instance.roomIdForCall(this.props.call);
 | 
			
		||||
        const secondaryCallRoomId = CallHandler.instance.roomIdForCall(this.props.secondaryCall);
 | 
			
		||||
        const callRoom = client.getRoom(callRoomId);
 | 
			
		||||
        const secCallRoom = this.props.secondaryCall ? client.getRoom(secondaryCallRoomId) : null;
 | 
			
		||||
        const avatarSize = this.props.pipMode ? 76 : 160;
 | 
			
		||||
        const transfereeCall = CallHandler.instance.getTransfereeForCallId(this.props.call.callId);
 | 
			
		||||
        const isOnHold = this.state.isLocalOnHold || this.state.isRemoteOnHold;
 | 
			
		||||
        const isScreensharing = this.props.call.isScreensharing();
 | 
			
		||||
        const sidebarShown = this.state.sidebarShown;
 | 
			
		||||
        const someoneIsScreensharing = this.props.call.getFeeds().some((feed) => {
 | 
			
		||||
    private renderToast(): JSX.Element {
 | 
			
		||||
        const { call } = this.props;
 | 
			
		||||
        const someoneIsScreensharing = call.getFeeds().some((feed) => {
 | 
			
		||||
            return feed.purpose === SDPStreamMetadataPurpose.Screenshare;
 | 
			
		||||
        });
 | 
			
		||||
        const call = this.props.call;
 | 
			
		||||
 | 
			
		||||
        let contentView: React.ReactNode;
 | 
			
		||||
        let holdTransferContent;
 | 
			
		||||
        if (!someoneIsScreensharing) return null;
 | 
			
		||||
 | 
			
		||||
        if (transfereeCall) {
 | 
			
		||||
            const transferTargetRoom = MatrixClientPeg.get().getRoom(
 | 
			
		||||
                CallHandler.instance.roomIdForCall(this.props.call),
 | 
			
		||||
            );
 | 
			
		||||
            const transferTargetName = transferTargetRoom ? transferTargetRoom.name : _t("unknown person");
 | 
			
		||||
        const isScreensharing = call.isScreensharing();
 | 
			
		||||
        const { primaryFeed, sidebarShown } = this.state;
 | 
			
		||||
        const sharerName = primaryFeed.getMember().name;
 | 
			
		||||
 | 
			
		||||
            const transfereeRoom = MatrixClientPeg.get().getRoom(
 | 
			
		||||
                CallHandler.instance.roomIdForCall(transfereeCall),
 | 
			
		||||
            );
 | 
			
		||||
            const transfereeName = transfereeRoom ? transfereeRoom.name : _t("unknown person");
 | 
			
		||||
 | 
			
		||||
            holdTransferContent = <div className="mx_CallView_holdTransferContent">
 | 
			
		||||
                { _t(
 | 
			
		||||
                    "Consulting with %(transferTarget)s. <a>Transfer to %(transferee)s</a>",
 | 
			
		||||
                    {
 | 
			
		||||
                        transferTarget: transferTargetName,
 | 
			
		||||
                        transferee: transfereeName,
 | 
			
		||||
                    },
 | 
			
		||||
                    {
 | 
			
		||||
                        a: sub => <AccessibleButton kind="link" onClick={this.onTransferClick}>
 | 
			
		||||
                            { sub }
 | 
			
		||||
                        </AccessibleButton>,
 | 
			
		||||
                    },
 | 
			
		||||
                ) }
 | 
			
		||||
            </div>;
 | 
			
		||||
        } else if (isOnHold) {
 | 
			
		||||
            let onHoldText = null;
 | 
			
		||||
            if (this.state.isRemoteOnHold) {
 | 
			
		||||
                const holdString = CallHandler.instance.hasAnyUnheldCall() ?
 | 
			
		||||
                    _td("You held the call <a>Switch</a>") : _td("You held the call <a>Resume</a>");
 | 
			
		||||
                onHoldText = _t(holdString, {}, {
 | 
			
		||||
                    a: sub => <AccessibleButton kind="link" onClick={this.onCallResumeClick}>
 | 
			
		||||
                        { sub }
 | 
			
		||||
                    </AccessibleButton>,
 | 
			
		||||
                });
 | 
			
		||||
            } else if (this.state.isLocalOnHold) {
 | 
			
		||||
                onHoldText = _t("%(peerName)s held the call", {
 | 
			
		||||
                    peerName: this.props.call.getOpponentMember().name,
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
            holdTransferContent = <div className="mx_CallView_holdTransferContent">
 | 
			
		||||
                { onHoldText }
 | 
			
		||||
            </div>;
 | 
			
		||||
        let text = isScreensharing
 | 
			
		||||
            ? _t("You are presenting")
 | 
			
		||||
            : _t('%(sharerName)s is presenting', { sharerName });
 | 
			
		||||
        if (!sidebarShown) {
 | 
			
		||||
            text += " • " + (call.isLocalVideoMuted()
 | 
			
		||||
                ? _t("Your camera is turned off")
 | 
			
		||||
                : _t("Your camera is still enabled"));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let sidebar;
 | 
			
		||||
        if (
 | 
			
		||||
            !isOnHold &&
 | 
			
		||||
            !transfereeCall &&
 | 
			
		||||
            sidebarShown &&
 | 
			
		||||
            (call.hasLocalUserMediaVideoTrack || someoneIsScreensharing)
 | 
			
		||||
        ) {
 | 
			
		||||
            sidebar = (
 | 
			
		||||
                <CallViewSidebar
 | 
			
		||||
                    feeds={this.state.secondaryFeeds}
 | 
			
		||||
                    call={this.props.call}
 | 
			
		||||
                    pipMode={this.props.pipMode}
 | 
			
		||||
        return (
 | 
			
		||||
            <div className="mx_CallView_toast">
 | 
			
		||||
                { text }
 | 
			
		||||
            </div>
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private renderContent(): JSX.Element {
 | 
			
		||||
        const { pipMode, call, onResize } = this.props;
 | 
			
		||||
        const { isLocalOnHold, isRemoteOnHold, sidebarShown, primaryFeed, secondaryFeed, sidebarFeeds } = this.state;
 | 
			
		||||
 | 
			
		||||
        const callRoom = MatrixClientPeg.get().getRoom(call.roomId);
 | 
			
		||||
        const avatarSize = pipMode ? 76 : 160;
 | 
			
		||||
        const transfereeCall = CallHandler.instance.getTransfereeForCallId(call.callId);
 | 
			
		||||
        const isOnHold = isLocalOnHold || isRemoteOnHold;
 | 
			
		||||
 | 
			
		||||
        let secondaryFeedElement: React.ReactNode;
 | 
			
		||||
        if (sidebarShown && secondaryFeed && !secondaryFeed.isVideoMuted()) {
 | 
			
		||||
            secondaryFeedElement = (
 | 
			
		||||
                <VideoFeed
 | 
			
		||||
                    feed={secondaryFeed}
 | 
			
		||||
                    call={call}
 | 
			
		||||
                    pipMode={pipMode}
 | 
			
		||||
                    onResize={onResize}
 | 
			
		||||
                    secondary={true}
 | 
			
		||||
                />
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // This is a bit messy. I can't see a reason to have two onHold/transfer screens
 | 
			
		||||
        if (isOnHold || transfereeCall) {
 | 
			
		||||
            if (call.hasLocalUserMediaVideoTrack || call.hasRemoteUserMediaVideoTrack) {
 | 
			
		||||
                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?
 | 
			
		||||
                    this.props.call.getOpponentMember(), 1024, 1024, 'crop',
 | 
			
		||||
                );
 | 
			
		||||
                backgroundStyle.backgroundImage = 'url(' + backgroundAvatarUrl + ')';
 | 
			
		||||
                onHoldBackground = <div className="mx_CallView_video_holdBackground" style={backgroundStyle} />;
 | 
			
		||||
        if (transfereeCall || isOnHold) {
 | 
			
		||||
            const containerClasses = classNames("mx_CallView_content", {
 | 
			
		||||
                mx_CallView_content_hold: isOnHold,
 | 
			
		||||
            });
 | 
			
		||||
            const backgroundAvatarUrl = avatarUrlForMember(call.getOpponentMember(), 1024, 1024, 'crop');
 | 
			
		||||
 | 
			
		||||
                contentView = (
 | 
			
		||||
                    <div className={containerClasses} ref={this.contentRef} onMouseMove={this.onMouseMove}>
 | 
			
		||||
                        { onHoldBackground }
 | 
			
		||||
                        { holdTransferContent }
 | 
			
		||||
                        { this.renderCallControls() }
 | 
			
		||||
                    </div>
 | 
			
		||||
            let holdTransferContent: React.ReactNode;
 | 
			
		||||
            if (transfereeCall) {
 | 
			
		||||
                const transferTargetRoom = MatrixClientPeg.get().getRoom(
 | 
			
		||||
                    CallHandler.instance.roomIdForCall(call),
 | 
			
		||||
                );
 | 
			
		||||
                const transferTargetName = transferTargetRoom ? transferTargetRoom.name : _t("unknown person");
 | 
			
		||||
                const transfereeRoom = MatrixClientPeg.get().getRoom(
 | 
			
		||||
                    CallHandler.instance.roomIdForCall(transfereeCall),
 | 
			
		||||
                );
 | 
			
		||||
                const transfereeName = transfereeRoom ? transfereeRoom.name : _t("unknown person");
 | 
			
		||||
 | 
			
		||||
                holdTransferContent = <div className="mx_CallView_status">
 | 
			
		||||
                    { _t(
 | 
			
		||||
                        "Consulting with %(transferTarget)s. <a>Transfer to %(transferee)s</a>",
 | 
			
		||||
                        {
 | 
			
		||||
                            transferTarget: transferTargetName,
 | 
			
		||||
                            transferee: transfereeName,
 | 
			
		||||
                        },
 | 
			
		||||
                        {
 | 
			
		||||
                            a: sub => <AccessibleButton kind="link" onClick={this.onTransferClick}>
 | 
			
		||||
                                { sub }
 | 
			
		||||
                            </AccessibleButton>,
 | 
			
		||||
                        },
 | 
			
		||||
                    ) }
 | 
			
		||||
                </div>;
 | 
			
		||||
            } else {
 | 
			
		||||
                const classes = classNames({
 | 
			
		||||
                    mx_CallView_content: true,
 | 
			
		||||
                    mx_CallView_voice: true,
 | 
			
		||||
                    mx_CallView_voice_hold: isOnHold,
 | 
			
		||||
                });
 | 
			
		||||
                let onHoldText: React.ReactNode;
 | 
			
		||||
                if (isRemoteOnHold) {
 | 
			
		||||
                    onHoldText = _t(
 | 
			
		||||
                        CallHandler.instance.hasAnyUnheldCall()
 | 
			
		||||
                            ? _td("You held the call <a>Switch</a>")
 | 
			
		||||
                            : _td("You held the call <a>Resume</a>"),
 | 
			
		||||
                        {},
 | 
			
		||||
                        {
 | 
			
		||||
                            a: sub => <AccessibleButton kind="link" onClick={this.onCallResumeClick}>
 | 
			
		||||
                                { sub }
 | 
			
		||||
                            </AccessibleButton>,
 | 
			
		||||
                        },
 | 
			
		||||
                    );
 | 
			
		||||
                } else if (isLocalOnHold) {
 | 
			
		||||
                    onHoldText = _t("%(peerName)s held the call", {
 | 
			
		||||
                        peerName: call.getOpponentMember().name,
 | 
			
		||||
                    });
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                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 }
 | 
			
		||||
                        { this.renderCallControls() }
 | 
			
		||||
                holdTransferContent = (
 | 
			
		||||
                    <div className="mx_CallView_status">
 | 
			
		||||
                        { onHoldText }
 | 
			
		||||
                    </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,
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            // 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}
 | 
			
		||||
                    ref={this.contentRef}
 | 
			
		||||
                >
 | 
			
		||||
                    { sidebar }
 | 
			
		||||
                    <div className="mx_CallView_voice_avatarsContainer">
 | 
			
		||||
            return (
 | 
			
		||||
                <div className={containerClasses} onMouseMove={this.onMouseMove}>
 | 
			
		||||
                    <div className="mx_CallView_holdBackground" style={{ backgroundImage: 'url(' + backgroundAvatarUrl + ')' }} />
 | 
			
		||||
                    { holdTransferContent }
 | 
			
		||||
                </div>
 | 
			
		||||
            );
 | 
			
		||||
        } else if (call.noIncomingFeeds()) {
 | 
			
		||||
            return (
 | 
			
		||||
                <div className="mx_CallView_content" onMouseMove={this.onMouseMove}>
 | 
			
		||||
                    <div className="mx_CallView_avatarsContainer">
 | 
			
		||||
                        <div
 | 
			
		||||
                            className="mx_CallView_voice_avatarContainer"
 | 
			
		||||
                            className="mx_CallView_avatarContainer"
 | 
			
		||||
                            style={{ width: avatarSize, height: avatarSize }}
 | 
			
		||||
                        >
 | 
			
		||||
                            <RoomAvatar
 | 
			
		||||
| 
						 | 
				
			
			@ -552,69 +530,96 @@ export default class CallView extends React.Component<IProps, IState> {
 | 
			
		|||
                            />
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div className="mx_CallView_holdTransferContent">{ _t("Connecting") }</div>
 | 
			
		||||
                    { this.renderCallControls() }
 | 
			
		||||
                    <div className="mx_CallView_status">{ _t("Connecting") }</div>
 | 
			
		||||
                    { secondaryFeedElement }
 | 
			
		||||
                </div>
 | 
			
		||||
            );
 | 
			
		||||
        } else if (pipMode) {
 | 
			
		||||
            return (
 | 
			
		||||
                <div
 | 
			
		||||
                    className="mx_CallView_content"
 | 
			
		||||
                    onMouseMove={this.onMouseMove}
 | 
			
		||||
                >
 | 
			
		||||
                    <VideoFeed
 | 
			
		||||
                        feed={primaryFeed}
 | 
			
		||||
                        call={call}
 | 
			
		||||
                        pipMode={pipMode}
 | 
			
		||||
                        onResize={onResize}
 | 
			
		||||
                        primary={true}
 | 
			
		||||
                    />
 | 
			
		||||
                </div>
 | 
			
		||||
            );
 | 
			
		||||
        } else if (secondaryFeed) {
 | 
			
		||||
            return (
 | 
			
		||||
                <div className="mx_CallView_content" onMouseMove={this.onMouseMove}>
 | 
			
		||||
                    <VideoFeed
 | 
			
		||||
                        feed={primaryFeed}
 | 
			
		||||
                        call={call}
 | 
			
		||||
                        pipMode={pipMode}
 | 
			
		||||
                        onResize={onResize}
 | 
			
		||||
                        primary={true}
 | 
			
		||||
                    />
 | 
			
		||||
                    { secondaryFeedElement }
 | 
			
		||||
                </div>
 | 
			
		||||
            );
 | 
			
		||||
        } else {
 | 
			
		||||
            const containerClasses = classNames({
 | 
			
		||||
                mx_CallView_content: true,
 | 
			
		||||
                mx_CallView_video: true,
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            let toast;
 | 
			
		||||
            if (someoneIsScreensharing) {
 | 
			
		||||
                const sharerName = this.state.primaryFeed.getMember().name;
 | 
			
		||||
                let text = isScreensharing
 | 
			
		||||
                    ? _t("You are presenting")
 | 
			
		||||
                    : _t('%(sharerName)s is presenting', { sharerName });
 | 
			
		||||
                if (!this.state.sidebarShown) {
 | 
			
		||||
                    text += " • " + (this.props.call.isLocalVideoMuted()
 | 
			
		||||
                        ? _t("Your camera is turned off")
 | 
			
		||||
                        : _t("Your camera is still enabled"));
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                toast = (
 | 
			
		||||
                    <div className="mx_CallView_presenting">
 | 
			
		||||
                        { text }
 | 
			
		||||
                    </div>
 | 
			
		||||
                );
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            contentView = (
 | 
			
		||||
                <div
 | 
			
		||||
                    className={containerClasses}
 | 
			
		||||
                    ref={this.contentRef}
 | 
			
		||||
                    onMouseMove={this.onMouseMove}
 | 
			
		||||
                >
 | 
			
		||||
                    { toast }
 | 
			
		||||
                    { sidebar }
 | 
			
		||||
            return (
 | 
			
		||||
                <div className="mx_CallView_content" onMouseMove={this.onMouseMove}>
 | 
			
		||||
                    <VideoFeed
 | 
			
		||||
                        feed={this.state.primaryFeed}
 | 
			
		||||
                        call={this.props.call}
 | 
			
		||||
                        pipMode={this.props.pipMode}
 | 
			
		||||
                        onResize={this.props.onResize}
 | 
			
		||||
                        feed={primaryFeed}
 | 
			
		||||
                        call={call}
 | 
			
		||||
                        pipMode={pipMode}
 | 
			
		||||
                        onResize={onResize}
 | 
			
		||||
                        primary={true}
 | 
			
		||||
                    />
 | 
			
		||||
                    { this.renderCallControls() }
 | 
			
		||||
                    { sidebarShown && <CallViewSidebar
 | 
			
		||||
                        feeds={sidebarFeeds}
 | 
			
		||||
                        call={call}
 | 
			
		||||
                        pipMode={pipMode}
 | 
			
		||||
                    /> }
 | 
			
		||||
                </div>
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public render(): JSX.Element {
 | 
			
		||||
        const {
 | 
			
		||||
            call,
 | 
			
		||||
            secondaryCall,
 | 
			
		||||
            pipMode,
 | 
			
		||||
            showApps,
 | 
			
		||||
            onMouseDownOnHeader,
 | 
			
		||||
        } = this.props;
 | 
			
		||||
        const {
 | 
			
		||||
            sidebarShown,
 | 
			
		||||
            sidebarFeeds,
 | 
			
		||||
        } = this.state;
 | 
			
		||||
 | 
			
		||||
        const client = MatrixClientPeg.get();
 | 
			
		||||
        const callRoomId = CallHandler.instance.roomIdForCall(call);
 | 
			
		||||
        const secondaryCallRoomId = CallHandler.instance.roomIdForCall(secondaryCall);
 | 
			
		||||
        const callRoom = client.getRoom(callRoomId);
 | 
			
		||||
        const secCallRoom = secondaryCall ? client.getRoom(secondaryCallRoomId) : null;
 | 
			
		||||
 | 
			
		||||
        const callViewClasses = classNames({
 | 
			
		||||
            mx_CallView: true,
 | 
			
		||||
            mx_CallView_pip: this.props.pipMode,
 | 
			
		||||
            mx_CallView_large: !this.props.pipMode,
 | 
			
		||||
            mx_CallView_belowWidget: this.props.showApps, // css to correct the margins if the call is below the AppsDrawer.
 | 
			
		||||
            mx_CallView_pip: pipMode,
 | 
			
		||||
            mx_CallView_large: !pipMode,
 | 
			
		||||
            mx_CallView_sidebar: sidebarShown && sidebarFeeds.length !== 0 && !pipMode,
 | 
			
		||||
            mx_CallView_belowWidget: showApps, // css to correct the margins if the call is below the AppsDrawer.
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return <div className={callViewClasses}>
 | 
			
		||||
            <CallViewHeader
 | 
			
		||||
                onPipMouseDown={this.props.onMouseDownOnHeader}
 | 
			
		||||
                pipMode={this.props.pipMode}
 | 
			
		||||
                onPipMouseDown={onMouseDownOnHeader}
 | 
			
		||||
                pipMode={pipMode}
 | 
			
		||||
                callRooms={[callRoom, secCallRoom]}
 | 
			
		||||
            />
 | 
			
		||||
            { contentView }
 | 
			
		||||
            <div className="mx_CallView_content_wrapper" ref={this.contentWrapperRef}>
 | 
			
		||||
                { this.renderToast() }
 | 
			
		||||
                { this.renderContent() }
 | 
			
		||||
                { this.renderCallControls() }
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,6 @@
 | 
			
		|||
/*
 | 
			
		||||
Copyright 2015, 2016, 2019 The Matrix.org Foundation C.I.C.
 | 
			
		||||
Copyright 2015, 2016, 2019, 2020, 2021 The Matrix.org Foundation C.I.C.
 | 
			
		||||
Copyright 2021 - 2022 Š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.
 | 
			
		||||
| 
						 | 
				
			
			@ -39,7 +40,8 @@ interface IProps {
 | 
			
		|||
    // due to a change in video metadata
 | 
			
		||||
    onResize?: (e: Event) => void;
 | 
			
		||||
 | 
			
		||||
    primary: boolean;
 | 
			
		||||
    primary?: boolean;
 | 
			
		||||
    secondary?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface IState {
 | 
			
		||||
| 
						 | 
				
			
			@ -178,9 +180,11 @@ export default class VideoFeed extends React.PureComponent<IProps, IState> {
 | 
			
		|||
    };
 | 
			
		||||
 | 
			
		||||
    render() {
 | 
			
		||||
        const { pipMode, primary, feed } = this.props;
 | 
			
		||||
        const { pipMode, primary, secondary, feed } = this.props;
 | 
			
		||||
 | 
			
		||||
        const wrapperClasses = classnames("mx_VideoFeed", {
 | 
			
		||||
            mx_VideoFeed_primary: primary,
 | 
			
		||||
            mx_VideoFeed_secondary: secondary,
 | 
			
		||||
            mx_VideoFeed_voice: this.state.videoMuted,
 | 
			
		||||
        });
 | 
			
		||||
        const micIconClasses = classnames("mx_VideoFeed_mic", {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1014,16 +1014,16 @@
 | 
			
		|||
    "You can use <code>/help</code> to list available commands. Did you mean to send this as a message?": "You can use <code>/help</code> to list available commands. Did you mean to send this as a message?",
 | 
			
		||||
    "Hint: Begin your message with <code>//</code> to start it with a slash.": "Hint: Begin your message with <code>//</code> to start it with a slash.",
 | 
			
		||||
    "Send as message": "Send as message",
 | 
			
		||||
    "You are presenting": "You are presenting",
 | 
			
		||||
    "%(sharerName)s is presenting": "%(sharerName)s is presenting",
 | 
			
		||||
    "Your camera is turned off": "Your camera is turned off",
 | 
			
		||||
    "Your camera is still enabled": "Your camera is still enabled",
 | 
			
		||||
    "unknown person": "unknown person",
 | 
			
		||||
    "Consulting with %(transferTarget)s. <a>Transfer to %(transferee)s</a>": "Consulting with %(transferTarget)s. <a>Transfer to %(transferee)s</a>",
 | 
			
		||||
    "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",
 | 
			
		||||
    "You are presenting": "You are presenting",
 | 
			
		||||
    "%(sharerName)s is presenting": "%(sharerName)s is presenting",
 | 
			
		||||
    "Your camera is turned off": "Your camera is turned off",
 | 
			
		||||
    "Your camera is still enabled": "Your camera is still enabled",
 | 
			
		||||
    "Dial": "Dial",
 | 
			
		||||
    "%(count)s people connected|other": "%(count)s people connected",
 | 
			
		||||
    "%(count)s people connected|one": "%(count)s person connected",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue