just enough view code to join a call

This commit is contained in:
Bruno Windels 2022-03-23 12:23:10 +01:00
parent 9efd191f4e
commit 0a37fd561e
13 changed files with 141 additions and 28 deletions

View file

@ -48,7 +48,8 @@ export class RoomViewModel extends ViewModel {
_setupCallViewModel() {
// pick call for this room with lowest key
this._callObservable = new PickMapObservableValue(this.getOption("session").callHandler.calls.filterValues(c => c.roomId === this._room.id));
const calls = this.getOption("session").callHandler.calls;
this._callObservable = new PickMapObservableValue(calls.filterValues(c => c.roomId === this._room.id && c.hasJoined));
this._callViewModel = undefined;
this.track(this._callObservable.subscribe(call => {
this._callViewModel = this.disposeTracked(this._callViewModel);
@ -68,6 +69,7 @@ export class RoomViewModel extends ViewModel {
try {
const timeline = await this._room.openTimeline();
this._tilesCreator = tilesCreator(this.childOptions({
session: this.getOption("session"),
roomVM: this,
timeline,
}));
@ -349,9 +351,8 @@ export class RoomViewModel extends ViewModel {
async startCall() {
try {
const session = this.getOption("session");
const mediaTracks = await this.platform.mediaDevices.getMediaTracks(true, true);
const mediaTracks = await this.platform.mediaDevices.getMediaTracks(false, true);
const localMedia = new LocalMedia().withTracks(mediaTracks);
console.log("localMedia", localMedia.tracks);
// this will set the callViewModel above as a call will be added to callHandler.calls
await session.callHandler.createCall(this._room.id, localMedia, "A call " + Math.round(this.platform.random() * 100));
} catch (err) {

View file

@ -0,0 +1,48 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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 {SimpleTile} from "./SimpleTile.js";
import {LocalMedia} from "../../../../../matrix/calls/LocalMedia";
// TODO: timeline entries for state events with the same state key and type
// should also update previous entries in the timeline, so we can update the name of the call, whether it is terminated, etc ...
// alternatively, we could just subscribe to the GroupCall and spontanously emit an update when it updates
export class CallTile extends SimpleTile {
get shape() {
return "call";
}
get name() {
return this._entry.content["m.name"];
}
get _call() {
const calls = this.getOption("session").callHandler.calls;
return calls.get(this._entry.stateKey);
}
async join() {
const call = this._call;
if (call) {
const mediaTracks = await this.platform.mediaDevices.getMediaTracks(false, true);
const localMedia = new LocalMedia().withTracks(mediaTracks);
await call.join(localMedia);
}
}
}

View file

@ -26,6 +26,7 @@ import {RoomMemberTile} from "./tiles/RoomMemberTile.js";
import {EncryptedEventTile} from "./tiles/EncryptedEventTile.js";
import {EncryptionEnabledTile} from "./tiles/EncryptionEnabledTile.js";
import {MissingAttachmentTile} from "./tiles/MissingAttachmentTile.js";
import {CallTile} from "./tiles/CallTile.js";
export function tilesCreator(baseOptions) {
const tilesCreator = function tilesCreator(entry, emitUpdate) {
@ -71,6 +72,8 @@ export function tilesCreator(baseOptions) {
return new EncryptedEventTile(options);
case "m.room.encryption":
return new EncryptionEnabledTile(options);
case "m.call":
return entry.stateKey ? new CallTile(options) : null;
default:
// unknown type not rendered
return null;

View file

@ -86,6 +86,7 @@ export class DeviceMessageHandler {
this._senderDeviceCache.set(device);
}
}
console.log("incoming device message", senderKey, device, this._senderDeviceCache);
return device;
}
}

View file

@ -78,14 +78,15 @@ export class Session {
this._callHandler = new CallHandler({
createTimeout: this._platform.clock.createTimeout,
hsApi: this._hsApi,
encryptDeviceMessage: async (roomId, message, log) => {
encryptDeviceMessage: async (roomId, userId, message, log) => {
if (!this._deviceTracker || !this._olmEncryption) {
throw new Error("encryption is not enabled");
}
// TODO: just get the devices we're sending the message to, not all the room devices
// although we probably already fetched all devices to send messages in the likely e2ee room
await this._deviceTracker.trackRoom(roomId, log);
const devices = await this._deviceTracker.devicesForTrackedRoom(roomId, this._hsApi, log);
await this._deviceTracker.trackRoom(this.rooms.get(roomId), log);
const devices = await this._deviceTracker.devicesForRoomMembers(roomId, [userId], this._hsApi, log);
console.log("devices", devices);
const encryptedMessage = await this._olmEncryption.encrypt(message.type, message.content, devices, this._hsApi, log);
return encryptedMessage;
},

View file

@ -224,6 +224,7 @@ export class Sync {
_openPrepareSyncTxn() {
const storeNames = this._storage.storeNames;
return this._storage.readTxn([
storeNames.deviceIdentities, // to read device from olm messages
storeNames.olmSessions,
storeNames.inboundGroupSessions,
// to read fragments when loading sync writer when rejoining archived room

View file

@ -114,6 +114,7 @@ export class PeerCall implements IDisposable {
}
});
this.logger = {
info(...args) { console.info.apply(console, ["WebRTC debug:", ...args])},
debug(...args) { console.log.apply(console, ["WebRTC debug:", ...args])},
log(...args) { console.log.apply(console, ["WebRTC log:", ...args])},
warn(...args) { console.log.apply(console, ["WebRTC warn:", ...args])},

View file

@ -41,7 +41,7 @@ export enum GroupCallState {
export type Options = Omit<MemberOptions, "emitUpdate" | "confId" | "encryptDeviceMessage"> & {
emitUpdate: (call: GroupCall, params?: any) => void;
encryptDeviceMessage: (roomId: string, message: SignallingMessage<MGroupCallBase>, log: ILogItem) => Promise<EncryptedMessage>,
encryptDeviceMessage: (roomId: string, userId: string, message: SignallingMessage<MGroupCallBase>, log: ILogItem) => Promise<EncryptedMessage>,
storage: Storage,
ownDeviceId: string
};
@ -61,13 +61,13 @@ export class GroupCall {
) {
this.id = id ?? makeId("conf-");
this._state = id ? GroupCallState.Created : GroupCallState.Fledgling;
this._memberOptions = Object.assign({
this._memberOptions = Object.assign({}, options, {
confId: this.id,
emitUpdate: member => this._members.update(member.member.userId, member),
encryptDeviceMessage: (message: SignallingMessage<MGroupCallBase>, log) => {
return this.options.encryptDeviceMessage(this.roomId, message, log);
encryptDeviceMessage: (userId: string, message: SignallingMessage<MGroupCallBase>, log) => {
return this.options.encryptDeviceMessage(this.roomId, userId, message, log);
}
}, options);
});
}
get localMedia(): LocalMedia | undefined { return this._localMedia; }
@ -99,6 +99,10 @@ export class GroupCall {
}
}
get hasJoined() {
return this._state === GroupCallState.Joining || this._state === GroupCallState.Joined;
}
async leave() {
const memberContent = await this._leaveCallMemberContent();
// send m.call.member state event
@ -165,6 +169,7 @@ export class GroupCall {
/** @internal */
handleDeviceMessage(message: SignallingMessage<MGroupCallBase>, userId: string, deviceId: string, log: ILogItem) {
console.log("incoming to_device call signalling message from", userId, deviceId, message);
// TODO: return if we are not membering to the call
let member = this._members.get(userId);
if (member) {

View file

@ -17,6 +17,7 @@ limitations under the License.
import {PeerCall, CallState} from "../PeerCall";
import {makeTxnId, makeId} from "../../common";
import {EventType} from "../callEventTypes";
import {formatToDeviceMessagesPayload} from "../../common";
import type {Options as PeerCallOptions} from "../PeerCall";
import type {LocalMedia} from "../LocalMedia";
@ -32,7 +33,7 @@ export type Options = Omit<PeerCallOptions, "emitUpdate" | "sendSignallingMessag
confId: string,
ownUserId: string,
hsApi: HomeServerApi,
encryptDeviceMessage: (message: SignallingMessage<MGroupCallBase>, log: ILogItem) => Promise<EncryptedMessage>,
encryptDeviceMessage: (userId: string, message: SignallingMessage<MGroupCallBase>, log: ILogItem) => Promise<EncryptedMessage>,
emitUpdate: (participant: Member, params?: any) => void,
}
@ -81,13 +82,14 @@ export class Member {
sendSignallingMessage = async (message: SignallingMessage<MCallBase>, log: ILogItem) => {
const groupMessage = message as SignallingMessage<MGroupCallBase>;
groupMessage.content.conf_id = this.options.confId;
const encryptedMessage = await this.options.encryptDeviceMessage(groupMessage, log);
const encryptedMessages = await this.options.encryptDeviceMessage(this.member.userId, groupMessage, log);
const payload = formatToDeviceMessagesPayload(encryptedMessages);
const request = this.options.hsApi.sendToDevice(
"m.room.encrypted",
{[this.member.userId]: {
["*"]: encryptedMessage.content
}
}, makeTxnId(), {log});
payload,
makeTxnId(),
{log}
);
await request.response();
}

View file

@ -15,6 +15,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {groupBy} from "../utils/groupBy";
export function makeTxnId() {
return makeId("t");
}
@ -29,6 +32,20 @@ export function isTxnId(txnId) {
return txnId.startsWith("t") && txnId.length === 15;
}
export function formatToDeviceMessagesPayload(messages) {
const messagesByUser = groupBy(messages, message => message.device.userId);
const payload = {
messages: Array.from(messagesByUser.entries()).reduce((userMap, [userId, messages]) => {
userMap[userId] = messages.reduce((deviceMap, message) => {
deviceMap[message.device.deviceId] = message.content;
return deviceMap;
}, {});
return userMap;
}, {})
};
return payload;
}
export function tests() {
return {
"isTxnId succeeds on result of makeTxnId": assert => {

View file

@ -18,7 +18,7 @@ import {MEGOLM_ALGORITHM, DecryptionSource} from "./common.js";
import {groupEventsBySession} from "./megolm/decryption/utils";
import {mergeMap} from "../../utils/mergeMap";
import {groupBy} from "../../utils/groupBy";
import {makeTxnId} from "../common.js";
import {makeTxnId, formatToDeviceMessagesPayload} from "../common.js";
const ENCRYPTED_TYPE = "m.room.encrypted";
// how often ensureMessageKeyIsShared can check if it needs to
@ -386,6 +386,7 @@ export class RoomEncryption {
await writeTxn.complete();
}
// TODO: make this use _sendMessagesToDevices
async _sendSharedMessageToDevices(type, message, devices, hsApi, log) {
const devicesByUser = groupBy(devices, device => device.userId);
const payload = {
@ -403,16 +404,7 @@ export class RoomEncryption {
async _sendMessagesToDevices(type, messages, hsApi, log) {
log.set("messages", messages.length);
const messagesByUser = groupBy(messages, message => message.device.userId);
const payload = {
messages: Array.from(messagesByUser.entries()).reduce((userMap, [userId, messages]) => {
userMap[userId] = messages.reduce((deviceMap, message) => {
deviceMap[message.device.deviceId] = message.content;
return deviceMap;
}, {});
return userMap;
}, {})
};
const payload = formatToDeviceMessagesPayload(messages);
const txnId = makeTxnId();
await hsApi.sendToDevice(type, payload, txnId, {log}).response();
}

View file

@ -24,6 +24,7 @@ import {AnnouncementView} from "./timeline/AnnouncementView.js";
import {RedactedView} from "./timeline/RedactedView.js";
import {SimpleTile} from "../../../../../domain/session/room/timeline/tiles/SimpleTile.js";
import {GapView} from "./timeline/GapView.js";
import {CallTileView} from "./timeline/CallTileView";
export type TileView = GapView | AnnouncementView | TextMessageView |
ImageView | VideoView | FileView | LocationView | MissingAttachmentView | RedactedView;
@ -51,5 +52,7 @@ export function viewClassForEntry(entry: SimpleTile): TileViewConstructor | unde
return MissingAttachmentView;
case "redacted":
return RedactedView;
case "call":
return CallTileView;
}
}

View file

@ -0,0 +1,38 @@
/*
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.
*/
import {TemplateView} from "../../../general/TemplateView";
import type {CallTile} from "../../../../../../domain/session/room/timeline/tiles/CallTile";
export class CallTileView extends TemplateView<CallTile> {
render(t, vm) {
return t.li(
{className: "AnnouncementView"},
t.div([
"Call ",
vm => vm.name,
t.button({className: "CallTileView_join"}, "Join")
])
);
}
/* This is called by the parent ListView, which just has 1 listener for the whole list */
onClick(evt) {
if (evt.target.className === "CallTileView_join") {
this.value.join();
}
}
}