diff --git a/bridge/config/config.go b/bridge/config/config.go
index 67a7dc13..7f8d3a4a 100644
--- a/bridge/config/config.go
+++ b/bridge/config/config.go
@@ -154,7 +154,7 @@ type Protocol struct {
 	UseTLS                 bool       // IRC
 	UseDiscriminator       bool       // discord
 	UseFirstName           bool       // telegram
-	UseUserName            bool       // discord
+	UseUserName            bool       // discord, matrix
 	UseInsecureURL         bool       // telegram
 	VerboseJoinPart        bool       // IRC
 	WebhookBindAddress     string     // mattermost, slack
diff --git a/bridge/matrix/helpers.go b/bridge/matrix/helpers.go
new file mode 100644
index 00000000..91aea805
--- /dev/null
+++ b/bridge/matrix/helpers.go
@@ -0,0 +1,166 @@
+package bmatrix
+
+import (
+	"encoding/json"
+	"errors"
+	"html"
+	"strings"
+	"time"
+
+	matrix "github.com/matrix-org/gomatrix"
+)
+
+func newMatrixUsername(username string) *matrixUsername {
+	mUsername := new(matrixUsername)
+
+	// check if we have a </tag>. if we have, we don't escape HTML. #696
+	if htmlTag.MatchString(username) {
+		mUsername.formatted = username
+		// remove the HTML formatting for beautiful push messages #1188
+		mUsername.plain = htmlReplacementTag.ReplaceAllString(username, "")
+	} else {
+		mUsername.formatted = html.EscapeString(username)
+		mUsername.plain = username
+	}
+
+	return mUsername
+}
+
+// getRoomID retrieves a matching room ID from the channel name.
+func (b *Bmatrix) getRoomID(channel string) string {
+	b.RLock()
+	defer b.RUnlock()
+	for ID, name := range b.RoomMap {
+		if name == channel {
+			return ID
+		}
+	}
+
+	return ""
+}
+
+// interface2Struct marshals and immediately unmarshals an interface.
+// Useful for converting map[string]interface{} to a struct.
+func interface2Struct(in interface{}, out interface{}) error {
+	jsonObj, err := json.Marshal(in)
+	if err != nil {
+		return err //nolint:wrapcheck
+	}
+
+	return json.Unmarshal(jsonObj, out)
+}
+
+// getDisplayName retrieves the displayName for mxid, querying the homserver if the mxid is not in the cache.
+func (b *Bmatrix) getDisplayName(mxid string) string {
+	if b.GetBool("UseUserName") {
+		return mxid[1:]
+	}
+
+	b.RLock()
+	if val, present := b.NicknameMap[mxid]; present {
+		b.RUnlock()
+
+		return val.displayName
+	}
+	b.RUnlock()
+
+	displayName, err := b.mc.GetDisplayName(mxid)
+	var httpError *matrix.HTTPError
+	if errors.As(err, &httpError) {
+		b.Log.Warnf("Couldn't retrieve the display name for %s", mxid)
+	}
+
+	if err != nil {
+		return b.cacheDisplayName(mxid, mxid[1:])
+	}
+
+	return b.cacheDisplayName(mxid, displayName.DisplayName)
+}
+
+// cacheDisplayName stores the mapping between a mxid and a display name, to be reused later without performing a query to the homserver.
+// Note that old entries are cleaned when this function is called.
+func (b *Bmatrix) cacheDisplayName(mxid string, displayName string) string {
+	now := time.Now()
+
+	// scan to delete old entries, to stop memory usage from becoming too high with old entries
+	toDelete := []string{}
+	b.RLock()
+	for k, v := range b.NicknameMap {
+		if now.Sub(v.lastUpdated) > 10*time.Minute {
+			toDelete = append(toDelete, k)
+		}
+	}
+	b.RUnlock()
+
+	b.Lock()
+	for _, v := range toDelete {
+		delete(b.NicknameMap, v)
+	}
+	b.NicknameMap[mxid] = NicknameCacheEntry{
+		displayName: displayName,
+		lastUpdated: now,
+	}
+	b.Unlock()
+
+	return displayName
+}
+
+// handleError converts errors into httpError.
+//nolint:exhaustivestruct
+func handleError(err error) *httpError {
+	var mErr matrix.HTTPError
+	if !errors.As(err, &mErr) {
+		return &httpError{
+			Err: "not a HTTPError",
+		}
+	}
+
+	var httpErr httpError
+
+	if err := json.Unmarshal(mErr.Contents, &httpErr); err != nil {
+		return &httpError{
+			Err: "unmarshal failed",
+		}
+	}
+
+	return &httpErr
+}
+
+func (b *Bmatrix) containsAttachment(content map[string]interface{}) bool {
+	// Skip empty messages
+	if content["msgtype"] == nil {
+		return false
+	}
+
+	// Only allow image,video or file msgtypes
+	if !(content["msgtype"].(string) == "m.image" ||
+		content["msgtype"].(string) == "m.video" ||
+		content["msgtype"].(string) == "m.file") {
+		return false
+	}
+
+	return true
+}
+
+// getAvatarURL returns the avatar URL of the specified sender.
+func (b *Bmatrix) getAvatarURL(sender string) string {
+	urlPath := b.mc.BuildURL("profile", sender, "avatar_url")
+
+	s := struct {
+		AvatarURL string `json:"avatar_url"`
+	}{}
+
+	err := b.mc.MakeRequest("GET", urlPath, nil, &s)
+	if err != nil {
+		b.Log.Errorf("getAvatarURL failed: %s", err)
+
+		return ""
+	}
+
+	url := strings.ReplaceAll(s.AvatarURL, "mxc://", b.GetString("Server")+"/_matrix/media/r0/thumbnail/")
+	if url != "" {
+		url += "?width=37&height=37&method=crop"
+	}
+
+	return url
+}
diff --git a/bridge/matrix/matrix.go b/bridge/matrix/matrix.go
index fa2a3f80..725f49a6 100644
--- a/bridge/matrix/matrix.go
+++ b/bridge/matrix/matrix.go
@@ -2,9 +2,7 @@ package bmatrix
 
 import (
 	"bytes"
-	"encoding/json"
 	"fmt"
-	"html"
 	"mime"
 	"regexp"
 	"strings"
@@ -22,10 +20,16 @@ var (
 	htmlReplacementTag = regexp.MustCompile("<[^>]*>")
 )
 
+type NicknameCacheEntry struct {
+	displayName string
+	lastUpdated time.Time
+}
+
 type Bmatrix struct {
-	mc      *matrix.Client
-	UserID  string
-	RoomMap map[string]string
+	mc          *matrix.Client
+	UserID      string
+	NicknameMap map[string]NicknameCacheEntry
+	RoomMap     map[string]string
 	sync.RWMutex
 	*bridge.Config
 }
@@ -41,25 +45,29 @@ type matrixUsername struct {
 	formatted string
 }
 
-func newMatrixUsername(username string) *matrixUsername {
-	mUsername := new(matrixUsername)
+// SubTextMessage represents the new content of the message in edit messages.
+type SubTextMessage struct {
+	MsgType string `json:"msgtype"`
+	Body    string `json:"body"`
+}
 
-	// check if we have a </tag>. if we have, we don't escape HTML. #696
-	if htmlTag.MatchString(username) {
-		mUsername.formatted = username
-		// remove the HTML formatting for beautiful push messages #1188
-		mUsername.plain = htmlReplacementTag.ReplaceAllString(username, "")
-	} else {
-		mUsername.formatted = html.EscapeString(username)
-		mUsername.plain = username
-	}
+// MessageRelation explains how the current message relates to a previous message.
+// Notably used for message edits.
+type MessageRelation struct {
+	EventID string `json:"event_id"`
+	Type    string `json:"rel_type"`
+}
 
-	return mUsername
+type EditedMessage struct {
+	NewContent SubTextMessage  `json:"m.new_content"`
+	RelatedTo  MessageRelation `json:"m.relates_to"`
+	matrix.TextMessage
 }
 
 func New(cfg *bridge.Config) bridge.Bridger {
 	b := &Bmatrix{Config: cfg}
 	b.RoomMap = make(map[string]string)
+	b.NicknameMap = make(map[string]NicknameCacheEntry)
 	return b
 }
 
@@ -112,22 +120,6 @@ retry:
 	return nil
 }
 
-type SubTextMessage struct {
-	MsgType string `json:"msgtype"`
-	Body    string `json:"body"`
-}
-
-type MessageRelation struct {
-	EventID string `json:"event_id"`
-	Type    string `json:"rel_type"`
-}
-
-type EditedMessage struct {
-	NewContent SubTextMessage  `json:"m.new_content"`
-	RelatedTo  MessageRelation `json:"m.relates_to"`
-	matrix.TextMessage
-}
-
 func (b *Bmatrix) Send(msg config.Message) (string, error) {
 	b.Log.Debugf("=> Receiving %#v", msg)
 
@@ -233,21 +225,11 @@ func (b *Bmatrix) Send(msg config.Message) (string, error) {
 	return resp.EventID, err
 }
 
-func (b *Bmatrix) getRoomID(channel string) string {
-	b.RLock()
-	defer b.RUnlock()
-	for ID, name := range b.RoomMap {
-		if name == channel {
-			return ID
-		}
-	}
-	return ""
-}
-
 func (b *Bmatrix) handlematrix() {
 	syncer := b.mc.Syncer.(*matrix.DefaultSyncer)
 	syncer.OnEventType("m.room.redaction", b.handleEvent)
 	syncer.OnEventType("m.room.message", b.handleEvent)
+	syncer.OnEventType("m.room.member", b.handleMemberChange)
 	go func() {
 		for {
 			if err := b.mc.Sync(); err != nil {
@@ -257,15 +239,6 @@ func (b *Bmatrix) handlematrix() {
 	}()
 }
 
-func interface2Struct(in interface{}, out interface{}) error {
-	jsonObj, err := json.Marshal(in)
-	if err != nil {
-		return err
-	}
-
-	return json.Unmarshal(jsonObj, out)
-}
-
 func (b *Bmatrix) handleEdit(ev *matrix.Event, rmsg config.Message) bool {
 	relationInterface, present := ev.Content["m.relates_to"]
 	newContentInterface, present2 := ev.Content["m.new_content"]
@@ -296,6 +269,15 @@ func (b *Bmatrix) handleEdit(ev *matrix.Event, rmsg config.Message) bool {
 	return true
 }
 
+func (b *Bmatrix) handleMemberChange(ev *matrix.Event) {
+	// Update the displayname on join messages, according to https://matrix.org/docs/spec/client_server/r0.6.1#events-on-change-of-profile-information
+	if ev.Content["membership"] == "join" {
+		if dn, ok := ev.Content["displayname"].(string); ok {
+			b.cacheDisplayName(ev.Sender, dn)
+		}
+	}
+}
+
 func (b *Bmatrix) handleEvent(ev *matrix.Event) {
 	b.Log.Debugf("== Receiving event: %#v", ev)
 	if ev.Sender != b.UserID {
@@ -309,7 +291,7 @@ func (b *Bmatrix) handleEvent(ev *matrix.Event) {
 
 		// Create our message
 		rmsg := config.Message{
-			Username: ev.Sender[1:],
+			Username: b.getDisplayName(ev.Sender),
 			Channel:  channel,
 			Account:  b.Account,
 			UserID:   ev.Sender,
@@ -494,58 +476,3 @@ func (b *Bmatrix) handleUploadFile(msg *config.Message, channel string, fi *conf
 	}
 	b.Log.Debugf("result: %#v", res)
 }
-
-// skipMessages returns true if this message should not be handled
-func (b *Bmatrix) containsAttachment(content map[string]interface{}) bool {
-	// Skip empty messages
-	if content["msgtype"] == nil {
-		return false
-	}
-
-	// Only allow image,video or file msgtypes
-	if !(content["msgtype"].(string) == "m.image" ||
-		content["msgtype"].(string) == "m.video" ||
-		content["msgtype"].(string) == "m.file") {
-		return false
-	}
-	return true
-}
-
-// getAvatarURL returns the avatar URL of the specified sender
-func (b *Bmatrix) getAvatarURL(sender string) string {
-	urlPath := b.mc.BuildURL("profile", sender, "avatar_url")
-
-	s := struct {
-		AvatarURL string `json:"avatar_url"`
-	}{}
-
-	err := b.mc.MakeRequest("GET", urlPath, nil, &s)
-	if err != nil {
-		b.Log.Errorf("getAvatarURL failed: %s", err)
-		return ""
-	}
-	url := strings.ReplaceAll(s.AvatarURL, "mxc://", b.GetString("Server")+"/_matrix/media/r0/thumbnail/")
-	if url != "" {
-		url += "?width=37&height=37&method=crop"
-	}
-	return url
-}
-
-func handleError(err error) *httpError {
-	mErr, ok := err.(matrix.HTTPError)
-	if !ok {
-		return &httpError{
-			Err: "not a HTTPError",
-		}
-	}
-
-	var httpErr httpError
-
-	if err := json.Unmarshal(mErr.Contents, &httpErr); err != nil {
-		return &httpError{
-			Err: "unmarshal failed",
-		}
-	}
-
-	return &httpErr
-}
diff --git a/matterbridge.toml.sample b/matterbridge.toml.sample
index 6b38580f..fcaac2a4 100644
--- a/matterbridge.toml.sample
+++ b/matterbridge.toml.sample
@@ -1231,6 +1231,9 @@ HTMLDisable=false
 ## RELOADABLE SETTINGS
 ## Settings below can be reloaded by editing the file
 
+# UseUserName shows the username instead of the server nickname
+UseUserName=false
+
 #Whether to prefix messages from other bridges to matrix with the sender's nick.
 #Useful if username overrides for incoming webhooks isn't enabled on the
 #matrix server. If you set PrefixMessagesWithNick to true, each message