feat: Add slashcommand api

This commit is contained in:
2026-06-05 17:12:26 +02:00
parent 4070ef6caf
commit 8ecfc9a1fe
101 changed files with 3526 additions and 147 deletions

View File

@@ -25,10 +25,7 @@ import {
import { UserAvatarComponent } from '../../../../shared';
import { ViewportService } from '../../../../core/platform';
import {
MobileAppLifecycleService,
MobilePictureInPictureService
} from '../../../../infrastructure/mobile';
import { MobileAppLifecycleService, MobilePictureInPictureService } from '../../../../infrastructure/mobile';
import { VoiceWorkspacePlaybackService } from '../voice-workspace-playback.service';
import { VoiceWorkspaceStreamItem } from '../voice-workspace.models';
@@ -84,6 +81,7 @@ export class VoiceWorkspaceStreamTileComponent implements OnDestroy {
this.mobileLifecycle.onAppStateChange((isActive) => {
void this.handleAppStateChange(isActive);
});
effect(() => {
const ref = this.videoRef();
const item = this.item();

View File

@@ -41,7 +41,7 @@
/>
<span class="text-sm">Process RAM</span>
</div>
<span class="font-mono text-sm text-foreground">{{ ramLabel() ?? '' }}</span>
<span class="font-mono text-sm text-foreground">{{ ramLabel() ?? '-' }}</span>
</div>
<p class="mt-1 text-xs text-muted-foreground">Live total working set from Electron app metrics. Updates every 2 seconds.</p>
</section>

View File

@@ -14,11 +14,104 @@
</div>
</section>
@if (!isElectron) {
<section class="rounded-lg border border-border bg-secondary/30 p-5">
<p class="text-sm text-muted-foreground">Automatic updates are only available in the packaged Electron desktop app.</p>
@if (isCapacitor) {
<section class="rounded-lg border border-border bg-card/60 p-5">
<div class="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div>
<h4 class="text-base font-semibold text-foreground">Mobile app updates</h4>
<p class="mt-1 text-sm text-muted-foreground">
Check the Play Store or App Store for newer native builds. Android can install in-app updates when Google Play allows it.
</p>
</div>
<span class="inline-flex items-center rounded-full border border-primary/30 bg-primary/10 px-3 py-1 text-xs font-medium text-primary">
{{ mobileStatusLabel() }}
</span>
</div>
</section>
} @else {
@if (!mobileState().isSupported) {
<section class="rounded-lg border border-border bg-secondary/30 p-5">
<p class="text-sm text-muted-foreground">Store updates are only available in the packaged Android or iOS app.</p>
</section>
} @else {
<section class="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
<div class="rounded-lg border border-border bg-secondary/20 p-4">
<p class="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground/70">Installed</p>
<p class="mt-2 text-lg font-semibold text-foreground">{{ mobileState().currentVersion }}</p>
</div>
<div class="rounded-lg border border-border bg-secondary/20 p-4">
<p class="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground/70">Store version</p>
<p class="mt-2 text-lg font-semibold text-foreground">{{ mobileState().availableVersion || 'Unknown' }}</p>
</div>
<div class="rounded-lg border border-border bg-secondary/20 p-4">
<p class="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground/70">Last checked</p>
<p class="mt-2 text-sm font-medium text-foreground">
{{ mobileState().lastCheckedAt ? (mobileState().lastCheckedAt | date: 'medium') : 'Not checked yet' }}
</p>
</div>
</section>
<section class="space-y-4 rounded-lg border border-border bg-card/60 p-5">
<div class="rounded-lg border border-border bg-secondary/20 p-4">
<p class="text-sm font-medium text-foreground">Status</p>
<p class="mt-1 text-sm text-muted-foreground">
{{ mobileState().statusMessage || 'Waiting for the first store update check.' }}
</p>
</div>
<div class="flex flex-wrap gap-3">
<button
type="button"
(click)="refreshMobileReleaseInfo()"
class="inline-flex items-center rounded-lg border border-border bg-secondary px-4 py-2 text-sm font-medium text-foreground transition-colors hover:bg-secondary/80"
>
Check for updates
</button>
@if (mobileState().status === 'update-available') {
<button
type="button"
(click)="openMobileAppStore()"
class="inline-flex items-center rounded-lg border border-border bg-secondary px-4 py-2 text-sm font-medium text-foreground transition-colors hover:bg-secondary/80"
>
Open app store
</button>
@if (mobileState().immediateUpdateAllowed || mobileState().flexibleUpdateAllowed) {
<button
type="button"
(click)="installMobileUpdateNow()"
class="inline-flex items-center rounded-lg bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90"
>
Install update
</button>
}
@if (mobileState().flexibleUpdateAllowed && mobileState().status === 'downloading') {
<button
type="button"
(click)="completeMobileFlexibleUpdate()"
class="inline-flex items-center rounded-lg bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90"
>
Restart to finish update
</button>
}
}
</div>
</section>
}
}
@if (!isElectron && !isCapacitor) {
<section class="rounded-lg border border-border bg-secondary/30 p-5">
<p class="text-sm text-muted-foreground">Automatic updates are only available in the packaged Electron desktop app or native mobile app.</p>
</section>
}
@if (isElectron) {
<section class="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
<div class="rounded-lg border border-border bg-secondary/20 p-4">
<p class="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground/70">Installed</p>

View File

@@ -7,6 +7,7 @@ import {
} from '@angular/core';
import { CommonModule } from '@angular/common';
import { DesktopAppUpdateService } from '../../../../core/services/desktop-app-update.service';
import { MobileAppUpdateService, getMobileUpdateStatusLabel } from '../../../../infrastructure/mobile';
type AutoUpdateMode = 'auto' | 'off' | 'version';
type DesktopUpdateStatus =
@@ -29,9 +30,15 @@ type DesktopUpdateStatus =
templateUrl: './updates-settings.component.html'
})
export class UpdatesSettingsComponent {
readonly updates = inject(DesktopAppUpdateService);
readonly isElectron = this.updates.isElectron;
readonly state = this.updates.state;
readonly desktopUpdates = inject(DesktopAppUpdateService);
readonly mobileUpdates = inject(MobileAppUpdateService);
readonly isElectron = this.desktopUpdates.isElectron;
readonly isCapacitor = this.mobileUpdates.isCapacitor;
readonly state = this.desktopUpdates.state;
readonly mobileState = this.mobileUpdates.state;
readonly mobileStatusLabel = computed(() =>
getMobileUpdateStatusLabel(this.mobileState().status)
);
readonly hasPendingManifestUrlChanges = signal(false);
readonly manifestUrlsText = signal('');
readonly statusLabel = computed(() => this.getStatusLabel(this.state().status));
@@ -60,18 +67,18 @@ export class UpdatesSettingsComponent {
? this.state().preferredVersion ?? this.state().availableVersions[0] ?? null
: this.state().preferredVersion;
await this.updates.saveUpdatePreferences(mode, preferredVersion);
await this.desktopUpdates.saveUpdatePreferences(mode, preferredVersion);
}
async onVersionChange(event: Event): Promise<void> {
const select = event.target as HTMLSelectElement;
await this.updates.saveUpdatePreferences('version', select.value || null);
await this.desktopUpdates.saveUpdatePreferences('version', select.value || null);
}
async refreshReleaseInfo(): Promise<void> {
await this.updates.refreshServerContext();
await this.updates.checkForUpdates();
await this.desktopUpdates.refreshServerContext();
await this.desktopUpdates.checkForUpdates();
}
onManifestUrlsInput(event: Event): void {
@@ -82,7 +89,7 @@ export class UpdatesSettingsComponent {
}
async saveManifestUrls(): Promise<void> {
await this.updates.saveManifestUrls(
await this.desktopUpdates.saveManifestUrls(
this.parseManifestUrls(this.manifestUrlsText())
);
@@ -90,12 +97,37 @@ export class UpdatesSettingsComponent {
}
async useConnectedServerDefaults(): Promise<void> {
await this.updates.saveManifestUrls([]);
await this.desktopUpdates.saveManifestUrls([]);
this.hasPendingManifestUrlChanges.set(false);
}
async restartNow(): Promise<void> {
await this.updates.restartToApplyUpdate();
await this.desktopUpdates.restartToApplyUpdate();
}
async refreshMobileReleaseInfo(): Promise<void> {
await this.mobileUpdates.checkForUpdates();
}
async openMobileAppStore(): Promise<void> {
await this.mobileUpdates.openAppStore();
}
async installMobileUpdateNow(): Promise<void> {
const mobileState = this.mobileState();
if (mobileState.immediateUpdateAllowed) {
await this.mobileUpdates.performImmediateUpdate();
return;
}
if (mobileState.flexibleUpdateAllowed) {
await this.mobileUpdates.startFlexibleUpdate();
}
}
async completeMobileFlexibleUpdate(): Promise<void> {
await this.mobileUpdates.completeFlexibleUpdate();
}
private parseManifestUrls(rawValue: string): string[] {

View File

@@ -134,12 +134,6 @@ export class NativeContextMenuComponent implements OnInit, OnDestroy {
this.cleanup = null;
}
private readonly onDocumentContextMenuCapture = (event: MouseEvent): void => {
if (resolveCustomEmojiContextMenuTarget(event.target)) {
event.preventDefault();
}
};
close(): void {
this.params.set(null);
this.customEmojiMenu.set(null);
@@ -186,6 +180,12 @@ export class NativeContextMenuComponent implements OnInit, OnDestroy {
void this.runAction(action);
}
private readonly onDocumentContextMenuCapture = (event: MouseEvent): void => {
if (resolveCustomEmojiContextMenuTarget(event.target)) {
event.preventDefault();
}
};
private openCustomEmojiMenu(event: MouseEvent, target: CustomEmojiContextMenuTarget): void {
event.preventDefault();
event.stopPropagation();