Skip to content

Commit abf030d

Browse files
authored
Merge branch 'main' into fix/working-thread-references
2 parents 2f02d85 + efecd3c commit abf030d

369 files changed

Lines changed: 19695 additions & 3678 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/release.yml‎

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1347,6 +1347,24 @@ jobs:
13471347
args:
13481348
- --filter=@t3tools/scripts...
13491349
1350+
- name: Read published release notes
1351+
if: needs.preflight.outputs.is_prerelease == 'true'
1352+
continue-on-error: true
1353+
env:
1354+
GH_TOKEN: ${{ github.token }}
1355+
GH_REPO: ${{ github.repository }}
1356+
RELEASE_TAG: ${{ needs.preflight.outputs.tag }}
1357+
RELEASE_CHANNEL: ${{ needs.preflight.outputs.release_channel }}
1358+
run: |
1359+
if [ "$RELEASE_CHANNEL" = 'nightly' ]; then
1360+
if ! gh release view "$RELEASE_TAG" --json body --jq '.body // ""' > "$RUNNER_TEMP/discord-release-notes.md"; then
1361+
: > "$RUNNER_TEMP/discord-release-notes.md"
1362+
echo '::warning::Could not read published release notes; announcing without a changelog.'
1363+
fi
1364+
else
1365+
: > "$RUNNER_TEMP/discord-release-notes.md"
1366+
fi
1367+
13501368
- name: Announce prerelease on Discord
13511369
if: needs.preflight.outputs.is_prerelease == 'true'
13521370
continue-on-error: true
@@ -1359,7 +1377,8 @@ jobs:
13591377
--release-name "${{ needs.preflight.outputs.release_name }}" \
13601378
--release-version "${{ needs.preflight.outputs.version }}" \
13611379
--tag "${{ needs.preflight.outputs.tag }}" \
1362-
--release-url "https://github.com/${{ github.repository }}/releases/tag/${{ needs.preflight.outputs.tag }}"
1380+
--release-url "https://github.com/${{ github.repository }}/releases/tag/${{ needs.preflight.outputs.tag }}" \
1381+
--release-notes-file "$RUNNER_TEMP/discord-release-notes.md"
13631382
13641383
- name: Announce latest release on Discord
13651384
if: needs.preflight.outputs.make_latest == 'true'

‎apps/desktop/src/main.ts‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ for (const stream of [process.stdout, process.stderr]) {
88
import * as NodeHttpClient from "@effect/platform-node/NodeHttpClient";
99
import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
1010
import * as NodeServices from "@effect/platform-node/NodeServices";
11+
// @effect-diagnostics-next-line nodeBuiltinImport:off - Version output must flush before Electron exits, without acquiring the runtime.
12+
import * as NodeFS from "node:fs";
1113
import * as NodeOS from "node:os";
1214
import * as Effect from "effect/Effect";
1315
import * as Layer from "effect/Layer";
@@ -69,6 +71,15 @@ import * as DesktopWslBackend from "./wsl/DesktopWslBackend.ts";
6971
import * as DesktopWslEnvironment from "./wsl/DesktopWslEnvironment.ts";
7072
import * as DesktopWslServerTree from "./wsl/DesktopWslServerTree.ts";
7173

74+
if (process.argv.includes("--version")) {
75+
try {
76+
NodeFS.writeSync(process.stdout.fd, `${Electron.app.getVersion()}\n`);
77+
} catch (error) {
78+
if (!(error instanceof Error) || !("code" in error) || error.code !== "EPIPE") throw error;
79+
}
80+
Electron.app.exit(0);
81+
}
82+
7283
const desktopEnvironmentLayer = Layer.unwrap(
7384
Effect.gen(function* () {
7485
const metadata = yield* Effect.service(ElectronApp.ElectronApp).pipe(

‎apps/desktop/src/settings/DesktopAppSettings.test.ts‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,34 @@ describe("DesktopSettings", () => {
357357
),
358358
);
359359

360+
it.effect("saves through a symlinked settings file without replacing the link", () =>
361+
withSettings(
362+
Effect.gen(function* () {
363+
const environment = yield* DesktopEnvironment.DesktopEnvironment;
364+
const fileSystem = yield* FileSystem.FileSystem;
365+
const settings = yield* DesktopAppSettings.DesktopAppSettings;
366+
const dotfiles = yield* fileSystem.makeTempDirectoryScoped({
367+
prefix: "t3-desktop-settings-dotfiles-",
368+
});
369+
const linkedSettingsPath = `${dotfiles}/desktop-settings.json`;
370+
yield* fileSystem.writeFileString(linkedSettingsPath, "{}\n");
371+
yield* fileSystem.makeDirectory(environment.stateDir, { recursive: true });
372+
yield* fileSystem.symlink(linkedSettingsPath, environment.desktopSettingsPath);
373+
374+
yield* settings.setServerExposureMode("network-accessible");
375+
376+
assert.equal(
377+
yield* fileSystem.readLink(environment.desktopSettingsPath),
378+
linkedSettingsPath,
379+
);
380+
const persisted = yield* decodeDesktopSettingsPatch(
381+
yield* fileSystem.readFileString(linkedSettingsPath),
382+
);
383+
assert.equal(persisted.serverExposureMode, "network-accessible");
384+
}),
385+
),
386+
);
387+
360388
it.effect("migrates legacy implicit update channels to the runtime default", () =>
361389
withSettings(
362390
Effect.gen(function* () {

‎apps/desktop/src/settings/DesktopAppSettings.ts‎

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
type DesktopUpdateChannel,
66
} from "@t3tools/contracts";
77
import { fromLenientJson } from "@t3tools/shared/schemaJson";
8+
import { resolveSymlinkTarget } from "@t3tools/shared/symlink";
89
import * as Context from "effect/Context";
910
import * as Crypto from "effect/Crypto";
1011
import * as Effect from "effect/Effect";
@@ -130,6 +131,7 @@ const settingsChange = (settings: DesktopSettings, changed: boolean): DesktopSet
130131

131132
const DesktopSettingsWriteOperation = Schema.Literals([
132133
"create-temporary-file-name",
134+
"resolve-symlink",
133135
"encode-document",
134136
"create-directory",
135137
"write-temporary-file",
@@ -421,8 +423,20 @@ const writeSettings = Effect.fn("desktop.settings.writeSettings")(function* (inp
421423
readonly defaultSettings: DesktopSettings;
422424
readonly suffix: string;
423425
}): Effect.fn.Return<void, DesktopSettingsWriteError> {
424-
const directory = input.path.dirname(input.settingsPath);
425-
const tempPath = `${input.settingsPath}.${process.pid}.${input.suffix}.tmp`;
426+
const targetPath = yield* resolveSymlinkTarget(input.settingsPath).pipe(
427+
Effect.provideService(FileSystem.FileSystem, input.fileSystem),
428+
Effect.provideService(Path.Path, input.path),
429+
Effect.mapError(
430+
(cause) =>
431+
new DesktopSettingsWriteError({
432+
operation: "resolve-symlink",
433+
path: input.settingsPath,
434+
cause,
435+
}),
436+
),
437+
);
438+
const directory = input.path.dirname(targetPath);
439+
const tempPath = `${targetPath}.${process.pid}.${input.suffix}.tmp`;
426440
const encoded = yield* encodeDesktopSettingsJson(
427441
toDesktopSettingsDocument(input.settings, input.defaultSettings),
428442
).pipe(
@@ -455,7 +469,7 @@ const writeSettings = Effect.fn("desktop.settings.writeSettings")(function* (inp
455469
}),
456470
),
457471
);
458-
yield* input.fileSystem.rename(tempPath, input.settingsPath).pipe(
472+
yield* input.fileSystem.rename(tempPath, targetPath).pipe(
459473
Effect.mapError(
460474
(cause) =>
461475
new DesktopSettingsWriteError({

‎apps/desktop/src/settings/DesktopClientSettings.test.ts‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,34 @@ describe("DesktopClientSettings", () => {
149149
),
150150
);
151151

152+
it.effect("saves through a symlinked client settings file without replacing the link", () =>
153+
withClientSettings(
154+
Effect.gen(function* () {
155+
const environment = yield* DesktopEnvironment.DesktopEnvironment;
156+
const fileSystem = yield* FileSystem.FileSystem;
157+
const settings = yield* DesktopClientSettings.DesktopClientSettings;
158+
const dotfiles = yield* fileSystem.makeTempDirectoryScoped({
159+
prefix: "t3-desktop-client-settings-dotfiles-",
160+
});
161+
const linkedSettingsPath = `${dotfiles}/client-settings.json`;
162+
yield* fileSystem.writeFileString(linkedSettingsPath, "{}\n");
163+
yield* fileSystem.makeDirectory(environment.stateDir, { recursive: true });
164+
yield* fileSystem.symlink(linkedSettingsPath, environment.clientSettingsPath);
165+
166+
yield* settings.set(clientSettings);
167+
168+
assert.equal(
169+
yield* fileSystem.readLink(environment.clientSettingsPath),
170+
linkedSettingsPath,
171+
);
172+
assert.deepEqual(
173+
yield* decodeClientSettingsJson(yield* fileSystem.readFileString(linkedSettingsPath)),
174+
clientSettings,
175+
);
176+
}),
177+
),
178+
);
179+
152180
it.effect.each([
153181
{ label: "permission", reason: "PermissionDenied" },
154182
{ label: "I/O", reason: "Unknown" },

‎apps/desktop/src/settings/DesktopClientSettings.ts‎

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { ClientSettingsSchema, type ClientSettings } from "@t3tools/contracts";
22
import { fromLenientJson } from "@t3tools/shared/schemaJson";
3+
import { resolveSymlinkTarget } from "@t3tools/shared/symlink";
34
import * as Context from "effect/Context";
45
import * as Crypto from "effect/Crypto";
56
import * as Effect from "effect/Effect";
@@ -41,6 +42,7 @@ export class DesktopClientSettingsReadError extends Schema.TaggedError<DesktopCl
4142

4243
const DesktopClientSettingsWriteOperation = Schema.Literals([
4344
"create-temporary-file-name",
45+
"resolve-symlink",
4446
"encode-document",
4547
"create-directory",
4648
"write-temporary-file",
@@ -126,8 +128,20 @@ const writeClientSettings = Effect.fnUntraced(function* (input: {
126128
readonly settings: ClientSettings;
127129
readonly suffix: string;
128130
}): Effect.fn.Return<void, DesktopClientSettingsWriteError> {
129-
const directory = input.path.dirname(input.settingsPath);
130-
const tempPath = `${input.settingsPath}.${process.pid}.${input.suffix}.tmp`;
131+
const targetPath = yield* resolveSymlinkTarget(input.settingsPath).pipe(
132+
Effect.provideService(FileSystem.FileSystem, input.fileSystem),
133+
Effect.provideService(Path.Path, input.path),
134+
Effect.mapError(
135+
(cause) =>
136+
new DesktopClientSettingsWriteError({
137+
operation: "resolve-symlink",
138+
path: input.settingsPath,
139+
cause,
140+
}),
141+
),
142+
);
143+
const directory = input.path.dirname(targetPath);
144+
const tempPath = `${targetPath}.${process.pid}.${input.suffix}.tmp`;
131145
const encoded = yield* encodeClientSettingsJson(input.settings).pipe(
132146
Effect.mapError(
133147
(cause) =>
@@ -158,7 +172,7 @@ const writeClientSettings = Effect.fnUntraced(function* (input: {
158172
}),
159173
),
160174
);
161-
yield* input.fileSystem.rename(tempPath, input.settingsPath).pipe(
175+
yield* input.fileSystem.rename(tempPath, targetPath).pipe(
162176
Effect.mapError(
163177
(cause) =>
164178
new DesktopClientSettingsWriteError({

‎apps/mobile/app.config.ts‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,8 @@ const config: ExpoConfig = {
306306
// JS back handling survives it via react-native's Android 16 shim plus
307307
// withAndroidPredictiveBackCompat on Android 13-15.
308308
predictiveBackGestureEnabled: true,
309+
// expo-sensors declares this for its pedometer, which the app does not use.
310+
blockedPermissions: ["android.permission.ACTIVITY_RECOGNITION"],
309311
},
310312
web: {
311313
favicon: variant.assets.appIcon,
@@ -418,7 +420,16 @@ const config: ExpoConfig = {
418420
minSdkVersion: 24,
419421
// kotlinx-io uses Kotlin 2.3's return-value checker annotation, while
420422
// SDK 58 builds with Kotlin 2.2. It has no runtime behavior.
421-
extraProguardRules: "-dontwarn kotlin.MustUseReturnValues",
423+
//
424+
// WorkManager 2.9 keeps InputMerger classes but not their constructors,
425+
// and R8 full mode no longer keeps a default constructor implicitly.
426+
// Without it no work request can start, so the Glance session behind
427+
// the widget never renders and it stays on "Loading widget". WorkManager
428+
// 2.10 ships this rule itself; drop it once the resolved version gets there.
429+
extraProguardRules: [
430+
"-dontwarn kotlin.MustUseReturnValues",
431+
"-keep class * extends androidx.work.InputMerger { <init>(); }",
432+
].join("\n"),
422433
},
423434
ios: {
424435
deploymentTarget: "18.0",
@@ -431,6 +442,9 @@ const config: ExpoConfig = {
431442
},
432443
],
433444
"./plugins/withIosCocoaPodsUuidCache.cjs",
445+
// Only the accelerometer is used (device viewer shake). Compile out the
446+
// pedometer so iOS needs no motion purpose string.
447+
["expo-sensors", { motionPermission: false }],
434448
// Must be listed BEFORE expo-widgets: same-type mods run last-registered-
435449
// first, so registering earlier makes this plugin's mods run AFTER
436450
// expo-widgets' — its dangerous mod wipes ios/ExpoWidgetsTarget/ (which

‎apps/mobile/modules/t3-markdown-text/ios/T3MarkdownText.mm‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,12 @@ - (instancetype)initWithFrame:(CGRect)frame
311311
_textView.editable = false;
312312
_textView.textContainerInset = UIEdgeInsetsZero;
313313
_textView.textContainer.lineFragmentPadding = 0;
314+
// The shadow node already sized this view to fit every line. A container
315+
// that tracks the frame can come out a fraction of a point shorter after
316+
// pixel rounding, and TextKit then drops the last line. Size it by hand
317+
// with unbounded height and let the view's bounds clip.
318+
_textView.textContainer.widthTracksTextView = NO;
319+
_textView.textContainer.heightTracksTextView = NO;
314320
_textView.delegate = self;
315321
// Chat text supports selection and contextual actions, but not drag-and-drop.
316322
_textView.textDragInteraction.enabled = NO;
@@ -477,6 +483,7 @@ - (void)drawRect:(CGRect)rect
477483
}
478484
if (frameChanged) {
479485
_textView.frame = _view.frame;
486+
_textView.textContainer.size = CGSizeMake(CGRectGetWidth(_view.frame), CGFLOAT_MAX);
480487
}
481488

482489
// Text attachments have no native link element. Expose their existing runs

0 commit comments

Comments
 (0)