Compare commits
2 Commits
24c525e6bb
...
9ce8ad58b1
| Author | SHA1 | Date | |
|---|---|---|---|
| 9ce8ad58b1 | |||
| 95ac291a61 |
@@ -46,7 +46,7 @@ A full custom theme engine layered on top of Cinny's vanilla-extract theming:
|
|||||||
|
|
||||||
### Voice / Video Call Improvements
|
### Voice / Video Call Improvements
|
||||||
|
|
||||||
- **Element Call 0.19.3**: Upgraded from 0.16.3. Dist copied to `public/element-call/` by vite at build time.
|
- **Element Call 0.19.4**: Upgraded from 0.16.3. Dist copied to `public/element-call/` by vite at build time.
|
||||||
- **Camera default OFF**: Camera no longer persists across sessions via localStorage. Always starts disabled. Optional `cameraOnJoin` setting for explicit opt-in.
|
- **Camera default OFF**: Camera no longer persists across sessions via localStorage. Always starts disabled. Optional `cameraOnJoin` setting for explicit opt-in.
|
||||||
- **Deafen button**: Tooltip corrected to "Deafen" / "Undeafen" (was "Turn Off Sound" / "Turn On Sound")
|
- **Deafen button**: Tooltip corrected to "Deafen" / "Undeafen" (was "Turn Off Sound" / "Turn On Sound")
|
||||||
- **Screenshare confirmation**: A confirm dialog appears before screenshare is broadcast to call participants
|
- **Screenshare confirmation**: A confirm dialog appears before screenshare is broadcast to call participants
|
||||||
@@ -63,6 +63,9 @@ A full custom theme engine layered on top of Cinny's vanilla-extract theming:
|
|||||||
- **Poll display**: `m.poll.start` events (both stable Matrix 1.7 `m.poll` content key and MSC3381 unstable `org.matrix.msc3381.poll.start`) render as read-only poll cards inside the standard message bubble — question and answer options shown. Registered as top-level event renderers AND inside the `EncryptedContent` callback so encrypted polls also display after decryption. "Open in Element to vote" note displayed. Implemented in `PollContent.tsx`.
|
- **Poll display**: `m.poll.start` events (both stable Matrix 1.7 `m.poll` content key and MSC3381 unstable `org.matrix.msc3381.poll.start`) render as read-only poll cards inside the standard message bubble — question and answer options shown. Registered as top-level event renderers AND inside the `EncryptedContent` callback so encrypted polls also display after decryption. "Open in Element to vote" note displayed. Implemented in `PollContent.tsx`.
|
||||||
- **Deleted message placeholder**: Redacted `m.room.message`, `m.room.encrypted`, and `m.sticker` events no longer disappear from the timeline. Instead they reach the existing `RedactedContent` component (trash icon + italic "This message has been deleted" with reason if provided), matching Element, FluffyChat, Commet, and Nheko behaviour. One-line change in the `eventRenderer` filter in `RoomTimeline.tsx`.
|
- **Deleted message placeholder**: Redacted `m.room.message`, `m.room.encrypted`, and `m.sticker` events no longer disappear from the timeline. Instead they reach the existing `RedactedContent` component (trash icon + italic "This message has been deleted" with reason if provided), matching Element, FluffyChat, Commet, and Nheko behaviour. One-line change in the `eventRenderer` filter in `RoomTimeline.tsx`.
|
||||||
- **Picture-in-picture (PiP)**: When navigating away from a call room while in an active call, the call embed shrinks to a 280x158px floating window in the bottom-right corner. The PiP window is **draggable** — drag it anywhere on screen to move it out of the way. Clicking (without dragging) navigates back to the call room. Drag vs click distinguished by a 5px movement threshold; touch drag supported. Imperative style overrides on `callEmbedRef.current` via `useEffect` — a wrapper div cannot be used because `useCallEmbedPlacementSync` writes `top/left/width/height` directly onto that element.
|
- **Picture-in-picture (PiP)**: When navigating away from a call room while in an active call, the call embed shrinks to a 280x158px floating window in the bottom-right corner. The PiP window is **draggable** — drag it anywhere on screen to move it out of the way. Clicking (without dragging) navigates back to the call room. Drag vs click distinguished by a 5px movement threshold; touch drag supported. Imperative style overrides on `callEmbedRef.current` via `useEffect` — a wrapper div cannot be used because `useCallEmbedPlacementSync` writes `top/left/width/height` directly onto that element.
|
||||||
|
- **Call embed positioning**: `useCallEmbedPlacementSync` uses `getBoundingClientRect()` (not `offsetTop/Left`) for accurate viewport-relative coordinates on the `position:fixed` container. Position is synced immediately on mount via `useEffect` in addition to the ResizeObserver, so the embed is placed correctly the instant the call view renders. The `[pipMode, callVisible]` effect in `CallEmbedProvider` only clears pip-specific styles when actually exiting pip mode — no longer clobbers the position set by `syncCallEmbedPlacement` on every `callVisible` toggle.
|
||||||
|
- **Dark mode in element-call**: After joining, `CallEmbed.applyStyles()` injects `:root { color-scheme: dark|light }` into the iframe document so `@media (prefers-color-scheme)` rules inside element-call resolve to the correct Cinny theme regardless of the OS system preference. `themeKind` is stored on the `CallEmbed` instance and updated on every `setTheme()` call, so live theme switching also re-injects the CSS. Without this, users with OS light mode would see a white background even when Cinny is in dark mode.
|
||||||
|
- **Call embed wallpaper**: The user's `chatBackground` pattern (Blueprint, Carbon, Stars…) is applied as the `backgroundImage`/`backgroundColor` of `div[data-call-embed-container]` when the call is in full view (not PiP). The iframe `html, body` is forced to `background: none !important` so the pattern shows through. When `chatBackground` is `none`, behaviour is unchanged.
|
||||||
|
|
||||||
### Messaging Enhancements
|
### Messaging Enhancements
|
||||||
|
|
||||||
@@ -116,6 +119,8 @@ Emoji reaction buttons styled for terminal mode via `button[data-reaction-key]`
|
|||||||
|
|
||||||
- **Authenticated media**: All avatar/media loads use `mxcUrlToHttp(mx, mxcUrl, useAuthentication, w, h, 'crop')` from `../../utils/matrix` — the Lotus utility that handles MSC3916 authenticated media. (Upstream Cinny uses the SDK method with incorrect argument order for authenticated endpoints.)
|
- **Authenticated media**: All avatar/media loads use `mxcUrlToHttp(mx, mxcUrl, useAuthentication, w, h, 'crop')` from `../../utils/matrix` — the Lotus utility that handles MSC3916 authenticated media. (Upstream Cinny uses the SDK method with incorrect argument order for authenticated endpoints.)
|
||||||
- **Upstream tracking**: `git remote add upstream https://github.com/cinnyapp/cinny.git`. Merge strategy: `git fetch upstream && git merge upstream/main`. Daily check via `cinny-upstream-check.sh` on LXC 106 — notifies Matrix on new upstream commits.
|
- **Upstream tracking**: `git remote add upstream https://github.com/cinnyapp/cinny.git`. Merge strategy: `git fetch upstream && git merge upstream/main`. Daily check via `cinny-upstream-check.sh` on LXC 106 — notifies Matrix on new upstream commits.
|
||||||
|
- **Rolldown CJS interop — millify**: `src/app/plugins/millify.ts` uses a named import (`import { millify as millifyPlugin } from 'millify'`) instead of a default import. Rolldown's `__toESM` helper with `mode=1` sets `a.default = module_object` (not the function itself) when `hasOwnProperty` prevents the copy — calling `millifyPlugin()` would throw `(0, zc.default) is not a function`. Named import bypasses the interop entirely.
|
||||||
|
- **Sentry noise filter**: `ignoreErrors: ['Request timed out']` added to `Sentry.init` in `src/index.tsx` to suppress unhandled rejections from the matrixRTC delayed-event heartbeat (matrix-sdk) and the widget PostmessageTransport initial-load race (matrix-widget-api). Neither is actionable from client code.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -157,4 +162,6 @@ Built files are served from `/var/www/html/` on LXC 106 (nginx). Config lives at
|
|||||||
| `src/app/components/GifPicker.tsx` | GIF search + send |
|
| `src/app/components/GifPicker.tsx` | GIF search + send |
|
||||||
| `src/app/features/call/CallControls.tsx` | PTT badge + keybind logic |
|
| `src/app/features/call/CallControls.tsx` | PTT badge + keybind logic |
|
||||||
| `src/app/plugins/call/CallControl.ts` | EC widget bridge (screenshare revert, PTT mic) |
|
| `src/app/plugins/call/CallControl.ts` | EC widget bridge (screenshare revert, PTT mic) |
|
||||||
| `src/app/components/CallEmbedProvider.tsx` | PiP + draggable call embed |
|
| `src/app/components/CallEmbedProvider.tsx` | PiP + draggable call embed, call wallpaper carry-over |
|
||||||
|
| `src/app/plugins/call/CallEmbed.ts` | EC widget bridge: iframe setup, `color-scheme` dark/light injection, built-in control hiding, theme sync |
|
||||||
|
| `src/app/plugins/millify.ts` | Named import fix for Rolldown CJS interop (prevents `zc.default is not a function` crash) |
|
||||||
|
|||||||
@@ -47,6 +47,10 @@ import { useMediaAuthentication } from '../hooks/useMediaAuthentication';
|
|||||||
import { mxcUrlToHttp, getMxIdLocalPart } from '../utils/matrix';
|
import { mxcUrlToHttp, getMxIdLocalPart } from '../utils/matrix';
|
||||||
import { RoomAvatar, RoomIcon } from './room-avatar';
|
import { RoomAvatar, RoomIcon } from './room-avatar';
|
||||||
import { useRoomNavigate } from '../hooks/useRoomNavigate';
|
import { useRoomNavigate } from '../hooks/useRoomNavigate';
|
||||||
|
import { getChatBg } from '../features/lotus/chatBackground';
|
||||||
|
import { useTheme, ThemeKind } from '../hooks/useTheme';
|
||||||
|
import { useSetting } from '../state/hooks/settings';
|
||||||
|
import { settingsAtom } from '../state/settings';
|
||||||
import { getStateEvent, getMemberDisplayName } from '../utils/room';
|
import { getStateEvent, getMemberDisplayName } from '../utils/room';
|
||||||
import { StateEvent } from '../../types/matrix/room';
|
import { StateEvent } from '../../types/matrix/room';
|
||||||
import { getPowersLevelFromMatrixEvent } from '../hooks/usePowerLevels';
|
import { getPowersLevelFromMatrixEvent } from '../hooks/usePowerLevels';
|
||||||
@@ -412,6 +416,14 @@ export function CallEmbedProvider({ children }: CallEmbedProviderProps) {
|
|||||||
const pipMode = callActive && !inCallRoom;
|
const pipMode = callActive && !inCallRoom;
|
||||||
const { navigateRoom } = useRoomNavigate();
|
const { navigateRoom } = useRoomNavigate();
|
||||||
|
|
||||||
|
const theme = useTheme();
|
||||||
|
const isDark = theme.kind === ThemeKind.Dark;
|
||||||
|
const [chatBackground] = useSetting(settingsAtom, 'chatBackground');
|
||||||
|
const wallpaperStyle = React.useMemo(
|
||||||
|
() => getChatBg(chatBackground, isDark),
|
||||||
|
[chatBackground, isDark],
|
||||||
|
);
|
||||||
|
|
||||||
const pipDragRef = React.useRef<{
|
const pipDragRef = React.useRef<{
|
||||||
startX: number;
|
startX: number;
|
||||||
startY: number;
|
startY: number;
|
||||||
@@ -427,13 +439,37 @@ export function CallEmbedProvider({ children }: CallEmbedProviderProps) {
|
|||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Track previous pipMode to only reset position when first entering pip (not on callVisible changes)
|
// Track previous pipMode to only reset position when entering/exiting pip
|
||||||
const prevPipModeRef = React.useRef(false);
|
const prevPipModeRef = React.useRef(false);
|
||||||
|
|
||||||
|
// Debug: log whenever call embed visibility/mode changes
|
||||||
|
React.useEffect(() => {
|
||||||
|
console.log('[CallEmbed] state:', {
|
||||||
|
callEmbedRoomId: callEmbed?.roomId,
|
||||||
|
selectedRoom,
|
||||||
|
joined,
|
||||||
|
inCallRoom: !!inCallRoom,
|
||||||
|
callVisible: !!callVisible,
|
||||||
|
pipMode: !!pipMode,
|
||||||
|
chatOnlyView: !!chatOnlyView,
|
||||||
|
});
|
||||||
|
const el = callEmbedRef.current;
|
||||||
|
if (el) {
|
||||||
|
console.log('[CallEmbed] container styles:', {
|
||||||
|
top: el.style.top, left: el.style.left,
|
||||||
|
width: el.style.width, height: el.style.height,
|
||||||
|
visibility: el.style.visibility, bottom: el.style.bottom, right: el.style.right,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [callEmbed?.roomId, selectedRoom, joined, inCallRoom, callVisible, pipMode, chatOnlyView, callEmbedRef]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const el = callEmbedRef.current;
|
const el = callEmbedRef.current;
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
|
const wasInPip = prevPipModeRef.current;
|
||||||
|
prevPipModeRef.current = !!pipMode;
|
||||||
if (pipMode) {
|
if (pipMode) {
|
||||||
if (!prevPipModeRef.current) {
|
if (!wasInPip) {
|
||||||
el.style.top = 'auto';
|
el.style.top = 'auto';
|
||||||
el.style.left = 'auto';
|
el.style.left = 'auto';
|
||||||
el.style.bottom = '72px';
|
el.style.bottom = '72px';
|
||||||
@@ -448,24 +484,23 @@ export function CallEmbedProvider({ children }: CallEmbedProviderProps) {
|
|||||||
}
|
}
|
||||||
el.style.visibility = 'visible';
|
el.style.visibility = 'visible';
|
||||||
} else {
|
} else {
|
||||||
[
|
if (wasInPip) {
|
||||||
'top',
|
// Exiting pip: clear all pip styles; syncCallEmbedPlacement will restore correct position
|
||||||
'left',
|
el.style.top = '';
|
||||||
'bottom',
|
el.style.left = '';
|
||||||
'right',
|
el.style.bottom = '';
|
||||||
'width',
|
el.style.right = '';
|
||||||
'height',
|
el.style.width = '';
|
||||||
'borderRadius',
|
el.style.height = '';
|
||||||
'overflow',
|
el.style.borderRadius = '';
|
||||||
'zIndex',
|
el.style.overflow = '';
|
||||||
'boxShadow',
|
el.style.zIndex = '';
|
||||||
'border',
|
el.style.boxShadow = '';
|
||||||
].forEach((p) => {
|
el.style.border = '';
|
||||||
(el.style as any)[p] = '';
|
}
|
||||||
});
|
// syncCallEmbedPlacement owns top/left/width/height; don't clear them on visibility changes
|
||||||
el.style.visibility = callVisible ? '' : 'hidden';
|
el.style.visibility = callVisible ? '' : 'hidden';
|
||||||
}
|
}
|
||||||
prevPipModeRef.current = !!pipMode;
|
|
||||||
}, [pipMode, callVisible]);
|
}, [pipMode, callVisible]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
@@ -669,6 +704,7 @@ export function CallEmbedProvider({ children }: CallEmbedProviderProps) {
|
|||||||
left: 0,
|
left: 0,
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: '50%',
|
height: '50%',
|
||||||
|
...(callVisible && !pipMode ? wallpaperStyle : {}),
|
||||||
}}
|
}}
|
||||||
ref={callEmbedRef}
|
ref={callEmbedRef}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ export const createCallEmbed = (
|
|||||||
const controlState =
|
const controlState =
|
||||||
pref && new CallControlState(forceAudioOff ? false : pref.microphone, pref.video, pref.sound);
|
pref && new CallControlState(forceAudioOff ? false : pref.microphone, pref.video, pref.sound);
|
||||||
|
|
||||||
const embed = new CallEmbed(mx, room, widget, container, controlState);
|
const embed = new CallEmbed(mx, room, widget, container, controlState, themeKind);
|
||||||
|
|
||||||
return embed;
|
return embed;
|
||||||
};
|
};
|
||||||
@@ -154,12 +154,21 @@ export const useCallEmbedPlacementSync = (containerViewRef: RefObject<HTMLDivEle
|
|||||||
const container = containerViewRef.current;
|
const container = containerViewRef.current;
|
||||||
if (!embedEl || !container) return;
|
if (!embedEl || !container) return;
|
||||||
|
|
||||||
embedEl.style.top = `${container.offsetTop}px`;
|
const rect = container.getBoundingClientRect();
|
||||||
embedEl.style.left = `${container.offsetLeft}px`;
|
console.log('[CallEmbed] syncCallEmbedPlacement:', {
|
||||||
embedEl.style.width = `${container.clientWidth}px`;
|
top: rect.top, left: rect.left, width: rect.width, height: rect.height,
|
||||||
embedEl.style.height = `${container.clientHeight}px`;
|
});
|
||||||
|
embedEl.style.top = `${rect.top}px`;
|
||||||
|
embedEl.style.left = `${rect.left}px`;
|
||||||
|
embedEl.style.width = `${rect.width}px`;
|
||||||
|
embedEl.style.height = `${rect.height}px`;
|
||||||
}, [callEmbedRef, containerViewRef]);
|
}, [callEmbedRef, containerViewRef]);
|
||||||
|
|
||||||
|
// Sync once on mount so the embed is positioned immediately (deps are stable refs)
|
||||||
|
useEffect(() => {
|
||||||
|
syncCallEmbedPlacement();
|
||||||
|
}, [syncCallEmbedPlacement]);
|
||||||
|
|
||||||
useResizeObserver(
|
useResizeObserver(
|
||||||
syncCallEmbedPlacement,
|
syncCallEmbedPlacement,
|
||||||
useCallback(() => containerViewRef.current, [containerViewRef]),
|
useCallback(() => containerViewRef.current, [containerViewRef]),
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ export class CallEmbed {
|
|||||||
|
|
||||||
private styleRetryObserver?: MutationObserver;
|
private styleRetryObserver?: MutationObserver;
|
||||||
|
|
||||||
|
private themeKind: ElementCallThemeKind = 'dark';
|
||||||
|
|
||||||
// Arrow-function class fields so dispose() passes the exact same reference to mx.off()
|
// Arrow-function class fields so dispose() passes the exact same reference to mx.off()
|
||||||
private readonly boundOnEvent = (ev: MatrixEvent) => this.onEvent(ev);
|
private readonly boundOnEvent = (ev: MatrixEvent) => this.onEvent(ev);
|
||||||
|
|
||||||
@@ -175,6 +177,7 @@ export class CallEmbed {
|
|||||||
widget: Widget,
|
widget: Widget,
|
||||||
container: HTMLElement,
|
container: HTMLElement,
|
||||||
initialControlState?: CallControlState,
|
initialControlState?: CallControlState,
|
||||||
|
themeKind: ElementCallThemeKind = 'dark',
|
||||||
) {
|
) {
|
||||||
const iframe = CallEmbed.getIframe(
|
const iframe = CallEmbed.getIframe(
|
||||||
widget.getCompleteUrl({ currentUserId: mx.getSafeUserId() }),
|
widget.getCompleteUrl({ currentUserId: mx.getSafeUserId() }),
|
||||||
@@ -189,6 +192,7 @@ export class CallEmbed {
|
|||||||
this.room = room;
|
this.room = room;
|
||||||
this.iframe = iframe;
|
this.iframe = iframe;
|
||||||
this.container = container;
|
this.container = container;
|
||||||
|
this.themeKind = themeKind;
|
||||||
|
|
||||||
const controlState = initialControlState ?? new CallControlState(true, false, true);
|
const controlState = initialControlState ?? new CallControlState(true, false, true);
|
||||||
this.control = new CallControl(controlState, call, iframe);
|
this.control = new CallControl(controlState, call, iframe);
|
||||||
@@ -218,8 +222,16 @@ export class CallEmbed {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public setTheme(theme: ElementCallThemeKind) {
|
public setTheme(theme: ElementCallThemeKind) {
|
||||||
return this.call.transport.send(WidgetApiToWidgetAction.ThemeChange, {
|
this.themeKind = theme;
|
||||||
name: theme,
|
const doc = this.document;
|
||||||
|
if (doc && this.joined) {
|
||||||
|
const styleEl = doc.getElementById('lotus-ec-styles');
|
||||||
|
if (styleEl) styleEl.textContent = this.buildStyleContent();
|
||||||
|
}
|
||||||
|
return this.call.transport
|
||||||
|
.send(WidgetApiToWidgetAction.ThemeChange, { name: theme })
|
||||||
|
.catch(() => {
|
||||||
|
// Widget transport not ready yet; theme URL param is the fallback
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -304,22 +316,28 @@ export class CallEmbed {
|
|||||||
this.control.forceState(this.initialState);
|
this.control.forceState(this.initialState);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private buildStyleContent(): string {
|
||||||
|
return [
|
||||||
|
'html, body { background: none !important; }',
|
||||||
|
`:root { color-scheme: ${this.themeKind}; }`,
|
||||||
|
'[style*="height: 0"][style*="z-index: 1"][style*="align-self: center"] { display: none !important; }',
|
||||||
|
].join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
private applyStyles(): void {
|
private applyStyles(): void {
|
||||||
const doc = this.document;
|
const doc = this.document;
|
||||||
if (!doc) return;
|
if (!doc) return;
|
||||||
|
|
||||||
doc.body.style.setProperty('background', 'none', 'important');
|
doc.body.style.setProperty('background', 'none', 'important');
|
||||||
|
|
||||||
// Inject CSS for things that can't be reliably caught by DOM timing
|
|
||||||
if (!doc.getElementById('lotus-ec-styles')) {
|
if (!doc.getElementById('lotus-ec-styles')) {
|
||||||
const style = doc.createElement('style');
|
const style = doc.createElement('style');
|
||||||
style.id = 'lotus-ec-styles';
|
style.id = 'lotus-ec-styles';
|
||||||
style.textContent = [
|
style.textContent = this.buildStyleContent();
|
||||||
'body { background: none !important; }',
|
|
||||||
// Hide "using to Device key transport" status line
|
|
||||||
'[style*="height: 0"][style*="z-index: 1"][style*="align-self: center"] { display: none !important; }',
|
|
||||||
].join('\n');
|
|
||||||
(doc.head ?? doc.body).appendChild(style);
|
(doc.head ?? doc.body).appendChild(style);
|
||||||
|
} else {
|
||||||
|
const styleEl = doc.getElementById('lotus-ec-styles');
|
||||||
|
if (styleEl) styleEl.textContent = this.buildStyleContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hide EC built-in controls (we provide our own)
|
// Hide EC built-in controls (we provide our own)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import millifyPlugin from 'millify';
|
import { millify as millifyPlugin } from 'millify';
|
||||||
import { MillifyOptions } from 'millify/dist/options';
|
import { MillifyOptions } from 'millify/dist/options';
|
||||||
|
|
||||||
export const millify = (count: number, options?: Partial<MillifyOptions>): string =>
|
export const millify = (count: number, options?: Partial<MillifyOptions>): string =>
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ if (sentryDsn) {
|
|||||||
sendDefaultPii: false,
|
sendDefaultPii: false,
|
||||||
// Forward Sentry logs to the dashboard
|
// Forward Sentry logs to the dashboard
|
||||||
enableLogs: true,
|
enableLogs: true,
|
||||||
|
// Suppress benign PostmessageTransport / matrixRTC heartbeat timeouts (upstream library noise)
|
||||||
|
ignoreErrors: ['Request timed out'],
|
||||||
beforeSend(event) {
|
beforeSend(event) {
|
||||||
// Drop any event that may have leaked an access token into breadcrumbs/data
|
// Drop any event that may have leaked an access token into breadcrumbs/data
|
||||||
if (JSON.stringify(event).includes('access_token')) return null;
|
if (JSON.stringify(event).includes('access_token')) return null;
|
||||||
|
|||||||
Reference in New Issue
Block a user