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
7 changed files with 44 additions and 34 deletions
@@ -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);
+2 -14
View File
@@ -12,8 +12,7 @@ import {
markAutoTranslateTried,
} from '../state/translation';
import { chromeTranslationEngine as engine } from '../utils/translation/chromeEngine';
import { sameLanguage, translateSkipMessage } from '../utils/translation/langUtils';
import { toastQueueAtom } from '../state/toast';
import { sameLanguage } from '../utils/translation/langUtils';
export type MsgTranslationStatus =
| 'idle' // not translating (original shown)
@@ -53,7 +52,6 @@ export const useMessageTranslation = (eventId: string, text: string): MsgTransla
// Write-only: reading the whole cache array here (via useAtom) would
// resubscribe this message to every other message's writes. [Gitea #39]
const setCache = useSetAtom(translationCacheAtom);
const setToast = useSetAtom(toastQueueAtom);
// The key includes a fingerprint of `text`, so an edit (same event id, new
// body) misses the old entry and re-translates instead of showing stale
@@ -126,16 +124,6 @@ export const useMessageTranslation = (eventId: string, text: string): MsgTransla
if (!from || sameLanguage(from, targetLang)) {
setStatus('skipped');
setActive(false);
// [Gitea #287] Say why nothing happened instead of silently undoing
// the click (no chip shows once translation is switched back off).
setToast({
id: `translate-skipped-${eventId}-${Date.now()}`,
displayName: 'Translate',
body: translateSkipMessage(from, targetLang),
roomName: '',
roomId: '',
onClick: () => undefined,
});
return;
}
setFromLang(from);
@@ -166,7 +154,7 @@ export const useMessageTranslation = (eventId: string, text: string): MsgTransla
// Invalidate this run's writes if inputs change / unmount.
runIdRef.current += 1;
};
}, [active, cached, supported, text, targetLang, key, setActive, setCache, setToast, eventId]);
}, [active, cached, supported, text, targetLang, key, setActive, setCache]);
// When switched off, drop back to idle (keep the cached translation around so
// toggling back on is instant).
+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);
};
-6
View File
@@ -85,9 +85,3 @@ export function languageName(code: string, uiLocale = 'en'): string {
export function isSupportedTargetLang(code: string): boolean {
return CURATED_NAME.has(normalizeLang(code));
}
/** Pure: why a manual Translate did nothing (#287). */
export const translateSkipMessage = (from: string | undefined, targetLang: string): string =>
from
? `This message is already in ${languageName(targetLang) || targetLang}.`
: "Couldn't tell what language this message is in, so it wasn't translated.";
@@ -1,8 +0,0 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { translateSkipMessage } from './langUtils';
test('translateSkipMessage explains a Translate that did nothing (#287)', () => {
assert.equal(translateSkipMessage('en', 'en'), 'This message is already in English.');
assert.match(translateSkipMessage(undefined, 'en'), /^Couldn't tell what language/);
});