Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: copy content on insecure connections (HTTP) #251

Merged
merged 6 commits into from
Dec 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/i18n/en/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,9 @@ const en = {
version: 'Version',
vocabOnly: 'Vocab only',
writePromptToStart: 'Write a prompt to start a new session',
you: 'You'
you: 'You',
copiedNotPrivate: 'Content copied, but your connection is not private',
notCopiedNotPrivate: "Couldn't copy content. Connection is not private"
} satisfies BaseTranslation;

export default en;
16 changes: 16 additions & 0 deletions src/i18n/i18n-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -620,6 +620,14 @@ type RootTranslation = {
* Y​o​u
*/
you: string
/**
* C​o​n​t​e​n​t​ ​c​o​p​i​e​d​,​ ​b​u​t​ ​y​o​u​r​ ​c​o​n​n​e​c​t​i​o​n​ ​i​s​ ​n​o​t​ ​p​r​i​v​a​t​e
*/
copiedNotPrivate: string
/**
* C​o​u​l​d​n​'​t​ ​c​o​p​y​ ​c​o​n​t​e​n​t​.​ ​C​o​n​n​e​c​t​i​o​n​ ​i​s​ ​n​o​t​ ​p​r​i​v​a​t​e
*/
notCopiedNotPrivate: string
}

export type TranslationFunctions = {
Expand Down Expand Up @@ -1220,6 +1228,14 @@ The completion in progress will stop
* You
*/
you: () => LocalizedString
/**
* Content copied, but your connection is not private
*/
copiedNotPrivate: () => LocalizedString
/**
* Couldn't copy content. Connection is not private
*/
notCopiedNotPrivate: () => LocalizedString
}

export type Formatters = {}
21 changes: 20 additions & 1 deletion src/lib/components/ButtonCopy.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<script lang="ts">
import { Files } from 'lucide-svelte';
import { toast } from 'svelte-sonner';

import LL from '$i18n/i18n-svelte';

Expand All @@ -8,7 +9,25 @@
export let content: string;

function copyContent() {
navigator.clipboard.writeText(content);
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(content);
} else {
// HACK
// This is a workaround to copy text content on HTTP connections.
// https://developer.mozilla.org/en-US/docs/Web/API/ClipboardItem
const textArea = document.createElement('textarea');
textArea.value = content;
document.body.appendChild(textArea);
textArea.select();
try {
document.execCommand('copy');
toast.warning($LL.copiedNotPrivate());
} catch (e) {
console.error(e);
toast.error($LL.notCopiedNotPrivate());
}
document.body.removeChild(textArea);
}
}
</script>

Expand Down
22 changes: 22 additions & 0 deletions tests/session-interaction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,28 @@ test.describe('Session interaction', () => {
);
});

test('can copy text on an insecure connection', async ({ page }) => {
// Mock insecure context before navigating
await page.addInitScript(() => {
Object.defineProperty(window, 'isSecureContext', { value: false });
});

await page.goto('/');
await mockCompletionResponse(page, MOCK_SESSION_1_RESPONSE_1);
await page.getByText('Sessions', { exact: true }).click();
await page.getByTestId('new-session').click();
await chooseModel(page, MOCK_API_TAGS_RESPONSE.models[0].name);
await promptTextarea.fill('Who would win in a fight between Emma Watson and Jessica Alba?');
await page.getByText('Run').click();
await expect(page.locator('.session__history').getByTitle('Copy')).toHaveCount(2);

const toastWarning = page.getByText('Content copied, but your connection is not private');
await expect(toastWarning).not.toBeVisible();

await page.locator('.session__history').getByTitle('Copy').first().click();
await expect(toastWarning).toBeVisible();
});

test('can copy the whole session content to clipboard', async ({ page }) => {
await page.goto('/');
await page.evaluate(() => navigator.clipboard.writeText(''));
Expand Down
Loading