(feat): improved mobile notifications
(feat): add lint rules (feat): improve markdown inline code box
This commit is contained in:
parent
336b9464c9
commit
4a841de073
35 changed files with 777 additions and 287 deletions
|
|
@ -17,22 +17,6 @@ type MediaShareStoreState = {
|
|||
cameraSession: LocalMediaShareSession | null;
|
||||
};
|
||||
|
||||
type MediaShareStoreSetState = (
|
||||
updater:
|
||||
| Partial<MediaShareStoreState>
|
||||
| ((state: MediaShareStoreState) => Partial<MediaShareStoreState>),
|
||||
) => void;
|
||||
|
||||
type MediaShareControllerOptions = {
|
||||
room: Room;
|
||||
getState: () => MediaShareStoreState;
|
||||
setState: MediaShareStoreSetState;
|
||||
getLocalParticipantId: () => number | null;
|
||||
startWatching: (participantId: number) => void;
|
||||
stopWatching: (participantId: number) => void;
|
||||
syncParticipantState: () => void;
|
||||
};
|
||||
|
||||
export function createMediaShareController({
|
||||
room,
|
||||
getState,
|
||||
|
|
@ -41,7 +25,19 @@ export function createMediaShareController({
|
|||
startWatching,
|
||||
stopWatching,
|
||||
syncParticipantState,
|
||||
}: MediaShareControllerOptions) {
|
||||
}: {
|
||||
room: Room;
|
||||
getState: () => MediaShareStoreState;
|
||||
setState: (
|
||||
updater:
|
||||
| Partial<MediaShareStoreState>
|
||||
| ((state: MediaShareStoreState) => Partial<MediaShareStoreState>),
|
||||
) => void;
|
||||
getLocalParticipantId: () => number | null;
|
||||
startWatching: (participantId: number) => void;
|
||||
stopWatching: (participantId: number) => void;
|
||||
syncParticipantState: () => void;
|
||||
}) {
|
||||
function getSession(kind: MediaShareKind) {
|
||||
return kind === "screen"
|
||||
? getState().screenShareSession
|
||||
|
|
|
|||
|
|
@ -8,32 +8,16 @@ import type {
|
|||
MediaShareSource,
|
||||
} from "./types";
|
||||
|
||||
type MobileMediaApi = {
|
||||
startScreenShare: (includeAudio: boolean) => void;
|
||||
stopScreenShare: () => void;
|
||||
requestCameraPermission: () => void;
|
||||
};
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
tensaminMobileMedia?: MobileMediaApi;
|
||||
tensaminMobileMedia?: {
|
||||
startScreenShare: (includeAudio: boolean) => void;
|
||||
stopScreenShare: () => void;
|
||||
requestCameraPermission: () => void;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
type FrameDetail = {
|
||||
data: string;
|
||||
mimeType: string;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
|
||||
type AudioDetail = {
|
||||
data: string;
|
||||
sampleRate: number;
|
||||
channelCount: number;
|
||||
encoding: "pcm16le";
|
||||
};
|
||||
|
||||
function eventDetail<T>(event: Event): T {
|
||||
return (event as CustomEvent<T>).detail;
|
||||
}
|
||||
|
|
@ -164,7 +148,12 @@ async function startMobileScreen(
|
|||
};
|
||||
|
||||
const onFrame = (event: Event) => {
|
||||
const detail = eventDetail<FrameDetail>(event);
|
||||
const detail = eventDetail<{
|
||||
data: string;
|
||||
mimeType: string;
|
||||
width: number;
|
||||
height: number;
|
||||
}>(event);
|
||||
const image = new Image();
|
||||
image.onload = () => {
|
||||
if (canvas.width !== detail.width || canvas.height !== detail.height) {
|
||||
|
|
@ -179,7 +168,14 @@ async function startMobileScreen(
|
|||
};
|
||||
|
||||
const onAudio = (event: Event) => {
|
||||
const bytes = decodeBase64(eventDetail<AudioDetail>(event).data);
|
||||
const bytes = decodeBase64(
|
||||
eventDetail<{
|
||||
data: string;
|
||||
sampleRate: number;
|
||||
channelCount: number;
|
||||
encoding: "pcm16le";
|
||||
}>(event).data,
|
||||
);
|
||||
const samples = new Int16Array(
|
||||
bytes.buffer,
|
||||
bytes.byteOffset,
|
||||
|
|
|
|||
|
|
@ -10,18 +10,19 @@ const SPEAKING_HANGTIME_MS = 500;
|
|||
const ANALYSIS_INTERVAL_MS = 30;
|
||||
const FFT_SIZE = 256;
|
||||
|
||||
type AnalyserEntry = {
|
||||
source: MediaStreamAudioSourceNode;
|
||||
analyser: AnalyserNode;
|
||||
track: MediaStreamTrack;
|
||||
originalTrack?: MediaStreamTrack;
|
||||
lastSpeakingTime: number;
|
||||
isSpeaking: boolean;
|
||||
};
|
||||
|
||||
class SpeakingDetector {
|
||||
private audioContext: AudioContext | null = null;
|
||||
private entries = new Map<number, AnalyserEntry>();
|
||||
private entries = new Map<
|
||||
number,
|
||||
{
|
||||
source: MediaStreamAudioSourceNode;
|
||||
analyser: AnalyserNode;
|
||||
track: MediaStreamTrack;
|
||||
originalTrack?: MediaStreamTrack;
|
||||
lastSpeakingTime: number;
|
||||
isSpeaking: boolean;
|
||||
}
|
||||
>();
|
||||
private intervalId: ReturnType<typeof setInterval> | null = null;
|
||||
private deaf = false;
|
||||
private gateThresholdStart = -50;
|
||||
|
|
|
|||
|
|
@ -1,12 +1,10 @@
|
|||
import { create } from "zustand";
|
||||
|
||||
type SpeakingState = {
|
||||
const useSpeakingState = create<{
|
||||
speakingParticipantIds: Set<number>;
|
||||
lastSpeakingParticipantId: number | null;
|
||||
micGated: boolean;
|
||||
};
|
||||
|
||||
const useSpeakingState = create<SpeakingState>(() => ({
|
||||
}>(() => ({
|
||||
speakingParticipantIds: new Set(),
|
||||
lastSpeakingParticipantId: null,
|
||||
micGated: false,
|
||||
|
|
|
|||
|
|
@ -51,7 +51,6 @@ setLogExtension(
|
|||
getLogger("tensamin"),
|
||||
);
|
||||
|
||||
type CallState = "closed" | "closing" | "connecting" | "open" | "encrypting";
|
||||
type CallView = "preview" | "focused" | "grid";
|
||||
type ProtocolCallSecret = NonNullable<
|
||||
z.infer<typeof mtp.CallInvite.response>["CallSecret"]
|
||||
|
|
@ -63,18 +62,9 @@ type WrappedCallSecret = {
|
|||
kemCiphertext: Uint8Array;
|
||||
wrappingScheme: string;
|
||||
};
|
||||
type IncomingCallInvite = {
|
||||
callId: string;
|
||||
callSecret: WrappedCallSecret;
|
||||
senderId: number;
|
||||
};
|
||||
type CurrentCallData =
|
||||
(z.infer<typeof mtp.CallData.response> & { exists: boolean }) | null;
|
||||
|
||||
type NavigateFn = (options: {
|
||||
to: string;
|
||||
search?: Record<string, unknown>;
|
||||
}) => Promise<void>;
|
||||
type SendFn = (
|
||||
type: string,
|
||||
data: Record<string, unknown>,
|
||||
|
|
@ -84,44 +74,15 @@ type GetUserFn = (userId: number) => Promise<{ PublicKey: string }>;
|
|||
type RemoteVideoTrackSelector = Track.Kind | Track.Source;
|
||||
|
||||
type Runtime = {
|
||||
navigate: NavigateFn;
|
||||
navigate: (options: {
|
||||
to: string;
|
||||
search?: Record<string, unknown>;
|
||||
}) => Promise<void>;
|
||||
send: SendFn;
|
||||
load: LoadFn;
|
||||
getUser: GetUserFn;
|
||||
};
|
||||
|
||||
type CallStore = {
|
||||
state: CallState;
|
||||
view: CallView;
|
||||
invitedUserId: number | null;
|
||||
callId: string | null;
|
||||
incomingCallInvite: IncomingCallInvite | null;
|
||||
callSecret: string | null;
|
||||
livekitToken: string | null;
|
||||
currentCallData: CurrentCallData;
|
||||
deaf: boolean;
|
||||
micEnabled: boolean;
|
||||
cameraEnabled: boolean;
|
||||
screenShareEnabled: boolean;
|
||||
screenShareSession: LocalMediaShareSession | null;
|
||||
cameraSession: LocalMediaShareSession | null;
|
||||
disabledCameraParticipantIds: number[];
|
||||
focusedParticipantId: number | null;
|
||||
focusedParticipantType: "user" | "stream" | null;
|
||||
usersInFocusedViewHidden: boolean;
|
||||
watchedStreamParticipantIds: number[];
|
||||
pendingWatchedParticipantIds: number[];
|
||||
activeScreenShareParticipantIds: number[];
|
||||
isEncrypted: boolean;
|
||||
ownCallSecretInvitePending: boolean;
|
||||
callIsFullscreen: boolean;
|
||||
callIsPopout: boolean;
|
||||
layoutVersion: number;
|
||||
screenRef: React.RefObject<HTMLDivElement | null> | null;
|
||||
runtime: Runtime | null;
|
||||
lastFocusedParticipantId: number | null;
|
||||
};
|
||||
|
||||
let _keyProvider: ExternalE2EEKeyProvider | null = null;
|
||||
let _e2eeWorker: Worker | null = null;
|
||||
let _room: Room | null = null;
|
||||
|
|
@ -1195,7 +1156,41 @@ async function ensureNoiseFilter(
|
|||
}
|
||||
}
|
||||
|
||||
export const useCall = create<CallStore>(() => ({
|
||||
export const useCall = create<{
|
||||
state: "closed" | "closing" | "connecting" | "open" | "encrypting";
|
||||
view: CallView;
|
||||
invitedUserId: number | null;
|
||||
callId: string | null;
|
||||
incomingCallInvite: {
|
||||
callId: string;
|
||||
callSecret: WrappedCallSecret;
|
||||
senderId: number;
|
||||
} | null;
|
||||
callSecret: string | null;
|
||||
livekitToken: string | null;
|
||||
currentCallData: CurrentCallData;
|
||||
deaf: boolean;
|
||||
micEnabled: boolean;
|
||||
cameraEnabled: boolean;
|
||||
screenShareEnabled: boolean;
|
||||
screenShareSession: LocalMediaShareSession | null;
|
||||
cameraSession: LocalMediaShareSession | null;
|
||||
disabledCameraParticipantIds: number[];
|
||||
focusedParticipantId: number | null;
|
||||
focusedParticipantType: "user" | "stream" | null;
|
||||
usersInFocusedViewHidden: boolean;
|
||||
watchedStreamParticipantIds: number[];
|
||||
pendingWatchedParticipantIds: number[];
|
||||
activeScreenShareParticipantIds: number[];
|
||||
isEncrypted: boolean;
|
||||
ownCallSecretInvitePending: boolean;
|
||||
callIsFullscreen: boolean;
|
||||
callIsPopout: boolean;
|
||||
layoutVersion: number;
|
||||
screenRef: React.RefObject<HTMLDivElement | null> | null;
|
||||
runtime: Runtime | null;
|
||||
lastFocusedParticipantId: number | null;
|
||||
}>(() => ({
|
||||
state: "closed",
|
||||
view: "preview",
|
||||
invitedUserId: null,
|
||||
|
|
|
|||
Loading…
Reference in a new issue