From 24c80b5830ac30ad159254c031f0188d9ab65ba8 Mon Sep 17 00:00:00 2001
From: Devin Binnie <52460000+devinbinnie@users.noreply.github.com>
Date: Fri, 5 Dec 2025 11:48:37 -0500
Subject: [PATCH 001/416] [MM-66781] Use loadStatuses to ensure statuses are
loaded completely and properly (#34638)
* [MM-66781] Use loadStatuses to ensure statuses are loaded completely and properly
* New approach
* PR feedback
---
.../popout_controller.test.tsx | 43 +++--
.../popout_controller/popout_controller.tsx | 8 +-
.../thread_popout/thread_popout.tsx | 22 +++
.../src/actions/status_profile_polling.ts | 155 +++++++++++-------
4 files changed, 156 insertions(+), 72 deletions(-)
diff --git a/webapp/channels/src/components/popout_controller/popout_controller.test.tsx b/webapp/channels/src/components/popout_controller/popout_controller.test.tsx
index 1aaf237e8bd..bbcc0caac89 100644
--- a/webapp/channels/src/components/popout_controller/popout_controller.test.tsx
+++ b/webapp/channels/src/components/popout_controller/popout_controller.test.tsx
@@ -1,21 +1,29 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
-import {screen} from '@testing-library/react';
import React from 'react';
import {MemoryRouter} from 'react-router-dom';
import type {RouteComponentProps} from 'react-router-dom';
-import {getProfiles, getStatusesByIds} from 'mattermost-redux/actions/users';
+import type {UserProfile} from '@mattermost/types/users';
-import {renderWithContext} from 'tests/react_testing_utils';
+import {getMe} from 'mattermost-redux/actions/users';
+import type {ActionResult} from 'mattermost-redux/types/actions';
+
+import {loadStatusesByIds} from 'actions/status_actions';
+
+import {renderWithContext, screen} from 'tests/react_testing_utils';
+import {TestHelper} from 'utils/test_helper';
import PopoutController from './popout_controller';
// Mock dependencies
jest.mock('mattermost-redux/actions/users', () => ({
- getProfiles: jest.fn().mockReturnValue(() => ({type: 'GET_PROFILES'})),
- getStatusesByIds: jest.fn().mockReturnValue(() => ({type: 'GET_STATUSES_BY_IDS'})),
+ getMe: jest.fn(),
+}));
+
+jest.mock('actions/status_actions', () => ({
+ loadStatusesByIds: jest.fn(),
}));
jest.mock('components/modal_controller', () => ({
@@ -37,8 +45,8 @@ jest.mock('components/logged_in', () => ({
default: ({children}: {children: React.ReactNode}) =>
{children}
,
}));
-const mockGetProfiles = getProfiles as jest.MockedFunction;
-const mockGetStatusesByIds = getStatusesByIds as jest.MockedFunction;
+const mockGetMe = getMe as jest.MockedFunction;
+const mockLoadStatusesByIds = loadStatusesByIds as jest.MockedFunction;
// Base mock route props with meaningful route data
const baseRouteProps: RouteComponentProps = {
@@ -73,6 +81,17 @@ describe('PopoutController', () => {
// Reset document.body classes
document.body.className = '';
+
+ // Setup default mock for getMe to return a promise that resolves
+ const mockUser = TestHelper.getUserMock({id: 'user-123', username: 'testuser'});
+ mockGetMe.mockReturnValue(() => Promise.resolve({
+ data: mockUser,
+ } as ActionResult));
+
+ // Setup default mocks for status actions
+ mockLoadStatusesByIds.mockReturnValue(() => ({
+ data: true,
+ } as ActionResult));
});
it('should render modal controller', () => {
@@ -92,12 +111,12 @@ describe('PopoutController', () => {
expect(document.body.classList.contains('popout')).toBe(true);
});
- it('should dispatch getProfiles action on mount', () => {
+ it('should dispatch getMe action on mount', () => {
renderWithContext(
,
);
- expect(mockGetProfiles).toHaveBeenCalledTimes(1);
+ expect(mockGetMe).toHaveBeenCalledTimes(1);
});
it('should render thread popout for thread route', () => {
@@ -140,7 +159,7 @@ describe('PopoutController', () => {
expect(document.body.classList.contains('popout')).toBe(true);
});
- it('should dispatch getStatusesByIds with current user ID', () => {
+ it('should dispatch loadStatusesByIds with current user ID', () => {
const currentUserId = 'current-user-id-123';
const initialState = {
entities: {
@@ -155,7 +174,7 @@ describe('PopoutController', () => {
initialState,
);
- expect(mockGetStatusesByIds).toHaveBeenCalledTimes(1);
- expect(mockGetStatusesByIds).toHaveBeenCalledWith([currentUserId]);
+ expect(mockLoadStatusesByIds).toHaveBeenCalledTimes(1);
+ expect(mockLoadStatusesByIds).toHaveBeenCalledWith([currentUserId]);
});
});
diff --git a/webapp/channels/src/components/popout_controller/popout_controller.tsx b/webapp/channels/src/components/popout_controller/popout_controller.tsx
index f2492a7df52..6e09ad49ca8 100644
--- a/webapp/channels/src/components/popout_controller/popout_controller.tsx
+++ b/webapp/channels/src/components/popout_controller/popout_controller.tsx
@@ -6,9 +6,11 @@ import {useDispatch, useSelector} from 'react-redux';
import {Route, Switch} from 'react-router-dom';
import type {RouteComponentProps} from 'react-router-dom';
-import {getProfiles, getStatusesByIds} from 'mattermost-redux/actions/users';
+import {getMe} from 'mattermost-redux/actions/users';
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
+import {loadStatusesByIds} from 'actions/status_actions';
+
import LoggedIn from 'components/logged_in';
import ModalController from 'components/modal_controller';
import ThreadPopout from 'components/thread_popout';
@@ -24,12 +26,12 @@ const PopoutController: React.FC = (routeProps) => {
useBrowserPopout();
useEffect(() => {
document.body.classList.add('app__body', 'popout');
- dispatch(getProfiles());
+ dispatch(getMe());
}, []);
useEffect(() => {
if (currentUserId) {
- dispatch(getStatusesByIds([currentUserId]));
+ dispatch(loadStatusesByIds([currentUserId]));
}
}, [dispatch, currentUserId]);
diff --git a/webapp/channels/src/components/thread_popout/thread_popout.tsx b/webapp/channels/src/components/thread_popout/thread_popout.tsx
index d9e0b80d22a..6f5c1bf7517 100644
--- a/webapp/channels/src/components/thread_popout/thread_popout.tsx
+++ b/webapp/channels/src/components/thread_popout/thread_popout.tsx
@@ -6,12 +6,16 @@ import {useDispatch, useSelector} from 'react-redux';
import {useParams} from 'react-router-dom';
import {fetchChannelsAndMembers, selectChannel} from 'mattermost-redux/actions/channels';
+import {getPostThread} from 'mattermost-redux/actions/posts';
+import {extractUserIdsAndMentionsFromPosts} from 'mattermost-redux/actions/status_profile_polling';
import {selectTeam} from 'mattermost-redux/actions/teams';
import {getThread} from 'mattermost-redux/actions/threads';
+import {getProfilesByIds} from 'mattermost-redux/actions/users';
import {getTeamByName} from 'mattermost-redux/selectors/entities/teams';
import {makeGetThreadOrSynthetic} from 'mattermost-redux/selectors/entities/threads';
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
+import {loadStatusesByIds} from 'actions/status_actions';
import {markThreadAsRead} from 'actions/views/threads';
import {usePost} from 'components/common/hooks/usePost';
@@ -55,6 +59,24 @@ export default function ThreadPopout() {
useEffect(() => {
if (teamId) {
dispatch(getThread(currentUserId, teamId, postId));
+
+ // Since the statuses are fetched properly and timely by the thread viewer, manually fetch them here
+ async function fetchPostThread() {
+ const {data: posts} = await dispatch(getPostThread(postId, true));
+ if (posts) {
+ const {data: result} = await dispatch(extractUserIdsAndMentionsFromPosts(Array.from(Object.values(posts.posts))));
+ if (result) {
+ if (result.userIdsForProfilePoll.length > 0) {
+ await dispatch(getProfilesByIds(result.userIdsForProfilePoll));
+ }
+ if (result.userIdsForStatusPoll.length > 0) {
+ await dispatch(loadStatusesByIds(result.userIdsForStatusPoll));
+ }
+ }
+ }
+ }
+
+ fetchPostThread();
}
}, [postId, dispatch, currentUserId, teamId]);
diff --git a/webapp/channels/src/packages/mattermost-redux/src/actions/status_profile_polling.ts b/webapp/channels/src/packages/mattermost-redux/src/actions/status_profile_polling.ts
index c44dc1939d1..660ad4eca14 100644
--- a/webapp/channels/src/packages/mattermost-redux/src/actions/status_profile_polling.ts
+++ b/webapp/channels/src/packages/mattermost-redux/src/actions/status_profile_polling.ts
@@ -87,6 +87,92 @@ export function cleanUpStatusAndProfileFetchingPoll(): ThunkActionFunc {
};
}
+interface UserIdsAndMentions {
+ userIdsForProfilePoll: Array;
+ userIdsForStatusPoll: Array;
+ mentionedUsernamesAndGroups: string[];
+}
+
+export function extractUserIdsAndMentionsFromPosts(posts: Post[]): ActionFunc {
+ return (dispatch, getState) => {
+ if (posts.length === 0) {
+ return {data: {
+ userIdsForProfilePoll: [],
+ userIdsForStatusPoll: [],
+ mentionedUsernamesAndGroups: [],
+ }};
+ }
+
+ const userIdsForProfilePoll = new Set();
+ const userIdsForStatusPoll = new Set();
+ const mentionedUsernamesAndGroupsInPosts = new Set();
+
+ const state = getState();
+ const currentUser = getCurrentUser(state);
+ const currentUserId = getCurrentUserId(state);
+ const isUserStatusesConfigEnabled = getIsUserStatusesConfigEnabled(state);
+ const users = getUsers(state);
+ const userStatuses = getUserStatuses(state);
+
+ posts.forEach((post) => {
+ if (post.metadata) {
+ // Add users listed in permalink previews
+ if (post.metadata.embeds) {
+ post.metadata.embeds.forEach((embed: PostEmbed) => {
+ if (embed.type === 'permalink' && embed.data) {
+ const permalinkPostPreviewMetaData = embed.data as PostPreviewMetadata;
+
+ if (permalinkPostPreviewMetaData.post?.user_id && !users[permalinkPostPreviewMetaData.post.user_id] && permalinkPostPreviewMetaData.post.user_id !== currentUserId) {
+ userIdsForProfilePoll.add(permalinkPostPreviewMetaData.post.user_id);
+ }
+ if (permalinkPostPreviewMetaData.post?.user_id && !userStatuses[permalinkPostPreviewMetaData.post.user_id] && permalinkPostPreviewMetaData.post.user_id !== currentUserId && isUserStatusesConfigEnabled) {
+ userIdsForStatusPoll.add(permalinkPostPreviewMetaData.post.user_id);
+ }
+ }
+ });
+ }
+
+ // Add users listed in the Post Acknowledgement feature
+ if (post.metadata.acknowledgements) {
+ post.metadata.acknowledgements.forEach((ack: PostAcknowledgement) => {
+ if (ack.acknowledged_at > 0 && ack.user_id && !users[ack.user_id] && ack.user_id !== currentUserId) {
+ userIdsForProfilePoll.add(ack.user_id);
+ }
+ });
+ }
+ }
+
+ // This is sufficient to check if the profile is already fetched
+ // as we receive the websocket events for the profiles changes
+ if (!users[post.user_id] && post.user_id !== currentUserId) {
+ userIdsForProfilePoll.add(post.user_id);
+ }
+
+ // This is sufficient to check if the status is already fetched
+ // as we do the polling for statuses for current channel's channel members every 1 minute in channel_controller
+ if (!userStatuses[post.user_id] && post.user_id !== currentUserId && isUserStatusesConfigEnabled) {
+ userIdsForStatusPoll.add(post.user_id);
+ }
+
+ // We need to check for all @mentions in the post, they can be either users or groups
+ const mentioned = getNeededAtMentionedUsernamesAndGroups(state, [post]);
+ if (mentioned.size > 0) {
+ mentioned.forEach((atMention) => {
+ if (atMention !== currentUser.username) {
+ mentionedUsernamesAndGroupsInPosts.add(atMention);
+ }
+ });
+ }
+ });
+
+ return {data: {
+ userIdsForProfilePoll: Array.from(userIdsForProfilePoll),
+ userIdsForStatusPoll: Array.from(userIdsForStatusPoll),
+ mentionedUsernamesAndGroups: Array.from(mentionedUsernamesAndGroupsInPosts),
+ }};
+ };
+}
+
/**
* Gets in batch the user profiles, user statuses and user groups for the users in the posts list
* This action however doesn't refetch the profiles and statuses except for groups if they are already fetched once
@@ -110,68 +196,23 @@ export function batchFetchStatusesProfilesGroupsFromPosts(postsArrayOrMap: Post[
return {data: false};
}
- const mentionedUsernamesAndGroupsInPosts = new Set();
-
const state = getState();
- const currentUser = getCurrentUser(state);
- const currentUserId = getCurrentUserId(state);
- const isUserStatusesConfigEnabled = getIsUserStatusesConfigEnabled(state);
- const users = getUsers(state);
- const userStatuses = getUserStatuses(state);
+ const {data: result} = dispatch(extractUserIdsAndMentionsFromPosts(posts));
- posts.forEach((post) => {
- if (post.metadata) {
- // Add users listed in permalink previews
- if (post.metadata.embeds) {
- post.metadata.embeds.forEach((embed: PostEmbed) => {
- if (embed.type === 'permalink' && embed.data) {
- const permalinkPostPreviewMetaData = embed.data as PostPreviewMetadata;
+ if (!result) {
+ return {data: false};
+ }
- if (permalinkPostPreviewMetaData.post?.user_id && !users[permalinkPostPreviewMetaData.post.user_id] && permalinkPostPreviewMetaData.post.user_id !== currentUserId) {
- dispatch(addUserIdsForProfileFetchingPoll([permalinkPostPreviewMetaData.post.user_id]));
- }
- if (permalinkPostPreviewMetaData.post?.user_id && !userStatuses[permalinkPostPreviewMetaData.post.user_id] && permalinkPostPreviewMetaData.post.user_id !== currentUserId && isUserStatusesConfigEnabled) {
- dispatch(addUserIdsForStatusFetchingPoll([permalinkPostPreviewMetaData.post.user_id]));
- }
- }
- });
- }
+ if (result.userIdsForProfilePoll.length > 0) {
+ dispatch(addUserIdsForProfileFetchingPoll(result.userIdsForProfilePoll));
+ }
- // Add users listed in the Post Acknowledgement feature
- if (post.metadata.acknowledgements) {
- post.metadata.acknowledgements.forEach((ack: PostAcknowledgement) => {
- if (ack.acknowledged_at > 0 && ack.user_id && !users[ack.user_id] && ack.user_id !== currentUserId) {
- dispatch(addUserIdsForProfileFetchingPoll([ack.user_id]));
- }
- });
- }
- }
+ if (result.userIdsForStatusPoll.length > 0) {
+ dispatch(addUserIdsForStatusFetchingPoll(result.userIdsForStatusPoll));
+ }
- // This is sufficient to check if the profile is already fetched
- // as we receive the websocket events for the profiles changes
- if (!users[post.user_id] && post.user_id !== currentUserId) {
- dispatch(addUserIdsForProfileFetchingPoll([post.user_id]));
- }
-
- // This is sufficient to check if the status is already fetched
- // as we do the polling for statuses for current channel's channel members every 1 minute in channel_controller
- if (!userStatuses[post.user_id] && post.user_id !== currentUserId && isUserStatusesConfigEnabled) {
- dispatch(addUserIdsForStatusFetchingPoll([post.user_id]));
- }
-
- // We need to check for all @mentions in the post, they can be either users or groups
- const mentioned = getNeededAtMentionedUsernamesAndGroups(state, [post]);
- if (mentioned.size > 0) {
- mentioned.forEach((atMention) => {
- if (atMention !== currentUser.username) {
- mentionedUsernamesAndGroupsInPosts.add(atMention);
- }
- });
- }
- });
-
- if (mentionedUsernamesAndGroupsInPosts.size > 0) {
- dispatch(getUsersFromMentionedUsernamesAndGroups(Array.from(mentionedUsernamesAndGroupsInPosts), getLicense(state).IsLicensed === 'true'));
+ if (result.mentionedUsernamesAndGroups.length > 0) {
+ dispatch(getUsersFromMentionedUsernamesAndGroups(result.mentionedUsernamesAndGroups, getLicense(state).IsLicensed === 'true'));
}
return {data: true};
From 6e87c94f29775ea85ab48b1fc22e2ec1603c13ba Mon Sep 17 00:00:00 2001
From: Ben Schumacher
Date: Mon, 8 Dec 2025 10:54:33 +0100
Subject: [PATCH 002/416] [MM-62770] Validate log levels in AdvancedLoggingJSON
(#34414)
---
server/public/model/config.go | 4 +-
server/public/model/config_test.go | 221 +++++++++++++++---
server/public/shared/mlog/levels.go | 65 +++++-
server/public/shared/mlog/mlog.go | 16 +-
.../admin_console/admin_definition.tsx | 3 +-
webapp/channels/src/i18n/en.json | 1 +
6 files changed, 268 insertions(+), 42 deletions(-)
diff --git a/server/public/model/config.go b/server/public/model/config.go
index 0e09a7c7c46..34c7b68c3ff 100644
--- a/server/public/model/config.go
+++ b/server/public/model/config.go
@@ -1494,7 +1494,7 @@ func (s *LogSettings) isValid() *AppError {
return NewAppError("LogSettings.isValid", "model.config.is_valid.log.advanced_logging.json", map[string]any{"Error": err}, "", http.StatusBadRequest).Wrap(err)
}
- err = cfg.IsValid()
+ err = cfg.IsValid(mlog.MlvlAll)
if err != nil {
return NewAppError("LogSettings.isValid", "model.config.is_valid.log.advanced_logging.parse", map[string]any{"Error": err}, "", http.StatusBadRequest).Wrap(err)
}
@@ -1610,7 +1610,7 @@ func (s *ExperimentalAuditSettings) isValid() *AppError {
return NewAppError("ExperimentalAuditSettings.isValid", "model.config.is_valid.log.advanced_logging.json", map[string]any{"Error": err}, "", http.StatusBadRequest).Wrap(err)
}
- err = cfg.IsValid()
+ err = cfg.IsValid(mlog.MLvlAuditAll)
if err != nil {
return NewAppError("ExperimentalAuditSettings.isValid", "model.config.is_valid.log.advanced_logging.parse", map[string]any{"Error": err}, "", http.StatusBadRequest).Wrap(err)
}
diff --git a/server/public/model/config_test.go b/server/public/model/config_test.go
index dec42d178c5..677b2d6c809 100644
--- a/server/public/model/config_test.go
+++ b/server/public/model/config_test.go
@@ -1449,21 +1449,21 @@ func TestLogSettingsIsValid(t *testing.T) {
AdvancedLoggingJSON: json.RawMessage(`
{
"console-log": {
- "Type": "XYZ",
- "Format": "json",
- "Levels": [
- {"ID": 10, "Name": "stdlog", "Stacktrace": false},
- {"ID": 5, "Name": "debug", "Stacktrace": false},
- {"ID": 4, "Name": "info", "Stacktrace": false, "color": 36},
- {"ID": 3, "Name": "warn", "Stacktrace": false, "color": 33},
- {"ID": 2, "Name": "error", "Stacktrace": true, "color": 31},
- {"ID": 1, "Name": "fatal", "Stacktrace": true},
- {"ID": 0, "Name": "panic", "Stacktrace": true}
- ],
- "Options": {
- "Out": "stdout"
- },
- "MaxQueueSize": 1000
+ "Type": "XYZ",
+ "Format": "json",
+ "Levels": [
+ {"ID": 10, "Name": "stdlog", "Stacktrace": false},
+ {"ID": 5, "Name": "debug", "Stacktrace": false},
+ {"ID": 4, "Name": "info", "Stacktrace": false, "color": 36},
+ {"ID": 3, "Name": "warn", "Stacktrace": false, "color": 33},
+ {"ID": 2, "Name": "error", "Stacktrace": true, "color": 31},
+ {"ID": 1, "Name": "fatal", "Stacktrace": true},
+ {"ID": 0, "Name": "panic", "Stacktrace": true}
+ ],
+ "Options": {
+ "Out": "stdout"
+ },
+ "MaxQueueSize": 1000
}
}
`),
@@ -1475,18 +1475,85 @@ func TestLogSettingsIsValid(t *testing.T) {
AdvancedLoggingJSON: json.RawMessage(`
{
"console-log": {
- "Type": "console",
- "Format": "json",
- "Levels": [
- {"ID": 5, "Name": "debug", "Stacktrace": false},
- {"ID": 4, "Name": "info", "Stacktrace": false, "color": 36},
- {"ID": 3, "Name": "warn", "Stacktrace": false, "color": 33},
- {"ID": 2, "Name": "error", "Stacktrace": true, "color": 31}
- ],
- "Options": {
- "Out": "stdout"
- },
- "MaxQueueSize": 1000
+ "Type": "console",
+ "Format": "json",
+ "Levels": [
+ {"ID": 5, "Name": "debug", "Stacktrace": false},
+ {"ID": 4, "Name": "info", "Stacktrace": false, "color": 36},
+ {"ID": 3, "Name": "warn", "Stacktrace": false, "color": 33},
+ {"ID": 2, "Name": "error", "Stacktrace": true, "color": 31}
+ ],
+ "Options": {
+ "Out": "stdout"
+ },
+ "MaxQueueSize": 1000
+ }
+ }
+ `),
+ },
+ ExpectError: false,
+ },
+ "AdvancedLoggingJSON with invalid log level": {
+ LogSettings: LogSettings{
+ AdvancedLoggingJSON: json.RawMessage(`
+ {
+ "console-log": {
+ "Type": "console",
+ "Format": "json",
+ "Levels": [
+ {"ID": 999, "Name": "info", "Stacktrace": false}
+ ],
+ "Options": {
+ "Out": "stdout"
+ },
+ "MaxQueueSize": 1000
+ }
+ }
+ `),
+ },
+ ExpectError: true,
+ },
+ "AdvancedLoggingJSON with audit log level": {
+ LogSettings: LogSettings{
+ AdvancedLoggingJSON: json.RawMessage(`
+ {
+ "console-log": {
+ "Type": "console",
+ "Format": "json",
+ "Levels": [
+ { "id": 100, "name": "audit-api" },
+ { "id": 101, "name": "audit-content" },
+ { "id": 102, "name": "audit-permissions" },
+ { "id": 103, "name": "audit-cli" }
+ ],
+ "Options": {
+ "Out": "stdout"
+ },
+ "MaxQueueSize": 1000
+ }
+ }
+ `),
+ },
+ ExpectError: true,
+ },
+ "AdvancedLoggingJSON with custom log levels": {
+ LogSettings: LogSettings{
+ AdvancedLoggingJSON: json.RawMessage(`
+ {
+ "audit-log": {
+ "Type": "console",
+ "Format": "json",
+ "Levels": [
+ {"ID": 140, "Name": "LDAPError", "Stacktrace": false},
+ {"ID": 141, "Name": "LDAPWarn", "Stacktrace": false},
+ {"ID": 142, "Name": "LDAPInfo", "Stacktrace": false},
+ {"ID": 143, "Name": "LDAPDebug", "Stacktrace": false},
+ {"ID": 144, "Name": "LDAPTrace", "Stacktrace": false}
+ ],
+ "Options": {
+ "Out": "stdout"
+ },
+ "MaxQueueSize": 1000
}
}
`),
@@ -2281,6 +2348,106 @@ func TestExperimentalAuditSettingsIsValid(t *testing.T) {
},
ExpectError: true,
},
+ "AdvancedLoggingJSON has missing target": {
+ ExperimentalAuditSettings: ExperimentalAuditSettings{
+ AdvancedLoggingJSON: json.RawMessage(`
+ {
+ "foo": "bar",
+ }
+ `),
+ },
+ ExpectError: true,
+ },
+ "AdvancedLoggingJSON has an unknown Type": {
+ ExperimentalAuditSettings: ExperimentalAuditSettings{
+ AdvancedLoggingJSON: json.RawMessage(`
+ {
+ "console-log": {
+ "Type": "XYZ",
+ "Format": "json",
+ "Levels": [
+ { "id": 100, "name": "audit-api" },
+ { "id": 101, "name": "audit-content" },
+ { "id": 102, "name": "audit-permissions" },
+ { "id": 103, "name": "audit-cli" }
+ ],
+ "Options": {
+ "Out": "stdout"
+ },
+ "MaxQueueSize": 1000
+ }
+ }
+ `),
+ },
+ ExpectError: true,
+ },
+ "AdvancedLoggingJSON is valid": {
+ ExperimentalAuditSettings: ExperimentalAuditSettings{
+ AdvancedLoggingJSON: json.RawMessage(`
+ {
+ "console-log": {
+ "Type": "console",
+ "Format": "json",
+ "Levels": [
+ { "id": 100, "name": "audit-api" },
+ { "id": 101, "name": "audit-content" },
+ { "id": 102, "name": "audit-permissions" },
+ { "id": 103, "name": "audit-cli" }
+ ],
+ "Options": {
+ "Out": "stdout"
+ },
+ "MaxQueueSize": 1000
+ }
+ }
+ `),
+ },
+ ExpectError: false,
+ },
+
+ "AdvancedLoggingJSON with standard log levels": {
+ ExperimentalAuditSettings: ExperimentalAuditSettings{
+ AdvancedLoggingJSON: json.RawMessage(`
+ {
+ "console-log": {
+ "Type": "console",
+ "Format": "json",
+ "Levels": [
+ {"ID": 5, "Name": "debug", "Stacktrace": false},
+ {"ID": 4, "Name": "info", "Stacktrace": false, "color": 36},
+ {"ID": 3, "Name": "warn", "Stacktrace": false, "color": 33},
+ {"ID": 2, "Name": "error", "Stacktrace": true, "color": 31}
+ ],
+ "Options": {
+ "Out": "stdout"
+ },
+ "MaxQueueSize": 1000
+ }
+ }
+ `),
+ },
+ ExpectError: true,
+ },
+ "AdvancedLoggingJSON with unknown log level": {
+ ExperimentalAuditSettings: ExperimentalAuditSettings{
+ AdvancedLoggingJSON: json.RawMessage(`
+ {
+ "audit-log": {
+ "Type": "console",
+ "Format": "json",
+ "Levels": [
+ {"ID": 999, "Name": "info", "Stacktrace": false}
+ ],
+ "Options": {
+ "Out": "stdout"
+ },
+ "MaxQueueSize": 1000
+ }
+ }
+ `),
+ },
+ ExpectError: true,
+ },
} {
t.Run(name, func(t *testing.T) {
test.ExperimentalAuditSettings.SetDefaults()
diff --git a/server/public/shared/mlog/levels.go b/server/public/shared/mlog/levels.go
index 906109f52f3..f898c49dc90 100644
--- a/server/public/shared/mlog/levels.go
+++ b/server/public/shared/mlog/levels.go
@@ -3,7 +3,11 @@
package mlog
-import "github.com/mattermost/logr/v2"
+import (
+ "slices"
+
+ "github.com/mattermost/logr/v2"
+)
// Standard levels.
var (
@@ -14,20 +18,22 @@ var (
LvlInfo = logr.Info // ID = 4
LvlDebug = logr.Debug // ID = 5
LvlTrace = logr.Trace // ID = 6
- StdAll = []Level{LvlPanic, LvlFatal, LvlError, LvlWarn, LvlInfo, LvlDebug, LvlTrace, LvlStdLog}
// non-standard "critical" level
LvlCritical = Level{ID: 7, Name: "critical"}
// used by redirected standard logger
LvlStdLog = Level{ID: 10, Name: "stdlog"}
// used only by the logger
LvlLogError = Level{ID: 11, Name: "logerror", Stacktrace: true}
+
+ StdAll = []Level{LvlPanic, LvlFatal, LvlError, LvlWarn, LvlInfo, LvlDebug, LvlTrace, LvlStdLog}
)
// Register custom (discrete) levels here.
// !!!!! Custom ID's must be between 20 and 32,768 !!!!!!
+
var (
- // Audit system levels.
- //
+ /* Audit system */
+
// LvlAuditAPI is used for auditing REST API endpoint access. This is the most commonly used
// audit level and should be applied whenever a REST API endpoint is accessed. It provides
// a record of API usage patterns and access.
@@ -52,39 +58,65 @@ var (
// LvlAuditCLI is intended for auditing command-line interface operations. This level was
// originally designed for the legacy CLI. It's mostly unused now.
- LvlAuditCLI = Level{ID: 103, Name: "audit-cli"}
+ LvlAuditCLI = Level{ID: 103, Name: "audit-cli"}
+ MLvlAuditAll = []Level{
+ LvlAuditAPI,
+ LvlAuditContent,
+ LvlAuditPerms,
+ LvlAuditCLI,
+ }
- // used by Remote Cluster Service
LvlRemoteClusterServiceDebug = Level{ID: 130, Name: "RemoteClusterServiceDebug"}
LvlRemoteClusterServiceError = Level{ID: 131, Name: "RemoteClusterServiceError"}
LvlRemoteClusterServiceWarn = Level{ID: 132, Name: "RemoteClusterServiceWarn"}
+ MLvlRemoteClusterServiceAll = []Level{
+ LvlRemoteClusterServiceDebug,
+ LvlRemoteClusterServiceError,
+ LvlRemoteClusterServiceWarn,
+ }
+
+ /* LDAP sync job */
- // used by LDAP sync job
LvlLDAPError = Level{ID: 140, Name: "LDAPError"}
LvlLDAPWarn = Level{ID: 141, Name: "LDAPWarn"}
LvlLDAPInfo = Level{ID: 142, Name: "LDAPInfo"}
LvlLDAPDebug = Level{ID: 143, Name: "LDAPDebug"}
LvlLDAPTrace = Level{ID: 144, Name: "LDAPTrace"}
+ MLvlLDAPAll = []Level{LvlLDAPError, LvlLDAPWarn, LvlLDAPInfo, LvlLDAPDebug, LvlLDAPTrace}
+
+ /* Shared Channel Sync Service */
- // used by Shared Channel Sync Service
LvlSharedChannelServiceDebug = Level{ID: 200, Name: "SharedChannelServiceDebug"}
LvlSharedChannelServiceError = Level{ID: 201, Name: "SharedChannelServiceError"}
LvlSharedChannelServiceWarn = Level{ID: 202, Name: "SharedChannelServiceWarn"}
LvlSharedChannelServiceMessagesInbound = Level{ID: 203, Name: "SharedChannelServiceMsgInbound"}
LvlSharedChannelServiceMessagesOutbound = Level{ID: 204, Name: "SharedChannelServiceMsgOutbound"}
+ MlvlSharedChannelServiceAll = []Level{
+ LvlSharedChannelServiceDebug,
+ LvlSharedChannelServiceError,
+ LvlSharedChannelServiceWarn,
+ LvlSharedChannelServiceMessagesInbound,
+ LvlSharedChannelServiceMessagesOutbound,
+ }
+
+ /* Notification Service */
- // used by Notification Service
LvlNotificationError = Level{ID: 300, Name: "NotificationError"}
LvlNotificationWarn = Level{ID: 301, Name: "NotificationWarn"}
LvlNotificationInfo = Level{ID: 302, Name: "NotificationInfo"}
LvlNotificationDebug = Level{ID: 303, Name: "NotificationDebug"}
LvlNotificationTrace = Level{ID: 304, Name: "NotificationTrace"}
+ MlvlNotificationAll = []Level{
+ LvlNotificationError,
+ LvlNotificationWarn,
+ LvlNotificationInfo,
+ LvlNotificationDebug,
+ LvlNotificationTrace,
+ }
)
// Combinations for LogM (log multi).
var (
- MLvlAuditAll = []Level{LvlAuditAPI, LvlAuditContent, LvlAuditPerms, LvlAuditCLI}
-
MlvlLDAPError = []Level{LvlError, LvlLDAPError}
MlvlLDAPWarn = []Level{LvlWarn, LvlLDAPWarn}
MlvlLDAPInfo = []Level{LvlInfo, LvlLDAPInfo}
@@ -96,3 +128,14 @@ var (
MlvlNotificationDebug = []Level{LvlDebug, LvlNotificationDebug}
MlvlNotificationTrace = []Level{LvlTrace, LvlNotificationTrace}
)
+
+var (
+ // MlvlAll contains all log levels except the audit ones
+ MlvlAll = slices.Concat(
+ StdAll,
+ MLvlRemoteClusterServiceAll,
+ MLvlLDAPAll,
+ MlvlSharedChannelServiceAll,
+ MlvlNotificationAll,
+ )
+)
diff --git a/server/public/shared/mlog/mlog.go b/server/public/shared/mlog/mlog.go
index 6c10a60b876..0d9b7a4e257 100644
--- a/server/public/shared/mlog/mlog.go
+++ b/server/public/shared/mlog/mlog.go
@@ -12,6 +12,7 @@ import (
"log"
"maps"
"os"
+ "slices"
"strings"
"sync/atomic"
"time"
@@ -69,7 +70,7 @@ func (lc LoggerConfiguration) Append(cfg LoggerConfiguration) {
maps.Copy(lc, cfg)
}
-func (lc LoggerConfiguration) IsValid() error {
+func (lc LoggerConfiguration) IsValid(validLevels []Level) error {
logger, err := logr.New()
if err != nil {
return errors.Wrap(err, "failed to create logger")
@@ -81,6 +82,19 @@ func (lc LoggerConfiguration) IsValid() error {
return errors.Wrap(err, "logger configuration is invalid")
}
+ validLevelIDs := make([]logr.LevelID, 0, len(validLevels))
+ for _, l := range validLevels {
+ validLevelIDs = append(validLevelIDs, l.ID)
+ }
+
+ for _, c := range lc {
+ for _, l := range c.Levels {
+ if !slices.Contains(validLevelIDs, l.ID) {
+ return errors.Errorf("invalid log level id %d", l.ID)
+ }
+ }
+ }
+
return nil
}
diff --git a/webapp/channels/src/components/admin_console/admin_definition.tsx b/webapp/channels/src/components/admin_console/admin_definition.tsx
index efbe5db720a..7cc0986b495 100644
--- a/webapp/channels/src/components/admin_console/admin_definition.tsx
+++ b/webapp/channels/src/components/admin_console/admin_definition.tsx
@@ -5664,7 +5664,8 @@ const AdminDefinition: AdminDefinitionType = {
),
schema: {
id: 'ExperimentalAuditSettings',
- name: 'Audit logging (Beta)',
+ isBeta: true,
+ name: defineMessage({id: 'admin.auditlogging.title', defaultMessage: 'Audit Logging'}),
settings: [
{
type: 'banner',
diff --git a/webapp/channels/src/i18n/en.json b/webapp/channels/src/i18n/en.json
index 01c96651674..64fbbd19466 100644
--- a/webapp/channels/src/i18n/en.json
+++ b/webapp/channels/src/i18n/en.json
@@ -384,6 +384,7 @@
"admin.audit_logging_experimental.file_max_size.title": "Max File Size (MB)",
"admin.audit_logging_experimental.file_name.help_text": "The name of the file to write to.",
"admin.audit_logging_experimental.file_name.title": "File Name",
+ "admin.auditlogging.title": "Audit Logging",
"admin.audits.reload": "Reload User Activity Logs",
"admin.authentication.email": "Email Authentication",
"admin.authentication.gitlab": "GitLab",
From 933751da8282f5ebd507c6b33be1ece3c4fc3859 Mon Sep 17 00:00:00 2001
From: "unified-ci-app[bot]"
<121569378+unified-ci-app[bot]@users.noreply.github.com>
Date: Mon, 8 Dec 2025 12:54:17 +0200
Subject: [PATCH 003/416] chore: Update NOTICE.txt file with updated
dependencies (#34675)
Automatic Merge
---
NOTICE.txt | 36 ------------------------------------
1 file changed, 36 deletions(-)
diff --git a/NOTICE.txt b/NOTICE.txt
index 7a57a00613d..fed2bfd9651 100644
--- a/NOTICE.txt
+++ b/NOTICE.txt
@@ -6819,42 +6819,6 @@ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
----
-
-## html-to-markdown
-
-This product contains 'html-to-markdown' by Johannes Kaufmann.
-
-A robust html-to-markdown converter that transforms HTML into clean, readable Markdown.
-
-* HOMEPAGE:
- * https://github.com/JohannesKaufmann/html-to-markdown
-
-* LICENSE: MIT
-
-MIT License
-
-Copyright (c) 2018 Johannes Kaufmann
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
-
-
---
## html-to-react
From 2397ad59fdd8b50a1b1bcc58e2bbcb94be44c8dc Mon Sep 17 00:00:00 2001
From: "unified-ci-app[bot]"
<121569378+unified-ci-app[bot]@users.noreply.github.com>
Date: Mon, 8 Dec 2025 13:24:15 +0200
Subject: [PATCH 004/416] Update latest minor version to 11.3.0 (#34673)
Automatic Merge
---
server/public/model/version.go | 1 +
1 file changed, 1 insertion(+)
diff --git a/server/public/model/version.go b/server/public/model/version.go
index 475323f715a..79e2a251a6d 100644
--- a/server/public/model/version.go
+++ b/server/public/model/version.go
@@ -13,6 +13,7 @@ import (
// It should be maintained in chronological order with most current
// release at the front of the list.
var versions = []string{
+ "11.3.0",
"11.2.0",
"11.1.0",
"11.0.0",
From d3cc09937fc334cc37b07086240a8b93438d0667 Mon Sep 17 00:00:00 2001
From: "Weblate (bot)"
Date: Mon, 8 Dec 2025 15:09:47 +0100
Subject: [PATCH 005/416] Translations update from Mattermost Weblate (#34676)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* Translated using Weblate (Polish)
Currently translated at 97.3% (2792 of 2867 strings)
Translation: Mattermost/server
Translate-URL: https://translate.mattermost.com/projects/mattermost/server/pl/
* Translated using Weblate (Polish)
Currently translated at 100.0% (6730 of 6730 strings)
Translation: Mattermost/webapp
Translate-URL: https://translate.mattermost.com/projects/mattermost/webapp/pl/
* Translated using Weblate (Polish)
Currently translated at 98.0% (2812 of 2867 strings)
Translation: Mattermost/server
Translate-URL: https://translate.mattermost.com/projects/mattermost/server/pl/
* Translated using Weblate (Swedish)
Currently translated at 98.0% (6596 of 6730 strings)
Translation: Mattermost/webapp
Translate-URL: https://translate.mattermost.com/projects/mattermost/webapp/sv/
* Translated using Weblate (Swedish)
Currently translated at 96.6% (2771 of 2867 strings)
Translation: Mattermost/server
Translate-URL: https://translate.mattermost.com/projects/mattermost/server/sv/
* Translated using Weblate (Swedish)
Currently translated at 98.7% (6644 of 6730 strings)
Translation: Mattermost/webapp
Translate-URL: https://translate.mattermost.com/projects/mattermost/webapp/sv/
* Translated using Weblate (Polish)
Currently translated at 98.7% (2832 of 2867 strings)
Translation: Mattermost/server
Translate-URL: https://translate.mattermost.com/projects/mattermost/server/pl/
* Translated using Weblate (Portuguese)
Currently translated at 30.3% (2045 of 6730 strings)
Translation: Mattermost/webapp
Translate-URL: https://translate.mattermost.com/projects/mattermost/webapp/pt/
* Translated using Weblate (Swedish)
Currently translated at 96.8% (2776 of 2867 strings)
Translation: Mattermost/server
Translate-URL: https://translate.mattermost.com/projects/mattermost/server/sv/
* Translated using Weblate (Swedish)
Currently translated at 98.7% (6649 of 6730 strings)
Translation: Mattermost/webapp
Translate-URL: https://translate.mattermost.com/projects/mattermost/webapp/sv/
* Translated using Weblate (Swedish)
Currently translated at 97.1% (2784 of 2867 strings)
Translation: Mattermost/server
Translate-URL: https://translate.mattermost.com/projects/mattermost/server/sv/
* Translated using Weblate (Swedish)
Currently translated at 97.1% (2786 of 2867 strings)
Translation: Mattermost/server
Translate-URL: https://translate.mattermost.com/projects/mattermost/server/sv/
* Translated using Weblate (Dutch)
Currently translated at 99.9% (6729 of 6730 strings)
Translation: Mattermost/webapp
Translate-URL: https://translate.mattermost.com/projects/mattermost/webapp/nl/
* Translated using Weblate (Swedish)
Currently translated at 98.8% (6654 of 6730 strings)
Translation: Mattermost/webapp
Translate-URL: https://translate.mattermost.com/projects/mattermost/webapp/sv/
* Translated using Weblate (Swedish)
Currently translated at 97.4% (2795 of 2867 strings)
Translation: Mattermost/server
Translate-URL: https://translate.mattermost.com/projects/mattermost/server/sv/
* Translated using Weblate (Swedish)
Currently translated at 97.4% (2795 of 2867 strings)
Translation: Mattermost/server
Translate-URL: https://translate.mattermost.com/projects/mattermost/server/sv/
* Translated using Weblate (Swedish)
Currently translated at 97.4% (2795 of 2867 strings)
Translation: Mattermost/server
Translate-URL: https://translate.mattermost.com/projects/mattermost/server/sv/
* Translated using Weblate (Swedish)
Currently translated at 97.4% (2795 of 2867 strings)
Translation: Mattermost/server
Translate-URL: https://translate.mattermost.com/projects/mattermost/server/sv/
* Translated using Weblate (Swedish)
Currently translated at 98.9% (6661 of 6730 strings)
Translation: Mattermost/webapp
Translate-URL: https://translate.mattermost.com/projects/mattermost/webapp/sv/
* Translated using Weblate (Swedish)
Currently translated at 98.9% (6661 of 6730 strings)
Translation: Mattermost/webapp
Translate-URL: https://translate.mattermost.com/projects/mattermost/webapp/sv/
* Translated using Weblate (Swedish)
Currently translated at 98.9% (6661 of 6730 strings)
Translation: Mattermost/webapp
Translate-URL: https://translate.mattermost.com/projects/mattermost/webapp/sv/
* Translated using Weblate (Swedish)
Currently translated at 98.9% (6661 of 6730 strings)
Translation: Mattermost/webapp
Translate-URL: https://translate.mattermost.com/projects/mattermost/webapp/sv/
* Translated using Weblate (Polish)
Currently translated at 100.0% (2867 of 2867 strings)
Translation: Mattermost/server
Translate-URL: https://translate.mattermost.com/projects/mattermost/server/pl/
* Update translation files
Updated by "Cleanup translation files" hook in Weblate.
Translation: Mattermost/webapp
Translate-URL: https://translate.mattermost.com/projects/mattermost/webapp/
---------
Co-authored-by: master7
Co-authored-by: Kristoffer Grundström
Co-authored-by: Manuela Silva
Co-authored-by: Tom De Moor
Co-authored-by: MArtin Johnson
---
server/i18n/pl.json | 380 ++++++++++++++++++++++++++++
server/i18n/sv.json | 136 ++++++++++
webapp/channels/src/i18n/be.json | 5 -
webapp/channels/src/i18n/bg.json | 5 -
webapp/channels/src/i18n/cs.json | 7 -
webapp/channels/src/i18n/de.json | 7 -
webapp/channels/src/i18n/en-AU.json | 7 -
webapp/channels/src/i18n/es.json | 6 -
webapp/channels/src/i18n/fa.json | 5 -
webapp/channels/src/i18n/fi.json | 1 -
webapp/channels/src/i18n/fr.json | 7 -
webapp/channels/src/i18n/fy.json | 5 -
webapp/channels/src/i18n/gl.json | 1 -
webapp/channels/src/i18n/hr.json | 2 -
webapp/channels/src/i18n/hu.json | 5 -
webapp/channels/src/i18n/it.json | 5 -
webapp/channels/src/i18n/ja.json | 7 -
webapp/channels/src/i18n/ko.json | 7 -
webapp/channels/src/i18n/lt.json | 5 -
webapp/channels/src/i18n/nb-NO.json | 7 -
webapp/channels/src/i18n/nl.json | 9 +-
webapp/channels/src/i18n/pl.json | 9 +-
webapp/channels/src/i18n/pt-BR.json | 5 -
webapp/channels/src/i18n/pt.json | 4 +-
webapp/channels/src/i18n/ro.json | 5 -
webapp/channels/src/i18n/ru.json | 7 -
webapp/channels/src/i18n/sv.json | 79 +++++-
webapp/channels/src/i18n/tr.json | 7 -
webapp/channels/src/i18n/uk.json | 7 -
webapp/channels/src/i18n/vi.json | 5 -
webapp/channels/src/i18n/zh-CN.json | 7 -
webapp/channels/src/i18n/zh-TW.json | 7 -
32 files changed, 592 insertions(+), 169 deletions(-)
diff --git a/server/i18n/pl.json b/server/i18n/pl.json
index c91a4c444a7..fe05e233bd6 100644
--- a/server/i18n/pl.json
+++ b/server/i18n/pl.json
@@ -11114,5 +11114,385 @@
{
"id": "app.agents.get_services.bridge_call_failed",
"translation": "Połączenie mostkowe nie powiodło się."
+ },
+ {
+ "id": "api.command.move_command.creator_no_permission.app_error",
+ "translation": "Brak pozwolenia na przeniesienie polecenia"
+ },
+ {
+ "id": "api.file.get_file.invalid_flagged_post.app_error",
+ "translation": "Podano nieprawidłowy identyfikator oflagowanego posta."
+ },
+ {
+ "id": "api.file.get_file_info.app_error",
+ "translation": "Nie udało się uzyskać informacji o pliku."
+ },
+ {
+ "id": "api.oauth.allow_oauth.pkce_required_public.app_error",
+ "translation": "PKCE (Proof Key for Code Exchange) jest wymagany dla publicznych klientów OAuth korzystających z przepływu kodu autoryzacji."
+ },
+ {
+ "id": "api.oauth.authorization_server_metadata.disabled.app_error",
+ "translation": "Dostawca usługi OAuth jest wyłączony."
+ },
+ {
+ "id": "api.oauth.authorization_server_metadata.invalid_url.app_error",
+ "translation": "Nieprawidłowa konfiguracja URL dla metadanych serwera autoryzacji."
+ },
+ {
+ "id": "api.oauth.authorization_server_metadata.site_url_required.app_error",
+ "translation": "Adres URL witryny musi być skonfigurowany tak, aby zapewniał metadane serwera autoryzacji."
+ },
+ {
+ "id": "api.oauth.get_access_token.resource_mismatch.app_error",
+ "translation": "Niezgodność parametrów zasobów między żądaniami autoryzacji i tokenów."
+ },
+ {
+ "id": "api.oauth.regenerate_secret.public_client.app_error",
+ "translation": "invalid_request: Nie można zregenerować sekretu dla publicznych klientów OAuth."
+ },
+ {
+ "id": "api.post.fill_in_post_props.invalid_ai_generated_user.app_error",
+ "translation": "Użytkownik wygenerowany przez AI musi być twórcą postu lub botem."
+ },
+ {
+ "id": "api.post.get_posts_for_reporting.channel_not_found",
+ "translation": "Nie znaleziono kanału"
+ },
+ {
+ "id": "api.team.invite_guests_to_channels.guest_magic_link_disabled.error",
+ "translation": "Magiczny link gościa nie jest włączony"
+ },
+ {
+ "id": "api.templates.guest_magic_link_body.footer.info",
+ "translation": "Ten e-mail został wysłany do Ciebie, ponieważ ktoś poprosił o link do logowania do Mattermost. Jeśli o to nie prosiłeś, możesz bezpiecznie zignorować tę wiadomość."
+ },
+ {
+ "id": "api.templates.guest_magic_link_body.footer.title",
+ "translation": "Nie prosiłeś o tę wiadomość e-mail?"
+ },
+ {
+ "id": "api.templates.guest_magic_link_body.subtitle",
+ "translation": "Kliknij poniższy przycisk, aby zalogować się na swoje konto. Pamiętaj, aby nie udostępniać tego linku nikomu innemu."
+ },
+ {
+ "id": "api.templates.guest_magic_link_body.title",
+ "translation": "Zaloguj się do Mattermost"
+ },
+ {
+ "id": "api.templates.guest_magic_link_subject",
+ "translation": "Zaloguj się do {{ .SiteName }}"
+ },
+ {
+ "id": "api.user.guest_magic_link.expired_token.app_error",
+ "translation": "Link do zaproszenia wygasł."
+ },
+ {
+ "id": "api.user.guest_magic_link.invalid_token.app_error",
+ "translation": "Nieprawidłowy link zaproszenia."
+ },
+ {
+ "id": "api.user.guest_magic_link.invalid_token_type.app_error",
+ "translation": "Nieprawidłowy typ linku zaproszenia."
+ },
+ {
+ "id": "api.user.guest_magic_link.missing_token.app_error",
+ "translation": "Brakujący token zaproszenia."
+ },
+ {
+ "id": "api.user.guest_magic_link.username_generation_failed.app_error",
+ "translation": "Nie można wygenerować unikalnej nazwy użytkownika. Skontaktuj się z administratorem systemu."
+ },
+ {
+ "id": "api.user.login.guest_magic_link.disabled.error",
+ "translation": "Logowanie za pomocą magicznego linku jest wyłączone."
+ },
+ {
+ "id": "api.user.promote_guest_to_user.magic_link_enabled.app_error",
+ "translation": "Nie można przekonwertować gościa na zwykłego użytkownika, ponieważ gość używa uwierzytelniania magic link."
+ },
+ {
+ "id": "api.user.send_password_reset.guest_magic_link.app_error",
+ "translation": "Nie można zresetować hasła dla kont gości magic link."
+ },
+ {
+ "id": "app.custom_profile_attributes.patch_field.app_error",
+ "translation": "Nie można załatać pola Atrybut profilu niestandardowego"
+ },
+ {
+ "id": "app.pap.update_access_control_policies_active.app_error",
+ "translation": "Nie można zaktualizować aktywnego statusu polityk kontroli dostępu."
+ },
+ {
+ "id": "app.post.get_posts_for_reporting.app_error",
+ "translation": "Nie można pobrać postów do raportowania."
+ },
+ {
+ "id": "app.post.get_posts_for_reporting.invalid_input_error",
+ "translation": "Nie można pobrać postów do raportowania. Nieprawidłowe dane wejściowe."
+ },
+ {
+ "id": "app.post.get_posts_for_reporting.license_error",
+ "translation": "Twoja licencja nie wspiera raportowania postów."
+ },
+ {
+ "id": "app.post.rewrite.agent_call_failed",
+ "translation": "Nie udało się nawiązać połączenia z agentem AI."
+ },
+ {
+ "id": "app.post.rewrite.empty_response",
+ "translation": "Pusta odpowiedź od AI."
+ },
+ {
+ "id": "app.post.rewrite.invalid_action",
+ "translation": "Nieprawidłowa akcja przepisania."
+ },
+ {
+ "id": "app.post.rewrite.parse_response_failed",
+ "translation": "Nie udało się przeanalizować odpowiedzi rewrite od AI."
+ },
+ {
+ "id": "ent.push_proxy.delete.app_error",
+ "translation": "Nie udało się usunąć tokenu autoryzacji push proxy."
+ },
+ {
+ "id": "ent.push_proxy.generate.bad_status.app_error",
+ "translation": "Nie udało się uzyskać tokenu autoryzacji z serwera proxy push."
+ },
+ {
+ "id": "ent.push_proxy.generate.create_request.app_error",
+ "translation": "Nie udało się utworzyć żądania HTTP dla uwierzytelniania push proxy."
+ },
+ {
+ "id": "ent.push_proxy.generate.empty_token.app_error",
+ "translation": "Serwer proxy push zwrócił pusty token uwierzytelniania."
+ },
+ {
+ "id": "ent.push_proxy.generate.encrypt.app_error",
+ "translation": "Nie udało się zaszyfrować ładunku dla uwierzytelniania push proxy."
+ },
+ {
+ "id": "ent.push_proxy.generate.http_request.app_error",
+ "translation": "Nie udało się wysłać żądania HTTP do serwera proxy push."
+ },
+ {
+ "id": "ent.push_proxy.generate.marshal.app_error",
+ "translation": "Nie udało się pobrać ładunku dla uwierzytelniania push proxy."
+ },
+ {
+ "id": "ent.push_proxy.generate.marshal_request.app_error",
+ "translation": "Nie udało się zebrać treści żądania dla uwierzytelniania push proxy."
+ },
+ {
+ "id": "ent.push_proxy.generate.no_key.app_error",
+ "translation": "Brak dostępnego klucza szyfrowania dla uwierzytelniania push proxy."
+ },
+ {
+ "id": "ent.push_proxy.generate.parse_response.app_error",
+ "translation": "Nie udało się przeanalizować odpowiedzi z punktu końcowego autoryzacji push proxy."
+ },
+ {
+ "id": "ent.push_proxy.generate.save_system.app_error",
+ "translation": "Nie udało się zapisać tokenu autoryzacji."
+ },
+ {
+ "id": "ent.push_proxy.worker.interface_nil.app_error",
+ "translation": "Interfejs proxy push jest zerowy."
+ },
+ {
+ "id": "model.authorize.is_valid.code_challenge.app_error",
+ "translation": "Podczas korzystania z PKCE wymagane jest sprawdzenie kodu."
+ },
+ {
+ "id": "model.authorize.is_valid.code_challenge.format.app_error",
+ "translation": "Kod wyzwania musi być zakodowany base64url (przy użyciu znaków A-Z, a-z, 0-9, -, _)."
+ },
+ {
+ "id": "model.authorize.is_valid.code_challenge.length.app_error",
+ "translation": "Kod wyzwania musi mieć długość od 43 do 128 znaków."
+ },
+ {
+ "id": "model.authorize.is_valid.code_challenge_method.app_error",
+ "translation": "Metoda kwestionowania kodu jest wymagana w przypadku korzystania z PKCE."
+ },
+ {
+ "id": "model.authorize.is_valid.code_challenge_method.unsupported.app_error",
+ "translation": "Wsparciem objęta jest tylko metoda weryfikacji kodu \"S256\"."
+ },
+ {
+ "id": "model.authorize.is_valid.resource.has_fragment.app_error",
+ "translation": "Parametr zasobu nie może zawierać składnika fragmentu."
+ },
+ {
+ "id": "model.authorize.is_valid.resource.invalid_uri.app_error",
+ "translation": "Parametr zasobu musi być prawidłowym identyfikatorem URI."
+ },
+ {
+ "id": "model.authorize.is_valid.resource.length.app_error",
+ "translation": "Parametr zasobu nie może przekraczać 512 znaków."
+ },
+ {
+ "id": "model.authorize.is_valid.resource.not_absolute.app_error",
+ "translation": "Parametr zasobu musi być bezwzględnym identyfikatorem URI."
+ },
+ {
+ "id": "model.authorize.validate_pkce.not_used_in_auth.app_error",
+ "translation": "Podano weryfikator kodu PKCE, ale nie został on użyty podczas autoryzacji."
+ },
+ {
+ "id": "model.authorize.validate_pkce.public_client_required.app_error",
+ "translation": "PKCE (Proof Key for Code Exchange) jest wymagany dla klientów publicznych."
+ },
+ {
+ "id": "model.authorize.validate_pkce.verification_failed.app_error",
+ "translation": "Weryfikator kodu PKCE nie jest zgodny z wyzwaniem kodu."
+ },
+ {
+ "id": "model.authorize.validate_pkce.verifier_required.app_error",
+ "translation": "Weryfikator kodu PKCE jest wymagany w przypadku korzystania z PKCE."
+ },
+ {
+ "id": "model.dcr.is_valid.client_name.app_error",
+ "translation": "Nazwa klienta nie może zawierać więcej niż 64 znaki."
+ },
+ {
+ "id": "model.dcr.is_valid.client_uri_format.app_error",
+ "translation": "Nieprawidłowy format URI klienta."
+ },
+ {
+ "id": "model.dcr.is_valid.client_uri_length.app_error",
+ "translation": "Identyfikator URI klienta musi mieć mniej niż 256 znaków."
+ },
+ {
+ "id": "model.dcr.is_valid.redirect_uri_format.app_error",
+ "translation": "Nieprawidłowy format URI przekierowania."
+ },
+ {
+ "id": "model.dcr.is_valid.redirect_uris.app_error",
+ "translation": "Wymagany jest co najmniej jeden URI przekierowania."
+ },
+ {
+ "id": "model.dcr.is_valid.unsupported_auth_method.app_error",
+ "translation": "Podano nieobsługiwaną metodę token_endpoint_auth_method."
+ },
+ {
+ "id": "model.oauth.validate_grant.credentials.app_error",
+ "translation": "Nieprawidłowe dane uwierzytelniające klienta."
+ },
+ {
+ "id": "model.oauth.validate_grant.pkce_required.app_error",
+ "translation": "PKCE (Proof Key for Code Exchange) jest wymagany dla klientów publicznych."
+ },
+ {
+ "id": "model.oauth.validate_grant.public_client_refresh_token.app_error",
+ "translation": "Klienci publiczni nie mogą korzystać z tokenów odświeżania typu grant."
+ },
+ {
+ "id": "model.oauth.validate_grant.public_client_secret.app_error",
+ "translation": "Klienci publiczni nie mogą udostępniać tajemnicy klienta."
+ },
+ {
+ "id": "model.post.decode_cursor.invalid_base64",
+ "translation": "Nieprawidłowy format kursora: nie można zdekodować base64."
+ },
+ {
+ "id": "model.post.decode_cursor.invalid_exclude_system_posts",
+ "translation": "Nieprawidłowy format kursora: exclude_system_posts musi być wartością logiczną."
+ },
+ {
+ "id": "model.post.decode_cursor.invalid_format",
+ "translation": "Nieprawidłowy format kursora: oczekiwano 8 części."
+ },
+ {
+ "id": "model.post.decode_cursor.invalid_include_deleted",
+ "translation": "Nieprawidłowy format kursora: include_deleted musi być wartością logiczną."
+ },
+ {
+ "id": "model.post.decode_cursor.invalid_timestamp",
+ "translation": "Nieprawidłowy format kursora: znacznik czasu musi być liczbą całkowitą."
+ },
+ {
+ "id": "model.post.decode_cursor.invalid_version",
+ "translation": "Nieprawidłowy format kursora: Wersja musi być liczbą całkowitą."
+ },
+ {
+ "id": "model.post.decode_cursor.unsupported_version",
+ "translation": "Nieobsługiwana wersja kursora."
+ },
+ {
+ "id": "model.post.query_params.invalid_channel_id",
+ "translation": "Nieprawidłowy identyfikator kanału."
+ },
+ {
+ "id": "model.post.query_params.invalid_cursor_id",
+ "translation": "Nieprawidłowy identyfikator kursora."
+ },
+ {
+ "id": "model.post.query_params.invalid_sort_direction",
+ "translation": "Nieprawidłowy kierunek sortowania."
+ },
+ {
+ "id": "model.post.query_params.invalid_time_field",
+ "translation": "Nieprawidłowe pole czasu."
+ },
+ {
+ "id": "store.sql_autotranslation.channel_not_found",
+ "translation": "Kanał nie został znaleziony."
+ },
+ {
+ "id": "store.sql_autotranslation.get.app_error",
+ "translation": "Nie można uzyskać tłumaczenia."
+ },
+ {
+ "id": "store.sql_autotranslation.get_active_languages.app_error",
+ "translation": "Nie można uzyskać aktywnych języków."
+ },
+ {
+ "id": "store.sql_autotranslation.get_channel_enabled.app_error",
+ "translation": "Nie można uzyskać statusu włączenia autotranslacji Kanału."
+ },
+ {
+ "id": "store.sql_autotranslation.get_user_enabled.app_error",
+ "translation": "Nie można uzyskać statusu włączonej autotranslacji użytkownika."
+ },
+ {
+ "id": "store.sql_autotranslation.get_user_language.app_error",
+ "translation": "Nie można uzyskać języka użytkownika."
+ },
+ {
+ "id": "store.sql_autotranslation.is_channel_enabled.app_error",
+ "translation": "Nie można uzyskać statusu włączenia autotranslacji Kanału."
+ },
+ {
+ "id": "store.sql_autotranslation.member_not_found",
+ "translation": "Nie znaleziono członka."
+ },
+ {
+ "id": "store.sql_autotranslation.meta_json.app_error",
+ "translation": "Nie udało się przeanalizować metadanych tłumaczenia JSON."
+ },
+ {
+ "id": "store.sql_autotranslation.query_build_error",
+ "translation": "Nie udało się utworzyć zapytania."
+ },
+ {
+ "id": "store.sql_autotranslation.save.app_error",
+ "translation": "Nie można zapisać tłumaczenia."
+ },
+ {
+ "id": "store.sql_autotranslation.save.invalid_translation",
+ "translation": "Walidacja tłumaczenia nie powiodła się. Obiekt tłumaczenia jest nieprawidłowy."
+ },
+ {
+ "id": "store.sql_autotranslation.save.meta_json.app_error",
+ "translation": "Nie udało się serializować metadanych tłumaczenia do JSON."
+ },
+ {
+ "id": "store.sql_autotranslation.set_channel_enabled.app_error",
+ "translation": "Nie można ustawić statusu włączenia autotranslacji Kanału."
+ },
+ {
+ "id": "store.sql_autotranslation.set_user_enabled.app_error",
+ "translation": "Nie można ustawić statusu włączonej autotranslacji użytkownika."
}
]
diff --git a/server/i18n/sv.json b/server/i18n/sv.json
index 8c45bc824b1..489d98924d9 100644
--- a/server/i18n/sv.json
+++ b/server/i18n/sv.json
@@ -11066,5 +11066,141 @@
{
"id": "model.config.is_valid.autotranslation.timeouts.notification.app_error",
"translation": "Tidsgränsen för avisering av autotranslation är ogiltig. Måste vara ett positivt tal."
+ },
+ {
+ "id": "model.oauth.validate_grant.credentials.app_error",
+ "translation": "Ogiltiga klient-uppgifter."
+ },
+ {
+ "id": "api.post.get_posts_for_reporting.channel_not_found",
+ "translation": "Kanal hittades inte"
+ },
+ {
+ "id": "api.user.guest_magic_link.invalid_token.app_error",
+ "translation": "Ogiltig inbjudningslänk."
+ },
+ {
+ "id": "model.post.query_params.invalid_channel_id",
+ "translation": "Ogiltigt kanal-ID."
+ },
+ {
+ "id": "model.post.query_params.invalid_time_field",
+ "translation": "Ogiltigt tidsfält."
+ },
+ {
+ "id": "store.sql_autotranslation.channel_not_found",
+ "translation": "Kanal hittades inte."
+ },
+ {
+ "id": "store.sql_autotranslation.member_not_found",
+ "translation": "Medlem hittades inte."
+ },
+ {
+ "id": "app.post.rewrite.empty_response",
+ "translation": "Tomt svar från AI."
+ },
+ {
+ "id": "api.templates.guest_magic_link_body.footer.title",
+ "translation": "Begärde du inte det här e-mailet?"
+ },
+ {
+ "id": "api.templates.guest_magic_link_body.title",
+ "translation": "Logga in till Mattermost"
+ },
+ {
+ "id": "store.sql_autotranslation.get.app_error",
+ "translation": "Kunde inte hämta översättning."
+ },
+ {
+ "id": "store.sql_autotranslation.save.app_error",
+ "translation": "Kunde inte spara översättningen."
+ },
+ {
+ "id": "app.agents.get_services.app_error",
+ "translation": "Misslyckades med hämtningen av LLM-tjänster."
+ },
+ {
+ "id": "api.file.get_file_info.app_error",
+ "translation": "Hämtningen av fil-infon misslyckades."
+ },
+ {
+ "id": "api.user.guest_magic_link.expired_token.app_error",
+ "translation": "Den här inbjudningslänken har slutat gälla."
+ },
+ {
+ "id": "app.agents.get_agents.bridge_call_failed",
+ "translation": "Samtalsbryggningen misslyckades."
+ },
+ {
+ "id": "app.agents.get_services.bridge_call_failed",
+ "translation": "Samtalsbryggningen misslyckades."
+ },
+ {
+ "id": "app.post.rewrite.invalid_action",
+ "translation": "Ogiltig omskrivningsåtgärd."
+ },
+ {
+ "id": "model.dcr.is_valid.unsupported_auth_method.app_error",
+ "translation": "Ej stödd token_endpoint_auth_method tillhandahölls."
+ },
+ {
+ "id": "api.user.guest_magic_link.missing_token.app_error",
+ "translation": "Inbjudningstoken saknas."
+ },
+ {
+ "id": "model.post.decode_cursor.unsupported_version",
+ "translation": "Pekar-version stöds inte."
+ },
+ {
+ "id": "store.sql_autotranslation.get_active_languages.app_error",
+ "translation": "Kunde inte hämta aktiva språk."
+ },
+ {
+ "id": "store.sql_autotranslation.get_user_language.app_error",
+ "translation": "Kunde inte hämta användarens språk."
+ },
+ {
+ "id": "app.agents.get_agents.app_error",
+ "translation": "Hämtningen av agenterna misslyckades."
+ },
+ {
+ "id": "ent.push_proxy.generate.save_system.app_error",
+ "translation": "Sparningen av auth-token misslyckades."
+ },
+ {
+ "id": "api.command.move_command.creator_no_permission.app_error",
+ "translation": "Ingen behörighet att flytta kommandot"
+ },
+ {
+ "id": "api.oauth.authorization_server_metadata.disabled.app_error",
+ "translation": "Tjänstleverantör för OAuth är avstängd."
+ },
+ {
+ "id": "api.user.guest_magic_link.invalid_token_type.app_error",
+ "translation": "Ogiltig länk-typ för inbjudningen."
+ },
+ {
+ "id": "app.post.rewrite.agent_call_failed",
+ "translation": "Misslyckat anrop till AI-agenten."
+ },
+ {
+ "id": "model.dcr.is_valid.client_uri_format.app_error",
+ "translation": "Ogiltigt URI-format för klienten."
+ },
+ {
+ "id": "model.dcr.is_valid.redirect_uri_format.app_error",
+ "translation": "Ogiltigt URI-format för omdirigeringen."
+ },
+ {
+ "id": "model.post.query_params.invalid_cursor_id",
+ "translation": "Ogiltigt cursor-id."
+ },
+ {
+ "id": "store.sql_autotranslation.query_build_error",
+ "translation": "Misslyckades att skapa frågan."
+ },
+ {
+ "id": "model.post.query_params.invalid_sort_direction",
+ "translation": "Ogiltig sorteringsriktning."
}
]
diff --git a/webapp/channels/src/i18n/be.json b/webapp/channels/src/i18n/be.json
index 85d09820007..e1ed35371e9 100644
--- a/webapp/channels/src/i18n/be.json
+++ b/webapp/channels/src/i18n/be.json
@@ -3725,13 +3725,8 @@
"suggestion.user.isCurrent": "(вы)",
"system_notice.adminVisible": "Відаць толькі сістэмным адміністратарам",
"system_notice.adminVisible.icon": "Значок \"Відаць толькі сістэмным адміністратарам\"",
- "system_notice.body.api3": "Калі вы стварылі ці ўсталявалі інтэграцыі за апошнія два гады, высветліце, як [апошнія змены](!https://about.mattermost.com/default-apiv3-deprecation-guide) маглі паўплываць на іх.",
- "system_notice.body.ee_upgrade_advice": "Карпаратыўная рэдакцыя рэкамендуецца для забеспячэння аптымальнай працы і надзейнасці. [Даведацца больш](!https://mattermost.com/performance).",
- "system_notice.body.ie11_deprecation": "Ваш браўзэр IE11 больш не будзе падтрымлівацца ў наступным выпуску. [Даведайцеся, як перайсці ў іншы браўзэр за адзін просты крок](!https://forum.mattermost.com/t/mattermost-is-dropping-support-for-internet-explorer-ie11-in-v5-16/7575 ).",
- "system_notice.body.permissions": "Некаторыя параметры сістэмнай кансолі палітыкі і дазволаў змяніліся з выпускам [пашыраных дазволаў](!https://docs.mattermost.com/onboard/advanced-permissions.html) у Enterprise E10 і E20.",
"system_notice.dont_show": "Больш не паказваць",
"system_notice.remind_me": "Нагадай мне пазней",
- "system_notice.title": "**Апавяшчэнне**\nад Mattermost",
"team.button.ariaLabel": "каманда {teamName}",
"team.button.mentions.ariaLabel": "каманда {teamName}, {mentionCount} згадак",
"team.button.name_undefined": "У гэтай каманды няма імя",
diff --git a/webapp/channels/src/i18n/bg.json b/webapp/channels/src/i18n/bg.json
index 2a715af8155..d462e7a5c72 100644
--- a/webapp/channels/src/i18n/bg.json
+++ b/webapp/channels/src/i18n/bg.json
@@ -4104,13 +4104,8 @@
"suggestion.user.isCurrent": "(Вие)",
"system_notice.adminVisible": "Вижда се само от системните администратори",
"system_notice.adminVisible.icon": "Икона за Видимо само за системните администратори",
- "system_notice.body.api3": "Ако сте създали или инсталирали интеграции през последните две години, разберете как последните промени може да са се отразили върху тях.",
- "system_notice.body.ee_upgrade_advice": "Корпоративна версия се препоръчва, за да се гарантира оптимална работа и надеждност. Научете повече.",
- "system_notice.body.ie11_deprecation": "Вашият браузър, IE11, вече няма да се поддържа в предстояща версия. Разберете как да преминете към друг браузър с една проста стъпка.",
- "system_notice.body.permissions": "Някои правила и настройки на системната конзола се преместиха с въвеждането на разширени права в Enterprise E10 и E20.",
"system_notice.dont_show": "Не показвай отново",
"system_notice.remind_me": "Напомни ми по-късно",
- "system_notice.title": "**Известие**\nот Mattermost",
"team.button.ariaLabel": "{teamName} екип",
"team.button.mentions.ariaLabel": "{teamName} екип, споменавания {referenceCount}",
"team.button.name_undefined": "Този екип няма име",
diff --git a/webapp/channels/src/i18n/cs.json b/webapp/channels/src/i18n/cs.json
index 176fa138e33..825efefd9f7 100644
--- a/webapp/channels/src/i18n/cs.json
+++ b/webapp/channels/src/i18n/cs.json
@@ -5608,15 +5608,8 @@
"suggestionList.noMatches": "Žádné položky neodpovídají {value}",
"system_notice.adminVisible": "Viditelné pouze systémovým administrátorům",
"system_notice.adminVisible.icon": "Viditelné pouze systémovým administrátorům",
- "system_notice.body.api3": "Pokud jste vytvořili nebo nainstalovali integraci v posledních dvou letech, zjistěte, jak nové změny mohou ovlivnit tyto integrace.",
- "system_notice.body.ee_upgrade_advice": "Enterprise Edice se doporučuje pro zajištění optimálního provozu a spolehlivosti. Další informace.",
- "system_notice.body.ie11_deprecation": "Váš prohlížeč IE11 nebude v nadcházející verzi podporován. Zjistěte, jak přejít do jiného prohlížeče v jediném jednoduchém kroku.",
- "system_notice.body.permissions": "Některá nastavení zásad a oprávnění systémové konzole se přesunula s vydáním pokročilých oprávnění do Mattermost Free a Professional.",
"system_notice.dont_show": "Nezobrazovat Znovu",
"system_notice.remind_me": "Upozornit později",
- "system_notice.title": "Oznámení od Mattermostu",
- "system_notice.title.gm_as_dm": "Aktualizace Skupinových Zpráv",
- "system_noticy.body.gm_as_dm": "Nyní budete upozorňováni na veškerou aktivitu ve zprávách skupiny spolu s oznamovacím odznakem pro každou novou zprávu.{br}{br}To můžete nastavit v předvolbách oznámení pro každou zprávu skupiny.",
"tag.default.beta": "BETA",
"tag.default.bot": "ROBOT",
"tag.default.guest": "HOST",
diff --git a/webapp/channels/src/i18n/de.json b/webapp/channels/src/i18n/de.json
index ae227d4cc2b..293fa177b11 100644
--- a/webapp/channels/src/i18n/de.json
+++ b/webapp/channels/src/i18n/de.json
@@ -5883,15 +5883,8 @@
"suggestionList.suggestionsAvailable": "{count, number} {count, plural, one {Vorschlag} other {Vorschläge}} vorhanden",
"system_notice.adminVisible": "Nur sichtbar für Systemadministratoren",
"system_notice.adminVisible.icon": "\"Nur sichtbar für Systemadministratoren\"-Symbol",
- "system_notice.body.api3": "Falls du innerhalb der letzten zwei Jahre Erweiterungen erstellt oder installiert hast, finde heraus, wie kürzliche Änderungen diese beeinflusst haben könnten.",
- "system_notice.body.ee_upgrade_advice": "Enterprise Edition ist empfohlen um optimalen Betrieb und Verlässlichkeit sicherzustellen. Mehr lernen.",
- "system_notice.body.ie11_deprecation": "Dein Browser, IE11, wird in einer zukünftigen Version nicht mehr unterstützt. Erfahre, wie du in einem einfachen Schritt zu einem anderen Browser wechseln kannst.",
- "system_notice.body.permissions": "Einige Richtlinien- und Berechtigungseinstellungen der Systemkonsole wurden mit der Freigabe von advanced permissions zu Mattermost Free und Professional verschoben.",
"system_notice.dont_show": "Nicht erneut anzeigen",
"system_notice.remind_me": "Später erinnern",
- "system_notice.title": "Hinweis von Mattermost",
- "system_notice.title.gm_as_dm": "Aktualisierungen von Gruppennachrichten",
- "system_noticy.body.gm_as_dm": "Du wirst nun über alle Aktivitäten in deinen Gruppennachrichten benachrichtigt und erhältst ein Benachrichtigungssymbol für jede neue Nachricht.{br}{br}Du kannst dies in den Benachrichtigungseinstellungen für jede Gruppennachricht konfigurieren.",
"system_policy_indicator.base_message": "Auf diese {resourceType} wird auf Systemebene zugegriffen {policyText}",
"system_policy_indicator.description_with_policies": "Auf diese {resourceType} werden Zugriffsrichtlinien auf Systemebene angewendet: {policyList}. Alle benutzerdefinierten Zugriffsregeln, die du hier einstellst, werden zusätzlich zu dieser Richtlinie angewendet.",
"system_policy_indicator.more_policies": "{count} mehr",
diff --git a/webapp/channels/src/i18n/en-AU.json b/webapp/channels/src/i18n/en-AU.json
index 85cbf1d8bd2..4c79fc7b748 100644
--- a/webapp/channels/src/i18n/en-AU.json
+++ b/webapp/channels/src/i18n/en-AU.json
@@ -5810,15 +5810,8 @@
"suggestionList.suggestionsAvailable": "{count, number} {count, plural, one {suggestion} other {suggestions}} available",
"system_notice.adminVisible": "Only visible to System Admins",
"system_notice.adminVisible.icon": "Only visible to System Admins Icon",
- "system_notice.body.api3": "If you’ve created or installed integrations in the last two years, find out how recent changes may have affected them.",
- "system_notice.body.ee_upgrade_advice": "Enterprise Edition is recommended to ensure optimal operation and reliability. Learn more.",
- "system_notice.body.ie11_deprecation": "Your browser, IE11, will no longer be supported in an upcoming release. Find out how to move to another browser in one simple step.",
- "system_notice.body.permissions": "Some policy and permission System Console settings have moved with the release of advanced permissions into Mattermost Free and Professional.",
"system_notice.dont_show": "Don't show again",
"system_notice.remind_me": "Remind me Later",
- "system_notice.title": "Notice from Mattermost",
- "system_notice.title.gm_as_dm": "Updates to Group Messages",
- "system_noticy.body.gm_as_dm": "You will now be notified for all activity in your group messages along with a notification badge for every new message.{br}{br}You can configure this in notification preferences for each group message.",
"system_policy_indicator.base_message": "This {resourceType} has system-level access {policyText} applied",
"system_policy_indicator.description_with_policies": "This {resourceType} has system-level access policies applied: {policyList}. Any custom access rules you set here will be applied in addition to this policy.",
"system_policy_indicator.more_policies": "{count} more",
diff --git a/webapp/channels/src/i18n/es.json b/webapp/channels/src/i18n/es.json
index 5eb93098cd9..db41291d97e 100644
--- a/webapp/channels/src/i18n/es.json
+++ b/webapp/channels/src/i18n/es.json
@@ -4072,14 +4072,8 @@
"suggestion.user.isCurrent": "(tú)",
"system_notice.adminVisible": "Sólo visible a Administradores del Sistema",
"system_notice.adminVisible.icon": "Icono de Sólo visible a Administradores del Sistema",
- "system_notice.body.api3": "Si creaste o instalaste una integración en los últimos dos años, averigua como los nuevos cambios pueden afectar dichas integraciones.",
- "system_notice.body.ee_upgrade_advice": "Se recomienda la Edición Empresarial para asegurar un óptimo funcionamiento y fiabilidad. Más información.",
- "system_notice.body.ie11_deprecation": "Su navegador, IE11, dejará de ser compatible en una próxima versión. Averiguar cómo cambiar a otro navegador en un sólo paso.",
- "system_notice.body.permissions": "Algunas configuraciones de políticas y permisos en la Consola del Sistema han sido movidas luego de haber incluido permisos avanzados en las versiones Empresariales E10 y E20.",
"system_notice.dont_show": "No Mostrar de Nuevo",
"system_notice.remind_me": "Recordarme más tarde",
- "system_notice.title": "**Aviso**\nde Mattermost",
- "system_notice.title.gm_as_dm": "Actualizaciones a Mensajes Grupales",
"team.button.ariaLabel": "equipo {teamName}",
"team.button.mentions.ariaLabel": "equipo {teamName}, {mentionCount} menciones",
"team.button.name_undefined": "Este equipo no tiene nombre",
diff --git a/webapp/channels/src/i18n/fa.json b/webapp/channels/src/i18n/fa.json
index 020a23ee1c2..c973c353e8f 100644
--- a/webapp/channels/src/i18n/fa.json
+++ b/webapp/channels/src/i18n/fa.json
@@ -3682,13 +3682,8 @@
"suggestion.user.isCurrent": "(شما)",
"system_notice.adminVisible": "فقط برای مدیران سیستم قابل مشاهده است",
"system_notice.adminVisible.icon": "فقط برای نماد مدیران سیستم قابل مشاهده است",
- "system_notice.body.api3": "اگر در دو سال گذشته ادغام هایی ایجاد یا نصب کرده اید، دریابید که چگونه تغییرات اخیر چه تاثیری بر آنها داشته است.",
- "system_notice.body.ee_upgrade_advice": "نسخه Enterprise برای اطمینان از عملکرد بهینه و قابلیت اطمینان توصیه می شود. بیشتر بیاموزید.",
- "system_notice.body.ie11_deprecation": "مرورگر شما، IE11، دیگر در نسخه آینده پشتیبانی نخواهد شد. در یک مرحله ساده نحوه انتقال به مرورگر دیگر را بیابید.",
- "system_notice.body.permissions": "برخی از تنظیمات کنسول سیستم خطمشی و مجوز با انتشار مجوزهای پیشرفته به Mattermost Starter و Professional منتقل شدهاند.",
"system_notice.dont_show": "دوباره نمایش داده نشود",
"system_notice.remind_me": "بعدا به من یادآوری کن",
- "system_notice.title": "**اطلاع**\nاز Mattermost",
"tag.default.beta": "آزمایشی",
"tag.default.bot": "بات",
"tag.default.guest": "مهمان",
diff --git a/webapp/channels/src/i18n/fi.json b/webapp/channels/src/i18n/fi.json
index fcd412be3da..8d99b5d06c3 100644
--- a/webapp/channels/src/i18n/fi.json
+++ b/webapp/channels/src/i18n/fi.json
@@ -1443,7 +1443,6 @@
"suggestion.mention.nonmembers": "Ei kanavalla",
"suggestion.search.direct": "Suoraviestit",
"suggestion.search.private": "Yksityiskanavat",
- "system_notice.body.permissions": "Jotkut käytäntö- ja järjestelmän konsoliasetukset ovat siirtyneet kehittyneiden lupien julkaisemisen myötä Mattermost Free:ssä ja Professionalissa.",
"system_notice.dont_show": "Älä näytä uudestaan",
"system_notice.remind_me": "Muista myöhemmin",
"team_channel_settings.group.group_user_row.numberOfGroups": "{amount, number} {amount, plural, one {käyttäjä} other {käyttäjää}}",
diff --git a/webapp/channels/src/i18n/fr.json b/webapp/channels/src/i18n/fr.json
index 2554ec6f7bf..9b8e27353cf 100644
--- a/webapp/channels/src/i18n/fr.json
+++ b/webapp/channels/src/i18n/fr.json
@@ -4393,15 +4393,8 @@
"suggestion.user.isCurrent": "(vous)",
"system_notice.adminVisible": "Uniquement visible des administrateurs système",
"system_notice.adminVisible.icon": "Icône uniquement visible des administrateurs système",
- "system_notice.body.api3": "Si vous avez créé ou installé des intégrations durant les deux dernières années, veuillez consulter les changements récents qui pourraient les affecter.",
- "system_notice.body.ee_upgrade_advice": "L’Édition Entreprise est recommandée pour vous assurer un fonctionnement optimal et une fiabilité accrue. En savoir plus.",
- "system_notice.body.ie11_deprecation": "Votre navigateur, Internet Explorer 11, ne sera plus supporté dans une future version de Mattermost. Consultez notre documentation pour trouver comment passer facilement à un autre navigateur.",
- "system_notice.body.permissions": "Suite à la sortie de la fonctionnalité des permissions avancées, certains paramètres accessibles via console système sont désormais réservés aux éditions Mattermost Starter et Professional.",
"system_notice.dont_show": "Ne pas montrer de nouveau",
"system_notice.remind_me": "Me le rappeler plus tard",
- "system_notice.title": "**Annonce** de Mattermost",
- "system_notice.title.gm_as_dm": "Mises à jour des conversations de groupe",
- "system_noticy.body.gm_as_dm": "Vous serez maintenant averti de toute activité dans cette conversation de groupe avec un indicateur de notification pour chaque nouveau message.{br}{br}Vous pouvez configurer cela dans les préférences de notification pour chaque conversation de groupe.",
"tag.default.beta": "BETA",
"team.button.ariaLabel": "Équipe {teamName}",
"team.button.mentions.ariaLabel": "Équipe {teamName}, {mentionCount} mentions",
diff --git a/webapp/channels/src/i18n/fy.json b/webapp/channels/src/i18n/fy.json
index a247f1895b9..fc0e8f292be 100644
--- a/webapp/channels/src/i18n/fy.json
+++ b/webapp/channels/src/i18n/fy.json
@@ -3962,13 +3962,8 @@
"suggestion.user.isCurrent": "(jij)",
"system_notice.adminVisible": "Alleen zichtbaar voor Systeembeheerders",
"system_notice.adminVisible.icon": "Alleen zichtbaar voor systeembeheerders-pictogram",
- "system_notice.body.api3": "Als je de laatste twee jaar integraties hebt gemaakt of geïnstalleerd, kan je er achter komen hoe recente wijzigingen deze kunnen hebben beïnvloed .",
- "system_notice.body.ee_upgrade_advice": "Enterprise Editie wordt aanbevolen om een optimale werking en betrouwbaarheid te garanderen. Meer informatie .",
- "system_notice.body.ie11_deprecation": "Jouw browser, IE11, wordt niet meer ondersteund in een volgende release. Ontdek hoe je in een eenvoudige stap naar een andere browser kan overschakelen .",
- "system_notice.body.permissions": "Sommige beleid-en machtigingsinstellingen voor de Systeem console zijn verplaatst met de release van geavanceerde machtigingen in Mattermost Starter en Professional.",
"system_notice.dont_show": "Niet Meer Weergeven",
"system_notice.remind_me": "Herinner me later",
- "system_notice.title": "**Melding**\nvan Mattermost",
"team.button.ariaLabel": "team {teamName}",
"team.button.mentions.ariaLabel": "team {teamName}, {mentionCount} vermeldingen",
"team.button.name_undefined": "Dit team heeft geen naam",
diff --git a/webapp/channels/src/i18n/gl.json b/webapp/channels/src/i18n/gl.json
index 7b48905a7eb..ac0b810f24b 100644
--- a/webapp/channels/src/i18n/gl.json
+++ b/webapp/channels/src/i18n/gl.json
@@ -1494,7 +1494,6 @@
"sidebar_left.sidebar_channel_menu.copyLink": "Copiar a ligazón",
"sidebar_left.sidebar_channel_menu.leaveChannel": "Abandonar a canle",
"sidebar_left.sidebar_channel_menu.leaveConversation": "Pechar a conversa",
- "system_notice.body.ee_upgrade_advice": "Recoméndase a Edición empresarial para garantir un funcionamento e fiabilidade óptimos. [Máis información](!https://mattermost.com/performance).",
"team_member_modal.invitePeople": "Convidar xente",
"unarchive_channel.cancel": "Cancelar",
"user.settings.advance.joinLeaveTitle": "Activar as mensaxes de unirse/abandonar",
diff --git a/webapp/channels/src/i18n/hr.json b/webapp/channels/src/i18n/hr.json
index 41beb533282..86e62ed3654 100644
--- a/webapp/channels/src/i18n/hr.json
+++ b/webapp/channels/src/i18n/hr.json
@@ -1586,8 +1586,6 @@
"suggestion.user.isCurrent": "(ti)",
"system_notice.dont_show": "Nemoj više prikazivati",
"system_notice.remind_me": "Podsjeti me kasnije",
- "system_notice.title": "Mattermost obavijest",
- "system_notice.title.gm_as_dm": "Aktualiziranja poruka grupa",
"tag.default.beta": "BETA",
"tag.default.bot": "BOT",
"tag.default.guest": "GOST",
diff --git a/webapp/channels/src/i18n/hu.json b/webapp/channels/src/i18n/hu.json
index 8ef18afbf89..0e6f17931b2 100644
--- a/webapp/channels/src/i18n/hu.json
+++ b/webapp/channels/src/i18n/hu.json
@@ -3908,13 +3908,8 @@
"suggestion.user.isCurrent": "(Ön)",
"system_notice.adminVisible": "Csak rendszergazdák számára látható",
"system_notice.adminVisible.icon": "Csak rendszergazdák számára látható ikon",
- "system_notice.body.api3": "Ha az elmúlt két évben létrehozott vagy telepített integrációkat, megtudhatja, hogy a legutóbbi módosítások hogyan befolyásolhatták őket.",
- "system_notice.body.ee_upgrade_advice": "Az Enterprise Edition ajánlott az optimális működés és megbízhatóság biztosítása érdekében. További információ.",
- "system_notice.body.ie11_deprecation": "Az Ön böngészőjét, az IE11-et, egy következő kiadás már nem támogatja. Tudja meg, hogyan válthat át egy másik böngészőbe egyetlen egyszerű lépésben.",
- "system_notice.body.permissions": "Egyes házirendek és jogosultságok a rendszerkonzolban áthelyeződtek a fejlett jogosultságok kiadásával a Mattermost Starter és Professional rendszerekben.",
"system_notice.dont_show": "Ne mutassa újra",
"system_notice.remind_me": "Emlékeztessen később",
- "system_notice.title": "**Értesítés**\na Mattermost-tól",
"team.button.ariaLabel": "{teamName} csapat",
"team.button.mentions.ariaLabel": "A {teamName} csapat {mentionCount} említései",
"team.button.name_undefined": "Ennek a csapatnak nincs neve",
diff --git a/webapp/channels/src/i18n/it.json b/webapp/channels/src/i18n/it.json
index 850a91a746d..2cddf4e1727 100644
--- a/webapp/channels/src/i18n/it.json
+++ b/webapp/channels/src/i18n/it.json
@@ -3050,13 +3050,8 @@
"suggestion.user.isCurrent": "(tu)",
"system_notice.adminVisible": "Visibile solo agli Amministratori di Sistema",
"system_notice.adminVisible.icon": "Icona Visibile solo agli Amministratori di Sistema",
- "system_notice.body.api3": "Se hai creato o installato delle integrazioni negli ultimi due anni, scopri come i prossimi aggiornamenti possono influenzarle.",
- "system_notice.body.ee_upgrade_advice": "La versione Enterprise è raccomandata perché assicura operatività e sicurezza ottimali. Scopri di più.",
- "system_notice.body.ie11_deprecation": "Il tuo browser, IE11, non sarà più supportato nei prossimi rilasci. Scopri come spostarti su un altro browser in un semplice passaggio.",
- "system_notice.body.permissions": "Alcune politiche e permessi nelle impostazioni della Console di Sistema sono state spostate con il rilascio dei permessi avanzati in Enterprise E10 and E20.",
"system_notice.dont_show": "Non visualizzare di nuovo",
"system_notice.remind_me": "Ricordamelo più tardi",
- "system_notice.title": "**Notifica**\nda Mattermost",
"team.button.ariaLabel": "squadra {teamName}",
"team.button.mentions.ariaLabel": "squadra {teamName}, {mentionCount} citazioni",
"team.button.name_undefined": "Questa squadra non ha un nome",
diff --git a/webapp/channels/src/i18n/ja.json b/webapp/channels/src/i18n/ja.json
index b823b4115c3..c8550ff3fa6 100644
--- a/webapp/channels/src/i18n/ja.json
+++ b/webapp/channels/src/i18n/ja.json
@@ -5762,15 +5762,8 @@
"suggestionList.suggestionsAvailable": "{count, number} {count, plural, one {提案} other {提案}} が利用可能",
"system_notice.adminVisible": "システム管理者のみ閲覧可能",
"system_notice.adminVisible.icon": "システム管理者のみ閲覧可能アイコン",
- "system_notice.body.api3": "過去2年間に統合機能を作成もしくはインストールしている場合、最近行われた変更によるそれら統合機能への影響について確認してください。",
- "system_notice.body.ee_upgrade_advice": "最適なオペレーションと信頼性を担保するためにEnterprise Editionが推奨されています。詳細.",
- "system_notice.body.ie11_deprecation": "あなたの使用しているブラウザーである Internet Explorer 11は、今後のリリースでサポートされなくなります。他のブラウザに移行する方法について確認してください。",
- "system_notice.body.permissions": "システムコンソールのいくつかポリシーや権限設定はMattermost Free/Professionalの高度な権限設定機能のリリースにより移動されました。",
"system_notice.dont_show": "二度と表示しない",
"system_notice.remind_me": "後で知らせる",
- "system_notice.title": "Mattermostからのお知らせ",
- "system_notice.title.gm_as_dm": "グループメッセージへの更新",
- "system_noticy.body.gm_as_dm": "グループメッセージのすべてのアクティビティが通知され、新着メッセージごとに通知バッジが表示されます。{br}{br}各グループメッセージの通知設定から設定を変更できます。",
"system_policy_indicator.base_message": "この {resourceType} にはシステムレベルのアクセス {policyText} が適用されています",
"system_policy_indicator.description_with_policies": "この {resourceType}にはシステムレベルのアクセスポリシーが適用されています: {policyList}。ここで設定したカスタムアクセスルールが、このポリシーに加えて適用されます。",
"system_policy_indicator.more_policies": "{count} more",
diff --git a/webapp/channels/src/i18n/ko.json b/webapp/channels/src/i18n/ko.json
index 9b107f95703..acd49ef6185 100644
--- a/webapp/channels/src/i18n/ko.json
+++ b/webapp/channels/src/i18n/ko.json
@@ -5707,15 +5707,8 @@
"suggestionList.suggestionsAvailable": "{count, plural, one {일치 항목} other {일치 항목}} {count, number}개를 선택 가능",
"system_notice.adminVisible": "시스템 관리자에게만 보여짐",
"system_notice.adminVisible.icon": "시스템 관리자에게만 보여짐",
- "system_notice.body.api3": "지난 2년 동안 통합을 만들거나 설치한 경우 최근 변경 사항( )이 통합에 어떤 영향을 미쳤는지 알아보세요.",
- "system_notice.body.ee_upgrade_advice": "최적의 작동 및 안정성을 보장하려면 Enterprise Edition이 권장됩니다. 자세히 알아보기.",
- "system_notice.body.ie11_deprecation": "사용 중인 브라우저인 IE11은 향후 릴리스에서 더 이상 지원되지 않습니다. 간단한 단계로 다른 브라우저로 이동하는 방법을 알아보세요.",
- "system_notice.body.permissions": "Enterprise E10 및 E20에서 고급 권한이 릴리즈되면서 일부 정책 및 권한 시스템 콘솔 설정이 이동했습니다.",
"system_notice.dont_show": "다시 표시하지 않음",
"system_notice.remind_me": "나중에 상기시켜 주세요.",
- "system_notice.title": "Mattermost의 공지 사항",
- "system_notice.title.gm_as_dm": "그룹 메시지 업데이트",
- "system_noticy.body.gm_as_dm": "이제 새 메시지마다 알림 배지와 함께 그룹 메시지의 모든 활동에 대한 알림을 받게 됩니다.{br}{br}각 그룹 메시지의 알림 환경설정에서 이를 구성할 수 있습니다.",
"system_policy_indicator.base_message": "이 {resourceType} 에는 시스템 수준 액세스 {policyText} 가 적용되었습니다.",
"system_policy_indicator.description_with_policies": "이 {resourceType} 에는 시스템 수준 액세스 정책이 적용됩니다. {policyList}. 여기에서 설정한 모든 사용자 지정 액세스 규칙이 이 정책과 함께 적용됩니다.",
"system_policy_indicator.more_policies": "{count} 더 보기",
diff --git a/webapp/channels/src/i18n/lt.json b/webapp/channels/src/i18n/lt.json
index 354b242c04d..7aade5d63b2 100644
--- a/webapp/channels/src/i18n/lt.json
+++ b/webapp/channels/src/i18n/lt.json
@@ -4202,13 +4202,8 @@
"suggestion.user.isCurrent": "(Jūs)",
"system_notice.adminVisible": "Mato tik sistemos administratoriai",
"system_notice.adminVisible.icon": "Mato tik sistemos administratorių piktograma",
- "system_notice.body.api3": "Jei per pastaruosius dvejus metus sukūrėte arba įdiegėte integracijas, sužinokite, kaip tai padaryti naujausi pakeitimai galėjo juos paveikti.",
- "system_notice.body.ee_upgrade_advice": "Siekiant užtikrinti optimalų veikimą ir patikimumą, rekomenduojama naudoti Enterprise Edition. Sužinokite daugiau.",
- "system_notice.body.ie11_deprecation": "Jūsų naršyklė IE11 nebebus palaikoma būsimame leidime. Sužinokite, kaip vienu paprastu veiksmu pereiti į kitą naršyklę.",
- "system_notice.body.permissions": "Kai kurie politikos ir leidimų sistemos konsolės nustatymai buvo perkelti išleidus išplėstiniai leidimai į Mattermost Nemokama ir profesionali.",
"system_notice.dont_show": "Daugiau nerodyti",
"system_notice.remind_me": "Primink man vėliau",
- "system_notice.title": "**Pranešimas**\niš Mattermost",
"tag.default.beta": "BETA",
"tag.default.bot": "BOTAS",
"tag.default.guest": "SVEČIAS",
diff --git a/webapp/channels/src/i18n/nb-NO.json b/webapp/channels/src/i18n/nb-NO.json
index 2cf498e0e4e..5aac47bcb9c 100644
--- a/webapp/channels/src/i18n/nb-NO.json
+++ b/webapp/channels/src/i18n/nb-NO.json
@@ -4898,15 +4898,8 @@
"suggestionList.label": "Forslag",
"suggestionList.noMatches": "Ingenting matcher {value}",
"system_notice.adminVisible": "Kun synlig for systemadministratorer",
- "system_notice.body.api3": "Hvis du har opprettet eller installert integrasjoner i løpet av de siste to årene, kan du finne ut hvordan nylige endringer kan ha påvirket dem.",
- "system_notice.body.ee_upgrade_advice": "Enterprise Edition anbefales for å sikre optimal drift og pålitelighet. Finn ut mer.",
- "system_notice.body.ie11_deprecation": "Nettleseren din, IE11, vil ikke lenger støttes i en kommende versjon. Finn ut hvordan du lett kan gå over til en annen nettleser.",
- "system_notice.body.permissions": "Noen innstillinger for policyer og tillatelser i systemkonsollen har blitt flyttet med lanseringen av Avanserte tillatelser til Mattermost Free og Professional.",
"system_notice.dont_show": "Ikke vis igjen",
"system_notice.remind_me": "Påminn meg senere",
- "system_notice.title": "Melding fra Mattermost",
- "system_notice.title.gm_as_dm": "Oppdateringer til gruppemeldinger",
- "system_noticy.body.gm_as_dm": "Du vil nå bli varslet for all aktivitet i gruppemeldingene dine sammen med et varslingsmerke for hver ny melding.{br}{br}Du kan konfigurere dette i varselinnstillingene for hver gruppemelding.",
"tag.default.beta": "BETA",
"tag.default.bot": "BOT",
"tag.default.guest": "GJEST",
diff --git a/webapp/channels/src/i18n/nl.json b/webapp/channels/src/i18n/nl.json
index 24486aae6ce..9b6baa41afc 100644
--- a/webapp/channels/src/i18n/nl.json
+++ b/webapp/channels/src/i18n/nl.json
@@ -172,7 +172,7 @@
"add_oauth_app.icon.help": "(Optioneel) Geef de URL van de afbeelding op voor jouw OAuth 2.0-toepassing. Gebruik HTTP of HTTPS in de URL.",
"add_oauth_app.name.help": "Geef de weergavenaam op van jouw OAuth 2.0-toepassing. Je kan maximaal 64 tekens gebruiken.",
"add_oauth_app.nameRequired": "Naam van de OAuth 2.0 applicatie is verplicht.",
- "add_oauth_app.public.help": "Als dit ingesteld is, is de OAuth 2.0 toepassing een publieke client (geen clientgeheim). Openbare clients moeten PKCE gebruiken voor autorisatie. Indien niet ingeschakeld dan is de applicatie een vertrouwelijke client met een clientgeheim.",
+ "add_oauth_app.public.help": "Als dit actief is, is de OAuth 2.0 toepassing een publieke client (geen clientgeheim). Openbare clients moeten PKCE gebruiken voor autorisatie. Indien niet ingeschakeld dan is de applicatie een vertrouwelijke client met een clientgeheim. Deze instelling kan niet worden gewijzigd nadat de toepassing is gemaakt.",
"add_oauth_app.trusted.help": "Als dit ingeschakeld is, wordt de OAuth 2.0-toepassing geacht te worden vertrouwd door de Mattermost-server en is het niet nodig dat de gebruiker de machtiging accepteert. Als dit uitgeschakeld is, wordt er een venster geopend waarin de gebruiker gevraagd wordt de machtiging te accepteren of te weigeren.",
"add_oauth_app.url": "URL(s): {url}",
"add_outgoing_oauth_connection.add": "Toevoegen",
@@ -5916,15 +5916,8 @@
"suggestionList.suggestionsAvailable": "{count, number} {count, plural, one {suggestie} other {suggesties}} beschikbaar",
"system_notice.adminVisible": "Alleen zichtbaar voor Systeembeheerders",
"system_notice.adminVisible.icon": "Alleen zichtbaar voor systeembeheerders-pictogram",
- "system_notice.body.api3": "Als je de laatste twee jaar integraties hebt gemaakt of geïnstalleerd, kan je er achter komen hoe recente wijzigingen deze kunnen hebben beïnvloed .",
- "system_notice.body.ee_upgrade_advice": "Enterprise Editie wordt aanbevolen om een optimale werking en betrouwbaarheid te garanderen. Meer informatie .",
- "system_notice.body.ie11_deprecation": "Jouw browser, IE11, wordt niet meer ondersteund in een volgende release. Ontdek hoe je in een eenvoudige stap naar een andere browser kan overschakelen .",
- "system_notice.body.permissions": "Sommige beleid-en machtigingsinstellingen voor de Systeem console zijn verplaatst met de release van geavanceerde machtigingen in Mattermost Free en Professional.",
"system_notice.dont_show": "Niet meer weergeven",
"system_notice.remind_me": "Herinner me later",
- "system_notice.title": "**Melding**\nvan Mattermost",
- "system_notice.title.gm_as_dm": "Updates voor groepsberichten",
- "system_noticy.body.gm_as_dm": "Je krijgt nu een melding voor alle activiteit in je groepsberichten, samen met een meldingsbadge voor elk nieuw bericht.{br}{br}Je kunt dit instellen in de meldingsvoorkeuren voor elk groepsbericht.",
"system_policy_indicator.base_message": "Op deze {resourceType} is toegang op systeemniveau {policyText} toegepast",
"system_policy_indicator.description_with_policies": "Op deze {resourceType} is toegangsbeleid op systeemniveau toegepast: {policyList}. Alle aangepaste toegangsregels die je hier instelt, worden naast dit beleid toegepast.",
"system_policy_indicator.more_policies": "{count} meer",
diff --git a/webapp/channels/src/i18n/pl.json b/webapp/channels/src/i18n/pl.json
index 7f936325078..72b6b03059d 100644
--- a/webapp/channels/src/i18n/pl.json
+++ b/webapp/channels/src/i18n/pl.json
@@ -172,7 +172,7 @@
"add_oauth_app.icon.help": "(Opcjonalnie) Określ adres URL obrazu dla aplikacji OAuth 2.0. W adresie URL należy użyć HTTP lub HTTPS.",
"add_oauth_app.name.help": "Określ nazwę wyświetlaną aplikacji OAuth 2.0. Możesz użyć do 64 znaków.",
"add_oauth_app.nameRequired": "Nazwa aplikacji OAuth 2.0 jest wymagana.",
- "add_oauth_app.public.help": "Jeśli włączone, aplikacja OAuth 2.0 jest klientem publicznym (bez sekretu klienta). Klienci publiczni muszą używać PKCE do autoryzacji. Jeśli wyłączone, aplikacja jest klientem poufnym z sekretem klienta.",
+ "add_oauth_app.public.help": "Jeśli włączone, aplikacja OAuth 2.0 jest klientem publicznym (bez sekretu klienta). Klienci publiczni muszą używać PKCE do autoryzacji. Jeśli wyłączone, aplikacja jest klientem poufnym z sekretem klienta. Tego ustawienia nie można zmienić po utworzeniu aplikacji.",
"add_oauth_app.trusted.help": "Gdy włączone, aplikacja OAuth 2.0 jest uznawana za zaufaną przez serwer Mattermost i nie wymaga akceptacji autoryzacji przez użytkownika. Gdy wyłączone, pojawi się dodatkowe okienko proszące użytkownika o zaakceptowanie lub odrzucenie autoryzacji.",
"add_oauth_app.url": "Adresy URL: {url}",
"add_outgoing_oauth_connection.add": "Dodaj",
@@ -5916,15 +5916,8 @@
"suggestionList.suggestionsAvailable": "{count, number} {count, plural, one {sugestia dostępna} other {sugestii dostępnych}}",
"system_notice.adminVisible": "Widoczne tylko dla Administratorów Systemu",
"system_notice.adminVisible.icon": "Ikona widoczne tylko dla administratorów",
- "system_notice.body.api3": "Jeśli utworzyłeś lub zainstalowałeś integracje w ciągu ostatnich dwóch lat, dowiedz się, jak ostatnie zmiany mogły na nie wpłynąć.",
- "system_notice.body.ee_upgrade_advice": "Edycja Enterprise jest zalecana w celu zapewnienia optymalnego działania i niezawodności. Dowiedz się więcej.",
- "system_notice.body.ie11_deprecation": "Twoja przeglądarka IE11 nie będzie już obsługiwana w nadchodzącym wydaniu. Dowiedz się, jak przejść do innej przeglądarki w jednym prostym kroku.",
- "system_notice.body.permissions": "Niektóre ustawienia zasad i uprawnień zostały przeniesione wraz z wydaniem zaawansowanych uprawnień wMattermost Free and Professional.",
"system_notice.dont_show": "Nie pokazuj ponownie",
"system_notice.remind_me": "Przypomnij mi później",
- "system_notice.title": "Uwaga od Mattermost",
- "system_notice.title.gm_as_dm": "Aktualizacje dotyczące wiadomości grupowych",
- "system_noticy.body.gm_as_dm": "Otrzymasz powiadomienia o wszelkiej aktywności w twoich wiadomościach grupowych wraz ze znacznikiem powiadomienia przy każdej wiadomości.{br}{br}Możesz skonfigurować te preferencje powiadomień dla każdej z wiadomości grupowej.",
"system_policy_indicator.base_message": "Ta strona {resourceType} ma zastosowany dostęp na poziomie systemu {policyText}",
"system_policy_indicator.description_with_policies": "Ta strona {resourceType} ma zastosowane polityki dostępu na poziomie systemu: {policyList}. Wszelkie niestandardowe reguły dostępu, które tutaj ustawisz, zostaną zastosowane oprócz tej polityki.",
"system_policy_indicator.more_policies": "{count} więcej",
diff --git a/webapp/channels/src/i18n/pt-BR.json b/webapp/channels/src/i18n/pt-BR.json
index 567b30e2a56..fb6bd2c4bc4 100644
--- a/webapp/channels/src/i18n/pt-BR.json
+++ b/webapp/channels/src/i18n/pt-BR.json
@@ -4247,13 +4247,8 @@
"suggestion.user.isCurrent": "(você)",
"system_notice.adminVisible": "Somente visivel para os Administradores de Sistema",
"system_notice.adminVisible.icon": "Ícone Somente visível para os Administradores de Sistema",
- "system_notice.body.api3": "Se você criou ou instalou integrações nos últimos dois anos, descubra como as mudanças recentes podem tê-las afetado.",
- "system_notice.body.ee_upgrade_advice": "Enterprise Edition é recomendado para garantir uma operação e confiabilidade ideais. Saiba mais.",
- "system_notice.body.ie11_deprecation": "Seu navegador, IE11, não será mais suportado na próxima versão. Descubra como mudar para outro navegador em um único passo.",
- "system_notice.body.permissions": "Algumas configurações de diretiva e permissão do Console do sistema foram movidas com o lançamento de permissões avançadas em Enterprise E10 e E20.",
"system_notice.dont_show": "Não Mostrar Novamente",
"system_notice.remind_me": "Lembre-me depois",
- "system_notice.title": "**Aviso**\nde Mattermost",
"tag.default.beta": "BETA",
"tag.default.bot": "BOT",
"tag.default.guest": "CONVIDADO",
diff --git a/webapp/channels/src/i18n/pt.json b/webapp/channels/src/i18n/pt.json
index 8565799e462..aac1f64620e 100644
--- a/webapp/channels/src/i18n/pt.json
+++ b/webapp/channels/src/i18n/pt.json
@@ -164,7 +164,7 @@
"add_oauth_app.icon.help": "(Opcional) Especifique o URL da imagem para a sua aplicação OAuth 2.0. Utilize HTTP ou HTTPS no URL.",
"add_oauth_app.name.help": "Especifique o nome a exibir da sua aplicação OAuth 2.0. Pode utilizar até 64 carateres.",
"add_oauth_app.nameRequired": "É necessário o nome para a sua aplicação OAuth 2.0.",
- "add_oauth_app.public.help": "Se for verdade, a aplicação OAuth 2.0 é um cliente público (sem segredo do cliente). Os clientes públicos devem utilizar PKCE para autorização. Se for falso, a aplicação é um cliente confidencial com um segredo do cliente.",
+ "add_oauth_app.public.help": "Se verdade, a aplicação OAuth 2.0 é um cliente público (sem segredo de cliente). Os clientes públicos devem utilizar PKCE para a autorização. Se falso, a aplicação é um cliente confidencial com um segredo de cliente. Esta definição não pode ser alterada depois de ser criada a aplicação.",
"add_oauth_app.trusted.help": "Se verdadeiro, a aplicação OAuth 2.0 é considerada confiável pelo servidor Mattermost e não requer que o utilizador aceite a autorização. Se falso, é aberta uma janela para pedir ao utilizador que aceite ou negue a autorização.",
"add_oauth_app.url": "URL(s): {url}",
"add_outgoing_oauth_connection.add": "Adicionar",
@@ -972,7 +972,7 @@
"admin.license.enterprise.upgrade": "Atualizar para Edição de Empresa",
"admin.license.enterprise.upgrade.eeLicenseLink": "Licença da Edição de Empresa",
"admin.license.enterprise.upgrading": "A atualizar {percentage}%",
- "admin.license.entryPlanSubtitle": "Compre um plano para desbloquear o acesso completo, ou inicie uma versão de testes para remover os limites enquanto avalia Enterprise Advanced.",
+ "admin.license.entryPlanSubtitle": "Compre um plano para desbloquear o acesso completo, ou inicie uma versão de testes para remover os limites enquanto avalia o Enterprise Advanced.",
"admin.license.freeEdition.title": "Gratuita",
"admin.license.key": "Código da licença: ",
"admin.license.keyAddNew": "Adicionar uma nova licença",
diff --git a/webapp/channels/src/i18n/ro.json b/webapp/channels/src/i18n/ro.json
index 04621ba4af3..598ff8ec908 100644
--- a/webapp/channels/src/i18n/ro.json
+++ b/webapp/channels/src/i18n/ro.json
@@ -3279,13 +3279,8 @@
"suggestion.user.isCurrent": "(tu)",
"system_notice.adminVisible": "Este vizibil numai pentru administratorii de sistem",
"system_notice.adminVisible.icon": "Este vizibil numai pentru pictograma System Admins",
- "system_notice.body.api3": "Dacă ați creat sau instalat integrări în ultimii doi ani, pentru a afla cât modificări recente poate fi afectat de ele.",
- "system_notice.body.ee_upgrade_advice": "Ediția Enterprise este recomandata pentru a asigura o funcționare optimă și fiabilitate. Aflați mai multe.",
- "system_notice.body.ie11_deprecation": "Browserul tău, IE11, nu va mai fi acceptat într-o lansare viitoare. Aflați cum să vă mutați într-un alt browser într-un singur pas simplu.",
- "system_notice.body.permissions": "Unele politică și permisiunea de Sistem Consola de setări-au mutat cu eliberarea de permisiuni avansate în Întreprindere E10 și E20.",
"system_notice.dont_show": "Nu mai arăta",
"system_notice.remind_me": "Amintește-mi mai târziu",
- "system_notice.title": "**Notă**\nde la Mattermost",
"team.button.ariaLabel": "Echipa {teamName}",
"team.button.mentions.ariaLabel": "{teamName} echipa, {mentionCount} mențiuni",
"team.button.name_undefined": "[NUME NEDEFINIT]",
diff --git a/webapp/channels/src/i18n/ru.json b/webapp/channels/src/i18n/ru.json
index 787a1fadb53..64383f65541 100644
--- a/webapp/channels/src/i18n/ru.json
+++ b/webapp/channels/src/i18n/ru.json
@@ -4937,15 +4937,8 @@
"suggestion.user.isCurrent": "(это вы)",
"system_notice.adminVisible": "Видно только системным администраторам",
"system_notice.adminVisible.icon": "Значок \"Видно только системным администраторам\"",
- "system_notice.body.api3": "Если вы создавали или устанавливали интеграции за последние два года, узнайте, как недавние изменения могли на них повлиять.",
- "system_notice.body.ee_upgrade_advice": "Корпоративная редакция рекомендуется для обеспечения оптимальной работы и надежности. Узнать больше.",
- "system_notice.body.ie11_deprecation": "Ваш браузер IE11 больше не будет поддерживаться в следующем выпуске. Узнайте, как перейти в другой браузер за один простой шаг.",
- "system_notice.body.permissions": "Некоторые параметры системной консоли политики и разрешений изменились с выпуском расширенных разрешений в Mattermost Free и Professional.",
"system_notice.dont_show": "Больше не показывать",
"system_notice.remind_me": "Напомни мне позже",
- "system_notice.title": "Уведомление от Mattermost",
- "system_notice.title.gm_as_dm": "Обновления в групповых сообщениях",
- "system_noticy.body.gm_as_dm": "Теперь вы будете получать уведомления обо всех действиях в групповых сообщениях, а также будет появляться значок уведомления о каждом новом сообщении.{br}{br}Это можно настроить в настройках уведомлений для каждого группового сообщения.",
"tag.default.beta": "БЕТА",
"tag.default.bot": "БОТ",
"tag.default.guest": "ГОСТЬ",
diff --git a/webapp/channels/src/i18n/sv.json b/webapp/channels/src/i18n/sv.json
index 93a85ba667f..e909138d671 100644
--- a/webapp/channels/src/i18n/sv.json
+++ b/webapp/channels/src/i18n/sv.json
@@ -288,6 +288,9 @@
"admin.access_control.policies.resources.channels": "{count, number} {count, plural, one {kanal} other {kanaler}}",
"admin.access_control.policies.resources.none": "Ingen",
"admin.access_control.policies.title": "Policyer för åtkomstkontroll",
+ "admin.access_control.policy.channel_list.autoAddHeader": "Lägg till medlemmar automatiskt",
+ "admin.access_control.policy.channel_list.off": "Av",
+ "admin.access_control.policy.channel_list.on": "På",
"admin.access_control.policy.channels_affected": "Är du säker på att du vill spara och tillämpa policyn för åtkomstkontroll?",
"admin.access_control.policy.edit_policy.access_rules.subtitle": "Välj användarattribut och värden att använda i regler för att begränsa medlemskap i kanaler.",
"admin.access_control.policy.edit_policy.access_rules.title": "Attributbaserade åtkomstregler",
@@ -394,6 +397,7 @@
"admin.authentication.openid": "OpenID Connect",
"admin.authentication.saml": "SAML 2.0",
"admin.authentication.signup": "Registrera användarkonto",
+ "admin.auto_translation_feature_discovery.title": "Ta bort språk-barriärer med automatisk översättning",
"admin.banner.heading": "Observera:",
"admin.billing.company_info.add": "Lägg till företagsinformation",
"admin.billing.company_info.address": "Adress",
@@ -980,6 +984,8 @@
"admin.email.pushOffHelp": "Läs dokumentationen om push-meddelanden för att lära dig mer om inställningsalternativen.",
"admin.email.pushServerEx": "T.ex.: \"https://push-test.mattermost.com\"",
"admin.email.pushServerLocationDE": "Tyskland",
+ "admin.email.pushServerLocationGlobal": "Global",
+ "admin.email.pushServerLocationJP": "Japan",
"admin.email.pushServerLocationTitle": "Platsen där servern för push-aviseringar finns:",
"admin.email.pushServerLocationUS": "USA",
"admin.email.pushServerTitle": "Server för push-meddelanden:",
@@ -1168,6 +1174,7 @@
"admin.false": "avaktiverad",
"admin.featureDiscovery.WarningDescription": "Din licens uppdateras för att ge dig full tillgång till alla Enterprise-funktioner. Den här sidan uppdateras automatiskt när licensuppdateringen är klar. Vänligen vänta ",
"admin.featureDiscovery.WarningTitle": "Din prova-på-period har startat och din licens uppdateras.",
+ "admin.feature_discovery.learn_more": "Lär dig mer",
"admin.feature_discovery.trial-request.accept-terms": "Genom att klicka på Starta prova-på-period godkänner jag Mattermost Software Evaluation Agreement, Privacy Policy och att få mejl med produktinformation.",
"admin.feature_discovery.trial-request.error": "Testlicens kunde inte hämtas. Besök https://mattermost.com/trial/ för att efterfråga en licens.",
"admin.feature_flags.flag": "Flagga",
@@ -1779,6 +1786,7 @@
"admin.notices.enableAdminNoticesTitle": "Aktivera administratörsnotiser: ",
"admin.notices.enableEndUserNoticesDescription": "När aktiverad kommer alla användare få notiser om tillgängliga klientuppdateringar och relevanta användarfunktioner för att förbättra användarupplevelsen. Läs mer om notiser i dokumentationen.",
"admin.notices.enableEndUserNoticesTitle": "Aktivera slutanvändarnotiser: ",
+ "admin.oauth.dcrTitle": "Aktivera OAuth 2.0 Dynamic Client Registration: ",
"admin.oauth.gitlab": "GitLab",
"admin.oauth.google": "Google Apps",
"admin.oauth.off": "Tillåt inte inloggning med en OAuth 2.0 provider",
@@ -1863,6 +1871,7 @@
"admin.permissions.group.guest_use_group_mentions.name": "Gruppomnämnanden",
"admin.permissions.group.integrations.description": "Hantera OAuth 2.0, slash-kommandon, webhooks och emoijer.",
"admin.permissions.group.integrations.name": "Integration & anpassningar",
+ "admin.permissions.group.manage_oauth.name": "Hantera OAuth-applikationer",
"admin.permissions.group.manage_private_channel_bookmarks.description": "Lägg till, redigera, ta bort och sortera bokmärken",
"admin.permissions.group.manage_private_channel_bookmarks.name": "Hantera bokmärken",
"admin.permissions.group.manage_private_channel_members_and_read_groups.description": "Lägg till och ta bort medlemmar i privata kanaler (inklusive kanaladministratörer).",
@@ -1873,6 +1882,7 @@
"admin.permissions.group.manage_public_channel_members_and_read_groups.name": "Hantera medlemmar i kanaler",
"admin.permissions.group.manage_shared_channels.description": "Hantera delade kanaler",
"admin.permissions.group.manage_shared_channels.name": "Delade kanaler",
+ "admin.permissions.group.manage_slash_commands.name": "Hantera Snedstreck-kommandon",
"admin.permissions.group.playbook_private.description": "Hantera privata playbooks.",
"admin.permissions.group.playbook_private.name": "Hantera privata Playbooks",
"admin.permissions.group.playbook_public.description": "Hantera publika playbooks.",
@@ -1899,6 +1909,7 @@
"admin.permissions.loadingMoreSchemes": "LAddar...",
"admin.permissions.permission.assign_system_admin_role.description": "Tilldela systemadministratörsroll",
"admin.permissions.permission.assign_system_admin_role.name": "Tilldela systemadministratörsroll",
+ "admin.permissions.permission.bypass_incoming_webhook_channel_lock.name": "Gå förbi kanal-lås",
"admin.permissions.permission.convert_private_channel_to_public.description": "Konvertera privata kanaler till publika",
"admin.permissions.permission.convert_private_channel_to_public.name": "Konvertera till publik",
"admin.permissions.permission.convert_public_channel_to_private.description": "Konvertera publika kanaler till privata",
@@ -1967,6 +1978,9 @@
"admin.permissions.permission.manage_outgoing_oauth_connections.name": "Hantera utgående OAuth-autentiseringsuppgifter",
"admin.permissions.permission.manage_outgoing_webhooks.description": "Skapa, ediera och radera utgående webhooks som ägs av andra.",
"admin.permissions.permission.manage_outgoing_webhooks.name": "Hantera andras",
+ "admin.permissions.permission.manage_own_incoming_webhooks.name": "Hantera egen",
+ "admin.permissions.permission.manage_own_outgoing_webhooks.name": "Hantera egen",
+ "admin.permissions.permission.manage_own_slash_commands.name": "Hantera egen",
"admin.permissions.permission.manage_private_channel_banner.description": "Aktivera, inaktivera och redigera kanalens banner.",
"admin.permissions.permission.manage_private_channel_banner.name": "Hantera kanalens banner",
"admin.permissions.permission.manage_private_channel_properties.description": "Uppdatera namn, kanalens sidhuvud och syfte på privata kanaler.",
@@ -2261,6 +2275,12 @@
"admin.plugins.settings.marketplaceUrlDesc.empty": " Marknadsplatsens URL är ett obligatoriskt fält.",
"admin.plugins.settings.requirePluginSignature": "Kräv Plugin-signatur:",
"admin.plugins.settings.requirePluginSignatureDesc": "När aktiverad kan plugins endast installeras via Marknadsplatsen, ej laddas upp manuellt. Plugins är alltid verifierade under Mattermost-serverns start och initiering. Läs dokumentationen för mer information.",
+ "admin.posts.burnOnRead.duration.10min": "10 minuter",
+ "admin.posts.burnOnRead.duration.1hour": "1 timme",
+ "admin.posts.burnOnRead.duration.1min": "1 minut",
+ "admin.posts.burnOnRead.duration.30min": "30 minuter",
+ "admin.posts.burnOnRead.duration.5min": "5 minuter",
+ "admin.posts.burnOnRead.duration.8hours": "8 timmar",
"admin.posts.persistentNotifications.desc": "När aktiverat kan användare skapa upprepade aviseringar för mottagarna av brådskande meddelanden. Läs mer om meddelandeprioritet och varaktiga aviseringar i vår dokumentation .",
"admin.posts.persistentNotifications.title": "Varaktig avisering",
"admin.posts.persistentNotificationsGuests.desc": "Huruvida en gäst kan begära återkommande aviseringar. Läs mer om meddelandeprioritet och varaktiga aviseringar i vår dokumentation .",
@@ -2533,6 +2553,7 @@
"admin.secure_connections.details.shared_channels.title": "Delade kanaler",
"admin.secure_connections.details.subtitle": "Anslutningsnamn och andra behörigheter",
"admin.secure_connections.details.team.help": "Välj vilket team som nya delade kanaler ska placeras i. {link}",
+ "admin.secure_connections.details.team.help.learn_more": "Lär dig mer",
"admin.secure_connections.details.team.label": "Mottagande Team",
"admin.secure_connections.details.title": "Anslutningsdetaljer",
"admin.secure_connections.menu.accept_invitation": "Acceptera en inbjudan",
@@ -2756,6 +2777,15 @@
"admin.site.emoji": "Emoij",
"admin.site.fileSharingDownloads": "Fildelning och nedladdningar",
"admin.site.localization": "Lokalisering",
+ "admin.site.localization.autoTranslationProviderLibreTranslateAPIKeyExample": "Fyll i API-nyckeln för LibreTranslate",
+ "admin.site.localization.autoTranslationProviderLibreTranslateAPIKeyTitle": "API-nyckel för LibreTranslate:",
+ "admin.site.localization.autoTranslationProviderLibreTranslateURLExample": "t.ex: \"https://libretranslate.dindomän.com\"",
+ "admin.site.localization.autoTranslationProviderLibreTranslateURLTitle": "Slutpunkt för LibreTranslate's API:",
+ "admin.site.localization.autoTranslationProviderTitle": "Översättningstjänst:",
+ "admin.site.localization.auto_translation.off": "Av",
+ "admin.site.localization.auto_translation.on": "På",
+ "admin.site.localization.enableAutoTranslationTitle": "Automatisk översättning",
+ "admin.site.localization.languages.title": "Språk",
"admin.site.move_thread": "Flytta tråd",
"admin.site.notices": "Meddelanden",
"admin.site.posts": "Meddelanden",
@@ -3196,6 +3226,12 @@
"advanced_text_editor.remote_user_hour": "Hos {user} är klockan {time}",
"advanced_textbox.max_length_error": "Texten överskrider maxantalet tecken på {maxLength} tecken.",
"advanced_textbox.min_length_error": "Texten måste bestå av minst {minLength} tecken.",
+ "agent.buttonAriaLabel": "Agent-väljare",
+ "agent.chooseBot": "VÄLJ EN BOT",
+ "agent.default": "standard",
+ "agent.generateWith": "GENERERA MED:",
+ "agent.menuAriaLabel": "Välj agent",
+ "agent.selectBot": "Välj en bot",
"air_gapped_contact_sales_modal.body": "Använd länken nedan för att kontakta säljavdelningen.",
"air_gapped_contact_sales_modal.title": "Det verkar som du inte har anslutning till internet",
"air_gapped_modal.close": "Stäng",
@@ -3479,6 +3515,13 @@
"bots.token.confirm": "Ta bort",
"bots.token.confirm_text": "Är du säker att du vill radera token?",
"bots.token.delete": "Radera token",
+ "burn_on_read.badge.recipient.click_to_reveal": "Klicka för att avslöja",
+ "burn_on_read.button.tooltip.title": "Bränn vid läsning",
+ "burn_on_read.label.remove": "Ta bort Bränn vid läsning",
+ "burn_on_read.label.text": "BRÄNN VID LÄSNING ({duration}m)",
+ "burn_on_read.tour_tip.badge": "NY",
+ "burn_on_read.tour_tip.dismiss": "Avvisa",
+ "burn_on_read.tour_tip.try_it_out": "Prova det",
"call_button.menuAriaLabel": "Välj samtalstyp",
"carousel.PreviousButton": "Föregående",
"carousel.nextButton": "Nästa",
@@ -3717,6 +3760,7 @@
"channel_settings.access_rules.auto_sync_requires_expression": "Definiera åtkomstregler för att aktivera automatisk tillägg av medlemmar",
"channel_settings.access_rules.confirm_modal.allowed_tab": "Tillåtet ({count})",
"channel_settings.access_rules.confirm_modal.cancel": "Avbryt",
+ "channel_settings.access_rules.confirm_modal.continue": "Fortsätt",
"channel_settings.access_rules.confirm_modal.hide_users": "Dölj användare",
"channel_settings.access_rules.confirm_modal.message": "Genom att tillämpa dessa åtkomstregler kommer {addCount, number} {addCount, plural, one {användare} other {användare}} att läggas till i kanalen och {removeCount, number} {removeCount, plural, one {medlem} other {medlemmar}} att tas bort från den aktuella kanalen.",
"channel_settings.access_rules.confirm_modal.no_users": "Inga användare i denna kategori",
@@ -3735,6 +3779,7 @@
"channel_settings.access_rules.save_error": "Misslyckades med att spara åtkomstregler",
"channel_settings.access_rules.subtitle": "Välj användarattribut och värden att använda i regler för att begränsa medlemskap i kanaler",
"channel_settings.access_rules.title": "Åtkomstregler",
+ "channel_settings.activity_warning.save_and_apply": "Spara och verkställ",
"channel_settings.archive.button": "Arkivera den här kanalen",
"channel_settings.archive.warning": "När du arkiverar en kanal tas den bort från användargränssnittet, men kanalen tas inte bort permanent. Nya meddelanden kan inte postas till arkiverade kanaler.",
"channel_settings.channel_info_tab.name": "Information om kanalen",
@@ -4494,6 +4539,7 @@
"installed_integrations.callback_urls": "Callback URLs: {urls}",
"installed_integrations.client_id": "Klient ID: ",
"installed_integrations.client_secret": "Klient-hemlighet: ",
+ "installed_integrations.client_type": "Typ av klient: ",
"installed_integrations.content_type": "Innehållstyp: {contentType}",
"installed_integrations.creation": "Skapad av {creator} den {createAt, date, full}",
"installed_integrations.delete": "Ta bort",
@@ -4516,6 +4562,8 @@
"installed_oauth_apps.cancel": "Avbryt",
"installed_oauth_apps.delete.confirm": "Den här åtgärden kommer radera slash-kommandot och skada eventuella integrationer som använder kommandot. Är du säker på att du vill radera?",
"installed_oauth_apps.description": "Beskrivning",
+ "installed_oauth_apps.dynamically_registered": "Dynamiskt registrerad: ",
+ "installed_oauth_apps.dynamically_registered.yes": "Ja",
"installed_oauth_apps.empty": "Inga OAuth 2.0 Applikationer hittades",
"installed_oauth_apps.emptySearch": "Inga OAuth 2.0-applikationer matchade **{searchTerm}**",
"installed_oauth_apps.header": "OAuth 2.0-applikationer",
@@ -4526,6 +4574,10 @@
"installed_oauth_apps.iconUrl": "Ion-URL",
"installed_oauth_apps.is_trusted": "Är tillförlitlig: ",
"installed_oauth_apps.name": "Visningsnamn",
+ "installed_oauth_apps.public": "Är Publik Klient",
+ "installed_oauth_apps.public.no": "Nej",
+ "installed_oauth_apps.public.yes": "Ja",
+ "installed_oauth_apps.public_client": "Publik klient (Ingen hemlighet)",
"installed_oauth_apps.save": "Spara",
"installed_oauth_apps.saving": "Sparar...",
"installed_oauth_apps.search": "Sök efter OAuth 2.0 Applikationer",
@@ -4741,6 +4793,7 @@
"login.contact_admin.detail": "För att få tillgång till ditt teams arbetsyta kontaktar du arbetsytans administratör. Om du redan har blivit inbjuden kan du kontrollera din mejl efter en inbjudan till arbetsytan från Mattermost.",
"login.contact_admin.title": "Kontakta administratören för din arbetsyta",
"login.createTeam": "Skapa team",
+ "login.deactivatedUser": "Ditt konto har stängts av.",
"login.email": "E-post",
"login.forgot": "Har du glömt ditt lösenord?",
"login.get_terms_error": "Det går inte att hämta användarvillkoren. Om problemet kvarstår, kontakta din systemadministratör.",
@@ -4773,6 +4826,7 @@
"login.saml": "SAML",
"login.session_expired": "Din session har avslutats. Var vänlig och logga in igen.",
"login.session_expired.notification": "Sessionen har upphört: Logga in igen för att ta emot notifieringar.",
+ "login.session_expired.notification.title": "Sessionen upphörde att gälla",
"login.session_expired.title": "* Sessionen har gått ut - {siteName}",
"login.subtitle": "Samarbeta med ditt team i realtid",
"login.terms_rejected": "Du måste godkänna användarvillkoren innan du använder {siteName}. Kontakta systemadministratören för detaljer.",
@@ -5185,6 +5239,8 @@
"post_info.actions.tutorialTip": "Meddelandeåtgärder som tillhandahålls\ngenom appar, integrationer eller plugins\n har flyttats till det här menyalternativet.",
"post_info.actions.tutorialTip.title": "Åtgärder för meddelanden",
"post_info.actions.visitMarketplace": "Besök Marketplace",
+ "post_info.ai_generated.by_user": "Meddelandet publicerades av @{username}",
+ "post_info.ai_generated.self": "AI-genererad",
"post_info.auto_responder": "AUTOMATISKT SVAR",
"post_info.comment_icon.tooltip.reply": "Svara",
"post_info.copy": "Kopiera text",
@@ -5793,15 +5849,8 @@
"suggestionList.suggestionsAvailable": "{count, number} {count, plural, one {förslag} other {förslag}} tillgängliga",
"system_notice.adminVisible": "Visas endast för systemadministratörer",
"system_notice.adminVisible.icon": "Ikon för Visas endast för systemadministratörer",
- "system_notice.body.api3": "Om du har skapat eller installerat integrationer de senaste två åren bör du kolla hur de senaste förändringar kan ha påverkat dem.",
- "system_notice.body.ee_upgrade_advice": "Enterprise Edition är rekommenderad för optimal funktionalitet och tillförlitlighet. Läs mer.",
- "system_notice.body.ie11_deprecation": "Din webbläsare, IE11, kommer inte stödjas i kommande versioner. Läs mer om hur du byter till annan webbläsare i ett enkelt steg.",
- "system_notice.body.permissions": "Några inställningar för policy och behörigheter har flyttats i konsolinställningar efter versionen med avancerade rättigheter in i Mattermost Free och Professional.",
"system_notice.dont_show": "Visa inte igen",
"system_notice.remind_me": "Påminn mig senare",
- "system_notice.title": "Notis från Mattermost",
- "system_notice.title.gm_as_dm": "Uppdateringar av gruppmeddelanden",
- "system_noticy.body.gm_as_dm": "Du kommer nu att notifieras om all aktivitet i dina gruppmeddelanden tillsammans med en aviseringsbricka för varje nytt meddelande.{br}{br}Du kan konfigurera detta i aviseringsinställningarna för varje gruppmeddelande.",
"system_policy_indicator.base_message": "På denna {resourceType} har åtkomst på systemnivå {policyText} tillämpats",
"system_policy_indicator.description_with_policies": "Denna {resourceType} har åtkomstpolicyer på systemnivå: {policyList}. Eventuella anpassade åtkomstregler som du anger här kommer att tillämpas utöver denna policy.",
"system_policy_indicator.more_policies": "{count} mer",
@@ -5844,6 +5893,7 @@
"terms_of_service.agreeButton": "Jag godkänner",
"terms_of_service.api_error": "Det går inte att slutföra begäran. Om problemet kvarstår, kontakta din systemadministratör.",
"terms_of_service.disagreeButton": "Jag håller inte med",
+ "test.description": "Formaterad beskrivning",
"test1": "Hjälptext",
"test2": "Knapptext",
"textbox.bold": "**fetstil**",
@@ -5855,6 +5905,20 @@
"textbox.preview": "Förhandsgranska",
"textbox.quote": ">citat",
"textbox.strike": "genomstruken",
+ "texteditor.rewrite": "Skriv om",
+ "texteditor.rewrite.create": "Skapa ett nytt meddelande...",
+ "texteditor.rewrite.discard": "Kasta bort",
+ "texteditor.rewrite.elaborate": "Utveckla",
+ "texteditor.rewrite.fixSpelling": "Fixa stavning och grammatik",
+ "texteditor.rewrite.improveWriting": "Förbättra skrivning",
+ "texteditor.rewrite.menu": "Alternativ för att Skriva om",
+ "texteditor.rewrite.nextPrompt": "Vad vill du att AI ska göra härnäst?",
+ "texteditor.rewrite.prompt": "Be AI att redigera meddelande...",
+ "texteditor.rewrite.regenerate": "Regenerera",
+ "texteditor.rewrite.rewriting": "Skriver om",
+ "texteditor.rewrite.shorten": "Förkorta",
+ "texteditor.rewrite.simplify": "Förenkla",
+ "texteditor.rewrite.summarize": "Sammanfatta",
"thread_popout.title": "Tråd - {channelName} - {teamName}",
"threading.filters.unreads": "Olästa",
"threading.following": "Följer",
@@ -5943,6 +6007,7 @@
"unarchive_channel.cancel": "Avbryt",
"unarchive_channel.confirm": "Bekräfta avarkivera kanal",
"unarchive_channel.del": "Dearkivera",
+ "unified_labels.remove_all": "Ta bort alla etiketter",
"update_command.confirm": "Redigera slash-kommando",
"update_command.question": "Dina ändringar kan skada befintligt slash-kommando. Är du säker på att du vill uppdatera?",
"update_command.update": "Uppdatera",
diff --git a/webapp/channels/src/i18n/tr.json b/webapp/channels/src/i18n/tr.json
index b6b2d40163e..8130a9d977f 100644
--- a/webapp/channels/src/i18n/tr.json
+++ b/webapp/channels/src/i18n/tr.json
@@ -5822,15 +5822,8 @@
"suggestionList.suggestionsAvailable": "{count, number} {count, plural, one {öneri} other {öneri}} kullanılabilir",
"system_notice.adminVisible": "Yalnızca sistem yöneticileri görebilir",
"system_notice.adminVisible.icon": "Yalnızca sistem yöneticileri görebilir simgesi",
- "system_notice.body.api3": "Son iki yılda bazı bütünleştirmeler eklediyseniz ya da kurduysanız, son değişiklikler bölümüne bakarak bu bütünleştirmelerin nasıl etkileneceğine bakabilirsiniz.",
- "system_notice.body.ee_upgrade_advice": "En iyi işlerlik ve güvenlik için Enterprise sürümü önerilir. Ayrıntılı bilgi alın.",
- "system_notice.body.ie11_deprecation": "IE11 tarayıcısını kullanıyorsunuz. Bu tarayıcı gelecek sürümlerde desteklenmeyecek. Tek adımda başka bir tarayıcıya nasıl geçebileceğinizi buradan öğrenebilirsiniz.",
- "system_notice.body.permissions": "Sistem panosunda bulunan bazı ilke ve izin ayarları Mattermost Free ve Professional sürümlerinde gelişmiş izinler bölümü altına taşındı.",
"system_notice.dont_show": "Yeniden görüntülenmesin",
"system_notice.remind_me": "Daha sonra anımsat",
- "system_notice.title": "Mattermost bildirimi",
- "system_notice.title.gm_as_dm": "Grup iletilerindeki güncellemeler",
- "system_noticy.body.gm_as_dm": "Artık her yeni ileti için bir bildirim duyurusu ile grup iletilerinizdeki tüm etkinlikler için bildirim alacaksınız.{br}{br}Bunu her grup iletisi için bildirim ayarları bölümünden yapılandırabilirsiniz.",
"system_policy_indicator.base_message": "Bu {resourceType}, {policyText} ile sistem düzeyinde erişime sahip",
"system_policy_indicator.description_with_policies": "Bu {resourceType} şu ilkeler ile sistem düzeyinde erişime sahip: {policyList}. Burada belirlediğiniz tüm özel erişim kuralları bu ilkeye ek olarak uygulanır.",
"system_policy_indicator.more_policies": "{count} diğer",
diff --git a/webapp/channels/src/i18n/uk.json b/webapp/channels/src/i18n/uk.json
index 15537749afc..865113dd42a 100644
--- a/webapp/channels/src/i18n/uk.json
+++ b/webapp/channels/src/i18n/uk.json
@@ -5405,15 +5405,8 @@
"suggestionList.noMatches": "Немає елементів, що відповідають {value}",
"system_notice.adminVisible": "Видиме лише Системним Адміністраторам",
"system_notice.adminVisible.icon": "Значок \"Видимий лише для системних адміністраторів\"",
- "system_notice.body.api3": "Якщо ви створили або встановили інтеграцію протягом останніх двох років, дізнайтеся, як останні зміни , можливо, вплинули на них.",
- "system_notice.body.ee_upgrade_advice": "Корпоративна версія рекомендується для забезпечення оптимальної роботи та надійності. Додаткова інформація .",
- "system_notice.body.ie11_deprecation": "Ваш браузер, IE11, більше не підтримуватиметься в наступних версіях. Дізнайтеся, як перейти на інший браузер одним простим кроком.",
- "system_notice.body.permissions": "Деякі налаштування політики та дозволів у Системній консолі перемістилися з випуском розширених дозволів у Mattermost Free та Professional.",
"system_notice.dont_show": "Не показувати знову",
"system_notice.remind_me": "Нагадати мені пізніше",
- "system_notice.title": "Повідомлення від Mattermost",
- "system_notice.title.gm_as_dm": "Оновлення у групових повідомленнях",
- "system_noticy.body.gm_as_dm": "Тепер ви отримуватимете сповіщення про всі дії у ваших групових повідомленнях разом зі значком сповіщення для кожного нового повідомлення.{br}{br}Ви можете налаштувати це в налаштуваннях сповіщень для кожного групового повідомлення.",
"tag.default.beta": "БЕТА",
"tag.default.bot": "БОТ",
"tag.default.guest": "ГІСТЬ",
diff --git a/webapp/channels/src/i18n/vi.json b/webapp/channels/src/i18n/vi.json
index 34e2d17803f..719e05af70d 100644
--- a/webapp/channels/src/i18n/vi.json
+++ b/webapp/channels/src/i18n/vi.json
@@ -4161,13 +4161,8 @@
"suggestion.user.isCurrent": "(bạn)",
"system_notice.adminVisible": "Chỉ hiển thị với Quản trị viên hệ thống",
"system_notice.adminVisible.icon": "Chỉ hiển thị với Biểu tượng quản trị viên hệ thống",
- "system_notice.body.api3": "Nếu bạn đã tạo hoặc cài đặt các tích hợp trong hai năm qua, hãy tìm hiểu cách thực hiện [các thay đổi gần đây] (! https://about.mattermost.com/default-apiv3-deprecation-guide) có thể đã ảnh hưởng đến họ.",
- "system_notice.body.ee_upgrade_advice": "Phiên bản Enterprise được khuyến nghị để đảm bảo hoạt động tối ưu và độ tin cậy. [Tìm hiểu thêm] (! https://mattermost.com/performance).",
- "system_notice.body.ie11_deprecation": "Trình duyệt của bạn, IE11, sẽ không còn được hỗ trợ trong bản phát hành sắp tới. [Tìm hiểu cách chuyển sang trình duyệt khác trong một bước đơn giản] (! https://forum.mattermost.org/ t / mattermost-is-drop-support-for-internet-explorer-ie11-in-v5-16 / 7575).",
- "system_notice.body.permissions": "Một số chính sách và quyền cài đặt Bảng điều khiển Hệ thống đã di chuyển cùng với việc phát hành [quyền nâng cao] (! https://about.mattermost.com/default-advanced-permissions) trong Enterprise E10 và E20.",
"system_notice.dont_show": "Không hiển thị lại",
"system_notice.remind_me": "Nhắc tôi sau",
- "system_notice.title": "**Thông báo ** \ntừ Mattermost",
"tag.default.beta": "BETA",
"tag.default.bot": "BOT",
"tag.default.guest": "GUEST",
diff --git a/webapp/channels/src/i18n/zh-CN.json b/webapp/channels/src/i18n/zh-CN.json
index 5a6c0ad1d84..d462abec2d4 100644
--- a/webapp/channels/src/i18n/zh-CN.json
+++ b/webapp/channels/src/i18n/zh-CN.json
@@ -5822,15 +5822,8 @@
"suggestionList.suggestionsAvailable": "有 {count, number} 个{count, plural, one {建议} other {建议}}可用",
"system_notice.adminVisible": "只对系统管理员可见",
"system_notice.adminVisible.icon": "只对系统管理员可见图标",
- "system_notice.body.api3": "如果您在上两年创建或安装了整合,请了解近期的更改带来的影响。",
- "system_notice.body.ee_upgrade_advice": "推荐企业版本以确保最佳运作与稳定性。了解更多。",
- "system_notice.body.ie11_deprecation": "您的浏览器,IE11,将今后不再支持。了解如和轻松转移到其他浏览器。",
- "system_notice.body.permissions": "系统控制台中的部分策略和权限设置已经随 Mattermost 免费版和专业版中的高级权限的发布而转移。",
"system_notice.dont_show": "不再显示",
"system_notice.remind_me": "稍后提醒",
- "system_notice.title": "来自 Mattermost 的通知",
- "system_notice.title.gm_as_dm": "群组消息的更新",
- "system_noticy.body.gm_as_dm": "您现在将收到您的群组消息中的所有活动通知,以及每条新消息的通知徽章。{br}{br}您可以在每个群组消息的通知偏好设置中进行配置。",
"system_policy_indicator.base_message": "此{resourceType}已应用系统级访问{policyText}",
"system_policy_indicator.description_with_policies": "此{resourceType}已应用系统级访问策略:{policyList}。您在此设置的任何自定义访问规则都将会在该策略的基础上生效。",
"system_policy_indicator.more_policies": "还有 {count} 个",
diff --git a/webapp/channels/src/i18n/zh-TW.json b/webapp/channels/src/i18n/zh-TW.json
index 2169bff1b55..89344aa0450 100644
--- a/webapp/channels/src/i18n/zh-TW.json
+++ b/webapp/channels/src/i18n/zh-TW.json
@@ -5073,15 +5073,8 @@
"suggestionList.noMatches": "沒有項目符合 {value}",
"system_notice.adminVisible": "僅系統管理員可見",
"system_notice.adminVisible.icon": "僅系統管理員可見圖示",
- "system_notice.body.api3": "如果在過去兩年有建立或安裝外部整合,請參閱最近的變更 以確認影響。",
- "system_notice.body.ee_upgrade_advice": "建議使用企業版以保證最好的運作跟可靠度。了解更多。",
- "system_notice.body.ie11_deprecation": "未來將不再支援您目前所用的瀏覽器:IE11。如何輕鬆轉移到其他瀏覽器。",
- "system_notice.body.permissions": "系統控制台中部份政策以及權限設定,隨著 Mattermost Free 跟 Professional 的進階權限功能釋出,已移動到其他地方。",
"system_notice.dont_show": "不再顯示",
"system_notice.remind_me": "稍後提醒我",
- "system_notice.title": "來自 Mattermost 的通知",
- "system_notice.title.gm_as_dm": "群組訊息的更新",
- "system_noticy.body.gm_as_dm": "現在起,您會收到群組傳訊中所有活動的通知,以及每個新訊息的通知徽章。{br}{br}您可以自行針對每個群組的訊息,調整偏好。",
"tag.default.beta": "測試版",
"tag.default.bot": "機器人",
"tag.default.guest": "訪客",
From fcdd6962ffffe5295856cdc975646f3c1228257e Mon Sep 17 00:00:00 2001
From: Jesse Hallam
Date: Mon, 8 Dec 2025 16:41:29 -0400
Subject: [PATCH 006/416] MM-65575: Fix server panic when bot posts trigger
persistent notifications (#34174)
* reproduce panic with test
* allow bots in the profile map
* explicitly prevent sending notifications to bots
* persistent notifications: handle senders not in the channel
---
server/channels/app/notification_push.go | 13 +++
server/channels/app/notification_push_test.go | 44 ++++++++
.../app/post_persistent_notification.go | 21 +++-
.../app/post_persistent_notification_test.go | 105 ++++++++++++++++++
server/public/model/notification.go | 1 +
5 files changed, 178 insertions(+), 6 deletions(-)
diff --git a/server/channels/app/notification_push.go b/server/channels/app/notification_push.go
index ccf3ee3d888..50cb1bf9ffe 100644
--- a/server/channels/app/notification_push.go
+++ b/server/channels/app/notification_push.go
@@ -604,6 +604,19 @@ func (a *App) getMobileAppSessions(userID string) ([]*model.Session, *model.AppE
}
func (a *App) ShouldSendPushNotification(rctx request.CTX, user *model.User, channelNotifyProps model.StringMap, wasMentioned bool, status *model.Status, post *model.Post, isGM bool) bool {
+ if user.IsBot {
+ a.CountNotificationReason(model.NotificationStatusNotSent, model.NotificationTypePush, model.NotificationReasonRecipientIsBot, model.NotificationNoPlatform)
+ rctx.Logger().LogM(mlog.MlvlNotificationDebug, "Notification not sent - recipient is bot",
+ mlog.String("type", model.NotificationTypePush),
+ mlog.String("post_id", post.Id),
+ mlog.String("status", model.NotificationStatusNotSent),
+ mlog.String("reason", model.NotificationReasonRecipientIsBot),
+ mlog.String("sender_id", post.UserId),
+ mlog.String("receiver_id", user.Id),
+ )
+ return false
+ }
+
if prop := post.GetProp(model.PostPropsForceNotification); prop != nil && prop != "" {
return true
}
diff --git a/server/channels/app/notification_push_test.go b/server/channels/app/notification_push_test.go
index a8ff4fd299d..20c06943a62 100644
--- a/server/channels/app/notification_push_test.go
+++ b/server/channels/app/notification_push_test.go
@@ -1136,6 +1136,50 @@ func TestShouldSendPushNotifications(t *testing.T) {
result := th.App.ShouldSendPushNotification(th.Context, user, channelNotifyProps, false, status, post, false)
assert.False(t, result)
})
+
+ t.Run("should return false if recipient is a bot", func(t *testing.T) {
+ botUser := &model.User{Id: model.NewId(), Email: "bot@test.com", IsBot: true, NotifyProps: make(map[string]string)}
+ botUser.NotifyProps[model.PushNotifyProp] = model.UserNotifyAll
+
+ post := &model.Post{UserId: model.NewId(), ChannelId: model.NewId()}
+
+ channelNotifyProps := map[string]string{model.PushNotifyProp: model.ChannelNotifyAll}
+
+ status := &model.Status{UserId: botUser.Id, Status: model.StatusOnline, Manual: false, LastActivityAt: model.GetMillis(), ActiveChannel: ""}
+
+ result := th.App.ShouldSendPushNotification(th.Context, botUser, channelNotifyProps, true, status, post, false)
+ assert.False(t, result)
+ })
+
+ t.Run("should return false if recipient is a bot even with force notification", func(t *testing.T) {
+ botUser := &model.User{Id: model.NewId(), Email: "bot@test.com", IsBot: true, NotifyProps: make(map[string]string)}
+ botUser.NotifyProps[model.PushNotifyProp] = model.UserNotifyAll
+
+ post := &model.Post{UserId: model.NewId(), ChannelId: model.NewId()}
+ post.AddProp(model.PostPropsForceNotification, model.NewId())
+
+ channelNotifyProps := map[string]string{model.PushNotifyProp: model.ChannelNotifyAll}
+
+ status := &model.Status{UserId: botUser.Id, Status: model.StatusOnline, Manual: false, LastActivityAt: model.GetMillis(), ActiveChannel: ""}
+
+ result := th.App.ShouldSendPushNotification(th.Context, botUser, channelNotifyProps, true, status, post, false)
+ assert.False(t, result)
+ })
+
+ t.Run("should return true for regular user even when post author is a bot", func(t *testing.T) {
+ botId := model.NewId()
+ regularUser := &model.User{Id: model.NewId(), Email: "user@test.com", IsBot: false, NotifyProps: make(map[string]string)}
+ regularUser.NotifyProps[model.PushNotifyProp] = model.UserNotifyAll
+
+ post := &model.Post{UserId: botId, ChannelId: model.NewId()}
+
+ channelNotifyProps := map[string]string{model.PushNotifyProp: model.ChannelNotifyAll}
+
+ status := &model.Status{UserId: regularUser.Id, Status: model.StatusOnline, Manual: false, LastActivityAt: model.GetMillis(), ActiveChannel: ""}
+
+ result := th.App.ShouldSendPushNotification(th.Context, regularUser, channelNotifyProps, true, status, post, false)
+ assert.True(t, result)
+ })
}
// testPushNotificationHandler is an HTTP handler to record push notifications
diff --git a/server/channels/app/post_persistent_notification.go b/server/channels/app/post_persistent_notification.go
index 76f03a52e7a..0babd91bf99 100644
--- a/server/channels/app/post_persistent_notification.go
+++ b/server/channels/app/post_persistent_notification.go
@@ -172,6 +172,18 @@ func (a *App) forEachPersistentNotificationPost(posts []*model.Post, fn func(pos
}
profileMap := channelProfileMap[channel.Id]
+ // Ensure the sender is always in the profile map: for example, system admins can post
+ // without being a member.
+ if _, ok := profileMap[post.UserId]; !ok {
+ var sender *model.User
+ sender, err = a.Srv().Store().User().Get(context.Background(), post.UserId)
+ if err != nil {
+ return errors.Wrapf(err, "failed to get profile for sender user %s for post %s", post.UserId, post.Id)
+ }
+
+ profileMap[post.UserId] = sender
+ }
+
mentions := &MentionResults{}
// In DMs, only the "other" user can be mentioned
if channel.Type == model.ChannelTypeDirect {
@@ -228,15 +240,12 @@ func (a *App) persistentNotificationsAuxiliaryData(channelsMap map[string]*model
}
channelKeywords[c.Id] = make(MentionKeywords, len(profileMap))
- validProfileMap := make(map[string]*model.User, len(profileMap))
for userID, user := range profileMap {
- if user.IsBot {
- continue
+ if !user.IsBot {
+ channelKeywords[c.Id].AddUserKeyword(userID, "@"+user.Username)
}
- validProfileMap[userID] = user
- channelKeywords[c.Id].AddUserKeyword(userID, "@"+user.Username)
}
- channelProfileMap[c.Id] = validProfileMap
+ channelProfileMap[c.Id] = profileMap
}
return channelGroupMap, channelProfileMap, channelKeywords, channelNotifyProps, nil
}
diff --git a/server/channels/app/post_persistent_notification_test.go b/server/channels/app/post_persistent_notification_test.go
index 5bbabb0b72d..e21522ffba8 100644
--- a/server/channels/app/post_persistent_notification_test.go
+++ b/server/channels/app/post_persistent_notification_test.go
@@ -5,10 +5,12 @@ package app
import (
"testing"
+ "time"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/v8/channels/store"
storemocks "github.com/mattermost/mattermost/server/v8/channels/store/storetest/mocks"
+ "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
@@ -216,3 +218,106 @@ func TestSendPersistentNotifications(t *testing.T) {
err := th.App.SendPersistentNotifications()
require.NoError(t, err)
}
+
+func TestSendPersistentNotificationsBotSender(t *testing.T) {
+ mainHelper.Parallel(t)
+ t.Run("should send notification when bot is sender", func(t *testing.T) {
+ th := Setup(t).InitBasic(t)
+
+ bot, appErr := th.App.CreateBot(th.Context, &model.Bot{
+ Username: "testbot",
+ DisplayName: "Test Bot",
+ OwnerId: th.BasicUser.Id,
+ })
+ require.Nil(t, appErr)
+
+ botUser, appErr := th.App.GetUser(bot.UserId)
+ require.Nil(t, appErr)
+
+ _, _, appErr = th.App.AddUserToTeam(th.Context, th.BasicTeam.Id, botUser.Id, "")
+ require.Nil(t, appErr)
+
+ _, appErr = th.App.AddUserToChannel(th.Context, botUser, th.BasicChannel, false)
+ require.Nil(t, appErr)
+
+ _, appErr = th.App.AddUserToChannel(th.Context, th.BasicUser2, th.BasicChannel, false)
+ require.Nil(t, appErr)
+
+ post := &model.Post{
+ UserId: bot.UserId,
+ ChannelId: th.BasicChannel.Id,
+ Message: "test " + "@" + th.BasicUser2.Username,
+ Metadata: &model.PostMetadata{
+ Priority: &model.PostPriority{
+ Priority: model.NewPointer(model.PostPriorityUrgent),
+ PersistentNotifications: model.NewPointer(true),
+ },
+ },
+ // Simulate old timestamp so persistent notifications are sent right away
+ CreateAt: time.Now().Add(-5 * time.Minute).UnixMilli(),
+ }
+ post, appErr = th.App.CreatePost(th.Context, post, th.BasicChannel, model.CreatePostFlags{})
+ require.Nil(t, appErr)
+
+ assert.EventuallyWithT(t, func(c *assert.CollectT) {
+ err := th.App.SendPersistentNotifications()
+ require.NoError(t, err)
+
+ persistentPostNotification, err := th.App.Srv().Store().PostPersistentNotification().GetSingle(post.Id)
+ require.NoError(c, err)
+ require.NotNil(c, persistentPostNotification)
+ assert.Greater(c, persistentPostNotification.SentCount, int16(0))
+ }, 5*time.Second, 100*time.Millisecond)
+ })
+}
+
+func TestSendPersistentNotificationsBotSenderNotInChannel(t *testing.T) {
+ mainHelper.Parallel(t)
+ t.Run("should send notification when bot sender is not a channel member", func(t *testing.T) {
+ th := Setup(t).InitBasic(t)
+
+ bot, appErr := th.App.CreateBot(th.Context, &model.Bot{
+ Username: "testbot",
+ DisplayName: "Test Bot",
+ OwnerId: th.BasicUser.Id,
+ })
+ require.Nil(t, appErr)
+
+ botUser, appErr := th.App.GetUser(bot.UserId)
+ require.Nil(t, appErr)
+
+ // Make the bot a system admin so it can post to channels it's not a member of
+ _, appErr = th.App.UpdateUserRoles(th.Context, botUser.Id, model.SystemUserRoleId+" "+model.SystemAdminRoleId, false)
+ require.Nil(t, appErr)
+
+ _, appErr = th.App.AddUserToChannel(th.Context, th.BasicUser2, th.BasicChannel, false)
+ require.Nil(t, appErr)
+
+ // Note: bot is NOT added to the team or channel
+
+ post := &model.Post{
+ UserId: bot.UserId,
+ ChannelId: th.BasicChannel.Id,
+ Message: "test " + "@" + th.BasicUser2.Username,
+ Metadata: &model.PostMetadata{
+ Priority: &model.PostPriority{
+ Priority: model.NewPointer(model.PostPriorityUrgent),
+ PersistentNotifications: model.NewPointer(true),
+ },
+ },
+ CreateAt: time.Now().Add(-5 * time.Minute).UnixMilli(),
+ }
+ post, appErr = th.App.CreatePost(th.Context, post, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
+ require.Nil(t, appErr)
+
+ assert.EventuallyWithT(t, func(c *assert.CollectT) {
+ err := th.App.SendPersistentNotifications()
+ require.NoError(t, err)
+
+ persistentPostNotification, err := th.App.Srv().Store().PostPersistentNotification().GetSingle(post.Id)
+ require.NoError(c, err)
+ require.NotNil(c, persistentPostNotification)
+ assert.Greater(c, persistentPostNotification.SentCount, int16(0))
+ }, 5*time.Second, 100*time.Millisecond)
+ })
+}
diff --git a/server/public/model/notification.go b/server/public/model/notification.go
index da23f132d19..8da00a07bb0 100644
--- a/server/public/model/notification.go
+++ b/server/public/model/notification.go
@@ -40,4 +40,5 @@ const (
NotificationReasonTooManyUsersInChannel NotificationReason = "too_many_users_in_channel"
NotificationReasonResolvePersistentNotificationError NotificationReason = "resolve_persistent_notification_error"
NotificationReasonMissingThreadMembership NotificationReason = "missing_thread_membership"
+ NotificationReasonRecipientIsBot NotificationReason = "recipient_is_bot"
)
From 3c47f2055b56b46967d9983dce602720639789d2 Mon Sep 17 00:00:00 2001
From: Jesse Hallam
Date: Mon, 8 Dec 2025 16:47:03 -0400
Subject: [PATCH 007/416] Fix missing server ci reporting (#34641)
* s/Server CI (PR|Master)/Server CI/
* whitespace changes
* fixup! s/Server CI (PR|Master)/Server CI/
* Check workflow event type instead of name for PR reporting
Address code review feedback by checking github.event.workflow_run.event
instead of github.event.workflow_run.name since Server CI now runs for
both PRs and master branches.
---------
Co-authored-by: Mattermost Build
---
.github/workflows/sentry.yaml | 2 +-
.github/workflows/server-ci-artifacts.yml | 8 ++++----
.github/workflows/server-ci-report.yml | 21 +++++++++++----------
.github/workflows/server-ci.yml | 5 +++++
4 files changed, 21 insertions(+), 15 deletions(-)
diff --git a/.github/workflows/sentry.yaml b/.github/workflows/sentry.yaml
index 41fd83b4216..bf481ac1bc7 100644
--- a/.github/workflows/sentry.yaml
+++ b/.github/workflows/sentry.yaml
@@ -4,7 +4,7 @@ name: Sentry Upload
on:
workflow_run:
workflows:
- - "Server CI Master"
+ - "Server CI"
types:
- completed
diff --git a/.github/workflows/server-ci-artifacts.yml b/.github/workflows/server-ci-artifacts.yml
index 3dde3cc0ada..d83ee881ad2 100644
--- a/.github/workflows/server-ci-artifacts.yml
+++ b/.github/workflows/server-ci-artifacts.yml
@@ -3,7 +3,7 @@ name: Server CI Artifacts
on:
workflow_run:
workflows:
- - "Server CI PR"
+ - "Server CI"
types:
- completed
@@ -65,7 +65,7 @@ jobs:
echo "|Download Link|" >> "${GITHUB_STEP_SUMMARY}"
echo "| --- |" >> "${GITHUB_STEP_SUMMARY}"
for package in ${PACKAGES_FILE_LIST}
- do
+ do
echo "|[${package}](https://pr-builds.mattermost.com/mattermost/commit/${{ github.event.workflow_run.head_sha }}/${package})|" >> "${GITHUB_STEP_SUMMARY}"
done
@@ -150,7 +150,7 @@ jobs:
./wizcli docker scan --image mattermostdevelopment/mattermost-team-edition:${{ needs.build-docker.outputs.TAG }} --policy "$POLICY"
update-failure-final-status:
- if: failure() || cancelled()
+ if: (failure() || cancelled()) && github.event.workflow_run.event == 'pull_request'
runs-on: ubuntu-22.04
needs:
- build-docker
@@ -166,7 +166,7 @@ jobs:
status: failure
update-success-final-status:
- if: success()
+ if: success() && github.event.workflow_run.event == 'pull_request'
runs-on: ubuntu-22.04
needs:
- build-docker
diff --git a/.github/workflows/server-ci-report.yml b/.github/workflows/server-ci-report.yml
index c5d25095f94..c42cdf8e458 100644
--- a/.github/workflows/server-ci-report.yml
+++ b/.github/workflows/server-ci-report.yml
@@ -1,10 +1,11 @@
+# Server CI Report can be triggered by any branch, but always runs on the default branch.
+# That means changes to this file won't reflect in a pull request but must first be merged.
name: Server CI Report
on:
workflow_run:
workflows:
- - "Server CI PR"
- - "Server CI Master"
+ - Server CI
types:
- completed
@@ -21,17 +22,17 @@ jobs:
github-token: ${{ github.token }}
pattern: "*-test-logs"
path: reports
-
+
- name: report/validate-and-prepare-data
id: validate
run: |
# Create validated data file
> /tmp/validated-tests.json
-
+
find "reports" -type f -name "test-name" | while read -r test_file; do
folder=$(basename "$(dirname "$test_file")")
test_name_raw=$(cat "$test_file" | tr -d '\n\r')
-
+
# Validate test name: allow alphanumeric, spaces, hyphens, underscores, parentheses, and dots
if [[ "$test_name_raw" =~ ^[a-zA-Z0-9\ \(\)_.-]+$ ]] && [[ ${#test_name_raw} -le 100 ]]; then
# Use jq to safely escape the test name as JSON
@@ -41,7 +42,7 @@ jobs:
echo "Warning: Skipping invalid test name in $test_file: '$test_name_raw'" >&2
fi
done
-
+
# Verify we have at least some valid tests
if [[ ! -s /tmp/validated-tests.json ]]; then
echo "Error: No valid test names found" >&2
@@ -54,11 +55,11 @@ jobs:
# Convert validated JSON objects to matrix format
jq -s '{ "test": . }' /tmp/validated-tests.json | tee /tmp/report-matrix
echo REPORT_MATRIX=$(cat /tmp/report-matrix | jq --compact-output --monochrome-output) >> ${GITHUB_OUTPUT}
-
+
publish-report:
runs-on: ubuntu-22.04
name: Publish Report ${{ matrix.test.name }}
- needs:
+ needs:
- generate-report-matrix
permissions:
pull-requests: write
@@ -75,7 +76,7 @@ jobs:
name: ${{ matrix.test.artifact }}
path: ${{ matrix.test.artifact }}
- name: report/fetch-pr-number
- if: github.event.workflow_run.name == 'Server CI PR'
+ if: github.event.workflow_run.event == 'pull_request'
id: incoming-pr
env:
ARTIFACT: "${{ matrix.test.artifact }}"
@@ -108,7 +109,7 @@ jobs:
- name: Report retried tests (pull request)
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
- if: ${{ steps.report.outputs.flaky_summary != '
' && github.event.workflow_run.event == 'pull_request' }}
env:
TEST_NAME: "${{ matrix.test.name }}"
FLAKY_SUMMARY: "${{ steps.report.outputs.flaky_summary }}"
diff --git a/.github/workflows/server-ci.yml b/.github/workflows/server-ci.yml
index d5c45c7c6b9..9f5ac90265b 100644
--- a/.github/workflows/server-ci.yml
+++ b/.github/workflows/server-ci.yml
@@ -1,3 +1,8 @@
+# NOTE: This workflow name is referenced by other workflows:
+# - server-ci-artifacts.yml
+# - server-ci-report.yml
+# - sentry.yaml
+# If you rename this workflow, be sure to update those workflows as well.
name: Server CI
on:
push:
From 761e56bb11ccb751ddbe4bab5898ccc2b384fd82 Mon Sep 17 00:00:00 2001
From: M-ZubairAhmed
Date: Tue, 9 Dec 2025 19:16:45 +0530
Subject: [PATCH 008/416] [MM-66791] Add permission check before resolving
channel mentions (#34679)
---
server/channels/app/post.go | 2 +-
server/channels/app/post_test.go | 62 ++++++++++++++++++++++++++++++++
2 files changed, 63 insertions(+), 1 deletion(-)
diff --git a/server/channels/app/post.go b/server/channels/app/post.go
index 4a68c1762d6..04909f7b93a 100644
--- a/server/channels/app/post.go
+++ b/server/channels/app/post.go
@@ -491,7 +491,7 @@ func (a *App) FillInPostProps(rctx request.CTX, post *model.Post, channel *model
}
for _, mentioned := range mentionedChannels {
- if mentioned.Type == model.ChannelTypeOpen {
+ if mentioned.Type == model.ChannelTypeOpen && a.HasPermissionToReadChannel(rctx, post.UserId, mentioned) {
team, err := a.Srv().Store().Team().Get(mentioned.TeamId)
if err != nil {
rctx.Logger().Warn("Failed to get team of the channel mention", mlog.String("team_id", channel.TeamId), mlog.String("channel_id", channel.Id), mlog.Err(err))
diff --git a/server/channels/app/post_test.go b/server/channels/app/post_test.go
index 4afb2897fba..ee9d643275d 100644
--- a/server/channels/app/post_test.go
+++ b/server/channels/app/post_test.go
@@ -3118,6 +3118,68 @@ func TestFillInPostProps(t *testing.T) {
assert.Nil(t, post1.GetProp(model.PostPropsAIGeneratedByUserID))
assert.Nil(t, post1.GetProp(model.PostPropsAIGeneratedByUsername))
})
+
+ t.Run("should not populate channel mentions for channels in teams where the user is not a member", func(t *testing.T) {
+ mainHelper.Parallel(t)
+ th := Setup(t).InitBasic(t)
+
+ user1 := th.BasicUser
+ user2 := th.BasicUser2
+
+ team2 := th.CreateTeam(t)
+ th.LinkUserToTeam(t, user2, team2)
+
+ // Create a channel in team2 which user1 is not a member of
+ channel2, err := th.App.CreateChannel(th.Context, &model.Channel{
+ DisplayName: "Channel in Team 2",
+ Name: "channel-in-team-2",
+ Type: model.ChannelTypeOpen,
+ TeamId: team2.Id,
+ CreatorId: user2.Id,
+ }, false)
+ require.Nil(t, err)
+
+ dmChannelBetweenUser1AndUser2 := th.CreateDmChannel(t, user2)
+
+ post, err := th.App.CreatePost(th.Context, &model.Post{
+ UserId: user1.Id,
+ ChannelId: dmChannelBetweenUser1AndUser2.Id,
+ Message: "Testing out i should not be able to mention channel2 from team2? ~" + channel2.Name,
+ }, dmChannelBetweenUser1AndUser2, model.CreatePostFlags{SetOnline: true})
+ require.Nil(t, err)
+
+ err = th.App.FillInPostProps(th.Context, post, dmChannelBetweenUser1AndUser2)
+ require.Nil(t, err)
+
+ mentions := post.GetProp(model.PostPropsChannelMentions)
+ require.Nil(t, mentions)
+ })
+
+ t.Run("should populate channel mentions for channels in teams where the user is a member", func(t *testing.T) {
+ mainHelper.Parallel(t)
+ th := Setup(t).InitBasic(t)
+
+ user1 := th.BasicUser
+ user2 := th.BasicUser2
+
+ channel := th.CreateChannel(t, th.BasicTeam)
+
+ dmChannel := th.CreateDmChannel(t, user2)
+
+ post, err := th.App.CreatePost(th.Context, &model.Post{
+ UserId: user1.Id,
+ ChannelId: dmChannel.Id,
+ Message: "Check out ~" + channel.Name,
+ }, dmChannel, model.CreatePostFlags{SetOnline: true})
+ require.Nil(t, err)
+
+ mentions := post.GetProp(model.PostPropsChannelMentions)
+ require.NotNil(t, mentions)
+
+ mentionsMap, ok := mentions.(map[string]any)
+ require.True(t, ok)
+ require.Contains(t, mentionsMap, channel.Name)
+ })
}
func TestThreadMembership(t *testing.T) {
From 5341b91566aca7af99935fc2f802e64f753c6611 Mon Sep 17 00:00:00 2001
From: Harrison Healey
Date: Tue, 9 Dec 2025 09:10:06 -0500
Subject: [PATCH 009/416] MM-66659/MM-66832 Remove caretPosition state from
AdvancedTextEditor (#34643)
* Mock react-virtualized-auto-sizer in all unit tests to ensure contents are mounted
Without mocking that, anything inside of an `AutoSizer` wouldn't be rendered
at all, leading to weird behaviour like the emoji picker being empty in tests.
* Fix EmojiPickerCategoryRow not having a defaultMessage
This caused any unit tests involving the emoji picker to error unless you
also provided an intlMessages object which is hard to do due to how Jest's
`moduleNameMapper` option is configured currently.
* Fixed incorrect usage of useCallback in EmojiPickerItem
The way this was previously written, we weren't specifying dependencies
properly for those callbacks which lead to some actual performance issues
because of how heavy the emoji picker is, and it also lead to issues where
click handlers would have to be triggered multiple times in unit tests
for some reason.
* MM-66659 Stop using caretPosition in useEditorEmojiPicker
This bug seems to be caused by a race condition between React and Chrome which
only occurs on Windows and only now that we've upgraded to React 18. That race
condition occurs with how we map the selection range in the post textbox to
its `caretPosition` state. That state has always been pretty janky, so I've
decided to remove it.
This also has some other benefits like improving UX by using `execCommand`
which preserves the undo/redo stack when using the emoji picker and simplifying
the code because we no longer have to manually mess with the cursor position.
Also, while I was in there, I fixed some minor weirdness around how whitespace
is handled when adding those emojis.
* MM-66659 Stop using caretPosition in useKeyHandler
* MM-66659 Stop using caretPosition for prefill message
* MM-66659 Remove remaining traces of caretPosition in AdvancedTextEditor
* Fix exclusive test
* Fix "Fixed incorrect usage of useCallback in EmojiPickerItem"
* Fixed incorrect usage of FixedSizeList in emoji picker
This is the actual fix to the issue I thought I fixed previously by changing
how `useCallback` was called in EmojiPickerItem.
It turns out the actual issue is that we were passing a new function as the
child of FixedSizeList on every render cycle which caused it to re-render all
of the rows which somehow also assigned them new DOM Nodes as well. The
EmojiPicker re-renders whenever we mouse over an emoji, so it would re-render
the FixedSizeList and all of its children frequently when mousing over things.
Apart from causing performance issues, that also seems to cause RTL to fail to
click on an emoji the first time you call `userAgent.click` because the first
call would move the mouse which would trigger a re-render, and that would
seemingly cause RTL to forget what to click on.
To fix that, I used a new React context to pass the extra props to the
EmojiPickerCategoryOrEmojiRow. I could've also done that by adding them to the
FixedSizeList's `itemData` prop, but that already contains a lot of heavy data,
and I didn't want to add to that.
* Revert changes to postMessageOnKeyPress
* Run Prettier in playwright folder
* Added docs to new Playwright test
* Address feedback
---
.../components/channels/emoji_gif_picker.ts | 8 +
.../emoji_picker/emoji_picker.spec.ts | 100 ++++
.../advanced_create_post/prewritten_chips.tsx | 4 +-
.../advanced_text_editor.test.tsx | 141 +++++-
.../advanced_text_editor.tsx | 32 +-
.../use_editor_emoji_picker.tsx | 78 +--
.../advanced_text_editor/use_key_handler.tsx | 12 +-
.../custom_status_modal.test.tsx | 14 -
.../drafts/draft_list/index.test.tsx | 3 -
.../edit_scheduled_post/edit_post.tsx | 2 -
.../__snapshots__/emoji_picker.test.tsx.snap | 471 +++++++++++++++++-
.../emoji_picker_category_or_emoji_row.tsx | 19 +-
.../components/emoji_picker_category_row.tsx | 6 +-
.../components/emoji_picker_context.tsx | 23 +
.../emoji_picker_current_results.tsx | 79 +--
.../components/emoji_picker_item.tsx | 16 +-
.../emoji_picker/constants/index.ts | 2 +-
.../onboarding_tour/send_message_tour_tip.tsx | 2 +-
.../user_group_popover.test.tsx.snap | 4 +-
.../group_member_list.test.tsx.snap | 4 +-
.../group_member_list.test.tsx | 4 -
.../user_group_popover.test.tsx | 4 -
.../react_virtualized_auto_sizer_mock.ts | 10 +
webapp/channels/src/tests/setup_jest.ts | 1 +
webapp/channels/src/utils/exec_commands.ts | 10 +
webapp/channels/src/utils/paste.test.tsx | 2 +-
webapp/channels/src/utils/paste.tsx | 4 +-
.../element_identification.test.tsx | 3 -
28 files changed, 859 insertions(+), 199 deletions(-)
create mode 100644 e2e-tests/playwright/specs/functional/channels/emoji_picker/emoji_picker.spec.ts
create mode 100644 webapp/channels/src/components/emoji_picker/components/emoji_picker_context.tsx
create mode 100644 webapp/channels/src/tests/react_virtualized_auto_sizer_mock.ts
diff --git a/e2e-tests/playwright/lib/src/ui/components/channels/emoji_gif_picker.ts b/e2e-tests/playwright/lib/src/ui/components/channels/emoji_gif_picker.ts
index 0622ff7f1cd..5087be6ffef 100644
--- a/e2e-tests/playwright/lib/src/ui/components/channels/emoji_gif_picker.ts
+++ b/e2e-tests/playwright/lib/src/ui/components/channels/emoji_gif_picker.ts
@@ -22,6 +22,14 @@ export default class EmojiGifPicker {
await expect(this.container).toBeVisible();
}
+ async notToBeVisible() {
+ await expect(this.container).not.toBeVisible();
+ }
+
+ async clickEmoji(emojiName: string) {
+ await this.container.getByRole('button', {name: `${emojiName} emoji`}).click();
+ }
+
async openGifTab() {
await expect(this.gifTab).toBeVisible();
diff --git a/e2e-tests/playwright/specs/functional/channels/emoji_picker/emoji_picker.spec.ts b/e2e-tests/playwright/specs/functional/channels/emoji_picker/emoji_picker.spec.ts
new file mode 100644
index 00000000000..8cf07c9ea25
--- /dev/null
+++ b/e2e-tests/playwright/specs/functional/channels/emoji_picker/emoji_picker.spec.ts
@@ -0,0 +1,100 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {Locator} from '@playwright/test';
+
+import {expect, test} from '@mattermost/playwright-lib';
+
+/**
+ * @objective Verify that using the emoji picker adds emojis to the correct place in the post textbox based on the
+ * location of the keyboard caret, that keyboard focus is moved back to the post textbox correctly, and that the
+ * keyboard caret is placed in the correct place.
+ */
+test(
+ 'Should add emoji to post textbox correctly and handle focus/selection correctly',
+ {tag: '@emoji_picker'},
+ async ({pw}) => {
+ // # Initialize a test user
+ const {user} = await pw.initSetup();
+
+ // # Log in as a user in new browser context
+ const {channelsPage} = await pw.testBrowser.login(user);
+ const {emojiGifPickerPopup} = channelsPage;
+ const {postCreate} = channelsPage.centerView;
+
+ // # Navigate to default channel page
+ await channelsPage.goto();
+ await channelsPage.toBeVisible();
+
+ // # Open emoji picker from center textbox
+ await postCreate.openEmojiPicker();
+
+ // * Verify emoji picker popup appears
+ await emojiGifPickerPopup.toBeVisible();
+
+ // # Click on an emoji
+ await emojiGifPickerPopup.clickEmoji('slightly smiling face');
+
+ // * Verify emoji picker popup disappears
+ await emojiGifPickerPopup.notToBeVisible();
+
+ // * Verify that the emoji was correctly added to the post textbox, followed by a space
+ await expectPostCreateState(postCreate.input, ':slightly_smiling_face: ', '');
+
+ // # Repeat those steps with another emoji
+ await postCreate.openEmojiPicker();
+ await emojiGifPickerPopup.clickEmoji('upside down face');
+
+ // * Verify that the second emoji was correctly added to the post textbox, also followed by a space
+ await expectPostCreateState(postCreate.input, ':slightly_smiling_face: :upside_down_face: ', '');
+
+ // # Clear the textbox and replace it with some text
+ await postCreate.writeMessage('ab');
+
+ // # Move left so that the caret is in the middle of the text
+ await postCreate.input.press('ArrowLeft');
+
+ // * Verify that the caret is in the right place
+ await expectPostCreateState(postCreate.input, 'a', 'b');
+
+ // # Open the emoji picker again and select another emoji
+ await postCreate.openEmojiPicker();
+ await emojiGifPickerPopup.clickEmoji('face with raised eyebrow');
+
+ // * Verify that the emoji was added with surrounding whitespace and that the caret is placed after that
+ await expectPostCreateState(postCreate.input, 'a :face_with_raised_eyebrow: ', 'b');
+
+ // # Clear the textbox and replace it with some words
+ await postCreate.writeMessage('this is a test');
+
+ // # Move left again so that the caret is between words but after a space
+ await postCreate.input.press('ArrowLeft');
+ await postCreate.input.press('ArrowLeft');
+ await postCreate.input.press('ArrowLeft');
+ await postCreate.input.press('ArrowLeft');
+
+ // * Again, verify that the caret is in the right place
+ await expectPostCreateState(postCreate.input, 'this is a ', 'test');
+
+ // # Open the emoji picker again and select another emoji
+ await postCreate.openEmojiPicker();
+ await emojiGifPickerPopup.clickEmoji('neutral face');
+
+ // * Verify that the emoji was added without an extra space before it
+ await expectPostCreateState(postCreate.input, 'this is a :neutral_face: ', 'test');
+ },
+);
+
+async function expectPostCreateState(input: Locator, textBeforeCaret: string, textAfterCaret: string) {
+ // * Verify that the post textbox is focused
+ await expect(input).toBeFocused();
+
+ // * Verify that the text in it is as expected
+ await expect(input).toHaveValue(textBeforeCaret + textAfterCaret);
+
+ // * Verify that the keyboard caret is in the correct place
+ const selectionStart = await input.evaluate((element: HTMLTextAreaElement) => element.selectionStart);
+ expect(selectionStart).toEqual(textBeforeCaret.length);
+ const selectionEnd = await input.evaluate((element: HTMLTextAreaElement) => element.selectionEnd);
+ expect(selectionEnd).toEqual(textBeforeCaret.length);
+}
diff --git a/webapp/channels/src/components/advanced_create_post/prewritten_chips.tsx b/webapp/channels/src/components/advanced_create_post/prewritten_chips.tsx
index c9b03c992d8..2cbfdb9823e 100644
--- a/webapp/channels/src/components/advanced_create_post/prewritten_chips.tsx
+++ b/webapp/channels/src/components/advanced_create_post/prewritten_chips.tsx
@@ -16,7 +16,7 @@ import Constants from 'utils/constants';
import type {GlobalState} from 'types/store';
type Props = {
- prefillMessage: (msg: string, shouldFocus: boolean) => void;
+ prefillMessage: (msg: string) => void;
channelId: string;
currentUserId: string;
}
@@ -178,7 +178,7 @@ const PrewrittenChips = ({channelId, currentUserId, prefillMessage}: Props) => {
additionalMarkup={additionalMarkup}
values={values}
onClick={() => {
- prefillMessage(messageToPrefill, true);
+ prefillMessage(messageToPrefill);
}}
otherOption={!message.id}
leadingIcon={leadingIcon}
diff --git a/webapp/channels/src/components/advanced_text_editor/advanced_text_editor.test.tsx b/webapp/channels/src/components/advanced_text_editor/advanced_text_editor.test.tsx
index ff853527988..0833e3607d6 100644
--- a/webapp/channels/src/components/advanced_text_editor/advanced_text_editor.test.tsx
+++ b/webapp/channels/src/components/advanced_text_editor/advanced_text_editor.test.tsx
@@ -3,8 +3,6 @@
import React from 'react';
-import type {Channel} from '@mattermost/types/channels';
-
import Permissions from 'mattermost-redux/constants/permissions';
import {removeDraft, updateDraft} from 'actions/views/drafts';
@@ -28,6 +26,22 @@ jest.mock('actions/views/drafts', () => ({
removeDraft: jest.fn((...args) => ({type: 'MOCK_REMOVE_DRAFT', args})),
}));
+jest.mock('utils/exec_commands.ts', () => ({
+ focusAndInsertText: (element: HTMLElement, text: string) => {
+ element.focus();
+
+ if ('value' in element && 'selectionStart' in element) {
+ const textbox = element as HTMLTextAreaElement;
+ const textBefore = textbox.value.substring(0, textbox.selectionStart);
+ const textAfter = textbox.value.substring(textbox.selectionEnd);
+ textbox.value = textBefore + text + textAfter;
+
+ textbox.selectionStart = textBefore.length + text.length;
+ textbox.selectionEnd = textbox.selectionStart;
+ }
+ },
+}));
+
const mockedRemoveDraft = jest.mocked(removeDraft);
const mockedUpdateDraft = jest.mocked(updateDraft);
@@ -128,7 +142,6 @@ const baseProps = {
message: '',
showEmojiPicker: false,
uploadsProgressPercent: {},
- currentChannel: initialState.entities.channels.channels.current_channel_id as Channel,
channelId,
rootId: '',
errorClass: null,
@@ -179,7 +192,6 @@ const baseProps = {
loadPrevMessage: jest.fn(),
loadNextMessage: jest.fn(),
replyToLastPost: jest.fn(),
- caretPosition: 0,
};
describe('components/avanced_text_editor/advanced_text_editor', () => {
@@ -481,4 +493,125 @@ describe('components/avanced_text_editor/advanced_text_editor', () => {
rerender();
expect(container.querySelector('#editPostFileDropOverlay')).toBeVisible();
});
+
+ describe('emoji picker', () => {
+ const testState = mergeObjects(initialState, {
+ entities: {
+ general: {
+ config: {
+ EnableEmojiPicker: 'true',
+ },
+ },
+ },
+ });
+
+ it('should add emojis to the end of the text', async () => {
+ renderWithContext(
+ ,
+ testState,
+ );
+
+ const textbox = screen.getByPlaceholderText('Write to Test Channel') as HTMLTextAreaElement;
+
+ expect(textbox).toHaveValue('');
+
+ // Open the emoji picker and select an emoji
+ await userEvent.click(screen.getByRole('button', {name: 'select an emoji'}));
+ await userEvent.click(screen.getByRole('button', {name: 'blush emoji'}));
+
+ expect(textbox).toHaveFocus();
+ expect(textbox).toHaveValue(':blush: ');
+ expect(textbox.selectionStart).toEqual(8);
+ expect(textbox.selectionEnd).toEqual(8);
+
+ // Do it again
+ await userEvent.click(screen.getByRole('button', {name: 'select an emoji'}));
+ await userEvent.click(screen.getByRole('button', {name: 'relaxed emoji'}));
+
+ expect(textbox).toHaveFocus();
+ expect(textbox).toHaveValue(':blush: :relaxed: ');
+ expect(textbox.selectionStart).toEqual(18);
+ expect(textbox.selectionEnd).toEqual(18);
+ });
+
+ it('should add a space after the existing text if needed', async () => {
+ renderWithContext(
+ ,
+ testState,
+ );
+
+ const textbox = screen.getByPlaceholderText('Write to Test Channel') as HTMLTextAreaElement;
+
+ await userEvent.type(textbox, 'This is some text');
+
+ expect(textbox).toHaveValue('This is some text');
+
+ // Open the emoji picker and select an emoji
+ await userEvent.click(screen.getByRole('button', {name: 'select an emoji'}));
+ await userEvent.click(screen.getByRole('button', {name: 'blush emoji'}));
+
+ expect(textbox).toHaveFocus();
+ expect(textbox).toHaveValue('This is some text :blush: ');
+ expect(textbox.selectionStart).toEqual(26);
+ expect(textbox.selectionEnd).toEqual(26);
+ });
+
+ it('should be able to add an emoji in the middle of the text', async () => {
+ renderWithContext(
+ ,
+ testState,
+ );
+
+ const textbox = screen.getByPlaceholderText('Write to Test Channel') as HTMLTextAreaElement;
+
+ await userEvent.type(textbox, 'aaabbb');
+ expect(textbox).toHaveValue('aaabbb');
+
+ // Move into the middle of the text
+ await userEvent.keyboard('{ArrowLeft}{ArrowLeft}{ArrowLeft}');
+
+ expect(textbox).toHaveValue('aaabbb');
+
+ // Open the emoji picker and select an emoji
+ await userEvent.click(screen.getByRole('button', {name: 'select an emoji'}));
+ await userEvent.click(screen.getByRole('button', {name: 'blush emoji'}));
+
+ expect(textbox).toHaveFocus();
+ expect(textbox).toHaveValue('aaa :blush: bbb');
+
+ // The caret should now be after the emoji
+ expect(textbox.selectionStart).toEqual(12);
+ expect(textbox.selectionEnd).toEqual(textbox.selectionEnd);
+ });
+
+ it('should be able to add an emoji in the middle of the text without adding an extra space', async () => {
+ renderWithContext(
+ ,
+ testState,
+ );
+
+ const textbox = screen.getByPlaceholderText('Write to Test Channel') as HTMLTextAreaElement;
+
+ await userEvent.type(textbox, 'aaabbb');
+ expect(textbox).toHaveValue('aaabbb');
+
+ // Move into the middle of the text
+ await userEvent.keyboard('{ArrowLeft}{ArrowLeft}{ArrowLeft} ');
+
+ expect(textbox).toHaveValue('aaa bbb');
+
+ // Open the emoji picker and select an emoji
+ await userEvent.click(screen.getByRole('button', {name: 'select an emoji'}));
+ await userEvent.click(screen.getByRole('button', {name: 'blush emoji'}));
+
+ expect(textbox).toHaveFocus();
+ expect(textbox).toHaveValue('aaa :blush: bbb');
+
+ // The caret should now be after the emoji
+ expect(textbox.selectionStart).toEqual(12);
+ expect(textbox.selectionEnd).toEqual(textbox.selectionEnd);
+ });
+ });
});
diff --git a/webapp/channels/src/components/advanced_text_editor/advanced_text_editor.tsx b/webapp/channels/src/components/advanced_text_editor/advanced_text_editor.tsx
index 1d53638873e..e0b27a1b69e 100644
--- a/webapp/channels/src/components/advanced_text_editor/advanced_text_editor.tsx
+++ b/webapp/channels/src/components/advanced_text_editor/advanced_text_editor.tsx
@@ -219,7 +219,6 @@ const AdvancedTextEditor = ({
const messageStatusRef = useRef(null);
const [draft, setDraft] = useState(draftFromStore);
- const [caretPosition, setCaretPosition] = useState(draft.message.length);
const [serverError, setServerError] = useState<(ServerError & { submittedMessage?: string }) | null>(null);
const [postError, setPostError] = useState(null);
const [showPreview, setShowPreview] = useState(false);
@@ -340,12 +339,7 @@ const AdvancedTextEditor = ({
} = useEditorEmojiPicker(
textboxId,
isDisabled,
- draft,
- caretPosition,
- setCaretPosition,
- handleDraftChange,
showPreview,
- focusTextbox,
);
const {
labels: priorityLabels,
@@ -442,7 +436,6 @@ const AdvancedTextEditor = ({
draft,
channelId,
rootId,
- caretPosition,
isValidPersistentNotifications,
location,
textboxRef,
@@ -532,32 +525,17 @@ const AdvancedTextEditor = ({
}
}, [hasDraftMessage]);
- const handleMouseUpKeyUp = useCallback((e: React.MouseEvent | React.KeyboardEvent) => {
- setCaretPosition((e.target as TextboxElement).selectionStart || 0);
- }, []);
-
- const prefillMessage = useCallback((message: string, shouldFocus?: boolean) => {
+ const prefillMessage = useCallback((message: string) => {
handleDraftChange({
...draft,
message,
});
- setCaretPosition(message.length);
- if (shouldFocus) {
- const inputBox = textboxRef.current?.getInputBox();
- inputBox?.click();
- focusTextbox(true);
- }
+ const inputBox = textboxRef.current?.getInputBox();
+ inputBox?.click();
+ focusTextbox(true);
}, [handleDraftChange, focusTextbox, draft, textboxRef]);
- // Update the caret position in the input box when changed by a side effect
- useEffect(() => {
- const textbox: HTMLInputElement | HTMLTextAreaElement | undefined = textboxRef.current?.getInputBox();
- if (textbox && textbox === document.activeElement && textbox.selectionStart !== caretPosition) {
- Utils.setCaretPosition(textbox, caretPosition);
- }
- }, [caretPosition]);
-
// Handle width change when there is no message.
useEffect(() => {
if (!hasDraftMessage) {
@@ -823,8 +801,6 @@ const AdvancedTextEditor = ({
onChange={handleChange}
onKeyPress={postMsgKeyPress}
onKeyDown={handleKeyDown}
- onMouseUp={handleMouseUpKeyUp}
- onKeyUp={handleMouseUpKeyUp}
onComposition={emitTypingEvent}
onHeightChange={handleHeightChange}
handlePostError={handlePostError}
diff --git a/webapp/channels/src/components/advanced_text_editor/use_editor_emoji_picker.tsx b/webapp/channels/src/components/advanced_text_editor/use_editor_emoji_picker.tsx
index bbcfa1861e1..a54169fddc3 100644
--- a/webapp/channels/src/components/advanced_text_editor/use_editor_emoji_picker.tsx
+++ b/webapp/channels/src/components/advanced_text_editor/use_editor_emoji_picker.tsx
@@ -13,28 +13,21 @@ import type {Emoji} from '@mattermost/types/emojis';
import {getConfig} from 'mattermost-redux/selectors/entities/general';
import {getEmojiName} from 'mattermost-redux/utils/emoji_utils';
-import useDidUpdate from 'components/common/hooks/useDidUpdate';
import useEmojiPicker, {useEmojiPickerOffset} from 'components/emoji_picker/use_emoji_picker';
import KeyboardShortcutSequence, {KEYBOARD_SHORTCUTS} from 'components/keyboard_shortcuts/keyboard_shortcuts_sequence';
import WithTooltip from 'components/with_tooltip';
+import {focusAndInsertText} from 'utils/exec_commands';
import {horizontallyWithin} from 'utils/floating';
-import {splitMessageBasedOnCaretPosition} from 'utils/post_utils';
import type {GlobalState} from 'types/store';
-import type {PostDraft} from 'types/store/draft';
import {IconContainer} from './formatting_bar/formatting_icon';
const useEditorEmojiPicker = (
textboxId: string,
isDisabled: boolean,
- draft: PostDraft,
- caretPosition: number,
- setCaretPosition: (pos: number) => void,
- handleDraftChange: (draft: PostDraft) => void,
shouldShowPreview: boolean,
- focusTextbox: () => void,
) => {
const intl = useIntl();
@@ -42,13 +35,26 @@ const useEditorEmojiPicker = (
const enableGifPicker = useSelector((state: GlobalState) => getConfig(state).EnableGifPicker === 'true');
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
- const [emojiSelected, setEmojiSelected] = useState(false);
const toggleEmojiPicker = useCallback((e?: React.MouseEvent): void => {
e?.stopPropagation();
setShowEmojiPicker((prev) => !prev);
}, []);
+ const insertTextAtCaret = useCallback((text: string) => {
+ const textbox = document.getElementById(textboxId) as HTMLTextAreaElement | undefined;
+ if (!textbox) {
+ return;
+ }
+
+ // Only add a space before the inserted text if we're not at the start of the textarea and there's not already
+ // a space there, but always add a space after the inserted text
+ const needsSpaceBefore = textbox.selectionStart !== 0 && !(/\s/).test(textbox.value[textbox.selectionStart - 1]);
+ const textToBeAdded = needsSpaceBefore ? ` ${text} ` : `${text} `;
+
+ focusAndInsertText(textbox, textToBeAdded);
+ }, [textboxId]);
+
const handleEmojiClick = useCallback((emoji: Emoji) => {
const emojiAlias = getEmojiName(emoji);
@@ -57,62 +63,16 @@ const useEditorEmojiPicker = (
return;
}
- let newMessage;
- if (draft.message === '') {
- newMessage = `:${emojiAlias}: `;
- setCaretPosition(newMessage.length);
- } else {
- const {message} = draft;
- const {firstPiece, lastPiece} = splitMessageBasedOnCaretPosition(caretPosition, message);
+ insertTextAtCaret(`:${emojiAlias}:`);
- // check whether the first piece of the message is empty when cursor is placed at beginning of message and avoid adding an empty string at the beginning of the message
- newMessage =
- firstPiece === '' ? `:${emojiAlias}: ${lastPiece}` : `${firstPiece} :${emojiAlias}: ${lastPiece}`;
-
- const newCaretPosition =
- firstPiece === '' ? `:${emojiAlias}: `.length : `${firstPiece} :${emojiAlias}: `.length;
- setCaretPosition(newCaretPosition);
- }
-
- handleDraftChange({
- ...draft,
- message: newMessage,
- });
-
- setEmojiSelected(true);
setShowEmojiPicker(false);
- }, [draft, caretPosition, handleDraftChange, setCaretPosition]);
+ }, [insertTextAtCaret]);
const handleGifClick = useCallback((gif: string) => {
- let newMessage: string;
- if (draft.message === '') {
- newMessage = gif;
- } else if ((/\s+$/).test(draft.message)) {
- // Check whether there is already a blank at the end of the current message
- newMessage = `${draft.message}${gif} `;
- } else {
- newMessage = `${draft.message} ${gif} `;
- }
-
- handleDraftChange({
- ...draft,
- message: newMessage,
- });
+ insertTextAtCaret(gif);
setShowEmojiPicker(false);
- }, [draft, handleDraftChange]);
-
- // Focus textbox when the emoji picker closes
- useDidUpdate(() => {
- if (!showEmojiPicker && emojiSelected) {
- setEmojiSelected(false);
-
- // Wait a frame to let the emoji picker's focus trap disappear before changing focus
- requestAnimationFrame(() => {
- focusTextbox();
- });
- }
- }, [showEmojiPicker, emojiSelected]);
+ }, [insertTextAtCaret]);
const {
emojiPicker,
diff --git a/webapp/channels/src/components/advanced_text_editor/use_key_handler.tsx b/webapp/channels/src/components/advanced_text_editor/use_key_handler.tsx
index 51d0cd06c18..70c68797bc8 100644
--- a/webapp/channels/src/components/advanced_text_editor/use_key_handler.tsx
+++ b/webapp/channels/src/components/advanced_text_editor/use_key_handler.tsx
@@ -34,7 +34,6 @@ const useKeyHandler = (
draft: PostDraft,
channelId: string,
postId: string,
- caretPosition: number,
isValidPersistentNotifications: boolean,
location: string,
textboxRef: React.RefObject,
@@ -119,7 +118,7 @@ const useKeyHandler = (
codeBlockOnCtrlEnter,
postId ? 0 : Date.now(),
postId ? 0 : lastChannelSwitchAt.current,
- caretPosition,
+ textboxRef.current?.getInputBox()?.selectionStart,
);
if (ignoreKeyPress) {
@@ -135,7 +134,7 @@ const useKeyHandler = (
}
emitTypingEvent();
- }, [draft, ctrlSend, codeBlockOnCtrlEnter, caretPosition, postId, emitTypingEvent, handleSubmit, isValidPersistentNotifications]);
+ }, [draft, ctrlSend, codeBlockOnCtrlEnter, postId, emitTypingEvent, handleSubmit, isValidPersistentNotifications, textboxRef]);
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
const ctrlOrMetaKeyPressed = e.ctrlKey || e.metaKey;
@@ -193,6 +192,8 @@ const useKeyHandler = (
const upKeyOnly = !ctrlOrMetaKeyPressed && !e.altKey && !e.shiftKey && Keyboard.isKeyPressed(e, KeyCodes.UP);
const messageIsEmpty = draft.message.length === 0;
const allowHistoryNavigation = draft.message.length === 0 || draft.message === messageHistory[messageHistoryIndex.current];
+
+ const caretPosition = (e.target as HTMLTextAreaElement).selectionStart;
const caretIsWithinCodeBlock = caretPosition && isWithinCodeBlock(draft.message, caretPosition); // REVIEW
if (upKeyOnly && messageIsEmpty) {
@@ -347,7 +348,6 @@ const useKeyHandler = (
}
}, [
applyMarkdown,
- caretPosition,
codeBlockOnCtrlEnter,
ctrlSend,
dispatch,
@@ -371,14 +371,14 @@ const useKeyHandler = (
// Register paste events
useEffect(() => {
function onPaste(event: ClipboardEvent) {
- pasteHandler(event, location, draft.message, isNonFormattedPaste.current, caretPosition, isInEditMode);
+ pasteHandler(event, location, draft.message, isNonFormattedPaste.current, isInEditMode);
}
document.addEventListener('paste', onPaste);
return () => {
document.removeEventListener('paste', onPaste);
};
- }, [location, draft.message, caretPosition, isInEditMode]);
+ }, [location, draft.message, isInEditMode]);
const reactToLastMessage = useCallback((e: KeyboardEvent) => {
e.preventDefault();
diff --git a/webapp/channels/src/components/custom_status/custom_status_modal.test.tsx b/webapp/channels/src/components/custom_status/custom_status_modal.test.tsx
index 0a2d1b9ad26..b5f50a111b7 100644
--- a/webapp/channels/src/components/custom_status/custom_status_modal.test.tsx
+++ b/webapp/channels/src/components/custom_status/custom_status_modal.test.tsx
@@ -2,7 +2,6 @@
// See LICENSE.txt for license information.
import React from 'react';
-import type {Props as AutoSizerProps} from 'react-virtualized-auto-sizer';
import type {DeepPartial} from '@mattermost/types/utilities';
@@ -16,7 +15,6 @@ import type {GlobalState} from 'types/store';
import CustomStatusModal from './custom_status_modal';
-jest.mock('react-virtualized-auto-sizer', () => (props: AutoSizerProps) => props.children({height: 100, width: 100, scaledHeight: 100, scaledWidth: 100}));
jest.mock('images/img_trans.gif', () => 'img_trans.gif');
describe('CustomStatusModal', () => {
@@ -35,21 +33,12 @@ describe('CustomStatusModal', () => {
},
};
- // The emoji picker renders emoji categories without passing a defaultMessage, and we don't pass translation strings
- // into the provider by default, so we need to pass something for this string to silence errors from FormatJS.
- const renderOptions = {
- intlMessages: {
- 'emoji_picker.smileys-emotion': 'Smileys & Emotions',
- },
- };
-
test('should render suggested statuses until the user starts typing', async () => {
renderWithContext(
,
initialState,
- renderOptions,
);
expect(screen.getByText('SUGGESTIONS')).toBeInTheDocument();
@@ -69,7 +58,6 @@ describe('CustomStatusModal', () => {
{...baseProps}
/>,
initialState,
- renderOptions,
);
expect(screen.getByText('SUGGESTIONS')).toBeInTheDocument();
@@ -106,7 +94,6 @@ describe('CustomStatusModal', () => {
{...baseProps}
/>,
testState,
- renderOptions,
);
expect(screen.getByText('SUGGESTIONS')).toBeInTheDocument();
@@ -144,7 +131,6 @@ describe('CustomStatusModal', () => {
{...baseProps}
/>,
testState,
- renderOptions,
);
expect(screen.getByText('SUGGESTIONS')).toBeInTheDocument();
diff --git a/webapp/channels/src/components/drafts/draft_list/index.test.tsx b/webapp/channels/src/components/drafts/draft_list/index.test.tsx
index 3906d10c62a..c8133fe1af3 100644
--- a/webapp/channels/src/components/drafts/draft_list/index.test.tsx
+++ b/webapp/channels/src/components/drafts/draft_list/index.test.tsx
@@ -2,7 +2,6 @@
// See LICENSE.txt for license information.
import React from 'react';
-import type {Props as AutoSizerProps} from 'react-virtualized-auto-sizer';
import type {Draft} from 'selectors/drafts';
@@ -13,8 +12,6 @@ import type {PostDraft} from 'types/store/draft';
import DraftList from './index';
-jest.mock('react-virtualized-auto-sizer', () => (props: AutoSizerProps) => props.children({height: 100, width: 100, scaledHeight: 100, scaledWidth: 100}));
-
jest.mock('components/drafts/draft_row', () => {
return function MockDraftRow(props: {item: PostDraft}) {
return (
diff --git a/webapp/channels/src/components/edit_scheduled_post/edit_post.tsx b/webapp/channels/src/components/edit_scheduled_post/edit_post.tsx
index c125c4e6c45..aa290d23fc6 100644
--- a/webapp/channels/src/components/edit_scheduled_post/edit_post.tsx
+++ b/webapp/channels/src/components/edit_scheduled_post/edit_post.tsx
@@ -404,7 +404,6 @@ const EditPost = ({editingPost, actions, canEditPost, config, channelId, draft,
const handleEditKeyPress = (e: React.KeyboardEvent) => {
const {ctrlSend, codeBlockOnCtrlEnter} = rest;
- const inputBox = textboxRef.current?.getInputBox();
const {allowSending, ignoreKeyPress} = postMessageOnKeyPress(
e,
@@ -413,7 +412,6 @@ const EditPost = ({editingPost, actions, canEditPost, config, channelId, draft,
codeBlockOnCtrlEnter,
Date.now(),
0,
- inputBox.selectionStart,
);
if (ignoreKeyPress) {
diff --git a/webapp/channels/src/components/emoji_picker/__snapshots__/emoji_picker.test.tsx.snap b/webapp/channels/src/components/emoji_picker/__snapshots__/emoji_picker.test.tsx.snap
index ecd3a224a99..ab22850e9df 100644
--- a/webapp/channels/src/components/emoji_picker/__snapshots__/emoji_picker.test.tsx.snap
+++ b/webapp/channels/src/components/emoji_picker/__snapshots__/emoji_picker.test.tsx.snap
@@ -136,8 +136,475 @@ exports[`components/emoji_picker/EmojiPicker should match snapshot 1`] = `
role="grid"
>
+ style="position: relative; height: 100px; width: 100px; overflow: auto; will-change: transform; direction: ltr;"
+ >
+
- {formatMessage({
- id: 'tutorial_threads.list.description-p1',
- defaultMessage: 'Here you’ll see a preview of all threads you’re following or participating in. Clicking on a thread in this list will open the full thread on the right.',
- })}
-
- >
- );
-
- const punchOutIds = isMobileView ? ['tutorial-threads-mobile-list', 'tutorial-threads-mobile-header'] : ['threads-list-container'];
- const overlayPunchOut = useMeasurePunchouts(punchOutIds, []);
-
- return (
-
- );
-};
-
-export default CRTListTutorialTip;
diff --git a/webapp/channels/src/components/tours/crt_tour/crt_threads_pane_tutorial_tip.tsx b/webapp/channels/src/components/tours/crt_tour/crt_threads_pane_tutorial_tip.tsx
deleted file mode 100644
index cbadf9abf1e..00000000000
--- a/webapp/channels/src/components/tours/crt_tour/crt_threads_pane_tutorial_tip.tsx
+++ /dev/null
@@ -1,90 +0,0 @@
-// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
-// See LICENSE.txt for license information.
-
-import React from 'react';
-import type {ReactNode} from 'react';
-import {FormattedMessage, useIntl} from 'react-intl';
-import {useDispatch, useSelector} from 'react-redux';
-
-import {TourTip, useFollowElementDimensions, useMeasurePunchouts} from '@mattermost/components';
-
-import {savePreferences} from 'mattermost-redux/actions/preferences';
-import {getCurrentUserId} from 'mattermost-redux/selectors/entities/common';
-
-import {Constants, Preferences} from 'utils/constants';
-
-const translate = {x: 2, y: 25};
-
-const CRTThreadsPaneTutorialTip = () => {
- const dispatch = useDispatch();
- const {formatMessage} = useIntl();
- const currentUserId = useSelector(getCurrentUserId);
-
- const dimensions = useFollowElementDimensions('sidebar-right');
-
- const title = (
-
- );
-
- const screen = (
-
- {formatMessage(
- {
- id: 'tutorial_threads.threads_pane.description',
- defaultMessage: 'Click the Follow button to be notified about replies and see it in your Threads view. Within a thread, the New Messages line shows you where you left off.',
- },
- {
- b: (value) => {value},
- },
- )}
-
- );
-
- const nextBtn = (): JSX.Element => {
- return (
-
- );
- };
-
- const onDismiss = (e: React.MouseEvent) => {
- e.preventDefault();
- const preferences = [
- {
- user_id: currentUserId,
- category: Preferences.CRT_THREAD_PANE_STEP,
- name: currentUserId,
- value: Constants.CrtThreadPaneSteps.FINISHED.toString(),
- },
- ];
- dispatch(savePreferences(currentUserId, preferences));
- };
-
- const overlayPunchOut = useMeasurePunchouts(['rhsContainer'], [dimensions?.width]);
-
- return (
-
- );
-};
-
-export default CRTThreadsPaneTutorialTip;
diff --git a/webapp/channels/src/components/tours/crt_tour/crt_tour_tip.tsx b/webapp/channels/src/components/tours/crt_tour/crt_tour_tip.tsx
deleted file mode 100644
index 7dd1a980a20..00000000000
--- a/webapp/channels/src/components/tours/crt_tour/crt_tour_tip.tsx
+++ /dev/null
@@ -1,18 +0,0 @@
-// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
-// See LICENSE.txt for license information.
-
-import React from 'react';
-
-import {ChannelsTourTip, TutorialTourName} from 'components/tours';
-import type {ChannelsTourTipProps} from 'components/tours';
-
-const CRTTourTip = (props: Omit) => {
- return (
-
- );
-};
-
-export default CRTTourTip;
diff --git a/webapp/channels/src/components/tours/crt_tour/crt_unread_tutorial_tip.tsx b/webapp/channels/src/components/tours/crt_tour/crt_unread_tutorial_tip.tsx
deleted file mode 100644
index 48fe0142ccf..00000000000
--- a/webapp/channels/src/components/tours/crt_tour/crt_unread_tutorial_tip.tsx
+++ /dev/null
@@ -1,47 +0,0 @@
-// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
-// See LICENSE.txt for license information.
-
-import React from 'react';
-import type {ReactNode} from 'react';
-import {FormattedMessage, useIntl} from 'react-intl';
-
-import {useMeasurePunchouts} from '@mattermost/components';
-
-import CRTTourTip from './crt_tour_tip';
-
-const CRTUnreadTutorialTip = () => {
- const {formatMessage} = useIntl();
- const title = (
-
- );
-
- const screen = (
-
- {formatMessage(
- {
- id: 'tutorial_threads.unread.description',
- defaultMessage: 'You can switch to Unreads to show only threads that are unread.',
- },
- {
- b: (value) => {value},
- })
- }
-
- );
- const overlayPunchOut = useMeasurePunchouts(['threads-list-unread-button'], []);
-
- return (
-
- );
-};
-
-export default CRTUnreadTutorialTip;
diff --git a/webapp/channels/src/components/tours/crt_tour/crt_welcome_tutorial_tip.tsx b/webapp/channels/src/components/tours/crt_tour/crt_welcome_tutorial_tip.tsx
deleted file mode 100644
index 5f143fadb37..00000000000
--- a/webapp/channels/src/components/tours/crt_tour/crt_welcome_tutorial_tip.tsx
+++ /dev/null
@@ -1,46 +0,0 @@
-// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
-// See LICENSE.txt for license information.
-
-import React from 'react';
-import {FormattedMessage, useIntl} from 'react-intl';
-
-import {useMeasurePunchouts} from '@mattermost/components';
-
-import CRTTourTip from './crt_tour_tip';
-
-const CRTWelcomeTutorialTip = () => {
- const {formatMessage} = useIntl();
- const title = (
-
- );
-
- const screen = (
-
- {formatMessage(
- {
- id: 'tutorial_threads.welcome.description',
- defaultMessage:
- 'All the conversations that you’re participating in or following will show here. If you have unread messages or mentions within your threads, you’ll see that here too.',
-
- })
- }
-