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) && (
-
-
-
-
-
- )}
- {loading || !mx ? (
-
- ) : (
-
-
- {(serverConfigs) => (
-
-
-
- {children}
-
-
-
- )}
-
-
- )}
-
+
+
+ {mx && }
+ {loading && }
+ {(loadState.status === AsyncStatus.Error || startState.status === AsyncStatus.Error) && (
+
+
+
+
+
+ )}
+ {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;