Compare commits

...

11 Commits

Author SHA1 Message Date
pengx17
bb606ac3e5 fix(core): audio job submittion timeout too short (#11918)
fix AF-2556
2025-04-24 17:07:49 +08:00
pengx17
851111e1e4 feat(core): add actions to transcription block (#11896) 2025-04-24 17:05:55 +08:00
pengx17
9982e0ea45 fix(core): sidebar audio player seek position issue (#11844)
fix AF-2541
2025-04-24 17:05:44 +08:00
pengx17
58f7a6166c fix(electron): use askForMeetingPermission for asking microphone permission (#11792) 2025-04-24 17:05:33 +08:00
pengx17
07d7a62071 fix(electron): sometimes pops up failed to save dialog (#11925)
fix AF-2557
2025-04-24 17:05:08 +08:00
Peng Xiao
ab3f056927 fix(core): remove image proxy in onboarding snapshots (#11954) 2025-04-24 17:04:32 +08:00
pengx17
61e3364717 fix(core): should not limit the number of docs of at menu (#11889)
fix AF-2544
2025-04-24 17:03:52 +08:00
yoyoyohamapi
e32d6b9347 fix(core): action items in the ai response are not optimized for dark mode (#11839)
### TL;DR

* Fix action items in the AI response are not optimized for dark mode.
* Fix answer content in the AI response are not optimized for edgeless theme.

![截屏2025-04-21 14.26.41.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/MyktQ6Qwc7H6TiRCFoYN/9c991df4-36b1-4969-ac0d-3c582edb1120.png)

[uploading 截屏2025-04-21 14.30.00.png...]

> CLOSE BS-3249
2025-04-24 16:48:09 +08:00
EYHN
075a2e9f99 fix(nbstore): fix indexer cache not working (#11922) 2025-04-24 16:43:04 +08:00
EYHN
8b486b4833 fix(core): subscribe search not unsubscribe (#11929) 2025-04-24 16:42:53 +08:00
darkskygit
21b7f02b0f fix(server): empty mimetype attachments fallback (#11869) 2025-04-23 15:57:12 +08:00
29 changed files with 309 additions and 116 deletions

View File

@@ -249,6 +249,7 @@ export class LinkedDocPopover extends SignalWatcher(
override disconnectedCallback() {
super.disconnectedCallback();
this._menusItemsEffectCleanup();
this._updateLinkedDocGroupAbortController?.abort();
}
override render() {

View File

@@ -385,6 +385,45 @@ test('should create message correctly', async t => {
t.truthy(messageId, 'should be able to create message with valid session');
}
{
// with attachment url
{
const { id } = await createWorkspace(app);
const sessionId = await createCopilotSession(
app,
id,
randomUUID(),
promptName
);
const messageId = await createCopilotMessage(app, sessionId, undefined, [
'http://example.com/cat.jpg',
]);
t.truthy(messageId, 'should be able to create message with url link');
}
// with attachment
{
const { id } = await createWorkspace(app);
const sessionId = await createCopilotSession(
app,
id,
randomUUID(),
promptName
);
const smallestPng =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII';
const pngData = await fetch(smallestPng).then(res => res.arrayBuffer());
const messageId = await createCopilotMessage(
app,
sessionId,
undefined,
undefined,
[new File([new Uint8Array(pngData)], '1.png', { type: 'image/png' })]
);
t.truthy(messageId, 'should be able to create message with blobs');
}
}
{
await t.throwsAsync(
createCopilotMessage(app, randomUUID()),

View File

@@ -490,19 +490,53 @@ export async function createCopilotMessage(
sessionId: string,
content?: string,
attachments?: string[],
blobs?: ArrayBuffer[],
blobs?: File[],
params?: Record<string, string>
): Promise<string> {
const res = await app.gql(
`
mutation createCopilotMessage($options: CreateChatMessageInput!) {
createCopilotMessage(options: $options)
let resp = app
.POST('/graphql')
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.field(
'operations',
JSON.stringify({
query: `
mutation createCopilotMessage($options: CreateChatMessageInput!) {
createCopilotMessage(options: $options)
}
`,
variables: {
options: { sessionId, content, attachments, blobs: [], params },
},
})
)
.field(
'map',
JSON.stringify(
Array.from<any>({ length: blobs?.length ?? 0 }).reduce(
(acc, _, idx) => {
acc[idx.toString()] = [`variables.options.blobs.${idx}`];
return acc;
},
{}
)
)
);
if (blobs && blobs.length) {
for (const [idx, file] of blobs.entries()) {
resp = resp.attach(
idx.toString(),
Buffer.from(await file.arrayBuffer()),
{
filename: file.name || `file${idx}`,
contentType: file.type || 'application/octet-stream',
}
);
}
`,
{ options: { sessionId, content, attachments, blobs, params } }
);
}
return res.createCopilotMessage;
const res = await resp.expect(200);
return res.body.data.createCopilotMessage;
}
export async function chatWithText(

View File

@@ -138,7 +138,15 @@ export class FalProvider
);
return {
model_name: options.modelName || undefined,
image_url: attachments?.[0],
image_url: attachments
?.map(v =>
typeof v === 'string'
? v
: v.mimeType.startsWith('image/')
? v.attachment
: undefined
)
.filter(v => !!v)[0],
prompt: content.trim(),
loras: lora.length ? lora : undefined,
controlnets: controlnets.length ? controlnets : undefined,

View File

@@ -51,7 +51,15 @@ export const ChatMessageRole = Object.values(AiPromptRole) as [
export const PureMessageSchema = z.object({
content: z.string(),
attachments: z.array(z.string()).optional().nullable(),
attachments: z
.array(
z.union([
z.string(),
z.object({ attachment: z.string(), mimeType: z.string() }),
])
)
.optional()
.nullable(),
params: z.record(z.any()).optional().nullable(),
});

View File

@@ -35,15 +35,26 @@ const FORMAT_INFER_MAP: Record<string, string> = {
flv: 'video/flv',
};
function inferMimeType(url: string) {
async function inferMimeType(url: string) {
if (url.startsWith('data:')) {
return url.split(';')[0].split(':')[1];
}
const extension = url.split('.').pop();
const pathname = new URL(url).pathname;
const extension = pathname.split('.').pop();
if (extension) {
return FORMAT_INFER_MAP[extension];
const ext = FORMAT_INFER_MAP[extension];
if (ext) {
return ext;
}
const mimeType = await fetch(url, {
method: 'HEAD',
redirect: 'follow',
}).then(res => res.headers.get('Content-Type'));
if (mimeType) {
return mimeType;
}
}
return undefined;
return 'application/octet-stream';
}
export async function chatToGPTMessage(
@@ -66,19 +77,24 @@ export async function chatToGPTMessage(
contents.push({ type: 'text', text: content });
}
for (const url of attachments) {
if (SIMPLE_IMAGE_URL_REGEX.test(url)) {
const mimeType =
typeof mimetype === 'string' ? mimetype : inferMimeType(url);
if (mimeType) {
if (mimeType.startsWith('image/')) {
contents.push({ type: 'image', image: url, mimeType });
} else {
const data = url.startsWith('data:')
? await fetch(url).then(r => r.arrayBuffer())
: new URL(url);
contents.push({ type: 'file' as const, data, mimeType });
}
for (let attachment of attachments) {
let mimeType: string;
if (typeof attachment === 'string') {
mimeType =
typeof mimetype === 'string'
? mimetype
: await inferMimeType(attachment);
} else {
({ attachment, mimeType } = attachment);
}
if (SIMPLE_IMAGE_URL_REGEX.test(attachment)) {
if (mimeType.startsWith('image/')) {
contents.push({ type: 'image', image: attachment, mimeType });
} else {
const data = attachment.startsWith('data:')
? await fetch(attachment).then(r => r.arrayBuffer())
: new URL(attachment);
contents.push({ type: 'file' as const, data, mimeType });
}
}
}

View File

@@ -34,6 +34,7 @@ import { Admin } from '../../core/common';
import { AccessController } from '../../core/permission';
import { UserType } from '../../core/user';
import { PromptService } from './prompt';
import { PromptMessage } from './providers';
import { ChatSessionService } from './session';
import { CopilotStorage } from './storage';
import {
@@ -113,7 +114,7 @@ class CreateChatMessageInput implements Omit<SubmittedMessage, 'content'> {
@Field(() => String, { nullable: true })
content!: string | undefined;
@Field(() => [String], { nullable: true })
@Field(() => [String], { nullable: true, deprecationReason: 'use blobs' })
attachments!: string[] | undefined;
@Field(() => [GraphQLUpload], { nullable: true })
@@ -527,8 +528,8 @@ export class CopilotResolver {
throw new BadRequestException('Session not found');
}
const attachments: PromptMessage['attachments'] = options.attachments || [];
if (options.blobs) {
options.attachments = options.attachments || [];
const { workspaceId } = session.config;
const blobs = await Promise.all(options.blobs);
@@ -539,18 +540,18 @@ export class CopilotResolver {
const filename = createHash('sha256')
.update(uploaded.buffer)
.digest('base64url');
const link = await this.storage.put(
const attachment = await this.storage.put(
user.id,
workspaceId,
filename,
uploaded.buffer
);
options.attachments.push(link);
attachments.push({ attachment, mimeType: blob.mimetype });
}
}
try {
return await this.chatSession.createMessage(options);
return await this.chatSession.createMessage({ ...options, attachments });
} catch (e: any) {
throw new CopilotFailedToCreateMessage(e.message);
}

View File

@@ -166,7 +166,11 @@ export class ChatSession implements AsyncDisposable {
firstMessage.attachments || [],
]
.flat()
.filter(v => !!v?.trim());
.filter(v =>
typeof v === 'string'
? !!v.trim()
: v && v.attachment.trim() && v.mimeType
);
return finished;
}
@@ -553,7 +557,12 @@ export class ChatSessionService {
action: prompt.action || null,
tokens: tokenCost,
createdAt,
messages: preload.concat(ret.data),
messages: preload.concat(ret.data).map(m => ({
...m,
attachments: m.attachments
?.map(a => (typeof a === 'string' ? a : a.attachment))
.filter(a => !!a),
})),
};
} else {
this.logger.error(

View File

@@ -265,40 +265,47 @@ export class DataStruct {
if (cached) {
return cached;
}
using _ = await this.measure(`query[${query.type}]`);
if (query.type === 'match') {
const iidx = this.invertedIndex.get(table)?.get(query.field as string);
if (!iidx) {
return new Match();
const result = await (async () => {
using _ = await this.measure(`query[${query.type}]`);
if (query.type === 'match') {
const iidx = this.invertedIndex.get(table)?.get(query.field as string);
if (!iidx) {
return new Match();
}
return await iidx.match(trx, query.match);
} else if (query.type === 'boolean') {
const weights = [];
for (const q of query.queries) {
weights.push(await this.queryRaw(trx, table, q, cache));
}
if (query.occur === 'must') {
return weights.reduce((acc, w) => acc.and(w));
} else if (query.occur === 'must_not') {
const total = weights.reduce((acc, w) => acc.and(w));
return (await this.matchAll(trx, table)).exclude(total);
} else if (query.occur === 'should') {
return weights.reduce((acc, w) => acc.or(w));
}
} else if (query.type === 'all') {
return await this.matchAll(trx, table);
} else if (query.type === 'boost') {
return (await this.queryRaw(trx, table, query.query, cache)).boost(
query.boost
);
} else if (query.type === 'exists') {
const iidx = this.invertedIndex.get(table)?.get(query.field as string);
if (!iidx) {
return new Match();
}
return await iidx.all(trx);
}
return await iidx.match(trx, query.match);
} else if (query.type === 'boolean') {
const weights = [];
for (const q of query.queries) {
weights.push(await this.queryRaw(trx, table, q, cache));
}
if (query.occur === 'must') {
return weights.reduce((acc, w) => acc.and(w));
} else if (query.occur === 'must_not') {
const total = weights.reduce((acc, w) => acc.and(w));
return (await this.matchAll(trx, table)).exclude(total);
} else if (query.occur === 'should') {
return weights.reduce((acc, w) => acc.or(w));
}
} else if (query.type === 'all') {
return await this.matchAll(trx, table);
} else if (query.type === 'boost') {
return (await this.queryRaw(trx, table, query.query, cache)).boost(
query.boost
);
} else if (query.type === 'exists') {
const iidx = this.invertedIndex.get(table)?.get(query.field as string);
if (!iidx) {
return new Match();
}
return await iidx.all(trx);
}
throw new Error(`Query type '${query.type}' not supported`);
throw new Error(`Query type '${query.type}' not supported`);
})();
cache.set(query, result);
return result;
}
async clear(trx: DataStructRWTransaction) {

View File

@@ -225,17 +225,26 @@ export class FullTextInvertedIndex implements InvertedIndex {
)?.value ?? 0;
for (const token of queryTokens) {
const key = InvertedIndexKey.forString(this.fieldKey, token.term);
const objs = await trx
.objectStore('invertedIndex')
.index('key')
.getAll(
IDBKeyRange.bound(
[this.table, key.buffer()],
[this.table, key.add1().buffer()],
false,
true
)
);
const objs = [
// match exact
await trx
.objectStore('invertedIndex')
.index('key')
.get([this.table, key.buffer()]),
// match prefix
...(await trx
.objectStore('invertedIndex')
.index('key')
.getAll(
IDBKeyRange.bound(
[this.table, key.buffer()],
[this.table, key.add1().buffer()],
true,
true
),
5000 // get maximum 5000 items for prefix match
)),
];
const submatched: {
nid: number;
score: number;
@@ -245,6 +254,9 @@ export class FullTextInvertedIndex implements InvertedIndex {
};
}[] = [];
for (const obj of objs) {
if (!obj) {
continue;
}
const key = InvertedIndexKey.fromBuffer(obj.key);
const originTokenTerm = key.asString();
const matchLength = token.term.length;

View File

@@ -531,6 +531,7 @@ export function startRecording(
// set a timeout to stop the recording after MAX_DURATION_FOR_TRANSCRIPTION
setTimeout(() => {
const state = recordingStateMachine.status$.value;
if (
state?.status === 'recording' &&
state.id === recordingStatus$.value?.id
@@ -780,6 +781,13 @@ export const checkMeetingPermissions = () => {
) as Record<(typeof mediaTypes)[number], boolean>;
};
export const askForMeetingPermission = async (type: 'microphone') => {
if (!isMacOS()) {
return false;
}
return systemPreferences.askForMediaAccess(type);
};
export const checkCanRecordMeeting = () => {
const features = checkMeetingPermissions();
return (

View File

@@ -9,6 +9,7 @@ import { shell } from 'electron';
import { isMacOS } from '../../shared/utils';
import type { NamespaceHandlers } from '../type';
import {
askForMeetingPermission,
checkMeetingPermissions,
checkRecordingAvailable,
disableRecordingFeature,
@@ -76,6 +77,9 @@ export const recordingHandlers = {
checkMeetingPermissions: async () => {
return checkMeetingPermissions();
},
askForMeetingPermission: async (_, type: 'microphone') => {
return askForMeetingPermission(type);
},
showRecordingPermissionSetting: async (_, type: 'screen' | 'microphone') => {
const urlMap = {
screen: 'Privacy_ScreenCapture',

View File

@@ -260,6 +260,8 @@ export class ChatPanelAddPopover extends SignalWatcher(
@query('.search-input')
accessor searchInput!: HTMLInputElement;
private _menuGroupAbortController = new AbortController();
override connectedCallback() {
super.connectedCallback();
this._updateSearchGroup();
@@ -273,6 +275,7 @@ export class ChatPanelAddPopover extends SignalWatcher(
override disconnectedCallback() {
super.disconnectedCallback();
document.removeEventListener('keydown', this._handleKeyDown);
this._menuGroupAbortController.abort();
}
override render() {
@@ -385,13 +388,15 @@ export class ChatPanelAddPopover extends SignalWatcher(
}
private _updateSearchGroup() {
this._menuGroupAbortController.abort();
this._menuGroupAbortController = new AbortController();
switch (this._mode) {
case AddPopoverMode.Tags: {
this._searchGroups = [
this.searchMenuConfig.getTagMenuGroup(
this._query,
this._addTagChip,
this.abortController.signal
this._menuGroupAbortController.signal
),
];
break;
@@ -401,7 +406,7 @@ export class ChatPanelAddPopover extends SignalWatcher(
this.searchMenuConfig.getCollectionMenuGroup(
this._query,
this._addCollectionChip,
this.abortController.signal
this._menuGroupAbortController.signal
),
];
break;
@@ -410,7 +415,7 @@ export class ChatPanelAddPopover extends SignalWatcher(
const docGroup = this.searchMenuConfig.getDocMenuGroup(
this._query,
this._addDocChip,
this.abortController.signal
this._menuGroupAbortController.signal
);
if (!this._query) {
this._searchGroups = [docGroup];
@@ -418,12 +423,12 @@ export class ChatPanelAddPopover extends SignalWatcher(
const tagGroup = this.searchMenuConfig.getTagMenuGroup(
this._query,
this._addTagChip,
this.abortController.signal
this._menuGroupAbortController.signal
);
const collectionGroup = this.searchMenuConfig.getCollectionMenuGroup(
this._query,
this._addCollectionChip,
this.abortController.signal
this._menuGroupAbortController.signal
);
const nothing = html``;
this._searchGroups = [

View File

@@ -1,6 +1,6 @@
import { createLitPortal } from '@blocksuite/affine/components/portal';
import { WithDisposable } from '@blocksuite/affine/global/lit';
import { ColorScheme } from '@blocksuite/affine/model';
import { ThemeProvider } from '@blocksuite/affine/shared/services';
import {
EditorHost,
PropTypes,
@@ -112,6 +112,7 @@ export class AIItemList extends WithDisposable(LitElement) {
}
override render() {
const theme = this.host.std.get(ThemeProvider).app$.value;
return html`${repeat(this.groups, group => {
return html`
${group.name
@@ -124,7 +125,7 @@ export class AIItemList extends WithDisposable(LitElement) {
item => item.name,
item =>
html`<ai-item
.theme=${this.theme}
.theme=${theme}
.onClick=${this.onClick}
.item=${item}
.host=${this.host}
@@ -147,9 +148,6 @@ export class AIItemList extends WithDisposable(LitElement) {
@property({ attribute: 'data-testid', reflect: true })
accessor testId = 'ai-item-list';
@property({ attribute: false })
accessor theme: ColorScheme = ColorScheme.Light;
}
declare global {

View File

@@ -86,10 +86,8 @@ export class AskAIPanel extends WithDisposable(LitElement) {
const style = styleMap({
minWidth: `${this.minWidth}px`,
});
const appTheme = this.host.std.get(ThemeProvider).app$.value;
return html`<div class="ask-ai-panel" style=${style}>
<ai-item-list
.theme=${appTheme}
.host=${this.host}
.groups=${this._actionGroups}
.onClick=${this.onItemClick}

View File

@@ -8,7 +8,11 @@ import { PageEditorBlockSpecs } from '@blocksuite/affine/extensions';
import { Container, type ServiceProvider } from '@blocksuite/affine/global/di';
import { WithDisposable } from '@blocksuite/affine/global/lit';
import { codeBlockWrapMiddleware } from '@blocksuite/affine/shared/adapters';
import { LinkPreviewerService } from '@blocksuite/affine/shared/services';
import {
LinkPreviewerService,
ThemeProvider,
} from '@blocksuite/affine/shared/services';
import { unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme';
import {
BlockStdScope,
BlockViewIdentifier,
@@ -22,7 +26,11 @@ import type {
Store,
TransformerMiddleware,
} from '@blocksuite/affine/store';
import { css, html, nothing, type PropertyValues } from 'lit';
import {
darkCssVariablesV2,
lightCssVariablesV2,
} from '@toeverything/theme/v2';
import { css, html, nothing, type PropertyValues, unsafeCSS } from 'lit';
import { property, query } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { keyed } from 'lit/directives/keyed.js';
@@ -109,7 +117,7 @@ export class TextRenderer extends WithDisposable(ShadowlessElement) {
padding: 0;
margin: 0;
line-height: var(--affine-line-height);
color: var(--affine-text-primary-color);
color: ${unsafeCSSVarV2('text/primary')};
font-weight: 400;
}
@@ -168,6 +176,18 @@ export class TextRenderer extends WithDisposable(ShadowlessElement) {
}
}
.text-renderer-container[data-app-theme='dark'] {
.ai-answer-text-editor .affine-page-root-block-container {
color: ${unsafeCSS(darkCssVariablesV2['--affine-v2-text-primary'])};
}
}
.text-renderer-container[data-app-theme='light'] {
.ai-answer-text-editor .affine-page-root-block-container {
color: ${unsafeCSS(lightCssVariablesV2['--affine-v2-text-primary'])};
}
}
${customHeadingStyles}
`;
@@ -288,8 +308,9 @@ export class TextRenderer extends WithDisposable(ShadowlessElement) {
'text-renderer-container': true,
'custom-heading': !!customHeading,
});
const theme = this.host?.std.get(ThemeProvider).app$.value;
return html`
<div class=${classes} data-testid=${testId}>
<div class=${classes} data-testid=${testId} data-app-theme=${theme}>
${keyed(
this._doc,
html`<div class="ai-answer-text-editor affine-page-viewport">

View File

@@ -30,9 +30,3 @@ export const notesButtonIcon = style({
export const error = style({
color: cssVarV2('aI/errorText'),
});
export const publicUserLabel = style({
fontSize: cssVar('fontXs'),
fontWeight: 500,
userSelect: 'none',
});

View File

@@ -37,12 +37,12 @@ export function patchQuickSearchService(framework: FrameworkProvider) {
searchResult = await new Promise((resolve, reject) =>
framework.get(QuickSearchService).quickSearch.show(
[
framework.get(RecentDocsQuickSearchSession),
framework.get(CreationQuickSearchSession),
framework.get(DocsQuickSearchSession),
framework.get(LinksQuickSearchSession),
framework.get(ExternalLinksQuickSearchSession),
framework.get(JournalsQuickSearchSession),
framework.createEntity(RecentDocsQuickSearchSession),
framework.createEntity(CreationQuickSearchSession),
framework.createEntity(DocsQuickSearchSession),
framework.createEntity(LinksQuickSearchSession),
framework.createEntity(ExternalLinksQuickSearchSession),
framework.createEntity(JournalsQuickSearchSession),
],
result => {
if (result === null) {

View File

@@ -210,7 +210,13 @@ export const MeetingsSettings = () => {
const handleOpenMicrophoneRecordingPermissionSetting =
useAsyncCallback(async () => {
await meetingSettingsService.showRecordingPermissionSetting('microphone');
const result =
await meetingSettingsService.askForMeetingPermission('microphone');
if (!result) {
await meetingSettingsService.showRecordingPermissionSetting(
'microphone'
);
}
}, [meetingSettingsService]);
const handleOpenSavedRecordings = useAsyncCallback(async () => {

View File

@@ -2,7 +2,7 @@ import { style } from '@vanilla-extract/css';
export const publicUserLabel = style({
fontSize: 'inherit',
display: 'flex',
display: 'inline-flex',
alignItems: 'center',
});

View File

@@ -242,7 +242,21 @@ export class AudioAttachmentBlock extends Entity<AttachmentBlockModel> {
);
};
const fillActions = async (actions: TranscriptionResult['actions']) => {
if (!actions) {
return;
}
const calloutId = addCalloutBlock('🎯', 'Todo');
await insertFromMarkdown(
undefined,
actions ?? '',
this.props.doc,
calloutId,
1
);
};
fillTranscription(result.segments);
await fillSummary(result.summary);
await fillActions(result.actions);
};
}

View File

@@ -43,6 +43,7 @@ export class AudioTranscriptionJobStore extends Entity<{
}
const files = await this.props.getAudioFiles();
const response = await graphqlService.gql({
timeout: 600_000, // default 15s is too short for audio transcription
query: submitAudioTranscriptionMutation,
variables: {
workspaceId: this.currentWorkspaceId,

View File

@@ -7,4 +7,5 @@ export interface TranscriptionResult {
end: string;
transcription: string;
}[];
actions?: string;
}

View File

@@ -188,8 +188,7 @@ export class AudioMediaManagerService extends Service {
if (!stats || !currentState) {
return;
}
const seekOffset =
currentState.seekOffset + (Date.now() - currentState.updateTime) / 1000;
const seekOffset = currentState.seekOffset;
this.globalMediaState.updatePlaybackState({
state: 'playing',
// rewind to the beginning if the seek offset is greater than the duration
@@ -207,7 +206,9 @@ export class AudioMediaManagerService extends Service {
this.globalMediaState.updatePlaybackState({
state: 'paused',
seekOffset: (Date.now() - state.updateTime) / 1000 + state.seekOffset,
seekOffset:
((Date.now() - state.updateTime) / 1000) * (state.playbackRate || 1.0) +
state.seekOffset,
updateTime: Date.now(),
});
}

View File

@@ -114,6 +114,12 @@ export class MeetingSettingsService extends Service {
);
}
async askForMeetingPermission(type: 'microphone') {
return this.desktopApiService?.handler.recording.askForMeetingPermission(
type
);
}
setRecordingMode = (mode: MeetingSettingsSchema['recordingMode']) => {
const currentMode = this.settings.recordingMode;

View File

@@ -116,4 +116,8 @@ export class DocsQuickSearchSession
setQuery(query: string) {
this.query$.next(query);
}
override dispose(): void {
this.query.unsubscribe();
}
}

View File

@@ -152,9 +152,6 @@ export class SearchMenuService extends Service {
},
{
fields: ['docId', 'title'],
pagination: {
limit: 1,
},
highlights: [
{
field: 'title',

View File

@@ -54,6 +54,6 @@ test('can add text property', async ({ page }) => {
await page.getByTestId('mobile-menu-back-button').last().click();
await expect(page.getByTestId('mobile-menu-back-button')).toContainText(
'How to use folder and Tags'
'Getting Started'
);
});