diff --git a/server/channels/api4/post.go b/server/channels/api4/post.go
index dcc8129aeee..ee38135d16a 100644
--- a/server/channels/api4/post.go
+++ b/server/channels/api4/post.go
@@ -1010,6 +1010,10 @@ func updatePost(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
+ // MM-67055: Strip client-supplied metadata.embeds to prevent spoofing.
+ // This matches createPost behavior.
+ post.SanitizeInput()
+
auditRec := c.MakeAuditRecord(model.AuditEventUpdatePost, model.AuditStatusFail)
model.AddEventParameterAuditableToAuditRec(auditRec, "post", &post)
defer c.LogAuditRecWithLevel(auditRec, app.LevelContent)
diff --git a/server/channels/api4/post_test.go b/server/channels/api4/post_test.go
index 23b65090ccf..67a6f69d4d9 100644
--- a/server/channels/api4/post_test.go
+++ b/server/channels/api4/post_test.go
@@ -1552,6 +1552,53 @@ func TestUpdatePost(t *testing.T) {
assert.NotEqual(t, rpost3.Attachments(), rrupost3.Attachments())
})
+ t.Run("should strip spoofed metadata embeds", func(t *testing.T) {
+ // MM-67055: Verify that client-supplied metadata.embeds are stripped
+ post := &model.Post{
+ ChannelId: channel.Id,
+ Message: "test message " + model.NewId(),
+ }
+ createdPost, _, err := client.CreatePost(context.Background(), post)
+ require.NoError(t, err)
+
+ // Try to update with spoofed embed
+ updatePost := &model.Post{
+ Id: createdPost.Id,
+ ChannelId: channel.Id,
+ Message: "updated message " + model.NewId(),
+ Metadata: &model.PostMetadata{
+ Embeds: []*model.PostEmbed{
+ {
+ Type: model.PostEmbedPermalink,
+ Data: &model.PreviewPost{
+ PostID: "spoofed-post-id",
+ Post: &model.Post{
+ Id: "spoofed-post-id",
+ UserId: th.BasicUser2.Id,
+ Message: "This is a spoofed message!",
+ },
+ },
+ },
+ },
+ },
+ }
+
+ updatedPost, _, err := client.UpdatePost(context.Background(), createdPost.Id, updatePost)
+ require.NoError(t, err)
+
+ // Verify spoofed embed was stripped
+ if updatedPost.Metadata != nil {
+ assert.Empty(t, updatedPost.Metadata.Embeds, "spoofed embeds should be stripped")
+ }
+
+ // Double-check by fetching the post
+ fetchedPost, _, err := client.GetPost(context.Background(), createdPost.Id, "")
+ require.NoError(t, err)
+ if fetchedPost.Metadata != nil {
+ assert.Empty(t, fetchedPost.Metadata.Embeds, "spoofed embeds should not be persisted")
+ }
+ })
+
t.Run("change message, but post too old", func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.PostEditTimeLimit = 1
diff --git a/server/channels/app/post.go b/server/channels/app/post.go
index cb5276d11f4..3704995fbb3 100644
--- a/server/channels/app/post.go
+++ b/server/channels/app/post.go
@@ -849,6 +849,8 @@ func (a *App) UpdatePost(rctx request.CTX, receivedUpdatedPost *model.Post, upda
// Always use incoming metadata when provided, otherwise retain existing
if receivedUpdatedPost.Metadata != nil {
newPost.Metadata = receivedUpdatedPost.Metadata.Copy()
+ // MM-67055: Strip embeds - always server-generated. Preserves Priority/Acks for Shared Channels sync.
+ newPost.Metadata.Embeds = nil
} else {
// Restore the post metadata that was stripped by the plugin. Set it to
// the last known good.
diff --git a/server/channels/app/post_test.go b/server/channels/app/post_test.go
index 493ccf179c3..2caf8c7a7b3 100644
--- a/server/channels/app/post_test.go
+++ b/server/channels/app/post_test.go
@@ -1952,6 +1952,60 @@ func TestUpdatePost(t *testing.T) {
}
})
+ t.Run("should strip client-supplied embeds", func(t *testing.T) {
+ // MM-67055: Verify that client-supplied metadata.embeds are stripped.
+ // This prevents WebSocket message spoofing via permalink embeds.
+ //
+ // Note: Priority and Acknowledgements are stored in separate database tables,
+ // not in post metadata. Shared Channels handles them separately via
+ // syncRemotePriorityMetadata and syncRemoteAcknowledgementsMetadata after
+ // calling UpdatePost. See sync_recv.go::upsertSyncPost
+ mainHelper.Parallel(t)
+ th := Setup(t).InitBasic(t)
+
+ th.AddUserToChannel(t, th.BasicUser, th.BasicChannel)
+ th.Context.Session().UserId = th.BasicUser.Id
+
+ // Create a basic post
+ post := &model.Post{
+ ChannelId: th.BasicChannel.Id,
+ Message: "original message",
+ UserId: th.BasicUser.Id,
+ }
+ createdPost, err := th.App.CreatePost(th.Context, post, th.BasicChannel, model.CreatePostFlags{})
+ require.Nil(t, err)
+
+ // Try to update with spoofed embeds (the attack vector)
+ updatePost := &model.Post{
+ Id: createdPost.Id,
+ ChannelId: th.BasicChannel.Id,
+ Message: "updated message",
+ UserId: th.BasicUser.Id,
+ Metadata: &model.PostMetadata{
+ Embeds: []*model.PostEmbed{
+ {
+ Type: model.PostEmbedPermalink,
+ Data: &model.PreviewPost{
+ PostID: "spoofed-post-id",
+ Post: &model.Post{
+ Id: "spoofed-post-id",
+ UserId: th.BasicUser2.Id,
+ Message: "Spoofed message from another user!",
+ },
+ },
+ },
+ },
+ },
+ }
+
+ updatedPost, err := th.App.UpdatePost(th.Context, updatePost, nil)
+ require.Nil(t, err)
+ require.NotNil(t, updatedPost.Metadata)
+
+ // Verify embeds were stripped
+ assert.Empty(t, updatedPost.Metadata.Embeds, "spoofed embeds should be stripped")
+ })
+
t.Run("cannot update post in restricted DM", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
diff --git a/server/go.mod b/server/go.mod
index 3761f8deb5f..4a2097bf04e 100644
--- a/server/go.mod
+++ b/server/go.mod
@@ -177,8 +177,8 @@ require (
github.com/prometheus/procfs v0.17.0 // indirect
github.com/redis/go-redis/v9 v9.14.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
- github.com/richardlehane/mscfb v1.0.4 // indirect
- github.com/richardlehane/msoleps v1.0.4 // indirect
+ github.com/richardlehane/mscfb v1.0.6 // indirect
+ github.com/richardlehane/msoleps v1.0.5 // indirect
github.com/rs/xid v1.6.0 // indirect
github.com/russellhaering/goxmldsig v1.5.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
diff --git a/server/go.sum b/server/go.sum
index 1719faab9df..02c392f14ad 100644
--- a/server/go.sum
+++ b/server/go.sum
@@ -540,9 +540,13 @@ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM=
github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk=
+github.com/richardlehane/mscfb v1.0.6 h1:eN3bvvZCp00bs7Zf52bxNwAx5lJDBK1tCuH19qq5aC8=
+github.com/richardlehane/mscfb v1.0.6/go.mod h1:pe0+IUIc0AHh0+teNzBlJCtSyZdFOGgV4ZK9bsoV+Jo=
github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM/9/g00=
github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
+github.com/richardlehane/msoleps v1.0.5 h1:kNlmACZuwC8ZWPLoJtD+HtZOsKJgYn7gXgUIcRB7dbo=
+github.com/richardlehane/msoleps v1.0.5/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
diff --git a/webapp/channels/src/components/advanced_text_editor/advanced_text_editor.scss b/webapp/channels/src/components/advanced_text_editor/advanced_text_editor.scss
index 9be8e654bdd..b41d26a8986 100644
--- a/webapp/channels/src/components/advanced_text_editor/advanced_text_editor.scss
+++ b/webapp/channels/src/components/advanced_text_editor/advanced_text_editor.scss
@@ -175,7 +175,8 @@
&__footer {
position: relative;
display: flex;
- flex-direction: column;
+ flex-direction: row;
+ justify-content: space-between;
padding: 0 24px;
font-size: 12px;
diff --git a/webapp/channels/src/components/advanced_text_editor/footer.test.tsx b/webapp/channels/src/components/advanced_text_editor/footer.test.tsx
new file mode 100644
index 00000000000..7f37f9c4612
--- /dev/null
+++ b/webapp/channels/src/components/advanced_text_editor/footer.test.tsx
@@ -0,0 +1,98 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import React from 'react';
+
+import {renderWithContext, screen} from 'tests/react_testing_utils';
+
+import Footer from './footer';
+
+describe('Footer Component', () => {
+ const baseProps = {
+ postError: null,
+ errorClass: null,
+ serverError: null,
+ channelId: 'channel_id',
+ rootId: '',
+ noArgumentHandleSubmit: jest.fn(),
+ isInEditMode: false,
+ };
+
+ describe('HelpButton visibility', () => {
+ it('should render HelpButton when not in edit mode', () => {
+ renderWithContext(
+ ,
+ );
+
+ expect(screen.getByText('Help')).toBeInTheDocument();
+ });
+
+ it('should not render HelpButton when in edit mode', () => {
+ renderWithContext(
+ ,
+ );
+
+ expect(screen.queryByText('Help')).not.toBeInTheDocument();
+ });
+
+ it('should render HelpButton as a button element with correct attributes', () => {
+ renderWithContext(
+ ,
+ );
+
+ const helpButton = screen.getByText('Help');
+ expect(helpButton.tagName).toBe('BUTTON');
+ expect(helpButton).toHaveAttribute('type', 'button');
+ expect(helpButton).toHaveAttribute('aria-label', 'Messaging help');
+ });
+ });
+
+ describe('Footer structure', () => {
+ it('should render the footer with correct id and role', () => {
+ const {container} = renderWithContext(
+ ,
+ );
+
+ const footer = container.querySelector('#postCreateFooter');
+ expect(footer).toBeInTheDocument();
+ expect(footer).toHaveAttribute('role', 'form');
+ });
+
+ it('should render MsgTyping when not in edit mode', () => {
+ renderWithContext(
+ ,
+ );
+
+ // MsgTyping component should be rendered (it renders an empty span by default)
+ const footer = screen.getByRole('form');
+ expect(footer).toBeInTheDocument();
+ });
+
+ it('should not render MsgTyping when in edit mode', () => {
+ const {container} = renderWithContext(
+ ,
+ );
+
+ // The MsgTyping component should not be in the DOM when in edit mode
+ // This is a basic structural check
+ const footer = container.querySelector('#postCreateFooter');
+ expect(footer).toBeInTheDocument();
+ });
+ });
+});
+
diff --git a/webapp/channels/src/components/advanced_text_editor/footer.tsx b/webapp/channels/src/components/advanced_text_editor/footer.tsx
index 0998c05eec0..4d3f644154c 100644
--- a/webapp/channels/src/components/advanced_text_editor/footer.tsx
+++ b/webapp/channels/src/components/advanced_text_editor/footer.tsx
@@ -12,6 +12,8 @@ import type {Post} from '@mattermost/types/posts';
import MessageSubmitError from 'components/message_submit_error';
import MsgTyping from 'components/msg_typing';
+import HelpButton from './help_button';
+
interface Props {
postError?: ReactNode;
errorClass: string | null;
@@ -55,6 +57,7 @@ export default function Footer({
rootId={rootId}
/>
)}
+ {!isInEditMode &&
+
+
+
+
+
+
+
+
+
+
+
+ {chunks},
+ }}
+ />
+
+
+ {chunks},
+ }}
+ />
+
+ {chunks},
+ }}
+ />
+
+
+ {chunks},
+ }}
+ />
+
|
+ |
+
+ |
+
|---|---|
{'_italic_'} |
+ {'italic'} | +
{'**bold**'} |
+ {'bold'} | +
{'~~strikethrough~~'} |
+ |
{'**_bold-italic_**'} |
+ {'bold-italic'} | +
+ {chunks},
+ }}
+ />
+
+
|
+ |
+
+ |
+
|---|---|
+ {'```'}+ {'Code block'}+ {'```'}
+ |
+
+ {'Code block'}
+ |
+
+ {chunks},
+ }}
+ />
+
+
|
+ |
+
+ |
+
|---|---|
+ {'```go'}+ {'package main'}+ {'import "fmt"'}+ {'func main() {'}+ {'\u00A0\u00A0\u00A0\u00A0fmt.Println("Hello, 世界")'}+ {'}'}+ {'```'}
+ |
+
+ + {'1'}{'package'}{' main'} + {'2'}{'import'}{' "fmt"'} + {'3'}{'func'}{' main() {'} + {'4'}{' fmt.Println("Hello, 世界")'} + {'5'}{'}'} ++ |
+
+
+
|
+ |
+
+ |
+
|---|---|
{'`monospace`'} |
+ {'monospace'} |
+
+
+
|
+ |
+
+ |
+
|---|---|
{'[Check out Mattermost!](https://mattermost.com/)'} |
+ {'Check out Mattermost!'} | +
+ {chunks},
+ link: (chunks: React.ReactNode) => (
+
+
|
+ |
+
+ |
+
|---|---|
{''} |
+
+
+ |
+
+ {chunks},
+ link: (chunks: React.ReactNode) => (
+
+
|
+ |
+
+ |
+
|---|---|
{':smile: :+1: :sheep:'} |
+ {'😄 👍 🐑'} | +
+ {chunks},
+ }}
+ />
+
+
|
+ |
+
+ |
+
|---|---|
{'---'} |
+
+ {chunks},
+ }}
+ />
+
+
|
+ |
+
+ |
+
|---|---|
{'> block quotes'} |
+ {'block quotes'} |
+
+ #. Alternatively, you can underline the text using === or --- to create headings.'}
+ values={{
+ code: (chunks: React.ReactNode) => {chunks},
+ }}
+ />
+
+
|
+ |
+
+ |
+
|---|---|
+ {'## Large Heading'}+ {'### Smaller Heading'}+ {'#### Even Smaller Heading'}
+ |
+
+ {'Large Heading'} + {'Smaller Heading'} + {'Even Smaller Heading'} + |
+
+ {'Large Heading'}+ {'-------------'}
+ |
+ + {'Large Heading'} + | +
+ {chunks},
+ }}
+ />
+
+
+
+
|
+ |
+
+ |
+
|---|---|
+ {'* list item one'}+ {'* list item two'}+ {'\u00A0\u00A0* item two sub-point'}
+ |
+
+
|
+
+ {'1. Item one'}+ {'2. Item two'}
+ |
+
+
|
+
+ {'- [ ] Item one'}+ {'- [ ] Item two'}+ {'- [x] Completed item'}
+ |
+
+
|
+
+ : within the header row.'}
+ values={{
+ code: (chunks: React.ReactNode) => {chunks},
+ }}
+ />
+
+
+ {'| Left-Aligned | Center Aligned | Right Aligned |'}
+ {'| :------------ |:---------------:| -----:|'}
+ {'| Left column 1 | this text | $100 |'}
+ {'| Left column 2 | is | $10 |'}
+ {'| Left column 3 | centered | $1 |'}
+
+
+
| {'Left-Aligned'} | +{'Center-Aligned'} | +{'Right-Aligned'} | +
|---|---|---|
| {'Left column 1'} | +{'this text'} | +{'$100'} | +
| {'Left column 2'} | +{'is'} | +{'$100'} | +
| {'Left column 3'} | +{'centered'} | +{'$100'} | +
+
+ {chunks},
+ }}
+ />
+
+ {chunks},
+ }}
+ />
+
+ {'@alice'} + {' how did your interview go with the new candidate?'} +
+
+
+ {chunks},
+ }}
+ />
+
+ {'@channel'} + {' great work on interviews this week. I think we found some excellent potential candidates!'} +
+
+ {chunks},
+ }}
+ />
+
+ {chunks},
+ }}
+ />
+
+
+
+ {chunks},
+ }}
+ />
+
+
|
+ |
+
+ |
+
|---|---|
{'_italic_'} |
+ {'italic'} | +
{'**bold**'} |
+ {'bold'} | +
{'~~strikethrough~~'} |
+ |
{'`In-line code`'} |
+ {'In-line code'} |
+
{'[hyperlink](https://www.mattermost.com)'} |
+ {'hyperlink'} | +
{''} |
+ + {'build'} + {'unknown'} + | +
{':smile: :sheep: :alien:'} |
+ {'😄 🐑 👽'} | +
+ {chunks},
+ link: (chunks: React.ReactNode) => (
+
+
+
+
+
+
+
+
+
/. A list of slash command options displays above the text input box. The autocomplete suggestions provide you with a format example in black text and a short description of the slash command in grey text.",
+ "help.commands.builtin.title": "Built-In Commands",
+ "help.commands.custom.description": "Custom slash commands can integrate with external applications. For example, a team might configure a custom slash command to check internal health records with /patient joe smith or check the weekly weather forecast in a city with /weather toronto week. Check with your System Admin, or open the autocomplete list by typing /, to determine whether custom slash commands are available for your organization.",
+ "help.commands.custom.note": "Custom slash commands are disabled by default and can be enabled by the System Admin in the System Console by going to Integrations > Integration Management. Learn about configuring custom slash commands in the developer documentation.",
+ "help.commands.custom.title": "Custom Commands",
+ "help.commands.example.away": "Set your status away",
+ "help.commands.example.code": "Display text as a code block",
+ "help.commands.example.collapse": "Turn on auto-collapsing of image previews",
+ "help.commands.example.dnd": "Do not disturb disables desktop and mobile notifications",
+ "help.commands.example.echo": "Echo back text from your account",
+ "help.commands.example.header": "COMMANDS",
+ "help.commands.intro": "You can execute commands, called slash commands, by typing into the text input box to perform operations in Mattermost. To run a slash command, type / followed by a command and some arguments to perform actions.",
+ "help.commands.title": "Executing Commands",
+ "help.document_title": "Help - {pageTitle} - {siteName}",
+ "help.formatting.blockquotes.description": "Create block quotes using >.",
+ "help.formatting.blockquotes.example_label": "Example:",
+ "help.formatting.blockquotes.title": "Blockquotes",
+ "help.formatting.code_block.description": "Create a code block by indenting each line by four spaces, or by placing ``` on the line above and below your code.",
+ "help.formatting.code_block.example_label": "Example:",
+ "help.formatting.code_block.title": "Code Block",
+ "help.formatting.emojis.description": "Open the emoji autocomplete by typing :. A full list of emojis can be found online. It is also possible to create your own Custom Emoji if the emoji you want to use doesn't exist.",
+ "help.formatting.emojis.example_label": "Example:",
+ "help.formatting.emojis.title": "Emojis",
+ "help.formatting.headings.description": "Make a heading by typing # and a space before your title. For smaller headings, use multiple #. Alternatively, you can underline the text using === or --- to create headings.",
+ "help.formatting.headings.this_text": "This text",
+ "help.formatting.headings.title": "Headings",
+ "help.formatting.images.description": "Create in-line images using an ! followed by the alt text in square brackets and the link in normal brackets. See the product documentation for details on working with in-line images.",
+ "help.formatting.images.example_label": "Example:",
+ "help.formatting.images.title": "In-line Images",
+ "help.formatting.inline_code.description": "Create in-line monospaced font by surrounding it with backticks.",
+ "help.formatting.inline_code.example_label": "Example:",
+ "help.formatting.inline_code.title": "In-line Code",
+ "help.formatting.lines.description": "Create a line by using three *, _, or -.",
+ "help.formatting.lines.example_label": "Example:",
+ "help.formatting.lines.title": "Lines",
+ "help.formatting.links.description": "Create labeled links by putting the desired text in square brackets and the associated link in normal brackets.",
+ "help.formatting.links.example_label": "Example:",
+ "help.formatting.links.title": "Links",
+ "help.formatting.lists.description": "Create a list by using * or - as bullets. Indent a bullet point by adding two spaces in front of it.",
+ "help.formatting.lists.example_label": "Examples:",
+ "help.formatting.lists.ordered": "Make it an ordered list by using numbers instead.",
+ "help.formatting.lists.task": "Make a task list by including square brackets.",
+ "help.formatting.lists.title": "Lists",
+ "help.formatting.style.description": "You can use either _ or * around a word to make it italic. Use two to make a word bold.",
+ "help.formatting.style.title": "Text Style",
+ "help.formatting.syntax.description": "To add syntax highlighting, type the language to be highlighted after the ``` at the beginning of the code block. Mattermost also offers four different code themes (GitHub, Solarized Dark, Solarized Light, Monokai) that can be changed in Settings > Display > Theme > Custom Theme > Center Channel Styles > Code Theme.",
+ "help.formatting.syntax.example_label": "Example:",
+ "help.formatting.syntax.title": "Syntax Highlighting",
+ "help.formatting.table.how_it_appears": "How it appears",
+ "help.formatting.table.text_entered": "Text Entered",
+ "help.formatting.tables.description": "Create a table by placing a dashed line under the header row and separating the columns with a pipe |. (The columns don't need to line up exactly for it to work). Choose how to align table columns by including colons : within the header row.",
+ "help.formatting.tables.renders_as": "Renders as:",
+ "help.formatting.tables.this_text": "This text:",
+ "help.formatting.tables.title": "Tables",
+ "help.formatting.title": "Formatting Messages Using Markdown",
+ "help.learn_more.title": "Learn more about:",
+ "help.link.attaching": "Attaching Files",
+ "help.link.commands": "Executing Commands",
+ "help.link.formatting": "Formatting Messages Using Markdown",
+ "help.link.mentioning": "Mentioning Teammates",
+ "help.link.messaging": "Messaging Basics",
+ "help.link.sending": "Sending Messages",
+ "help.mentioning.channel.description": "You can mention an entire channel by typing @channel. All members of the channel will receive a mention notification that behaves the same way as if the members had been mentioned personally.",
+ "help.mentioning.channel.title": "@Channel",
+ "help.mentioning.keywords.description": "In addition to being notified by @username and @channel, you can customize words that trigger mention notifications in Settings > Notifications > Keywords that trigger mentions. By default, you will receive mention notifications on your first name, and you can add more words by typing them into the input box separated by commas. This is useful if you want to be notified of all posts on certain topics, for example, \"interviewing\" or \"marketing\".",
+ "help.mentioning.keywords.title": "Keywords That Trigger Mentions",
+ "help.mentioning.mentions.description": "Use @mentions to get the attention of specific team members.",
+ "help.mentioning.mentions.title": "@Mentions",
+ "help.mentioning.recent.description": "Click @ icon in right side of the top bar next to your profile picture to view your most recent @mentions and words that trigger mentions.",
+ "help.mentioning.recent.title": "Recent Mentions",
+ "help.mentioning.title": "Mentioning Teammates",
+ "help.mentioning.username.description": "You can mention a teammate by using the @ symbol plus their username to send them a mention notification.",
+ "help.mentioning.username.description2": "Type @ to bring up a list of team members who can be mentioned. To filter the list, type the first few letters of any username, first name, last name, or nickname. The Up and Down arrow keys can then be used to scroll through entries in the list, and pressing ENTER will select which user to mention. Once selected, the username will automatically replace the full name or nickname. The following example sends a special mention notification to alice that alerts her of the channel and message where she has been mentioned. If alice is away from Mattermost and has email notifications turned on, then she will receive an email alert of her mention along with the message text.",
+ "help.mentioning.username.not_in_channel": "If the user you mentioned does not belong to the channel, a System Message will be posted to let you know. This is a temporary message only seen by the person who triggered it. To add the mentioned user to the channel, go to the dropdown menu beside the channel name and select Add Members.",
+ "help.mentioning.username.title": "@Username",
+ "help.messaging.attach.description": "Drag and drop files into Mattermost, or select the Attachment icon in the text input box.",
+ "help.messaging.attach.title": "Attach Files",
+ "help.messaging.emoji.description": "Type : to open an emoji autocomplete. If the existing emojis don't say what you want to express, you can also create your own Custom Emoji.",
+ "help.messaging.emoji.title": "Add an Emoji",
+ "help.messaging.formatting.description": "Use Markdown to include text styling, headings, links, emoticons, code blocks, block quotes, tables, lists, and in-line images in your messages. See the following table for common formatting examples.",
+ "help.messaging.formatting.title": "Format Your Messages",
+ "help.messaging.notify.description": "Type @username to get the attention of specific team members.",
+ "help.messaging.notify.title": "Notify Teammates",
+ "help.messaging.reply.description": "Select the Reply Arrow icon next to the text input box.",
+ "help.messaging.reply.title": "Reply to Messages",
+ "help.messaging.table.how_it_appears": "How it appears",
+ "help.messaging.table.text_entered": "Text Entered",
+ "help.messaging.title": "Messaging Basics",
+ "help.messaging.write.description": "Use the text input box at the bottom of the Mattermost interface to write a message. Press ENTER to send the message. Use SHIFT+ENTER to create a new line without sending a message.",
+ "help.messaging.write.title": "Write Messages",
+ "help.sending.delete.description": "Delete a message by selecting the More Actions [...] icon next to any message text that you've composed, then select Delete. System and Team Admins can delete any message on their system or team.",
+ "help.sending.delete.title": "Delete a Message",
+ "help.sending.edit.description": "Edit a message by selecting the More Actions [...] icon next to any message text that you've composed, then select Edit. After making modifications to the message text, press ENTER to save the modifications. Message edits don't trigger new @mention notifications, desktop notifications, or notification sounds.",
+ "help.sending.edit.title": "Edit a Message",
+ "help.sending.link.description": "Get a permanent link to a message by selecting the More Actions [...] icon next to any message, then select Copy Link. Users must be a member of the channel to view the message link.",
+ "help.sending.link.title": "Link to a message",
+ "help.sending.post.description": "Write a message by typing into the text input box, then press ENTER to send it. Use SHIFT+ENTER to create a new line without sending a message. To send messages by pressing CTRL+ENTER, go to Settings > Advanced > Send Messages on CTRL/CMD+ENTER.",
+ "help.sending.post.title": "Post a Message",
+ "help.sending.title": "Sending Messages",
+ "help.sending.types.posts.description": "Posts are considered parent messages when they start a thread of replies. Posts are composed and sent from the text input box at the bottom of the center pane.",
+ "help.sending.types.posts.title": "Posts",
+ "help.sending.types.replies.description": "Select the Reply icon next to any message to open the right-hand sidebar to respond to a thread.",
+ "help.sending.types.replies.description2": "When composing a reply, select the Expand Sidebar/Collapse Sidebar icon in the top right corner of the right-hand sidebar to make conversations easier to read.",
+ "help.sending.types.replies.title": "Replies",
+ "help.sending.types.title": "Message Types",
"incoming_webhooks.header": "Incoming Webhooks",
"inProduct_notices.adminOnlyMessage": "Visible to Admins only",
"input.clear": "Clear",
diff --git a/webapp/channels/src/utils/popouts/popout_windows.ts b/webapp/channels/src/utils/popouts/popout_windows.ts
index 93be28a07e0..55290eb51e4 100644
--- a/webapp/channels/src/utils/popouts/popout_windows.ts
+++ b/webapp/channels/src/utils/popouts/popout_windows.ts
@@ -66,6 +66,21 @@ export async function popoutRhsPlugin(
return listeners;
}
+export async function popoutHelp() {
+ return popout(
+ '/_popout/help',
+ {
+
+ // Not really RHS, but this gives a desirable window size.
+ isRHS: true,
+
+ // Note: titleTemplate is intentionally omitted so that the desktop
+ // app uses document.title, allowing dynamic title updates as the
+ // user navigates between help pages.
+ },
+ );
+}
+
/**
* Below this is generic popout code
* You likely do not need to add anything below this.