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:
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user