Skip to content
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: 1 addition & 3 deletions src/app/features/room/composerMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,6 @@ const applyPerMessageProfileFallback = (
content.formatted_body = htmlPrefix + content.formatted_body;
} else {
// we don't have a formatted body, but the fallback needs one
// set before content.body so we don't double fallback
content.format = 'org.matrix.custom.html';
const escapedBody = sanitizeText(bodyWithoutFallback).replaceAll('\n', '<br/>');
content.formatted_body = `${htmlPrefix}${escapedBody}`;
Expand Down Expand Up @@ -223,12 +222,11 @@ export async function buildOutgoingMessage(
// PluralKit-style proxy wrappers must be stripped before building `content`, otherwise
// the wrapper itself gets sent verbatim.
const catalog = new ProfileCatalog(mx);
const personas = await catalog.list({ migrate: false });
let proxiedPerMessageProfile: PerMessageProfileMsc4461 | undefined;
let proxyStripped = false;
if (pmpProxyingEnable) {
const proxy = resolvePersonaProxy(
personas,
await catalog.list({ migrate: false }),
toPlainText(serializedChildren, true, false, nicknameReplacement).trim()
);
if (proxy) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
MATRIX_UNSTABLE_COLORS,
MATRIX_UNSTABLE_PROFILE_PRONOUNS_PROPERTY_NAME,
} from '$unstable/prefixes';
import { downloadJsonFile } from '$app/utils/common';
import { downloadJsonFile } from '$app/utils/download';
import { selectFile } from '$app/utils/dom';
import { ModalOverlay } from '$components/modal-overlay/ModalOverlay';
import { AsyncError } from '$components/AsyncError';
Expand Down Expand Up @@ -96,7 +96,7 @@ export function PerMessageProfileOverview({
useCallback(async () => {
const personas = await new ProfileCatalog(mx).list();
const data = { personas };
downloadJsonFile(JSON.stringify(data), 'persona');
await downloadJsonFile(JSON.stringify(data), 'persona');
}, [mx])
);

Expand Down
5 changes: 2 additions & 3 deletions src/app/features/settings/general/General.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@ import { useSetting } from '$state/hooks/settings';
import type { EditorButtonId } from '$state/settings';
import { MessageLayout, RightSwipeAction, settingsAtom } from '$state/settings';
import { SettingTile, SettingToggle } from '$components/setting-tile';
import { downloadJsonFile } from '$utils/common';
import { getDebugLogger } from '$utils/debugLogger';
import { KeySymbol } from '$utils/key-symbol';
import { isDesktopTauri, isMacOS, isMobileOrTablet, isMobileTauri } from '$utils/platform';
Expand All @@ -66,7 +65,7 @@ import { settingsSyncLastSyncedAtom, settingsSyncStatusAtom } from '$hooks/useSe
import { sanitizeDiagnosticsLogs } from '$utils/sentryScrubbers';
import { diagnosticCaptureActiveAtom } from '$state/debugLogger';
import { exportSettingsAsJson, importSettingsFromJson } from '$utils/settingsSync';
import { saveFileToDevice } from '$utils/download';
import { downloadJsonFile, saveFileToDevice } from '$utils/download';
import { CallSoundSettings } from './CallSoundSettings';

type DateHintProps = {
Expand Down Expand Up @@ -1353,7 +1352,7 @@ function DiagnosticsAndPrivacy() {
setDiagnosticsState('error');
return;
}
downloadJsonFile(sanitizedLogs, 'sable-web-diagnostics');
await downloadJsonFile(sanitizedLogs, 'sable-web-diagnostics');
}
setDiagnosticsState('success');
setCaptureCompleted(false);
Expand Down
23 changes: 20 additions & 3 deletions src/app/hooks/useScreenSize.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createContext, useCallback, useContext, useState } from 'react';
import { createContext, useCallback, useContext, useState, useSyncExternalStore } from 'react';
import { useElementSizeObserver } from './useElementSizeObserver';

const TABLET_BREAKPOINT = 1124;
Expand Down Expand Up @@ -41,8 +41,25 @@ export const useScreenSizeContext = (): ScreenSize => {
return screenSize;
};

/** Tablet as well as Mobile, for touch presentation rather than available width. */
const coarsePointerQuery = () => globalThis.matchMedia?.('(pointer: coarse)');

const subscribeCoarsePointer = (onChange: () => void) => {
const query = coarsePointerQuery();
query?.addEventListener('change', onChange);
return () => query?.removeEventListener('change', onChange);
};

const getCoarsePointer = () => coarsePointerQuery()?.matches ?? false;

/** Mobile, or tablet width with a touch pointer — not a narrow desktop window. */
export const useCompactLayout = (): boolean => {
const screenSize = useContext(ScreenSizeContext);
return screenSize !== null && screenSize !== ScreenSize.Desktop;
const coarsePointer = useSyncExternalStore(
subscribeCoarsePointer,
getCoarsePointer,
getCoarsePointer
);

if (screenSize === ScreenSize.Mobile) return true;
return screenSize === ScreenSize.Tablet && coarsePointer;
};
16 changes: 9 additions & 7 deletions src/app/pages/client/sidebar/CreateTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { useNavigate } from 'react-router-dom';
import { SidebarAvatar, SidebarItemLeft, SidebarItemTooltip } from '$components/sidebar';
import { stopPropagation } from '$utils/keyboard';
import { SequenceCard } from '$components/sequence-card';
import { SettingTile } from '$components/setting-tile';
import { ContainerColor } from '$styles/ContainerColor.css';
import {
encodeSearchParamValueArray,
Expand Down Expand Up @@ -130,9 +129,10 @@ export function CreateTab() {
type="button"
onClick={handleCreateSpace}
>
<SettingTile before={composerIcon(SquaresFour)}>
<Box alignItems="Center" gap="300">
<Box shrink="No">{composerIcon(SquaresFour)}</Box>
<Text size="H6">Create a New Space</Text>
</SettingTile>
</Box>
</SequenceCard>
<SequenceCard
style={{ padding: config.space.S300 }}
Expand All @@ -144,9 +144,10 @@ export function CreateTab() {
type="button"
onClick={handleJoinWithAddress}
>
<SettingTile before={composerIcon(Link)}>
<Box alignItems="Center" gap="300">
<Box shrink="No">{composerIcon(Link)}</Box>
<Text size="H6">Join Community via Address</Text>
</SettingTile>
</Box>
</SequenceCard>
<SequenceCard
style={{ padding: config.space.S300 }}
Expand All @@ -158,9 +159,10 @@ export function CreateTab() {
type="button"
onClick={handleExploreClick}
>
<SettingTile before={composerIcon(UsersThree)}>
<Box alignItems="Center" gap="300">
<Box shrink="No">{composerIcon(UsersThree)}</Box>
<Text size="H6">Explore Recommended Spaces</Text>
</SettingTile>
</Box>
</SequenceCard>
</Box>
</Menu>
Expand Down
11 changes: 0 additions & 11 deletions src/app/utils/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,17 +135,6 @@ export const suffixRename = (name: string, validator: (newName: string) => boole

export const replaceSpaceWithDash = (str: string): string => str.replace(/ /g, '-');

/** Trigger a browser download of a JSON file. */
export const downloadJsonFile = (content: string, fileNamePrefix: string): void => {
const blob = new Blob([content], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${fileNamePrefix}-${Date.now()}.json`;
a.click();
URL.revokeObjectURL(url);
};

export const splitWithSpace = (content: string): string[] => {
const trimmedContent = content.trim();
if (trimmedContent === '') return [];
Expand Down
29 changes: 28 additions & 1 deletion src/app/utils/download.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import FileSaver from 'file-saver';
import { invoke, isTauri } from '@tauri-apps/api/core';
import { type as osType } from '@tauri-apps/plugin-os';
import { showToast } from '$state/toast';
import { saveFileToDevice, saveMediaToGallery } from './download';
import { downloadJsonFile, saveFileToDevice, saveMediaToGallery } from './download';

const mocks = vi.hoisted(() => ({
androidFs: {
Expand Down Expand Up @@ -123,6 +123,33 @@ describe('saveFileToDevice', () => {
});
});

describe('downloadJsonFile', () => {
it('saves through the native desktop command instead of an anchor click', async () => {
vi.mocked(osType).mockReturnValue('linux');

const result = await downloadJsonFile('{"a":1}', 'persona');

expect(result).toBe('saved');
expect(invoke).toHaveBeenCalledWith('save_download', {
filename: expect.stringMatching(/^persona-\d+\.json$/),
bytes: expect.any(Array),
});
expect(FileSaver.saveAs).not.toHaveBeenCalled();
});

it('routes Android exports to the public Downloads directory', async () => {
const result = await downloadJsonFile('{"a":1}', 'persona');

expect(result).toBe('saved');
expect(androidFs.createNewPublicFile).toHaveBeenCalledWith(
'Download',
expect.stringMatching(/^persona-\d+\.json$/),
'application/json',
{ isPending: true, requestPermission: true }
);
});
});

describe('saveMediaToGallery', () => {
it('saves Android images to Pictures through the public image API', async () => {
await saveMediaToGallery(new Blob(['data']), 'photo.png', 'image/png');
Expand Down
10 changes: 10 additions & 0 deletions src/app/utils/download.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,3 +185,13 @@ export async function saveFileToDevice(
FileSaver.saveAs(input, filename);
return 'saved';
}

export const downloadJsonFile = (
content: string,
fileNamePrefix: string
): Promise<'saved' | 'cancelled' | 'failed'> =>
saveFileToDevice(
new Blob([content], { type: 'application/json' }),
`${fileNamePrefix}-${Date.now()}.json`,
'application/json'
);
Loading