Compare commits

..
Author SHA1 Message Date
Lotus CIandClaude Opus 5.5 4486f02bc6 fix(privacy): "Remove Photo Metadata" also covers emoji/sticker packs and badges (#332)
CI / Build & Quality Checks (pull_request) Successful in 3m25s
CI / Trigger Desktop Build (pull_request) Skipped
CI / Docker image build & smoke test (pull_request) Skipped
CI / Secret scan (gitleaks) (pull_request) Successful in 8s
CI / Playwright smoke (e2e) (pull_request) Successful in 15m42s
Custom emoji and sticker images, pack avatars and power-level badge
images were uploaded with their EXIF intact (GPS location, timestamp),
although they are published to every member of the room or space. Route
those three pickers through a new useStrippedFilePicker, which applies
the same stripImageMetadata as attachments and avatars.

Fixes #332

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
2026-10-18 00:09:14 -04:00
5 changed files with 44 additions and 13 deletions
+2 -7
View File
@@ -89,8 +89,6 @@ import { zIndices } from '../styles/zIndex';
const PIP_MIN_W = 200;
const PIP_MIN_H = 112;
/** Default PiP top: just below the 64px-ish room header. */
const PIP_DEFAULT_TOP = 72;
type Corner = 'se' | 'sw' | 'ne' | 'nw';
@@ -1048,11 +1046,8 @@ export function CallEmbedProvider({ children }: CallEmbedProviderProps) {
el.style.left = `${Math.max(0, Math.min(savedPos.left, window.innerWidth - 280))}px`;
el.style.top = `${Math.max(0, Math.min(savedPos.top, window.innerHeight - 158))}px`;
} else {
// [Gitea #323] First use: top-right under the room header, over the
// timeline. Bottom-right sat on the composer (Send / Emoji, and on
// phones the whole message box), so typing landed on the call.
el.style.left = `${Math.max(0, window.innerWidth - 280 - 16)}px`;
el.style.top = `${PIP_DEFAULT_TOP}px`;
el.style.left = `${window.innerWidth - 280 - 16}px`;
el.style.top = `${window.innerHeight - 158 - 72}px`;
}
el.style.width = '280px';
el.style.height = '158px';
@@ -16,7 +16,7 @@ import { SettingTile } from '../setting-tile';
import { UsageSwitcher } from './UsageSwitcher';
import { ImagePackProfile, ImagePackProfileEdit } from './PackMeta';
import * as css from './style.css';
import { useFilePicker } from '../../hooks/useFilePicker';
import { useStrippedFilePicker } from '../../hooks/useStrippedFilePicker';
import { CompactUploadCardRenderer } from '../upload-card';
import { UploadSuccess } from '../../state/upload';
import { getImageInfo, TUploadContent } from '../../utils/matrix';
@@ -60,7 +60,7 @@ export const ImagePackContent = as<'div', ImagePackContentProps>(
[imagePack, savedImages, uploadedImages],
);
const pickFiles = useFilePicker(
const pickFiles = useStrippedFilePicker(
useCallback(
(pickedFiles: File[]) => {
const uniqueFiles = pickedFiles.map((file) => {
@@ -19,7 +19,7 @@ import { nameInitials } from '../../utils/common';
import { BreakWord } from '../../styles/Text.css';
import { LINKIFY_OPTS } from '../../plugins/react-custom-html-parser';
import { ContainerColor } from '../../styles/ContainerColor.css';
import { useFilePicker } from '../../hooks/useFilePicker';
import { useStrippedFilePicker } from '../../hooks/useStrippedFilePicker';
import { useObjectURL } from '../../hooks/useObjectURL';
import { createUploadAtom, UploadSuccess } from '../../state/upload';
import { CompactUploadCardRenderer } from '../upload-card';
@@ -111,7 +111,7 @@ export function ImagePackProfileEdit({ meta, onCancel, onSave }: ImagePackProfil
return undefined;
}, [imageFile]);
const pickFile = useFilePicker(setImageFile, false);
const pickFile = useStrippedFilePicker(setImageFile, false);
const handleRemoveUpload = useCallback(() => {
setImageFile(undefined);
@@ -40,7 +40,7 @@ import { useImagePackRooms } from '../../../hooks/useImagePackRooms';
import { roomToParentsAtom } from '../../../state/room/roomToParents';
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { useFilePicker } from '../../../hooks/useFilePicker';
import { useStrippedFilePicker } from '../../../hooks/useStrippedFilePicker';
import { CompactUploadCardRenderer } from '../../../components/upload-card';
import { createUploadAtom, UploadSuccess } from '../../../state/upload';
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
@@ -68,7 +68,7 @@ function EditPower({ maxPower, power, tag, onSave, onClose }: EditPowerProps) {
const imagePackRooms = useImagePackRooms(room.roomId, roomToParents);
const [iconFile, setIconFile] = useState<File>();
const pickFile = useFilePicker(setIconFile, false);
const pickFile = useStrippedFilePicker(setIconFile, false);
const [tagColor, setTagColor] = useState<string | undefined>(tag?.color);
const [tagIcon, setTagIcon] = useState<MemberPowerTagIcon | undefined>(tag?.icon);
+36
View File
@@ -0,0 +1,36 @@
import { useCallback } from 'react';
import { useFilePicker } from './useFilePicker';
import { useSetting } from '../state/hooks/settings';
import { settingsAtom } from '../state/settings';
import { stripImageMetadata } from '../utils/stripImageMetadata';
/**
* [Gitea #332] `useFilePicker` for images that get published (emoji / sticker
* packs, pack avatars, power-level badges): applies "Remove Photo Metadata"
* like message attachments and avatars, so a phone photo's GPS location and
* timestamp don't go to every member of the room or space.
*/
export const useStrippedFilePicker = <M extends boolean | undefined = undefined>(
onSelect: (file: M extends true ? File[] : File) => void,
multiple?: M,
) => {
const [strip] = useSetting(settingsAtom, 'stripImageMetadata');
const handle = useCallback(
(picked: File | File[]) => {
if (!strip) {
onSelect(picked as M extends true ? File[] : File);
return;
}
const clean = (f: File) => stripImageMetadata(f).then((r) => r.file);
if (Array.isArray(picked)) {
Promise.all(picked.map(clean)).then((files) =>
onSelect(files as M extends true ? File[] : File),
);
} else {
clean(picked).then((file) => onSelect(file as M extends true ? File[] : File));
}
},
[strip, onSelect],
);
return useFilePicker(handle as (file: M extends true ? File[] : File) => void, multiple);
};