diff --git a/.github/renovate.json b/.github/renovate.json index 62b0cf2a9..2c6c653e0 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -1,6 +1,10 @@ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "extends": ["config:recommended", ":dependencyDashboardApproval"], + "extends": [ + "config:recommended", + ":dependencyDashboardApproval", + ":semanticCommits" + ], "labels": ["Dependencies"], "packageRules": [ { diff --git a/.github/workflows/docker-pr.yml b/.github/workflows/docker-pr.yml index 822b8f3fc..960aeb8f6 100644 --- a/.github/workflows/docker-pr.yml +++ b/.github/workflows/docker-pr.yml @@ -30,6 +30,7 @@ jobs: with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} + continue-on-error: true - name: Login to the Github Container registry #Do not update this action from a outside PR if: github.event.pull_request.head.repo.fork == false @@ -38,6 +39,7 @@ jobs: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + continue-on-error: true - name: Extract metadata (tags, labels) for Docker, GHCR id: meta diff --git a/.github/workflows/pr-title.yml b/.github/workflows/pr-title.yml new file mode 100644 index 000000000..a52ee8e6d --- /dev/null +++ b/.github/workflows/pr-title.yml @@ -0,0 +1,15 @@ +name: Check PR title + +on: + pull_request_target: + types: + - opened + - edited + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6.1.1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/package-lock.json b/package-lock.json index ceb7e8c76..399f03a92 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "cinny", - "version": "4.10.5", + "version": "4.11.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cinny", - "version": "4.10.5", + "version": "4.11.1", "license": "AGPL-3.0-only", "dependencies": { "@atlaskit/pragmatic-drag-and-drop": "1.1.6", @@ -32,7 +32,7 @@ "emojibase-data": "15.3.2", "file-saver": "2.0.5", "focus-trap-react": "10.0.2", - "folds": "2.6.1", + "folds": "2.6.2", "html-dom-parser": "4.0.0", "html-react-parser": "4.2.0", "i18next": "23.12.2", @@ -59,10 +59,10 @@ "react-range": "1.8.14", "react-router-dom": "6.30.3", "sanitize-html": "2.12.1", - "slate": "0.112.0", - "slate-dom": "0.112.2", - "slate-history": "0.110.3", - "slate-react": "0.112.1", + "slate": "0.123.0", + "slate-dom": "0.123.0", + "slate-history": "0.113.1", + "slate-react": "0.123.0", "ua-parser-js": "1.0.35" }, "devDependencies": { @@ -7166,9 +7166,9 @@ } }, "node_modules/folds": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/folds/-/folds-2.6.1.tgz", - "integrity": "sha512-0L1ZSqwjFSg2fesa//C4DgP47Vp/KqDuzjAaOEYN21AvoptyVI+6OEXWrtIdE8DPQCZYr0bV+tqbrLyA6uAhaw==", + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/folds/-/folds-2.6.2.tgz", + "integrity": "sha512-1HemxxSnBm8/U5kq1pDQrFkpltWgQN90DmWCZWkZb7D2pe8BhOJSwIRLjk9WxHcw6nn69oz2XNYIXtSw0LvX1w==", "license": "Apache-2.0", "peerDependencies": { "@vanilla-extract/css": "1.9.2", @@ -10291,20 +10291,15 @@ } }, "node_modules/slate": { - "version": "0.112.0", - "resolved": "https://registry.npmjs.org/slate/-/slate-0.112.0.tgz", - "integrity": "sha512-PRnfFgDA3tSop4OH47zu4M1R4Uuhm/AmASu29Qp7sGghVFb713kPBKEnSf1op7Lx/nCHkRlCa3ThfHtCBy+5Yw==", - "license": "MIT", - "dependencies": { - "immer": "^10.0.3", - "is-plain-object": "^5.0.0", - "tiny-warning": "^1.0.3" - } + "version": "0.123.0", + "resolved": "https://registry.npmjs.org/slate/-/slate-0.123.0.tgz", + "integrity": "sha512-Oon3HR/QzJQBjuOUJT1jGGlp8Ff7t3Bkr/rJ2lDqxNT4H+cBnXpEVQ/si6hn1ZCHhD2xY/2N91PQoH/rD7kxTg==", + "license": "MIT" }, "node_modules/slate-dom": { - "version": "0.112.2", - "resolved": "https://registry.npmjs.org/slate-dom/-/slate-dom-0.112.2.tgz", - "integrity": "sha512-cozITMlpcBxrov854reM6+TooiHiqpfM/nZPrnjpN1wSiDsAQmYbWUyftC+jlwcpFj80vywfDHzlG6hXIc5h6A==", + "version": "0.123.0", + "resolved": "https://registry.npmjs.org/slate-dom/-/slate-dom-0.123.0.tgz", + "integrity": "sha512-OUinp4tvSrAlt64JL9y20Xin08jgnnj1gJmIuPdGvU5MELKXRNZh17a7EKKNOS6OZPAE8Dk9NI1MAIS/Qz0YBw==", "license": "MIT", "dependencies": { "@juggle/resize-observer": "^3.4.0", @@ -10316,13 +10311,13 @@ "tiny-invariant": "1.3.1" }, "peerDependencies": { - "slate": ">=0.99.0" + "slate": ">=0.121.0" } }, "node_modules/slate-history": { - "version": "0.110.3", - "resolved": "https://registry.npmjs.org/slate-history/-/slate-history-0.110.3.tgz", - "integrity": "sha512-sgdff4Usdflmw5ZUbhDkxFwCBQ2qlDKMMkF93w66KdV48vHOgN2BmLrf+2H8SdX8PYIpP/cTB0w8qWC2GwhDVA==", + "version": "0.113.1", + "resolved": "https://registry.npmjs.org/slate-history/-/slate-history-0.113.1.tgz", + "integrity": "sha512-J9NSJ+UG2GxoW0lw5mloaKcN0JI0x2IA5M5FxyGiInpn+QEutxT1WK7S/JneZCMFJBoHs1uu7S7e6pxQjubHmQ==", "license": "MIT", "dependencies": { "is-plain-object": "^5.0.0" @@ -10332,15 +10327,14 @@ } }, "node_modules/slate-react": { - "version": "0.112.1", - "resolved": "https://registry.npmjs.org/slate-react/-/slate-react-0.112.1.tgz", - "integrity": "sha512-V9b+waxPweXqAkSQmKQ1afG4Me6nVQACPpxQtHPIX02N7MXa5f5WilYv+bKt7vKKw+IZC2F0Gjzhv5BekVgP/A==", + "version": "0.123.0", + "resolved": "https://registry.npmjs.org/slate-react/-/slate-react-0.123.0.tgz", + "integrity": "sha512-nQwXL1FEacrY9ZFmatRhoBnsySNUX2x6qB77V3oNHd7wWxBJWuzz4GMrBXcVoRE8Gac7Angf8xaNGzb6zcPlHg==", "license": "MIT", "dependencies": { "@juggle/resize-observer": "^3.4.0", "direction": "^1.0.4", "is-hotkey": "^0.2.0", - "is-plain-object": "^5.0.0", "lodash": "^4.17.21", "scroll-into-view-if-needed": "^3.1.0", "tiny-invariant": "1.3.1" @@ -10348,18 +10342,8 @@ "peerDependencies": { "react": ">=18.2.0", "react-dom": ">=18.2.0", - "slate": ">=0.99.0", - "slate-dom": ">=0.110.2" - } - }, - "node_modules/slate/node_modules/immer": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz", - "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/immer" + "slate": ">=0.121.0", + "slate-dom": ">=0.119.1" } }, "node_modules/smob": { @@ -10729,11 +10713,6 @@ "integrity": "sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==", "license": "MIT" }, - "node_modules/tiny-warning": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", - "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" - }, "node_modules/tinyglobby": { "version": "0.2.10", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.10.tgz", diff --git a/package.json b/package.json index e6204a210..ba2cd751e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cinny", - "version": "4.10.5", + "version": "4.11.1", "description": "Yet another matrix client", "main": "index.js", "type": "module", @@ -44,7 +44,7 @@ "emojibase-data": "15.3.2", "file-saver": "2.0.5", "focus-trap-react": "10.0.2", - "folds": "2.6.1", + "folds": "2.6.2", "html-dom-parser": "4.0.0", "html-react-parser": "4.2.0", "i18next": "23.12.2", @@ -71,10 +71,10 @@ "react-range": "1.8.14", "react-router-dom": "6.30.3", "sanitize-html": "2.12.1", - "slate": "0.112.0", - "slate-dom": "0.112.2", - "slate-history": "0.110.3", - "slate-react": "0.112.1", + "slate": "0.123.0", + "slate-dom": "0.123.0", + "slate-history": "0.113.1", + "slate-react": "0.123.0", "ua-parser-js": "1.0.35" }, "devDependencies": { diff --git a/src/app/components/message/MsgTypeRenderers.tsx b/src/app/components/message/MsgTypeRenderers.tsx index a40ecae1e..abbf354de 100644 --- a/src/app/components/message/MsgTypeRenderers.tsx +++ b/src/app/components/message/MsgTypeRenderers.tsx @@ -389,6 +389,8 @@ export function MLocation({ content }: MLocationProps) { const geoUri = content.geo_uri; if (typeof geoUri !== 'string') return ; const location = parseGeoUri(geoUri); + if (!location) return ; + return ( {geoUri} diff --git a/src/app/cs-api.ts b/src/app/cs-api.ts index b9aac06ab..95a131a85 100644 --- a/src/app/cs-api.ts +++ b/src/app/cs-api.ts @@ -20,6 +20,16 @@ export type AutoDiscoveryInfo = Record & { 'm.identity_server'?: { base_url: string; }; + 'org.matrix.msc2965.authentication'?: { + account?: string; + issuer?: string; + }; + 'org.matrix.msc4143.rtc_foci'?: [ + { + livekit_service_url: string; + type: 'livekit'; + } + ]; }; export const autoDiscovery = async ( diff --git a/src/app/features/call-status/CallControl.tsx b/src/app/features/call-status/CallControl.tsx index 2f2bac7fb..6416fda52 100644 --- a/src/app/features/call-status/CallControl.tsx +++ b/src/app/features/call-status/CallControl.tsx @@ -1,14 +1,17 @@ import { Box, Chip, Icon, IconButton, Icons, Spinner, Text, Tooltip, TooltipProvider } from 'folds'; import React, { useCallback } from 'react'; +import { useSetAtom } from 'jotai'; import { StatusDivider } from './components'; import { CallEmbed, useCallControlState } from '../../plugins/call'; import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback'; +import { callEmbedAtom } from '../../state/callEmbed'; type MicrophoneButtonProps = { enabled: boolean; onToggle: () => Promise; + disabled?: boolean; }; -function MicrophoneButton({ enabled, onToggle }: MicrophoneButtonProps) { +function MicrophoneButton({ enabled, onToggle, disabled }: MicrophoneButtonProps) { return ( onToggle()} outlined + disabled={disabled} > @@ -38,8 +42,9 @@ function MicrophoneButton({ enabled, onToggle }: MicrophoneButtonProps) { type SoundButtonProps = { enabled: boolean; onToggle: () => void; + disabled?: boolean; }; -function SoundButton({ enabled, onToggle }: SoundButtonProps) { +function SoundButton({ enabled, onToggle, disabled }: SoundButtonProps) { return ( onToggle()} outlined + disabled={disabled} > Promise; + disabled?: boolean; }; -function VideoButton({ enabled, onToggle }: VideoButtonProps) { +function VideoButton({ enabled, onToggle, disabled }: VideoButtonProps) { return ( onToggle()} outlined + disabled={disabled} > void; + disabled?: boolean; }; -function ScreenShareButton({ enabled, onToggle }: ScreenShareButtonProps) { +function ScreenShareButton({ enabled, onToggle, disabled }: ScreenShareButtonProps) { return ( @@ -136,8 +146,17 @@ function ScreenShareButton({ enabled, onToggle }: ScreenShareButtonProps) { ); } -export function CallControl({ callEmbed, compact }: { callEmbed: CallEmbed; compact: boolean }) { +export function CallControl({ + callEmbed, + compact, + callJoined, +}: { + callEmbed: CallEmbed; + compact: boolean; + callJoined: boolean; +}) { const { microphone, video, sound, screenshare } = useCallControlState(callEmbed.control); + const setCallEmbed = useSetAtom(callEmbedAtom); const [hangupState, hangup] = useAsyncCallback( useCallback(() => callEmbed.hangup(), [callEmbed]) @@ -145,20 +164,38 @@ export function CallControl({ callEmbed, compact }: { callEmbed: CallEmbed; comp const exiting = hangupState.status === AsyncStatus.Loading || hangupState.status === AsyncStatus.Success; + const handleHangup = () => { + if (!callJoined) { + setCallEmbed(undefined); + return; + } + hangup(); + }; + return ( callEmbed.control.toggleMicrophone()} + disabled={!callJoined} + /> + callEmbed.control.toggleSound()} + disabled={!callJoined} /> - callEmbed.control.toggleSound()} /> {!compact && } - callEmbed.control.toggleVideo()} /> + callEmbed.control.toggleVideo()} + disabled={!callJoined} + /> {!compact && ( callEmbed.control.toggleScreenshare()} + disabled={!callJoined} /> )} @@ -176,7 +213,7 @@ export function CallControl({ callEmbed, compact }: { callEmbed: CallEmbed; comp } disabled={exiting} outlined - onClick={hangup} + onClick={handleHangup} > {!compact && ( diff --git a/src/app/features/call-status/CallStatus.tsx b/src/app/features/call-status/CallStatus.tsx index 5d2182c2c..1d30d1b40 100644 --- a/src/app/features/call-status/CallStatus.tsx +++ b/src/app/features/call-status/CallStatus.tsx @@ -74,7 +74,7 @@ export function CallStatus({ callEmbed }: CallStatusProps) { )} - + ); diff --git a/src/app/features/call/CallView.tsx b/src/app/features/call/CallView.tsx index 0cddd2be2..7c7bec6cc 100644 --- a/src/app/features/call/CallView.tsx +++ b/src/app/features/call/CallView.tsx @@ -13,10 +13,29 @@ import { useCallMembers, useCallSession } from '../../hooks/useCall'; import { CallMemberRenderer } from './CallMemberCard'; import * as css from './styles.css'; import { CallControls } from './CallControls'; +import { useLivekitSupport } from '../../hooks/useLivekitSupport'; -function JoinMessage({ hasParticipant }: { hasParticipant?: boolean }) { +function LivekitServerMissingMessage() { + return ( + + Your homeserver does not support calling. But you can still join call started by others. + + ); +} + +function JoinMessage({ + hasParticipant, + livekitSupported, +}: { + hasParticipant?: boolean; + livekitSupported?: boolean; +}) { if (hasParticipant) return null; + if (livekitSupported === false) { + return ; + } + return ( Voice chat’s empty — Be the first to hop in! @@ -43,12 +62,13 @@ function AlreadyInCallMessage() { function CallPrescreen() { const mx = useMatrixClient(); const room = useRoom(); + const livekitSupported = useLivekitSupport(); const powerLevels = usePowerLevelsContext(); const creators = useRoomCreators(room); const permissions = useRoomPermissions(creators, powerLevels); - const canJoin = permissions.event(StateEvent.GroupCallMemberPrefix, mx.getSafeUserId()); + const hasPermission = permissions.event(StateEvent.GroupCallMemberPrefix, mx.getSafeUserId()); const callSession = useCallSession(room); const callMembers = useCallMembers(room, callSession); @@ -57,6 +77,8 @@ function CallPrescreen() { const callEmbed = useCallEmbed(); const inOtherCall = callEmbed && callEmbed.roomId !== room.roomId; + const canJoin = hasPermission && (livekitSupported || hasParticipant); + return ( @@ -75,11 +97,15 @@ function CallPrescreen() { )} -
+ {!inOtherCall && - (canJoin ? : )} + (hasPermission ? ( + + ) : ( + + ))} {inOtherCall && } -
+
diff --git a/src/app/features/call/styles.css.ts b/src/app/features/call/styles.css.ts index 249edb08a..2b9f28ad6 100644 --- a/src/app/features/call/styles.css.ts +++ b/src/app/features/call/styles.css.ts @@ -22,3 +22,7 @@ export const CallMemberCard = style({ export const CallControlContainer = style({ padding: config.space.S400, }); + +export const PrescreenMessage = style({ + padding: config.space.S200, +}); diff --git a/src/app/features/room-nav/RoomNavItem.tsx b/src/app/features/room-nav/RoomNavItem.tsx index 9c5af9388..b317b13ab 100644 --- a/src/app/features/room-nav/RoomNavItem.tsx +++ b/src/app/features/room-nav/RoomNavItem.tsx @@ -57,6 +57,8 @@ import { useCallMembers, useCallSession } from '../../hooks/useCall'; import { useCallEmbed, useCallStart } from '../../hooks/useCallEmbed'; import { callChatAtom } from '../../state/callEmbed'; import { useCallPreferencesAtom } from '../../state/hooks/callPreferences'; +import { useAutoDiscoveryInfo } from '../../hooks/useAutoDiscoveryInfo'; +import { livekitSupport } from '../../hooks/useLivekitSupport'; type RoomNavItemMenuProps = { room: Room; @@ -282,8 +284,14 @@ export function RoomNavItem({ const startCall = useCallStart(direct); const callEmbed = useCallEmbed(); const callPref = useAtomValue(useCallPreferencesAtom()); + const autoDiscoveryInfo = useAutoDiscoveryInfo(); const handleStartCall: MouseEventHandler = (evt) => { + // Do not join if no livekit support or call is not started by others + if (!livekitSupport(autoDiscoveryInfo) && callMembers.length === 0) { + return; + } + // Do not join if already in call if (callEmbed) { return; diff --git a/src/app/features/room/RoomInput.tsx b/src/app/features/room/RoomInput.tsx index ae46d2d09..f88ccf938 100644 --- a/src/app/features/room/RoomInput.tsx +++ b/src/app/features/room/RoomInput.tsx @@ -221,7 +221,7 @@ export const RoomInput = forwardRef( const isComposing = useComposingCheck(); useElementSizeObserver( - useCallback(() => document.body, []), + useCallback(() => fileDropContainerRef.current, [fileDropContainerRef]), useCallback((width) => setHideStickerBtn(width < 500), []) ); diff --git a/src/app/features/room/RoomTimeline.tsx b/src/app/features/room/RoomTimeline.tsx index 640430540..39d7e50a6 100644 --- a/src/app/features/room/RoomTimeline.tsx +++ b/src/app/features/room/RoomTimeline.tsx @@ -1475,7 +1475,13 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli const senderId = mEvent.getSender() ?? ''; const senderName = getMemberDisplayName(room, senderId) || getMxIdLocalPart(senderId); - const callJoined = mEvent.getContent().application; + const content = mEvent.getContent(); + const prevContent = mEvent.getPrevContent(); + + const callJoined = content.application; + if (callJoined && 'application' in prevContent) { + return null; + } const timeJSX = (
Twitter diff --git a/src/app/pages/client/AutoDiscovery.tsx b/src/app/pages/client/AutoDiscovery.tsx new file mode 100644 index 000000000..76423477f --- /dev/null +++ b/src/app/pages/client/AutoDiscovery.tsx @@ -0,0 +1,32 @@ +import React, { ReactNode, useCallback, useMemo } from 'react'; +import { AutoDiscoveryInfoProvider } from '../../hooks/useAutoDiscoveryInfo'; +import { AsyncStatus, useAsyncCallbackValue } from '../../hooks/useAsyncCallback'; +import { autoDiscovery, AutoDiscoveryInfo } from '../../cs-api'; +import { getMxIdServer } from '../../utils/matrix'; + +type AutoDiscoveryProps = { + userId: string; + baseUrl: string; + children: ReactNode; +}; +export function AutoDiscovery({ userId, baseUrl, children }: AutoDiscoveryProps) { + const [state] = useAsyncCallbackValue( + useCallback(async () => { + const server = getMxIdServer(userId); + return autoDiscovery(fetch, server ?? userId); + }, [userId]) + ); + + const [, info] = state.status === AsyncStatus.Success ? state.data : []; + + const fallback: AutoDiscoveryInfo = useMemo( + () => ({ + 'm.homeserver': { + base_url: baseUrl, + }, + }), + [baseUrl] + ); + + return {children}; +} diff --git a/src/app/pages/client/ClientRoot.tsx b/src/app/pages/client/ClientRoot.tsx index e1a5dc0c0..93f0526e3 100644 --- a/src/app/pages/client/ClientRoot.tsx +++ b/src/app/pages/client/ClientRoot.tsx @@ -35,6 +35,7 @@ import { stopPropagation } from '../../utils/keyboard'; import { SyncStatus } from './SyncStatus'; import { AuthMetadataProvider } from '../../hooks/useAuthMetadata'; import { getFallbackSession } from '../../state/sessions'; +import { AutoDiscovery } from './AutoDiscovery'; function ClientRootLoading() { return ( @@ -143,7 +144,7 @@ type ClientRootProps = { }; export function ClientRoot({ children }: ClientRootProps) { const [loading, setLoading] = useState(true); - const { baseUrl } = getFallbackSession() ?? {}; + const { baseUrl, userId } = getFallbackSession() ?? {}; const [loadState, loadMatrix] = useAsyncCallback( useCallback(() => { @@ -183,47 +184,55 @@ export function ClientRoot({ children }: ClientRootProps) { ); return ( - - {mx && } - {loading && } - {(loadState.status === AsyncStatus.Error || startState.status === AsyncStatus.Error) && ( - - - - - {loadState.status === AsyncStatus.Error && ( - {`Failed to load. ${loadState.error.message}`} - )} - {startState.status === AsyncStatus.Error && ( - {`Failed to start. ${startState.error.message}`} - )} - - - - - - )} - {loading || !mx ? ( - - ) : ( - - - {(serverConfigs) => ( - - - - {children} - - - - )} - - - )} - + + + {mx && } + {loading && } + {(loadState.status === AsyncStatus.Error || startState.status === AsyncStatus.Error) && ( + + + + + {loadState.status === AsyncStatus.Error && ( + {`Failed to load. ${loadState.error.message}`} + )} + {startState.status === AsyncStatus.Error && ( + {`Failed to start. ${startState.error.message}`} + )} + + + + + + )} + {loading || !mx ? ( + + ) : ( + + + {(serverConfigs) => ( + + + + {children} + + + + )} + + + )} + + ); } diff --git a/src/app/pages/client/WelcomePage.tsx b/src/app/pages/client/WelcomePage.tsx index 79630990e..b3ec2eb44 100644 --- a/src/app/pages/client/WelcomePage.tsx +++ b/src/app/pages/client/WelcomePage.tsx @@ -24,7 +24,7 @@ export function WelcomePage() { target="_blank" rel="noreferrer noopener" > - v4.10.5 + v4.11.1 } diff --git a/src/app/plugins/call/CallEmbed.ts b/src/app/plugins/call/CallEmbed.ts index aeb28e360..870466769 100644 --- a/src/app/plugins/call/CallEmbed.ts +++ b/src/app/plugins/call/CallEmbed.ts @@ -146,7 +146,7 @@ export class CallEmbed { let initialMediaEvent = true; this.disposables.push( - this.listenEvent(ElementWidgetActions.DeviceMute, (evt) => { + this.listenAction(ElementWidgetActions.DeviceMute, (evt) => { if (initialMediaEvent) { initialMediaEvent = false; this.control.applyState(); @@ -177,18 +177,27 @@ export class CallEmbed { return this.call.transport.send(ElementWidgetActions.HangupCall, {}); } - public listenEvent(type: string, callback: (event: CustomEvent) => void) { - this.call.on(`action:${type}`, callback); - return () => { - this.call.off(`action:${type}`, callback); - }; + public onPreparing(callback: () => void) { + return this.listenEvent('preparing', callback); + } + + public onPreparingError(callback: (error: any) => void) { + return this.listenEvent('error:preparing', callback); + } + + public onReady(callback: () => void) { + return this.listenEvent('ready', callback); + } + + public onCapabilitiesNotified(callback: () => void) { + return this.listenEvent('capabilitiesNotified', callback); } private start() { // Room widgets get locked to the room they were added in this.call.setViewedRoomId(this.roomId); this.disposables.push( - this.listenEvent(ElementWidgetActions.JoinCall, this.onCallJoined.bind(this)) + this.listenAction(ElementWidgetActions.JoinCall, this.onCallJoined.bind(this)) ); // Populate the map of "read up to" events for this widget with the current event in every room. @@ -375,4 +384,15 @@ export class CallEmbed { } } } + + public listenAction(type: string, callback: (event: CustomEvent) => void) { + return this.listenEvent(`action:${type}`, callback); + } + + public listenEvent(type: string, callback: (event: T) => void) { + this.call.on(type, callback); + return () => { + this.call.off(type, callback); + }; + } } diff --git a/src/app/plugins/recent-emoji.ts b/src/app/plugins/recent-emoji.ts index 3634538fb..811ed9d5a 100644 --- a/src/app/plugins/recent-emoji.ts +++ b/src/app/plugins/recent-emoji.ts @@ -27,7 +27,11 @@ export const getRecentEmojis = (mx: MatrixClient, limit?: number): IEmoji[] => { export function addRecentEmoji(mx: MatrixClient, unicode: string) { const recentEmojiEvent = getAccountData(mx, AccountDataEvent.ElementRecentEmoji); - const recentEmoji = recentEmojiEvent?.getContent().recent_emoji ?? []; + const recentEmojiContent = recentEmojiEvent?.getContent(); + const recentEmoji = + recentEmojiContent && Array.isArray(recentEmojiContent.recent_emoji) + ? structuredClone(recentEmojiContent.recent_emoji) + : []; const emojiIndex = recentEmoji.findIndex(([u]) => u === unicode); let entry: [EmojiUnicode, EmojiUsageCount]; diff --git a/src/app/utils/common.ts b/src/app/utils/common.ts index 678f1b6ef..6bda28021 100644 --- a/src/app/utils/common.ts +++ b/src/app/utils/common.ts @@ -87,13 +87,21 @@ export const scaleYDimension = (x: number, scaledX: number, y: number): number = }; export const parseGeoUri = (location: string) => { - const [, data] = location.split(':'); - const [cords] = data.split(';'); - const [latitude, longitude] = cords.split(','); - return { - latitude, - longitude, - }; + try { + const [, data] = location.split(':'); + const [cords] = data.split(';'); + const [latitude, longitude] = cords.split(','); + + if (typeof latitude === 'string' && typeof longitude === 'string') { + return { + latitude, + longitude, + }; + } + return undefined; + } catch { + return undefined; + } }; const START_SLASHES_REG = /^\/+/g;