improve calls view

This commit is contained in:
Bruno Windels 2022-06-09 15:33:59 +02:00
parent bfdea03bbd
commit 10caba6872
12 changed files with 359 additions and 67 deletions

View file

@ -115,7 +115,7 @@ export class ViewModel<O extends Options = Options> extends EventEmitter<{change
return result; return result;
} }
emitChange(changedProps: any): void { emitChange(changedProps?: any): void {
if (this._options.emitChange) { if (this._options.emitChange) {
this._options.emitChange(changedProps); this._options.emitChange(changedProps);
} else { } else {

View file

@ -46,12 +46,32 @@ export class CallViewModel extends ViewModel<Options> {
.mapValues(member => new CallMemberViewModel(this.childOptions({member, mediaRepository: this.getOption("room").mediaRepository}))) .mapValues(member => new CallMemberViewModel(this.childOptions({member, mediaRepository: this.getOption("room").mediaRepository})))
.join(ownMemberViewModelMap) .join(ownMemberViewModelMap)
.sortValues((a, b) => a.compare(b)); .sortValues((a, b) => a.compare(b));
this.track(this.memberViewModels.subscribe({
onRemove: () => {
this.emitChange(); // update memberCount
},
onAdd: () => {
this.emitChange(); // update memberCount
},
onUpdate: () => {},
onReset: () => {},
onMove: () => {}
}))
} }
private get call(): GroupCall { get isCameraMuted(): boolean {
return this.getOption("call"); return isLocalCameraMuted(this.call);
} }
get isMicrophoneMuted(): boolean {
return isLocalMicrophoneMuted(this.call);
}
get memberCount(): number {
return this.memberViewModels.length;
}
get name(): string { get name(): string {
return this.call.name; return this.call.name;
} }
@ -60,19 +80,27 @@ export class CallViewModel extends ViewModel<Options> {
return this.call.id; return this.call.id;
} }
get stream(): Stream | undefined { private get call(): GroupCall {
return this.call.localMedia?.userMedia; return this.getOption("call");
} }
leave() { hangup() {
if (this.call.hasJoined) { if (this.call.hasJoined) {
this.call.leave(); this.call.leave();
} }
} }
async toggleVideo() { async toggleCamera() {
if (this.call.muteSettings) { if (this.call.muteSettings) {
this.call.setMuted(this.call.muteSettings.toggleCamera()); this.call.setMuted(this.call.muteSettings.toggleCamera());
this.emitChange();
}
}
async toggleMicrophone() {
if (this.call.muteSettings) {
this.call.setMuted(this.call.muteSettings.toggleMicrophone());
this.emitChange();
} }
} }
} }
@ -102,11 +130,11 @@ class OwnMemberViewModel extends ViewModel<Options> implements IStreamViewModel
} }
get isCameraMuted(): boolean { get isCameraMuted(): boolean {
return isMuted(this.call.muteSettings?.camera, !!getStreamVideoTrack(this.stream)); return isLocalCameraMuted(this.call);
} }
get isMicrophoneMuted(): boolean { get isMicrophoneMuted(): boolean {
return isMuted(this.call.muteSettings?.microphone, !!getStreamAudioTrack(this.stream)); return isLocalMicrophoneMuted(this.call);
} }
get avatarLetter(): string { get avatarLetter(): string {
@ -209,3 +237,11 @@ function isMuted(muted: boolean | undefined, hasTrack: boolean) {
return !hasTrack; return !hasTrack;
} }
} }
function isLocalCameraMuted(call: GroupCall): boolean {
return isMuted(call.muteSettings?.camera, !!getStreamVideoTrack(call.localMedia?.userMedia));
}
function isLocalMicrophoneMuted(call: GroupCall): boolean {
return isMuted(call.muteSettings?.microphone, !!getStreamAudioTrack(call.localMedia?.userMedia));
}

View file

@ -0,0 +1,206 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
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.
*/
.CallView {
height: 40vh;
display: grid;
}
.CallView > * {
grid-column: 1;
grid-row: 1;
}
.CallView_members {
display: grid;
gap: 12px;
background: var(--background-color-secondary--darker-60);
padding: 12px;
margin: 0;
min-height: 0;
list-style: none;
align-self: stretch;
}
.StreamView {
display: grid;
border-radius: 8px;
overflow: hidden;
background-color: black;
}
.StreamView > * {
grid-column: 1;
grid-row: 1;
}
.StreamView video {
width: 100%;
height: 100%;
object-fit: contain;
}
.StreamView_avatar {
align-self: center;
justify-self: center;
}
.StreamView_muteStatus {
align-self: start;
justify-self: end;
width: 24px;
height: 24px;
background-position: center;
background-repeat: no-repeat;
background-size: 14px;
display: block;
background-color: var(--text-color);
border-radius: 4px;
margin: 4px;
}
.StreamView_muteStatus.microphoneMuted {
background-image: url("./icons/mic-muted.svg?primary=text-color--lighter-80");
}
.StreamView_muteStatus.cameraMuted {
background-image: url("./icons/cam-muted.svg?primary=text-color--lighter-80");
}
.CallView_buttons {
align-self: end;
justify-self: center;
display: flex;
gap: 12px;
margin-bottom: 16px;
}
.CallView_buttons button {
border-radius: 100%;
width: 48px;
height: 48px;
border: none;
background-color: var(--accent-color);
background-position: center;
background-repeat: no-repeat;
}
.CallView_buttons .CallView_hangup {
background-color: var(--error-color);
background-image: url("./icons/hangup.svg?primary=background-color-primary");
}
.CallView_buttons .CallView_mutedMicrophone {
background-color: var(--background-color-primary);
background-image: url("./icons/mic-muted.svg?primary=text-color");
}
.CallView_buttons .CallView_unmutedMicrophone {
background-image: url("./icons/mic-unmuted.svg?primary=background-color-primary");
}
.CallView_buttons .CallView_mutedCamera {
background-color: var(--background-color-primary);
background-image: url("./icons/cam-muted.svg?primary=text-color");
}
.CallView_buttons .CallView_unmutedCamera {
background-image: url("./icons/cam-unmuted.svg?primary=background-color-primary");
}
.CallView_members.size1 {
grid-template-columns: 1fr;
grid-template-rows: 1fr;
}
.CallView_members.size2 {
grid-template-columns: 1fr;
grid-template-rows: repeat(2, 1fr);
}
/* square */
.CallView_members.square.size3,
.CallView_members.square.size4 {
grid-template-columns: repeat(2, 1fr);
grid-template-rows: repeat(2, 1fr);
}
.CallView_members.square.size5,
.CallView_members.square.size6 {
grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(2, 1fr);
}
.CallView_members.square.size7,
.CallView_members.square.size8,
.CallView_members.square.size9 {
grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(3, 1fr);
}
.CallView_members.square.size10 {
grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(4, 1fr);
}
/** tall */
.CallView_members.tall.size3 {
grid-template-columns: 1fr;
grid-template-rows: repeat(3, 1fr);
}
.CallView_members.tall.size4 {
grid-template-columns: 1fr;
grid-template-rows: repeat(4, 1fr);
}
.CallView_members.tall.size5,
.CallView_members.tall.size6 {
grid-template-rows: repeat(3, 1fr);
grid-template-columns: repeat(2, 1fr);
}
.CallView_members.tall.size7,
.CallView_members.tall.size8 {
grid-template-rows: repeat(4, 1fr);
grid-template-columns: repeat(2, 1fr);
}
.CallView_members.tall.size9,
.CallView_members.tall.size10 {
grid-template-rows: repeat(5, 1fr);
grid-template-columns: repeat(2, 1fr);
}
/** wide */
.CallView_members.wide.size2 {
grid-template-columns: repeat(2, 1fr);
grid-template-rows: 1fr;
}
.CallView_members.wide.size3 {
grid-template-rows: 1fr;
grid-template-columns: repeat(3, 1fr);
}
.CallView_members.wide.size4 {
grid-template-rows: 1fr;
grid-template-columns: repeat(4, 1fr);
}
.CallView_members.wide.size5,
.CallView_members.wide.size6 {
grid-template-rows: repeat(2, 1fr);
grid-template-columns: repeat(3, 1fr);
}
.CallView_members.wide.size7,
.CallView_members.wide.size8 {
grid-template-rows: repeat(2, 1fr);
grid-template-columns: repeat(4, 1fr);
}
.CallView_members.wide.size9,
.CallView_members.wide.size10 {
grid-template-rows: repeat(3, 1fr);
grid-template-columns: repeat(4, 1fr);
}

View file

@ -0,0 +1 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M0.290472 1.37627C0.677768 0.985743 1.3057 0.985743 1.69299 1.37627L16.569 16.3762C16.9563 16.7668 16.9563 17.3999 16.569 17.7904C16.1817 18.181 15.5538 18.181 15.1665 17.7904L0.290472 2.79048C-0.096824 2.39995 -0.096824 1.76679 0.290472 1.37627Z" fill="#ff00ff"></path><path d="M0.597515 5.19186C0.323238 5.646 0.165249 6.17941 0.165249 6.75001V14.0833C0.165249 15.7402 1.49729 17.0833 3.14045 17.0833H12.363L0.639137 5.2371C0.624608 5.22242 0.610733 5.20733 0.597515 5.19186Z" fill="#ff00ff"></path><path d="M14.2148 6.75002V11.9031L6.14598 3.75002H11.2396C12.8828 3.75002 14.2148 5.09317 14.2148 6.75002Z" fill="#ff00ff"></path><path d="M18.3887 5.88312L15.8677 7.91669V12.9167L18.3887 14.9503C19.038 15.4741 19.9999 15.0079 19.9999 14.1694V6.66399C19.9999 5.82548 19.038 5.35931 18.3887 5.88312Z" fill="#ff00ff"></path></svg>

After

Width:  |  Height:  |  Size: 934 B

View file

@ -0,0 +1 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M2.50001 3.33334C1.11929 3.33334 0 4.45264 0 5.83336V14.1667C0 15.5474 1.11929 16.6667 2.50001 16.6667H11.6667C13.0474 16.6667 14.1667 15.5474 14.1667 14.1667V5.83336C14.1667 4.45264 13.0474 3.33334 11.6667 3.33334H2.50001Z" fill="#ff00ff"></path><path d="M18.6462 5.24983L15.8334 7.50004V12.5001L18.6462 14.7503C19.1918 15.1868 20.0001 14.7983 20.0001 14.0996V5.90056C20.0001 5.2018 19.1918 4.81332 18.6462 5.24983Z" fill="#ff00ff"></path></svg>

After

Width:  |  Height:  |  Size: 551 B

View file

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.0084 7.75648C10.3211 7.69163 6.85136 8.12949 6.00781 8.35133C5.95792 8.36445 5.90044 8.37912 5.83616 8.39552C4.54101 8.72607 0.48272 9.76183 0.0442423 13.0436C-0.295466 15.5862 1.40575 16.3558 2.25618 16.2386C2.84479 16.1648 4.5301 15.8983 6.08724 15.6189C7.61629 15.3446 7.61551 14.3359 7.61499 13.6538C7.61498 13.6413 7.61497 13.6288 7.61497 13.6165L7.61497 12.2453C7.61497 11.8961 7.94315 11.6942 8.3958 11.6396C9.99822 11.422 11.3359 11.4213 12.0055 11.4213L12.0112 11.4213C12.6807 11.4213 14.0018 11.422 15.6042 11.6396C16.0569 11.6942 16.385 11.8961 16.385 12.2453L16.385 13.6165C16.385 13.6289 16.385 13.6413 16.385 13.6538C16.3845 14.3359 16.3837 15.3446 17.9128 15.619C19.4699 15.8983 21.1552 16.1648 21.7438 16.2386C22.5942 16.3558 24.2955 15.5862 23.9558 13.0436C23.5173 9.76183 19.459 8.72608 18.1638 8.39553C18.0996 8.37913 18.0421 8.36446 17.9922 8.35134C17.1487 8.1295 13.6956 7.69163 12.0084 7.75648Z" fill="#ff00ff"/>
</svg>

After

Width:  |  Height:  |  Size: 1 KiB

View file

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<path
d="m 4.1217459,2.7782531 c -0.3709886,-0.3710026 -0.9725054,-0.3710026 -1.343494,0 -0.3710025,0.3709886 -0.3710025,0.9725053 0,1.3434939 L 8.1999992,9.5435095 V 12 c 0,2.098686 1.7013144,3.8 3.7999998,3.8 0.704722,0 1.364635,-0.19183 1.93037,-0.526122 l 1.372342,1.372328 C 14.370936,17.309764 13.231045,17.7 11.999999,17.7 8.8519707,17.7 6.2999991,15.148029 6.2999991,12 c 0,-0.524663 -0.4253364,-0.95 -0.95,-0.95 -0.5246638,0 -0.9500002,0.425337 -0.9500002,0.95 0,3.875644 2.9009978,7.073739 6.6500001,7.541217 V 20.55 c 0,0.52471 0.425337,0.95 0.95,0.95 0.524664,0 0.95,-0.42529 0.95,-0.95 v -1.008783 c 1.387668,-0.173095 2.659163,-0.720139 3.710352,-1.537372 l 3.217902,3.217902 c 0.371004,0.371004 0.97249,0.371004 1.343494,0 0.371004,-0.371004 0.371004,-0.97249 0,-1.343494 z"
fill="#ff00ff" />
<path
d="m 17.50209,13.494223 1.488825,1.49188 C 19.383012,14.069434 19.6,13.060061 19.6,12 c 0,-0.524663 -0.425289,-0.95 -0.95,-0.95 -0.52471,0 -0.95,0.425337 -0.95,0.95 0,0.517048 -0.06887,1.017997 -0.19791,1.494223 z"
fill="#ff00ff" />
<path
d="M 8.609236,4.5827722 15.8,11.788534 V 6.3000001 c 0,-2.0986857 -1.701315,-3.8 -3.800001,-3.8 -1.480728,0 -2.7636389,0.8469192 -3.390763,2.0827721 z"
fill="#ff00ff" />
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<path
d="M 8.06739,6.2516702 C 8.06739,4.1796776 9.8281549,2.5 12.000157,2.5 c 2.172018,0 3.932783,1.6796776 3.932783,3.7516702 v 5.6107078 c 0,2.071992 -1.760765,3.75167 -3.932783,3.75167 -2.1720021,0 -3.932767,-1.679678 -3.932767,-3.75167 z"
fill="#ff00ff" />
<path
d="m 5.1176439,10.664726 c 0.69616,0 1.2605143,0.53836 1.2605143,1.20246 0,2.951033 2.507364,5.346879 5.6068308,5.354479 0.0051,0 0.01014,0 0.01523,0 0.0051,0 0.0101,0 0.01514,0 3.099312,-0.0078 5.606474,-2.403554 5.606474,-5.354479 0,-0.6641 0.564416,-1.20246 1.260515,-1.20246 0.696097,0 1.260514,0.53836 1.260514,1.20246 0,3.878047 -2.984629,7.089761 -6.882141,7.66705 v 0.763258 c 0,0.664146 -0.564339,1.202506 -1.260499,1.202506 -0.69616,0 -1.260514,-0.53836 -1.260514,-1.202506 v -0.763258 c -3.8976481,-0.577135 -6.8825558,-3.788863 -6.8825558,-7.66705 0,-0.6641 0.5643543,-1.20246 1.2604987,-1.20246 z"
fill="#ff00ff" />
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 7C0 5.34315 1.34315 4 3 4H14C15.6569 4 17 5.34315 17 7V17C17 18.6569 15.6569 20 14 20H3C1.34315 20 0 18.6569 0 17V7Z" fill="#ff00ff"/>
<path d="M19 9L22.3753 6.29976C23.0301 5.77595 24 6.24212 24 7.08062V16.9194C24 17.7579 23.0301 18.2241 22.3753 17.7002L19 15V9Z" fill="#ff00ff"/>
</svg>

After

Width:  |  Height:  |  Size: 397 B

View file

@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.02698 13.9613C7.16801 15.1932 9.91484 17.3263 10.6635 17.7641C10.7078 17.79 10.7585 17.8201 10.8152 17.8538C11.9576 18.5329 15.5373 20.6609 18.1454 18.6694C20.1661 17.1266 19.5091 15.3909 18.8289 14.875C18.3633 14.5128 16.9914 13.5145 15.7006 12.6152C14.4331 11.7322 13.7268 12.4397 13.2492 12.918C13.2404 12.9268 13.2317 12.9355 13.2231 12.9442L12.2621 13.9051C12.0174 14.1498 11.6451 14.0605 11.2886 13.7804C10.0092 12.8061 9.06809 11.8659 8.59723 11.395L8.59326 11.391C8.12246 10.9202 7.19387 9.99076 6.21957 8.7114C5.93949 8.35485 5.85018 7.9826 6.09489 7.73789L7.05586 6.77693C7.06448 6.7683 7.0732 6.7596 7.08199 6.75082C7.56034 6.27321 8.2678 5.56684 7.38479 4.29937C6.48555 3.0086 5.4872 1.6367 5.125 1.17106C4.60907 0.490937 2.87345 -0.166084 1.33056 1.85458C-0.660932 4.46274 1.46708 8.04241 2.1462 9.18482C2.17991 9.24152 2.21005 9.29221 2.23593 9.33649C2.67367 10.0851 4.79507 12.8203 6.02698 13.9613Z" fill="#ff00ff"/>
</svg>

After

Width:  |  Height:  |  Size: 1 KiB

View file

@ -18,6 +18,7 @@ limitations under the License.
@import url('../../main.css'); @import url('../../main.css');
@import url('inter.css'); @import url('inter.css');
@import url('timeline.css'); @import url('timeline.css');
@import url('call.css');
:root { :root {
font-size: 10px; font-size: 10px;
@ -1155,56 +1156,3 @@ button.RoomDetailsView_row::after {
background-position: center; background-position: center;
background-size: 36px; background-size: 36px;
} }
.CallView {
max-height: 50vh;
overflow-y: auto;
}
.CallView ul {
display: flex;
margin: 0;
gap: 12px;
padding: 0;
flex-wrap: wrap;
justify-content: center;
}
.StreamView {
width: 360px;
min-height: 200px;
border: 2px var(--accent-color) solid;
display: grid;
border-radius: 8px;
overflow: hidden;
background-color: black;
}
.StreamView > * {
grid-column: 1;
grid-row: 1;
}
.StreamView video {
width: 100%;
}
.StreamView_avatar {
align-self: center;
justify-self: center;
}
.StreamView_muteStatus {
align-self: end;
justify-self: start;
color: var(--text-color--lighter-80);
}
.StreamView_muteStatus.microphoneMuted::before {
content: "mic muted";
}
.StreamView_muteStatus.cameraMuted::before {
content: "cam muted";
}

View file

@ -17,26 +17,81 @@ limitations under the License.
import {TemplateView, Builder} from "../../general/TemplateView"; import {TemplateView, Builder} from "../../general/TemplateView";
import {AvatarView} from "../../AvatarView"; import {AvatarView} from "../../AvatarView";
import {ListView} from "../../general/ListView"; import {ListView} from "../../general/ListView";
import {classNames} from "../../general/html";
import {Stream} from "../../../../types/MediaDevices"; import {Stream} from "../../../../types/MediaDevices";
import type {CallViewModel, CallMemberViewModel, IStreamViewModel} from "../../../../../domain/session/room/CallViewModel"; import type {CallViewModel, CallMemberViewModel, IStreamViewModel} from "../../../../../domain/session/room/CallViewModel";
export class CallView extends TemplateView<CallViewModel> { export class CallView extends TemplateView<CallViewModel> {
private resizeObserver?: ResizeObserver;
render(t: Builder<CallViewModel>, vm: CallViewModel): Element { render(t: Builder<CallViewModel>, vm: CallViewModel): Element {
const members = t.view(new ListView({
className: "CallView_members",
list: vm.memberViewModels
}, vm => new StreamView(vm))) as HTMLElement;
this.bindMembersCssClasses(t, members);
return t.div({class: "CallView"}, [ return t.div({class: "CallView"}, [
t.p(vm => `Call ${vm.name} (${vm.id})`), members,
t.view(new ListView({list: vm.memberViewModels}, vm => new StreamView(vm))), //t.p(vm => `Call ${vm.name}`),
t.div({class: "buttons"}, [ t.div({class: "CallView_buttons"}, [
t.button({onClick: () => vm.leave()}, "Leave"), t.button({className: {
t.button({onClick: () => vm.toggleVideo()}, "Toggle video"), "CallView_mutedMicrophone": vm => vm.isMicrophoneMuted,
"CallView_unmutedMicrophone": vm => !vm.isMicrophoneMuted,
}, onClick: () => vm.toggleMicrophone()}),
t.button({className: {
"CallView_mutedCamera": vm => vm.isCameraMuted,
"CallView_unmutedCamera": vm => !vm.isCameraMuted,
}, onClick: () => vm.toggleCamera()}),
t.button({className: "CallView_hangup", onClick: () => vm.hangup()}),
]) ])
]); ]);
} }
private bindMembersCssClasses(t, members) {
t.mapSideEffect(vm => vm.memberCount, count => {
members.classList.forEach((c, _, list) => {
if (c.startsWith("size")) {
list.remove(c);
}
});
members.classList.add(`size${count}`);
});
// update classes describing aspect ratio categories
if (typeof ResizeObserver === "function") {
const set = (c, flag) => {
if (flag) {
members.classList.add(c);
} else {
members.classList.remove(c);
}
};
this.resizeObserver = new ResizeObserver(() => {
const ar = members.clientWidth / members.clientHeight;
const isTall = ar < 0.5;
const isSquare = !isTall && ar < 1.8
const isWide = !isTall && !isSquare;
set("tall", isTall);
set("square", isSquare);
set("wide", isWide);
});
this.resizeObserver!.observe(members);
}
}
public unmount() {
if (this.resizeObserver) {
this.resizeObserver.unobserve((this.root()! as Element).querySelector(".CallView_members"));
this.resizeObserver = undefined;
}
super.unmount();
}
} }
class StreamView extends TemplateView<IStreamViewModel> { class StreamView extends TemplateView<IStreamViewModel> {
render(t: Builder<IStreamViewModel>, vm: IStreamViewModel): Element { render(t: Builder<IStreamViewModel>, vm: IStreamViewModel): Element {
const video = t.video({ const video = t.video({
autoplay: true, autoplay: true,
disablePictureInPicture: true,
className: { className: {
hidden: vm => vm.isCameraMuted hidden: vm => vm.isCameraMuted
} }