just enough view code to join a call
This commit is contained in:
parent
9efd191f4e
commit
0a37fd561e
13 changed files with 141 additions and 28 deletions
|
@ -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) {
|
||||
|
|
48
src/domain/session/room/timeline/tiles/CallTile.js
Normal file
48
src/domain/session/room/timeline/tiles/CallTile.js
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -86,6 +86,7 @@ export class DeviceMessageHandler {
|
|||
this._senderDeviceCache.set(device);
|
||||
}
|
||||
}
|
||||
console.log("incoming device message", senderKey, device, this._senderDeviceCache);
|
||||
return device;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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])},
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
38
src/platform/web/ui/session/room/timeline/CallTileView.ts
Normal file
38
src/platform/web/ui/session/room/timeline/CallTileView.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
Reference in a new issue