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
13 changes: 13 additions & 0 deletions .changeset/change-settings-link-markers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
default: patch
---

# Change how settings links are shared

Settings links copied from Sable now stay on the current client URL and include a small Sable marker in the link. That lets Sable recognize settings links copied from other Sable instances without treating unrelated third-party `/settings/...` links as Sable settings links.

When you send a bare settings link in the composer, Sable now rewrites it into a labeled link so it looks better on non-Sable clients too. For example: `[Settings > Account > Display Name](https://client.example/settings/account?focus=display-name&moe.sable.client.action=settings)`.

Invalid or malformed settings-looking links now stay normal links instead of being shown as settings chips.

If you previously set `settingsLinkBaseUrl` in `config.json`, remove it. Sable now derives settings links from the runtime app URL, and the old config key is no longer used.
5 changes: 5 additions & 0 deletions .changeset/remove-dynamic-settings-links.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
default: patch
---

# Hide copied settings links on dynamic rows
2 changes: 0 additions & 2 deletions config.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@
"webPushAppID": "moe.sable.app.sygnal"
},

"settingsLinkBaseUrl": "https://app.sable.moe",

"slidingSync": {
"enabled": true
},
Expand Down
38 changes: 34 additions & 4 deletions src/app/components/RenderMessageContent.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { render, screen } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { MsgType } from '$types/matrix-sdk';
import { ClientConfigProvider } from '$hooks/useClientConfig';
import { RenderMessageContent } from './RenderMessageContent';
Expand All @@ -13,9 +13,9 @@ vi.mock('./url-preview', () => ({
youtubeUrl: () => false,
}));

function renderMessage(body: string, settingsLinkBaseUrl = 'https://app.sable.moe') {
function renderMessage(body: string) {
return render(
<ClientConfigProvider value={{ settingsLinkBaseUrl }}>
<ClientConfigProvider value={{}}>
<RenderMessageContent
displayName="Alice"
msgType={MsgType.Text}
Expand All @@ -30,9 +30,19 @@ function renderMessage(body: string, settingsLinkBaseUrl = 'https://app.sable.mo
);
}

beforeEach(() => {
vi.stubGlobal('location', { origin: 'https://app.example' } as Location);
});

afterEach(() => {
vi.unstubAllGlobals();
});

describe('RenderMessageContent', () => {
it('does not render url previews for settings links', () => {
renderMessage('https://app.sable.moe/settings/account?focus=status');
renderMessage(
'https://app.example/settings/account?focus=status&moe.sable.client.action=settings'
);

expect(screen.queryByTestId('url-preview-holder')).not.toBeInTheDocument();
expect(screen.queryByTestId('url-preview-card')).not.toBeInTheDocument();
Expand All @@ -45,4 +55,24 @@ describe('RenderMessageContent', () => {
expect(screen.getByTestId('url-preview-holder')).toBeInTheDocument();
expect(screen.getByTestId('url-preview-card')).toHaveTextContent('https://example.com');
});

it('still renders url previews for malformed settings-looking links', () => {
renderMessage(
'https://app.example/settings/account?focus=status&moe.sable.client.action=settings">Settings'
);

expect(screen.getByTestId('url-preview-holder')).toBeInTheDocument();
expect(screen.getByTestId('url-preview-card')).toHaveTextContent(
'https://app.example/settings/account?focus=status&moe.sable.client.action=settings">Settings'
);
});

it('still renders url previews for settings links with unknown focus ids', () => {
renderMessage('https://app.example/settings/account?focus=display-name2');

expect(screen.getByTestId('url-preview-holder')).toBeInTheDocument();
expect(screen.getByTestId('url-preview-card')).toHaveTextContent(
'https://app.example/settings/account?focus=display-name2'
);
});
});
4 changes: 2 additions & 2 deletions src/app/components/editor/output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ const elementToCustomHtml = (node: CustomElement, children: string): string => {
)}" title="${sanitizeText(node.shortcode)}" height="32" />`
: sanitizeText(node.key);
case BlockType.Link:
return `<a href="${encodeURI(node.href)}">${node.children}</a>`;
return `<a href="${encodeURI(node.href)}">${children}</a>`;
case BlockType.Command:
return `/${sanitizeText(node.command)}`;
default:
Expand Down Expand Up @@ -184,7 +184,7 @@ const elementToPlainText = (node: CustomElement, children: string): string => {
case BlockType.Emoticon:
return node.key.startsWith('mxc://') ? `:${node.shortcode}:` : node.key;
case BlockType.Link:
return `[${node.children}](${node.href})`;
return `[${children}](${node.href})`;
case BlockType.Command:
return `/${node.command}`;
case BlockType.Small:
Expand Down
4 changes: 2 additions & 2 deletions src/app/components/setting-tile/SettingTile.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ describe('SettingTile', () => {

await waitFor(() => {
expect(writeText).toHaveBeenCalledWith(
'https://settings.example/settings/appearance?focus=message-link-preview'
'https://settings.example/settings/appearance?focus=message-link-preview&moe.sable.client.action=settings'
);
});
expect(screen.getByRole('button', { name: /copied settings link/i })).toBeInTheDocument();
Expand All @@ -64,7 +64,7 @@ describe('SettingTile', () => {

await waitFor(() => {
expect(writeText).toHaveBeenCalledWith(
'https://settings.example/settings/appearance?focus=message-link-preview'
'https://settings.example/settings/appearance?focus=message-link-preview&moe.sable.client.action=settings'
);
});
expect(screen.getByRole('button', { name: /copy settings link/i })).toBeInTheDocument();
Expand Down
19 changes: 17 additions & 2 deletions src/app/features/room/RoomInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ import { getSupportedAudioExtension } from '$plugins/voice-recorder-kit/supporte
import { sanitizeText } from '$utils/sanitize';
import { PKitCommandMessageHandler } from '$plugins/pluralkit-handler/PKitCommandMessageHandler';
import { PKitProxyMessageHandler } from '$plugins/pluralkit-handler/PKitProxyMessageHandler';
import { useSettingsLinkBaseUrl } from '$features/settings/useSettingsLinkBaseUrl';
import { SchedulePickerDialog } from './schedule-send';
import * as css from './schedule-send/SchedulePickerDialog.css';
import {
Expand All @@ -155,6 +156,7 @@ import {
getImageMsgContent,
getVideoMsgContent,
} from './msgContent';
import { outgoingMessageTransforms } from './outgoingMessageTransforms';
import { CommandAutocomplete } from './CommandAutocomplete';
import {
AudioMessageRecorder,
Expand Down Expand Up @@ -251,6 +253,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
const [isMarkdown] = useSetting(settingsAtom, 'isMarkdown');
const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
const [mentionInReplies] = useSetting(settingsAtom, 'mentionInReplies');
const settingsLinkBaseUrl = useSettingsLinkBaseUrl();
const commands = useCommands(mx, room);
/**
* handle pluralkit-style messages
Expand Down Expand Up @@ -721,12 +724,23 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
/**
* the plain text we will send
*/
let plainText = toPlainText(editor.children, isMarkdown, true, nicknameReplacement).trim();
let serializedChildren = editor.children;
const outgoingTransformContext = {
isMarkdown,
settingsLinkBaseUrl,
};

outgoingMessageTransforms.forEach((transform) => {
if (!transform.shouldApply(serializedChildren, outgoingTransformContext)) return;
serializedChildren = transform.apply(serializedChildren, outgoingTransformContext);
});

let plainText = toPlainText(serializedChildren, isMarkdown, true, nicknameReplacement).trim();
/**
* the html we will send
*/
let customHtml = trimCustomHtml(
toMatrixCustomHTML(editor.children, {
toMatrixCustomHTML(serializedChildren, {
allowTextFormatting: true,
allowBlockMarkdown: isMarkdown,
allowInlineMarkdown: isMarkdown,
Expand Down Expand Up @@ -961,6 +975,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
queryClient,
threadRootId,
setReplyDraft,
settingsLinkBaseUrl,
isEncrypted,
setEditingScheduledDelayId,
setScheduledTime,
Expand Down
28 changes: 28 additions & 0 deletions src/app/features/room/outgoingMessageTransforms.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Descendant } from 'slate';
import {
hasSettingsLinksToRewriteInDescendants,
rewriteSettingsLinksInDescendants,
} from './settingsLinkMessage';

export type OutgoingMessageTransformContext = {
isMarkdown: boolean;
settingsLinkBaseUrl: string;
};

export type OutgoingMessageTransform = {
apply: (children: Descendant[], context: OutgoingMessageTransformContext) => Descendant[];
shouldApply: (children: Descendant[], context: OutgoingMessageTransformContext) => boolean;
};

export const outgoingMessageTransforms: OutgoingMessageTransform[] = [
{
apply: (children, context) =>
rewriteSettingsLinksInDescendants(children, context.settingsLinkBaseUrl, context.isMarkdown),
shouldApply: (children, context) =>
hasSettingsLinksToRewriteInDescendants(
children,
context.settingsLinkBaseUrl,
context.isMarkdown
),
},
];
Loading
Loading