Skip to content

Commit 15764b1

Browse files
author
root
committed
Cherry-pick PR mattermost#1123: Handle join_call links in webapp
Fixes: Call links (/call link) now work when clicked from within webapp. Previously join_call parameter was read once at initialization and only checked on channel changes. Now links work both when pasted and when clicked internally. Source: mattermost#1123
1 parent a76e60d commit 15764b1

File tree

1 file changed

+108
-6
lines changed

1 file changed

+108
-6
lines changed

webapp/src/index.tsx

Lines changed: 108 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -428,17 +428,27 @@ export default class Plugin {
428428
});
429429

430430
const connectToCall = async (channelId: string, teamId?: string, title?: string, rootId?: string) => {
431-
if (!channelIDForCurrentCall(store.getState())) {
431+
const currentCallChannelId = channelIDForCurrentCall(store.getState());
432+
433+
// Also check window.callsClient for active call (handles race condition during page load)
434+
const hasActiveClient = Boolean(window.callsClient);
435+
const activeClientChannel = window.callsClient?.channelID;
436+
437+
if (!currentCallChannelId && !hasActiveClient) {
438+
// Not in any call - join the new one
432439
connectCall(channelId, title, rootId);
433440

434441
// following the thread only on join. On call start
435442
// this is done in the call_start ws event handler.
436443
if (channelHasCall(store.getState(), channelId)) {
437444
followThread(store, channelId, teamId);
438445
}
439-
} else if (channelIDForCurrentCall(store.getState()) !== channelId) {
446+
} else if ((currentCallChannelId && currentCallChannelId !== channelId) || (activeClientChannel && activeClientChannel !== channelId)) {
447+
// In a different call - show switch modal
440448
store.dispatch(showSwitchCallModal(channelId));
441449
}
450+
451+
// If already in this call, do nothing
442452
};
443453

444454
const joinCall = async (channelId: string, teamId?: string, title?: string, rootId?: string) => {
@@ -1115,9 +1125,84 @@ export default class Plugin {
11151125
this.registerWebSocketEvents(registry, store);
11161126

11171127
let currChannelId = getCurrentChannelId(store.getState());
1118-
let joinCallParam = new URLSearchParams(window.location.search).get('join_call');
1128+
let processedJoinCallUrl = '';
1129+
let pendingJoinChannelId = '';
1130+
let lastCheckedUrl = '';
1131+
1132+
// Capture join_call parameter immediately at initialization, before React Router can strip it
1133+
// This is essential for pasted URLs where the channel ID isn't loaded in Redux yet
1134+
let initialJoinCallParam = new URLSearchParams(window.location.search).get('join_call');
1135+
1136+
// Function to check and handle join_call parameter
1137+
const handleJoinCallParam = () => {
1138+
const currentChannelId = getCurrentChannelId(store.getState());
1139+
const currentUrl = window.location.href;
1140+
const joinCallParam = new URLSearchParams(window.location.search).get('join_call');
1141+
1142+
// Check join_call parameter - only process each unique URL once
1143+
if (joinCallParam && currentChannelId && currentUrl !== processedJoinCallUrl) {
1144+
connectToCall(currentChannelId);
1145+
processedJoinCallUrl = currentUrl;
1146+
initialJoinCallParam = null; // Clear captured param since we processed it
1147+
}
1148+
};
1149+
1150+
// Intercept clicks on links with join_call parameter BEFORE React Router handles them
1151+
const handleLinkClick = (e: MouseEvent) => {
1152+
const target = e.target as HTMLElement;
1153+
const link = target.closest('a');
1154+
if (!link) {
1155+
return;
1156+
}
1157+
1158+
const href = link.getAttribute('href');
1159+
if (!href) {
1160+
return;
1161+
}
1162+
1163+
// Check if link contains join_call parameter
1164+
try {
1165+
const url = new URL(href, window.location.origin);
1166+
if (url.searchParams.get('join_call') === 'true') {
1167+
// Extract channel ID from URL
1168+
// URL format: /team-name/channels/channel-id?join_call=true
1169+
const channelMatch = url.pathname.match(/\/channels\/([a-z0-9]+)/i);
1170+
if (channelMatch) {
1171+
const targetChannelId = channelMatch[1];
1172+
const currentChannelId = getCurrentChannelId(store.getState());
1173+
1174+
// If clicking link in same channel, prevent navigation and join directly
1175+
if (targetChannelId === currentChannelId) {
1176+
e.preventDefault();
1177+
e.stopPropagation();
1178+
e.stopImmediatePropagation();
1179+
1180+
// Defer connectToCall to next tick to avoid the modal's closeOnBlur handler
1181+
// catching the same click event that triggered showing the modal, which would
1182+
// immediately hide the modal that was just shown
1183+
setTimeout(() => {
1184+
connectToCall(targetChannelId);
1185+
}, 0);
1186+
return;
1187+
}
1188+
1189+
// Different channel - set pending join and let navigation happen
1190+
// React Router will strip the query param, so we track it here
1191+
pendingJoinChannelId = targetChannelId;
1192+
}
1193+
}
1194+
} catch {
1195+
// Invalid URL, ignore
1196+
}
1197+
};
1198+
document.addEventListener('click', handleLinkClick, true);
1199+
this.unsubscribers.push(() => document.removeEventListener('click', handleLinkClick, true));
1200+
1201+
// Also check on Redux store updates (for navigation to different channels)
11191202
this.unsubscribers.push(store.subscribe(() => {
11201203
const currentChannelId = getCurrentChannelId(store.getState());
1204+
1205+
// Handle channel changes
11211206
if (currChannelId !== currentChannelId) {
11221207
const firstLoad = !currChannelId;
11231208
currChannelId = currentChannelId;
@@ -1126,12 +1211,29 @@ export default class Plugin {
11261211
// on every channel switch.
11271212
if (firstLoad) {
11281213
registerHeaderMenuComponentIfNeeded(currentChannelId);
1214+
1215+
// On first load, if we captured a join_call parameter, process it now
1216+
// This handles pasted URLs where the parameter is captured before channel loads
1217+
if (initialJoinCallParam && currentChannelId) {
1218+
initialJoinCallParam = null; // Clear it so we don't process again
1219+
connectToCall(currentChannelId);
1220+
}
11291221
}
11301222

1131-
if (currChannelId && Boolean(joinCallParam) && !channelIDForCurrentCall(store.getState())) {
1132-
connectCall(currChannelId);
1223+
// Check if we navigated to a pending join channel
1224+
if (pendingJoinChannelId && pendingJoinChannelId === currentChannelId) {
1225+
// Clear the flag immediately - connectToCall() will handle the rest
1226+
// (including showing switch modal if already in a call)
1227+
pendingJoinChannelId = '';
1228+
connectToCall(currentChannelId);
11331229
}
1134-
joinCallParam = '';
1230+
}
1231+
1232+
// Check for join_call parameter only when URL changes (optimization)
1233+
const currentUrl = window.location.href;
1234+
if (currentUrl !== lastCheckedUrl) {
1235+
lastCheckedUrl = currentUrl;
1236+
handleJoinCallParam();
11351237
}
11361238
}));
11371239

0 commit comments

Comments
 (0)