Skip to content
Open
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: 2 additions & 2 deletions app/actions/remote/post.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ type AuthorsRequest = {
error?: unknown;
}

export async function createPost(serverUrl: string, post: Partial<Post>, files: FileInfo[] = []): Promise<{data?: boolean; error?: unknown}> {
export async function createPost(serverUrl: string, post: Partial<Post>, files: FileInfo[] = []): Promise<{data?: boolean; post?: Post; error?: unknown}> {
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return {error: `${serverUrl} database not found`};
Expand Down Expand Up @@ -204,7 +204,7 @@ export async function createPost(serverUrl: string, post: Partial<Post>, files:

newPost = created;

return {data: true};
return {data: true, post: created};
}

export const retryFailedPost = async (serverUrl: string, post: PostModel) => {
Expand Down
2 changes: 2 additions & 0 deletions app/actions/websocket/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// See LICENSE.txt for license information.

import {handleAgentPostUpdate} from '@agents/actions/websocket';
import {handleAgentsEvents} from '@agents/actions/websocket/events';

import * as bookmark from '@actions/local/channel_bookmark';
import {handleBoRPostRevealedEvent} from '@actions/websocket/burn_on_read';
Expand Down Expand Up @@ -316,4 +317,5 @@ export async function handleWebSocketEvent(serverUrl: string, msg: WebSocketMess
break;
}
handlePlaybookEvents(serverUrl, msg);
handleAgentsEvents(serverUrl, msg);
}
3 changes: 3 additions & 0 deletions app/actions/websocket/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.

import {handleAgentsReconnect} from '@agents/actions/websocket/reconnect';

import {markChannelAsViewed} from '@actions/local/channel';
import {dataRetentionCleanup, expiredBoRPostCleanup} from '@actions/local/systems';
import {markChannelAsRead} from '@actions/remote/channel';
Expand Down Expand Up @@ -90,6 +92,7 @@ async function doReconnect(serverUrl: string, groupLabel?: BaseRequestGroupLabel
const config = await getConfig(database);

handlePlaybookReconnect(serverUrl);
handleAgentsReconnect(serverUrl);

if (isSupportedServerCalls(config?.Version)) {
loadConfigAndCalls(serverUrl, currentUserId, groupLabel);
Expand Down
9 changes: 6 additions & 3 deletions app/components/post_draft/draft_handler/draft_handler.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ type Props = {
updateValue: React.Dispatch<React.SetStateAction<string>>;
value: string;
setIsFocused: (isFocused: boolean) => void;
onPostCreated?: (postId: string) => void;
}

const emptyFileList: FileInfo[] = [];
Expand All @@ -49,6 +50,7 @@ export default function DraftHandler(props: Props) {
updateValue,
value,
setIsFocused,
onPostCreated,
} = props;

const serverUrl = useServerUrl();
Expand All @@ -60,7 +62,7 @@ export default function DraftHandler(props: Props) {
const clearDraft = useCallback(() => {
removeDraft(serverUrl, channelId, rootId);
updateValue('');
}, [serverUrl, channelId, rootId]);
}, [serverUrl, channelId, rootId, updateValue]);

const addFiles = useCallback((newFiles: FileInfo[]) => {
if (!newFiles.length) {
Expand Down Expand Up @@ -93,7 +95,7 @@ export default function DraftHandler(props: Props) {
}

newUploadError(null);
}, [intl, newUploadError, maxFileSize, serverUrl, files?.length, channelId, rootId]);
}, [intl, newUploadError, maxFileSize, serverUrl, files?.length, channelId, rootId, canUploadFiles, maxFileCount]);

// This effect mainly handles keeping clean the uploadErrorHandlers, and
// reinstantiate them on component mount and file retry.
Expand All @@ -115,7 +117,7 @@ export default function DraftHandler(props: Props) {
uploadErrorHandlers.current[file.clientId!] = DraftEditPostUploadManager.registerErrorHandler(file.clientId!, newUploadError);
}
}
}, [files]);
}, [files, newUploadError]);

return (
<SendHandler
Expand All @@ -135,6 +137,7 @@ export default function DraftHandler(props: Props) {
updatePostInputTop={updatePostInputTop}
updateValue={updateValue}
setIsFocused={setIsFocused}
onPostCreated={onPostCreated}
/>
);
}
4 changes: 4 additions & 0 deletions app/components/post_draft/post_draft.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ type Props = {
isChannelScreen: boolean;
canShowPostPriority?: boolean;
location: AvailableScreens;
onPostCreated?: (postId: string) => void;
}

function PostDraft({
Expand All @@ -50,6 +51,7 @@ function PostDraft({
isChannelScreen,
canShowPostPriority,
location,
onPostCreated,
}: Props) {
const [value, setValue] = useState(message);
const [cursorPosition, setCursorPosition] = useState(message.length);
Expand All @@ -64,6 +66,7 @@ function PostDraft({
useEffect(() => {
setValue(message);
setCursorPosition(message.length);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [channelId, rootId]);

const autocompletePosition = AUTOCOMPLETE_ADJUST + kbHeight + postInputTop;
Expand Down Expand Up @@ -105,6 +108,7 @@ function PostDraft({
updateValue={setValue}
value={value}
setIsFocused={setIsFocused}
onPostCreated={onPostCreated}
/>
);

Expand Down
3 changes: 3 additions & 0 deletions app/components/post_draft/send_handler/send_handler.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ type Props = {
channelDisplayName?: string;
isFromDraftView?: boolean;
draftReceiverUserName?: string;
onPostCreated?: (postId: string) => void;
}

export const INITIAL_PRIORITY = {
Expand Down Expand Up @@ -93,6 +94,7 @@ export default function SendHandler({
isFromDraftView,
draftType,
postId,
onPostCreated,
}: Props) {
const serverUrl = useServerUrl();

Expand All @@ -115,6 +117,7 @@ export default function SendHandler({
channelType,
postPriority,
clearDraft,
onPostCreated,
});

if (isFromDraftView) {
Expand Down
1 change: 1 addition & 0 deletions app/constants/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ export const SYSTEM_IDENTIFIERS = {
TEAM_HISTORY: 'teamHistory',
WEBSOCKET: 'WebSocket',
PLAYBOOKS_VERSION: 'playbooks_version',
AGENTS_VERSION: 'agents_version',
LAST_BOR_POST_CLEANUP_RUN: 'lastBoRPostCleanupRun',
};

Expand Down
3 changes: 3 additions & 0 deletions app/constants/screens.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.

import AGENTS_SCREENS from '@agents/constants/screens';

import PLAYBOOKS_SCREENS from '@playbooks/constants/screens';

export const ABOUT = 'About';
Expand Down Expand Up @@ -176,6 +178,7 @@ export default {
THREAD_FOLLOW_BUTTON,
THREAD_OPTIONS,
USER_PROFILE,
...AGENTS_SCREENS,
...PLAYBOOKS_SCREENS,
} as const;

Expand Down
20 changes: 17 additions & 3 deletions app/hooks/handle_send_message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ type Props = {
channelIsArchived?: boolean;
channelIsReadOnly?: boolean;
deactivatedChannel?: boolean;
onPostCreated?: (postId: string) => void;
}

export const useHandleSendMessage = ({
Expand All @@ -75,6 +76,7 @@ export const useHandleSendMessage = ({
channelIsReadOnly,
deactivatedChannel,
clearDraft,
onPostCreated,
}: Props) => {
const intl = useIntl();
const serverUrl = useServerUrl();
Expand Down Expand Up @@ -152,20 +154,32 @@ export const useHandleSendMessage = ({
return;
}

createPost(serverUrl, post, postFiles);
const {post: createdPost} = await createPost(serverUrl, post, postFiles);
clearDraft();

if (createdPost?.id && onPostCreated) {
// Use post ID or root ID for thread navigation
const threadRootId = createdPost.root_id || createdPost.id;
onPostCreated(threadRootId);
}

// Early return to avoid calling DeviceEventEmitter.emit
return;
} else {
// Response error is handled at the post level so don't have to wait to clear draft
createPost(serverUrl, post, postFiles);
const {post: createdPost} = await createPost(serverUrl, post, postFiles);
clearDraft();

if (createdPost?.id && onPostCreated) {
// Use post ID or root ID for thread navigation
const threadRootId = createdPost.root_id || createdPost.id;
onPostCreated(threadRootId);
}
}

setSendingMessage(false);
DeviceEventEmitter.emit(Events.POST_LIST_SCROLL_TO_BOTTOM, rootId ? Screens.THREAD : Screens.CHANNEL);
}, [files, currentUserId, channelId, rootId, value, postPriority, isFromDraftView, serverUrl, intl, canPost, channelIsArchived, channelIsReadOnly, deactivatedChannel, clearDraft]);
}, [files, currentUserId, channelId, rootId, value, postPriority, isFromDraftView, serverUrl, intl, canPost, channelIsArchived, channelIsReadOnly, deactivatedChannel, clearDraft, onPostCreated]);

const showSendToAllOrChannelOrHereAlert = useCallback((calculatedMembersCount: number, atHere: boolean, schedulingInfo?: SchedulingInfo) => {
const notifyAllMessage = DraftUtils.buildChannelWideMentionMessage(intl, calculatedMembersCount, channelTimezoneCount, atHere);
Expand Down
58 changes: 58 additions & 0 deletions app/products/agents/actions/local/version.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.

import {SYSTEM_IDENTIFIERS} from '@constants/database';
import DatabaseManager from '@database/manager';
import {querySystemValue} from '@queries/servers/system';

import {setAgentsVersion} from './version';

import type ServerDataOperator from '@database/operator/server_data_operator';

const serverUrl = 'agents-local-version.test.com';
let operator: ServerDataOperator;

beforeEach(async () => {
await DatabaseManager.init([serverUrl]);
operator = DatabaseManager.serverDatabases[serverUrl]!.operator;
});

afterEach(async () => {
await DatabaseManager.destroyServerDatabase(serverUrl);
});

describe('setAgentsVersion', () => {
it('should handle not found database', async () => {
const {error} = await setAgentsVersion('foo', '1.2.3');
expect(error).toBeTruthy();
});

it('should set agents version successfully', async () => {
const version = '2.0.0';
const {data, error} = await setAgentsVersion(serverUrl, version);
expect(error).toBeUndefined();
expect(data).toBe(true);

const systemValues = await querySystemValue(operator.database, SYSTEM_IDENTIFIERS.AGENTS_VERSION);
expect(systemValues[0].value).toBe(version);
});

it('should handle operator errors', async () => {
const originalHandleSystem = operator.handleSystem;
operator.handleSystem = jest.fn().mockImplementation(() => {
throw new Error('fail');
});
const {error} = await setAgentsVersion(serverUrl, '3.0.0');
expect(error).toBeTruthy();
operator.handleSystem = originalHandleSystem;
});

it('should handle empty version string', async () => {
const {data, error} = await setAgentsVersion(serverUrl, '');
expect(error).toBeUndefined();
expect(data).toBe(true);

const systemValues = await querySystemValue(operator.database, SYSTEM_IDENTIFIERS.AGENTS_VERSION);
expect(systemValues[0].value).toBe('');
});
});
22 changes: 22 additions & 0 deletions app/products/agents/actions/local/version.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.

import {SYSTEM_IDENTIFIERS} from '@constants/database';
import DatabaseManager from '@database/manager';

export const setAgentsVersion = async (serverUrl: string, version: string) => {
try {
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
await operator.handleSystem({
systems: [{
id: SYSTEM_IDENTIFIERS.AGENTS_VERSION,
value: version,
}],
prepareRecordsOnly: false,
});

return {data: true};
} catch (error) {
return {error};
}
};
58 changes: 58 additions & 0 deletions app/products/agents/actions/remote/bots.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.

import NetworkManager from '@managers/network_manager';
import {getFullErrorMessage} from '@utils/errors';
import {logError} from '@utils/log';

import type {LLMBot} from '@agents/types';

export type {LLMBot};

/**
* Fetch all AI bots from the server
* @param serverUrl The server URL
* @returns {bots, searchEnabled, allowUnsafeLinks, error} - Bot configuration on success, error on failure
*/
export async function fetchAIBots(
serverUrl: string,
): Promise<{bots?: LLMBot[]; searchEnabled?: boolean; allowUnsafeLinks?: boolean; error?: unknown}> {
try {
const client = NetworkManager.getClient(serverUrl);
const response = await client.getAIBots();

return {
bots: response.bots,
searchEnabled: response.searchEnabled,
allowUnsafeLinks: response.allowUnsafeLinks,
};
} catch (error) {
logError('[fetchAIBots] Failed to fetch AI bots', error);
return {error: getFullErrorMessage(error)};
}
}

/**
* Get or create a DM channel with a bot
* @param serverUrl The server URL
* @param currentUserId The current user's ID
* @param botUserId The bot's user ID
* @returns {channelId, error} - DM channel ID on success, error on failure
*/
export async function getBotDirectChannel(
serverUrl: string,
currentUserId: string,
botUserId: string,
): Promise<{channelId?: string; error?: unknown}> {
try {
const client = NetworkManager.getClient(serverUrl);

// Create or get existing DM channel
const channel = await client.createDirectChannel([currentUserId, botUserId]);

return {channelId: channel.id};
} catch (error) {
logError('[getBotDirectChannel] Failed to get bot direct channel', error);
return {error: getFullErrorMessage(error)};
}
}
Loading