fix: Bug - Two devices sharing same user says "Shared from your device"

Gate the "Shared from your device" label and the hidden download
affordance on whether this device actually holds the file bytes, not on
whether the current user uploaded it. uploaderPeerId is the user id, so
the old check claimed ownership on every device of the uploader,
blocking view/download on second devices that only synced metadata.

Also include attachment metadata in the account_sync chat-sync-batch so
sibling devices learn about synced attachments at all.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-11 03:29:47 +02:00
parent 49b602dbda
commit 182828bb1e
14 changed files with 339 additions and 20 deletions

View File

@@ -0,0 +1,69 @@
import {
deviceHasLocalCopy,
isSharingFromThisDevice,
isUploaderUser
} from './attachment-sharing.rules';
describe('attachment sharing rules', () => {
describe('isUploaderUser', () => {
it('is true when the attachment uploader matches the current user', () => {
expect(isUploaderUser({ uploaderPeerId: 'user-1' }, 'user-1')).toBe(true);
});
it('is false when the uploader is a different user', () => {
expect(isUploaderUser({ uploaderPeerId: 'user-2' }, 'user-1')).toBe(false);
});
it('is false when either id is missing', () => {
expect(isUploaderUser({ uploaderPeerId: undefined }, 'user-1')).toBe(false);
expect(isUploaderUser({ uploaderPeerId: 'user-1' }, null)).toBe(false);
expect(isUploaderUser({ uploaderPeerId: 'user-1' }, '')).toBe(false);
});
});
describe('deviceHasLocalCopy', () => {
it('is true when an available blob object url is present', () => {
expect(deviceHasLocalCopy({ available: true, objectUrl: 'blob:abc' })).toBe(true);
});
it('is true when a savedPath or filePath is present', () => {
expect(deviceHasLocalCopy({ available: false, savedPath: '/appdata/file.bin' })).toBe(true);
expect(deviceHasLocalCopy({ available: false, filePath: '/home/me/file.bin' })).toBe(true);
});
it('is false when only metadata exists (no bytes, no paths)', () => {
expect(deviceHasLocalCopy({ available: false })).toBe(false);
expect(deviceHasLocalCopy({ available: false, savedPath: ' ', filePath: '' })).toBe(false);
});
it('is false when marked available but no object url is present yet', () => {
expect(deviceHasLocalCopy({ available: true })).toBe(false);
});
});
describe('isSharingFromThisDevice', () => {
it('is true for the uploader device that still holds the file locally', () => {
expect(
isSharingFromThisDevice({ uploaderPeerId: 'user-1', available: true, objectUrl: 'blob:abc' }, 'user-1')
).toBe(true);
expect(
isSharingFromThisDevice({ uploaderPeerId: 'user-1', available: false, savedPath: '/appdata/file.bin' }, 'user-1')
).toBe(true);
});
it('is false on a second device of the same user that only synced metadata', () => {
// This is the regression: the user uploaded from another device, so the metadata
// carries uploaderPeerId === currentUserId, but this device holds no local bytes.
expect(
isSharingFromThisDevice({ uploaderPeerId: 'user-1', available: false }, 'user-1')
).toBe(false);
});
it('is false for a different user even if they downloaded the file locally', () => {
expect(
isSharingFromThisDevice({ uploaderPeerId: 'user-1', available: true, objectUrl: 'blob:abc' }, 'user-2')
).toBe(false);
});
});
});

View File

@@ -0,0 +1,40 @@
import type { Attachment } from '../models/attachment.model';
/** True when the current user is the one who originally uploaded this attachment. */
export function isUploaderUser(
attachment: Pick<Attachment, 'uploaderPeerId'>,
currentUserId: string | null | undefined
): boolean {
return !!attachment.uploaderPeerId && !!currentUserId && attachment.uploaderPeerId === currentUserId;
}
/**
* True when this specific device physically holds the file bytes - either hydrated
* in memory (available blob object url) or persisted to disk / present at its original
* upload path. Synced metadata alone (no bytes, no local paths) does not count.
*/
export function deviceHasLocalCopy(
attachment: Pick<Attachment, 'available' | 'objectUrl' | 'savedPath' | 'filePath'>
): boolean {
return (attachment.available === true && hasNonEmptyString(attachment.objectUrl)) ||
hasNonEmptyString(attachment.savedPath) ||
hasNonEmptyString(attachment.filePath);
}
/**
* True only when the current user uploaded the file AND this device still holds a local
* copy to serve. A second device of the same user that only synced the message metadata
* is the uploader-user but has no local bytes, so it must behave like any other peer
* (request/download from peers) instead of claiming "Shared from your device" and hiding
* the download affordance.
*/
export function isSharingFromThisDevice(
attachment: Pick<Attachment, 'uploaderPeerId' | 'available' | 'objectUrl' | 'savedPath' | 'filePath'>,
currentUserId: string | null | undefined
): boolean {
return isUploaderUser(attachment, currentUserId) && deviceHasLocalCopy(attachment);
}
function hasNonEmptyString(value: string | null | undefined): boolean {
return typeof value === 'string' && value.trim().length > 0;
}