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() { _setupCallViewModel() {
// pick call for this room with lowest key // 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._callViewModel = undefined;
this.track(this._callObservable.subscribe(call => { this.track(this._callObservable.subscribe(call => {
this._callViewModel = this.disposeTracked(this._callViewModel); this._callViewModel = this.disposeTracked(this._callViewModel);
@ -68,6 +69,7 @@ export class RoomViewModel extends ViewModel {
try { try {
const timeline = await this._room.openTimeline(); const timeline = await this._room.openTimeline();
this._tilesCreator = tilesCreator(this.childOptions({ this._tilesCreator = tilesCreator(this.childOptions({
session: this.getOption("session"),
roomVM: this, roomVM: this,
timeline, timeline,
})); }));
@ -349,9 +351,8 @@ export class RoomViewModel extends ViewModel {
async startCall() { async startCall() {
try { try {
const session = this.getOption("session"); 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); 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 // 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)); await session.callHandler.createCall(this._room.id, localMedia, "A call " + Math.round(this.platform.random() * 100));
} catch (err) { } 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 {EncryptedEventTile} from "./tiles/EncryptedEventTile.js";
import {EncryptionEnabledTile} from "./tiles/EncryptionEnabledTile.js"; import {EncryptionEnabledTile} from "./tiles/EncryptionEnabledTile.js";
import {MissingAttachmentTile} from "./tiles/MissingAttachmentTile.js"; import {MissingAttachmentTile} from "./tiles/MissingAttachmentTile.js";
import {CallTile} from "./tiles/CallTile.js";
export function tilesCreator(baseOptions) { export function tilesCreator(baseOptions) {
const tilesCreator = function tilesCreator(entry, emitUpdate) { const tilesCreator = function tilesCreator(entry, emitUpdate) {
@ -71,6 +72,8 @@ export function tilesCreator(baseOptions) {
return new EncryptedEventTile(options); return new EncryptedEventTile(options);
case "m.room.encryption": case "m.room.encryption":
return new EncryptionEnabledTile(options); return new EncryptionEnabledTile(options);
case "m.call":
return entry.stateKey ? new CallTile(options) : null;
default: default:
// unknown type not rendered // unknown type not rendered
return null; return null;

View file

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

View file

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

View file

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

View file

@ -114,6 +114,7 @@ export class PeerCall implements IDisposable {
} }
}); });
this.logger = { this.logger = {
info(...args) { console.info.apply(console, ["WebRTC debug:", ...args])},
debug(...args) { console.log.apply(console, ["WebRTC debug:", ...args])}, debug(...args) { console.log.apply(console, ["WebRTC debug:", ...args])},
log(...args) { console.log.apply(console, ["WebRTC log:", ...args])}, log(...args) { console.log.apply(console, ["WebRTC log:", ...args])},
warn(...args) { console.log.apply(console, ["WebRTC warn:", ...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"> & { export type Options = Omit<MemberOptions, "emitUpdate" | "confId" | "encryptDeviceMessage"> & {
emitUpdate: (call: GroupCall, params?: any) => void; 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, storage: Storage,
ownDeviceId: string ownDeviceId: string
}; };
@ -61,13 +61,13 @@ export class GroupCall {
) { ) {
this.id = id ?? makeId("conf-"); this.id = id ?? makeId("conf-");
this._state = id ? GroupCallState.Created : GroupCallState.Fledgling; this._state = id ? GroupCallState.Created : GroupCallState.Fledgling;
this._memberOptions = Object.assign({ this._memberOptions = Object.assign({}, options, {
confId: this.id, confId: this.id,
emitUpdate: member => this._members.update(member.member.userId, member), emitUpdate: member => this._members.update(member.member.userId, member),
encryptDeviceMessage: (message: SignallingMessage<MGroupCallBase>, log) => { encryptDeviceMessage: (userId: string, message: SignallingMessage<MGroupCallBase>, log) => {
return this.options.encryptDeviceMessage(this.roomId, message, log); return this.options.encryptDeviceMessage(this.roomId, userId, message, log);
} }
}, options); });
} }
get localMedia(): LocalMedia | undefined { return this._localMedia; } 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() { async leave() {
const memberContent = await this._leaveCallMemberContent(); const memberContent = await this._leaveCallMemberContent();
// send m.call.member state event // send m.call.member state event
@ -165,6 +169,7 @@ export class GroupCall {
/** @internal */ /** @internal */
handleDeviceMessage(message: SignallingMessage<MGroupCallBase>, userId: string, deviceId: string, log: ILogItem) { 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 // TODO: return if we are not membering to the call
let member = this._members.get(userId); let member = this._members.get(userId);
if (member) { if (member) {

View file

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

View file

@ -15,6 +15,9 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import {groupBy} from "../utils/groupBy";
export function makeTxnId() { export function makeTxnId() {
return makeId("t"); return makeId("t");
} }
@ -29,6 +32,20 @@ export function isTxnId(txnId) {
return txnId.startsWith("t") && txnId.length === 15; 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() { export function tests() {
return { return {
"isTxnId succeeds on result of makeTxnId": assert => { "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 {groupEventsBySession} from "./megolm/decryption/utils";
import {mergeMap} from "../../utils/mergeMap"; import {mergeMap} from "../../utils/mergeMap";
import {groupBy} from "../../utils/groupBy"; import {groupBy} from "../../utils/groupBy";
import {makeTxnId} from "../common.js"; import {makeTxnId, formatToDeviceMessagesPayload} from "../common.js";
const ENCRYPTED_TYPE = "m.room.encrypted"; const ENCRYPTED_TYPE = "m.room.encrypted";
// how often ensureMessageKeyIsShared can check if it needs to // how often ensureMessageKeyIsShared can check if it needs to
@ -386,6 +386,7 @@ export class RoomEncryption {
await writeTxn.complete(); await writeTxn.complete();
} }
// TODO: make this use _sendMessagesToDevices
async _sendSharedMessageToDevices(type, message, devices, hsApi, log) { async _sendSharedMessageToDevices(type, message, devices, hsApi, log) {
const devicesByUser = groupBy(devices, device => device.userId); const devicesByUser = groupBy(devices, device => device.userId);
const payload = { const payload = {
@ -403,16 +404,7 @@ export class RoomEncryption {
async _sendMessagesToDevices(type, messages, hsApi, log) { async _sendMessagesToDevices(type, messages, hsApi, log) {
log.set("messages", messages.length); log.set("messages", messages.length);
const messagesByUser = groupBy(messages, message => message.device.userId); const payload = formatToDeviceMessagesPayload(messages);
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 txnId = makeTxnId(); const txnId = makeTxnId();
await hsApi.sendToDevice(type, payload, txnId, {log}).response(); 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 {RedactedView} from "./timeline/RedactedView.js";
import {SimpleTile} from "../../../../../domain/session/room/timeline/tiles/SimpleTile.js"; import {SimpleTile} from "../../../../../domain/session/room/timeline/tiles/SimpleTile.js";
import {GapView} from "./timeline/GapView.js"; import {GapView} from "./timeline/GapView.js";
import {CallTileView} from "./timeline/CallTileView";
export type TileView = GapView | AnnouncementView | TextMessageView | export type TileView = GapView | AnnouncementView | TextMessageView |
ImageView | VideoView | FileView | LocationView | MissingAttachmentView | RedactedView; ImageView | VideoView | FileView | LocationView | MissingAttachmentView | RedactedView;
@ -51,5 +52,7 @@ export function viewClassForEntry(entry: SimpleTile): TileViewConstructor | unde
return MissingAttachmentView; return MissingAttachmentView;
case "redacted": case "redacted":
return RedactedView; 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();
}
}
}