# Message

The responsibility of rendering the messages is shared between multiple components that can be customized or totally replaced.

Here is a diagram that shows the different components that are involved in rendering a message:

![Digraph](data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIKICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPgo8IS0tIEdlbmVyYXRlZCBieSBncmFwaHZpeiB2ZXJzaW9uIDIuNDAuMSAoMjAxNjEyMjUuMDMwNCkKIC0tPgo8IS0tIFRpdGxlOiBHIFBhZ2VzOiAxIC0tPgo8c3ZnIHdpZHRoPSI3MTNwdCIgaGVpZ2h0PSI0NzZwdCIKIHZpZXdCb3g9IjAuMDAgMC4wMCA3MTIuNTQgNDc2LjAwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIj4KPGcgaWQ9ImdyYXBoMCIgY2xhc3M9ImdyYXBoIiB0cmFuc2Zvcm09InNjYWxlKDEgMSkgcm90YXRlKDApIHRyYW5zbGF0ZSg0IDQ3MikiPgo8dGl0bGU+RzwvdGl0bGU+CjwhLS0gQ2hhdE1lc3NhZ2VCdWJibGVWaWV3IC0tPgo8ZyBpZD0ibm9kZTEiIGNsYXNzPSJub2RlIj4KPHRpdGxlPkNoYXRNZXNzYWdlQnViYmxlVmlldzwvdGl0bGU+CjxwYXRoIGZpbGw9IiMzNDliZWIiIHN0cm9rZT0iIzM0OWJlYiIgZD0iTTIzNC4zOSwtMTgwQzIzNC4zOSwtMTgwIDgxLjg3MDgsLTE4MCA4MS44NzA4LC0xODAgNzUuODcwOCwtMTgwIDY5Ljg3MDgsLTE3NCA2OS44NzA4LC0xNjggNjkuODcwOCwtMTY4IDY5Ljg3MDgsLTE1NiA2OS44NzA4LC0xNTYgNjkuODcwOCwtMTUwIDc1Ljg3MDgsLTE0NCA4MS44NzA4LC0xNDQgODEuODcwOCwtMTQ0IDIzNC4zOSwtMTQ0IDIzNC4zOSwtMTQ0IDI0MC4zOSwtMTQ0IDI0Ni4zOSwtMTUwIDI0Ni4zOSwtMTU2IDI0Ni4zOSwtMTU2IDI0Ni4zOSwtMTY4IDI0Ni4zOSwtMTY4IDI0Ni4zOSwtMTc0IDI0MC4zOSwtMTgwIDIzNC4zOSwtMTgwIi8+Cjx0ZXh0IHRleHQtYW5jaG9yPSJtaWRkbGUiIHg9IjE1OC4xMzA0IiB5PSItMTU3LjgiIGZvbnQtZmFtaWx5PSJIZWx2ZXRpY2Esc2Fucy1TZXJpZiIgZm9udC1zaXplPSIxNC4wMCIgZmlsbD0iI2ZmZmZmZiI+Q2hhdE1lc3NhZ2VCdWJibGVWaWV3PC90ZXh0Pgo8L2c+CjwhLS0gQ2hhdFJlYWN0aW9uc0J1YmJsZVZpZXcgLS0+CjxnIGlkPSJub2RlMiIgY2xhc3M9Im5vZGUiPgo8dGl0bGU+Q2hhdFJlYWN0aW9uc0J1YmJsZVZpZXc8L3RpdGxlPgo8cGF0aCBmaWxsPSIjMzQ5YmViIiBzdHJva2U9IiMzNDliZWIiIGQ9Ik00MzMuODMzMiwtMTgwQzQzMy44MzMyLC0xODAgMjc2LjQyNzYsLTE4MCAyNzYuNDI3NiwtMTgwIDI3MC40Mjc2LC0xODAgMjY0LjQyNzYsLTE3NCAyNjQuNDI3NiwtMTY4IDI2NC40Mjc2LC0xNjggMjY0LjQyNzYsLTE1NiAyNjQuNDI3NiwtMTU2IDI2NC40Mjc2LC0xNTAgMjcwLjQyNzYsLTE0NCAyNzYuNDI3NiwtMTQ0IDI3Ni40Mjc2LC0xNDQgNDMzLjgzMzIsLTE0NCA0MzMuODMzMiwtMTQ0IDQzOS44MzMyLC0xNDQgNDQ1LjgzMzIsLTE1MCA0NDUuODMzMiwtMTU2IDQ0NS44MzMyLC0xNTYgNDQ1LjgzMzIsLTE2OCA0NDUuODMzMiwtMTY4IDQ0NS44MzMyLC0xNzQgNDM5LjgzMzIsLTE4MCA0MzMuODMzMiwtMTgwIi8+Cjx0ZXh0IHRleHQtYW5jaG9yPSJtaWRkbGUiIHg9IjM1NS4xMzA0IiB5PSItMTU3LjgiIGZvbnQtZmFtaWx5PSJIZWx2ZXRpY2Esc2Fucy1TZXJpZiIgZm9udC1zaXplPSIxNC4wMCIgZmlsbD0iI2ZmZmZmZiI+Q2hhdFJlYWN0aW9uc0J1YmJsZVZpZXc8L3RleHQ+CjwvZz4KPCEtLSBDaGF0TWVzc2FnZVJlYWN0aW9uc1ZpZXcgLS0+CjxnIGlkPSJub2RlMTAiIGNsYXNzPSJub2RlIj4KPHRpdGxlPkNoYXRNZXNzYWdlUmVhY3Rpb25zVmlldzwvdGl0bGU+CjxwYXRoIGZpbGw9IiMzNDliZWIiIHN0cm9rZT0iIzM0OWJlYiIgZD0iTTQ0MC41NTA0LC0xMDhDNDQwLjU1MDQsLTEwOCAyNjkuNzEwNCwtMTA4IDI2OS43MTA0LC0xMDggMjYzLjcxMDQsLTEwOCAyNTcuNzEwNCwtMTAyIDI1Ny43MTA0LC05NiAyNTcuNzEwNCwtOTYgMjU3LjcxMDQsLTg0IDI1Ny43MTA0LC04NCAyNTcuNzEwNCwtNzggMjYzLjcxMDQsLTcyIDI2OS43MTA0LC03MiAyNjkuNzEwNCwtNzIgNDQwLjU1MDQsLTcyIDQ0MC41NTA0LC03MiA0NDYuNTUwNCwtNzIgNDUyLjU1MDQsLTc4IDQ1Mi41NTA0LC04NCA0NTIuNTUwNCwtODQgNDUyLjU1MDQsLTk2IDQ1Mi41NTA0LC05NiA0NTIuNTUwNCwtMTAyIDQ0Ni41NTA0LC0xMDggNDQwLjU1MDQsLTEwOCIvPgo8dGV4dCB0ZXh0LWFuY2hvcj0ibWlkZGxlIiB4PSIzNTUuMTMwNCIgeT0iLTg1LjgiIGZvbnQtZmFtaWx5PSJIZWx2ZXRpY2Esc2Fucy1TZXJpZiIgZm9udC1zaXplPSIxNC4wMCIgZmlsbD0iI2ZmZmZmZiI+Q2hhdE1lc3NhZ2VSZWFjdGlvbnNWaWV3PC90ZXh0Pgo8L2c+CjwhLS0gQ2hhdFJlYWN0aW9uc0J1YmJsZVZpZXcmIzQ1OyZndDtDaGF0TWVzc2FnZVJlYWN0aW9uc1ZpZXcgLS0+CjxnIGlkPSJlZGdlMTAiIGNsYXNzPSJlZGdlIj4KPHRpdGxlPkNoYXRSZWFjdGlvbnNCdWJibGVWaWV3JiM0NTsmZ3Q7Q2hhdE1lc3NhZ2VSZWFjdGlvbnNWaWV3PC90aXRsZT4KPHBhdGggZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMDAwMDAwIiBkPSJNMzU1LjEzMDQsLTE0My44MzE0QzM1NS4xMzA0LC0xMzMgMzU1LjEzMDQsLTExOS4yODc2IDM1NS4xMzA0LC0xMDguNDEzMyIvPgo8L2c+CjwhLS0gQ2hhdE1lc3NhZ2VMaXN0VkMgLS0+CjxnIGlkPSJub2RlMyIgY2xhc3M9Im5vZGUiPgo8dGl0bGU+Q2hhdE1lc3NhZ2VMaXN0VkM8L3RpdGxlPgo8cGF0aCBmaWxsPSIjMzQ5YmViIiBzdHJva2U9IiMzNDliZWIiIGQ9Ik0xOTUuNzA3NCwtNDY4QzE5NS43MDc0LC00NjggNzYuNTUzNCwtNDY4IDc2LjU1MzQsLTQ2OCA3MC41NTM0LC00NjggNjQuNTUzNCwtNDYyIDY0LjU1MzQsLTQ1NiA2NC41NTM0LC00NTYgNjQuNTUzNCwtNDQ0IDY0LjU1MzQsLTQ0NCA2NC41NTM0LC00MzggNzAuNTUzNCwtNDMyIDc2LjU1MzQsLTQzMiA3Ni41NTM0LC00MzIgMTk1LjcwNzQsLTQzMiAxOTUuNzA3NCwtNDMyIDIwMS43MDc0LC00MzIgMjA3LjcwNzQsLTQzOCAyMDcuNzA3NCwtNDQ0IDIwNy43MDc0LC00NDQgMjA3LjcwNzQsLTQ1NiAyMDcuNzA3NCwtNDU2IDIwNy43MDc0LC00NjIgMjAxLjcwNzQsLTQ2OCAxOTUuNzA3NCwtNDY4Ii8+Cjx0ZXh0IHRleHQtYW5jaG9yPSJtaWRkbGUiIHg9IjEzNi4xMzA0IiB5PSItNDQ1LjgiIGZvbnQtZmFtaWx5PSJIZWx2ZXRpY2Esc2Fucy1TZXJpZiIgZm9udC1zaXplPSIxNC4wMCIgZmlsbD0iI2ZmZmZmZiI+Q2hhdE1lc3NhZ2VMaXN0VkM8L3RleHQ+CjwvZz4KPCEtLSBDaGF0TWVzc2FnZUxheW91dE9wdGlvbnNSZXNvbHZlciAtLT4KPGcgaWQ9Im5vZGU0IiBjbGFzcz0ibm9kZSI+Cjx0aXRsZT5DaGF0TWVzc2FnZUxheW91dE9wdGlvbnNSZXNvbHZlcjwvdGl0bGU+CjxnIGlkPSJhX25vZGU0Ij48YSB4bGluazpocmVmPSIuLi9tZXNzYWdlLWxheW91dC1vcHRpb25zLXJlc29sdmVyIiB4bGluazp0aXRsZT0iQ2hhdE1lc3NhZ2VMYXlvdXRPcHRpb25zUmVzb2x2ZXIiPgo8cGF0aCBmaWxsPSIjMzQ5YmViIiBzdHJva2U9IiMzNDliZWIiIGQ9Ik0yNDcuOTYyMSwtMzk2QzI0Ny45NjIxLC0zOTYgMjQuMjk4NywtMzk2IDI0LjI5ODcsLTM5NiAxOC4yOTg3LC0zOTYgMTIuMjk4NywtMzkwIDEyLjI5ODcsLTM4NCAxMi4yOTg3LC0zODQgMTIuMjk4NywtMzcyIDEyLjI5ODcsLTM3MiAxMi4yOTg3LC0zNjYgMTguMjk4NywtMzYwIDI0LjI5ODcsLTM2MCAyNC4yOTg3LC0zNjAgMjQ3Ljk2MjEsLTM2MCAyNDcuOTYyMSwtMzYwIDI1My45NjIxLC0zNjAgMjU5Ljk2MjEsLTM2NiAyNTkuOTYyMSwtMzcyIDI1OS45NjIxLC0zNzIgMjU5Ljk2MjEsLTM4NCAyNTkuOTYyMSwtMzg0IDI1OS45NjIxLC0zOTAgMjUzLjk2MjEsLTM5NiAyNDcuOTYyMSwtMzk2Ii8+Cjx0ZXh0IHRleHQtYW5jaG9yPSJtaWRkbGUiIHg9IjEzNi4xMzA0IiB5PSItMzczLjgiIGZvbnQtZmFtaWx5PSJIZWx2ZXRpY2Esc2Fucy1TZXJpZiIgZm9udC1zaXplPSIxNC4wMCIgZmlsbD0iI2ZmZmZmZiI+Q2hhdE1lc3NhZ2VMYXlvdXRPcHRpb25zUmVzb2x2ZXI8L3RleHQ+CjwvYT4KPC9nPgo8L2c+CjwhLS0gQ2hhdE1lc3NhZ2VMaXN0VkMmIzQ1OyZndDtDaGF0TWVzc2FnZUxheW91dE9wdGlvbnNSZXNvbHZlciAtLT4KPGcgaWQ9ImVkZ2UxIiBjbGFzcz0iZWRnZSI+Cjx0aXRsZT5DaGF0TWVzc2FnZUxpc3RWQyYjNDU7Jmd0O0NoYXRNZXNzYWdlTGF5b3V0T3B0aW9uc1Jlc29sdmVyPC90aXRsZT4KPHBhdGggZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMDAwMDAwIiBkPSJNMTMwLjE3OCwtNDMxLjgzMTRDMTI5LjEyMjUsLTQyMSAxMjkuMTE0NiwtNDA3LjI4NzYgMTMwLjE1NDQsLTM5Ni40MTMzIi8+CjwvZz4KPCEtLSBDaGF0TWVzc2FnZUxpc3RWaWV3IC0tPgo8ZyBpZD0ibm9kZTYiIGNsYXNzPSJub2RlIj4KPHRpdGxlPkNoYXRNZXNzYWdlTGlzdFZpZXc8L3RpdGxlPgo8cGF0aCBmaWxsPSIjMzQ5YmViIiBzdHJva2U9IiMzNDliZWIiIGQ9Ik00MjAuNTk2NywtMzk2QzQyMC41OTY3LC0zOTYgMjg5LjY2NDEsLTM5NiAyODkuNjY0MSwtMzk2IDI4My42NjQxLC0zOTYgMjc3LjY2NDEsLTM5MCAyNzcuNjY0MSwtMzg0IDI3Ny42NjQxLC0zODQgMjc3LjY2NDEsLTM3MiAyNzcuNjY0MSwtMzcyIDI3Ny42NjQxLC0zNjYgMjgzLjY2NDEsLTM2MCAyODkuNjY0MSwtMzYwIDI4OS42NjQxLC0zNjAgNDIwLjU5NjcsLTM2MCA0MjAuNTk2NywtMzYwIDQyNi41OTY3LC0zNjAgNDMyLjU5NjcsLTM2NiA0MzIuNTk2NywtMzcyIDQzMi41OTY3LC0zNzIgNDMyLjU5NjcsLTM4NCA0MzIuNTk2NywtMzg0IDQzMi41OTY3LC0zOTAgNDI2LjU5NjcsLTM5NiA0MjAuNTk2NywtMzk2Ii8+Cjx0ZXh0IHRleHQtYW5jaG9yPSJtaWRkbGUiIHg9IjM1NS4xMzA0IiB5PSItMzczLjgiIGZvbnQtZmFtaWx5PSJIZWx2ZXRpY2Esc2Fucy1TZXJpZiIgZm9udC1zaXplPSIxNC4wMCIgZmlsbD0iI2ZmZmZmZiI+Q2hhdE1lc3NhZ2VMaXN0VmlldzwvdGV4dD4KPC9nPgo8IS0tIENoYXRNZXNzYWdlTGlzdFZDJiM0NTsmZ3Q7Q2hhdE1lc3NhZ2VMaXN0VmlldyAtLT4KPGcgaWQ9ImVkZ2UzIiBjbGFzcz0iZWRnZSI+Cjx0aXRsZT5DaGF0TWVzc2FnZUxpc3RWQyYjNDU7Jmd0O0NoYXRNZXNzYWdlTGlzdFZpZXc8L3RpdGxlPgo8cGF0aCBmaWxsPSJub25lIiBzdHJva2U9IiMwMDAwMDAiIGQ9Ik0xOTEuMTEwNywtNDMxLjkyNDNDMjI0LjQzMzYsLTQyMC45Njg4IDI2Ni44MDA1LC00MDcuMDQgMzAwLjEyNzUsLTM5Ni4wODMxIi8+CjwvZz4KPCEtLSBDaGF0TWVzc2FnZUxheW91dE9wdGlvbnNSZXNvbHZlciYjNDU7Jmd0O0NoYXRNZXNzYWdlTGlzdFZDIC0tPgo8ZyBpZD0iZWRnZTIiIGNsYXNzPSJlZGdlIj4KPHRpdGxlPkNoYXRNZXNzYWdlTGF5b3V0T3B0aW9uc1Jlc29sdmVyJiM0NTsmZ3Q7Q2hhdE1lc3NhZ2VMaXN0VkM8L3RpdGxlPgo8cGF0aCBmaWxsPSJub25lIiBzdHJva2U9IiMwMDAwMDAiIGQ9Ik0xNDIuMTA2NCwtMzk2LjQxMzNDMTQzLjE0NjIsLTQwNy4yODc2IDE0My4xMzgzLC00MjEgMTQyLjA4MjgsLTQzMS44MzE0Ii8+CjwvZz4KPCEtLSBDaGF0QXZhdGFyVmlldyAtLT4KPGcgaWQ9Im5vZGU1IiBjbGFzcz0ibm9kZSI+Cjx0aXRsZT5DaGF0QXZhdGFyVmlldzwvdGl0bGU+CjxnIGlkPSJhX25vZGU1Ij48YSB4bGluazpocmVmPSIuLi9hdmF0YXIiIHhsaW5rOnRpdGxlPSJDaGF0QXZhdGFyVmlldyI+CjxwYXRoIGZpbGw9IiMzNDliZWIiIHN0cm9rZT0iIzM0OWJlYiIgZD0iTTU2OC40Nzg3LC0xODBDNTY4LjQ3ODcsLTE4MCA0NzUuNzgyMSwtMTgwIDQ3NS43ODIxLC0xODAgNDY5Ljc4MjEsLTE4MCA0NjMuNzgyMSwtMTc0IDQ2My43ODIxLC0xNjggNDYzLjc4MjEsLTE2OCA0NjMuNzgyMSwtMTU2IDQ2My43ODIxLC0xNTYgNDYzLjc4MjEsLTE1MCA0NjkuNzgyMSwtMTQ0IDQ3NS43ODIxLC0xNDQgNDc1Ljc4MjEsLTE0NCA1NjguNDc4NywtMTQ0IDU2OC40Nzg3LC0xNDQgNTc0LjQ3ODcsLTE0NCA1ODAuNDc4NywtMTUwIDU4MC40Nzg3LC0xNTYgNTgwLjQ3ODcsLTE1NiA1ODAuNDc4NywtMTY4IDU4MC40Nzg3LC0xNjggNTgwLjQ3ODcsLTE3NCA1NzQuNDc4NywtMTgwIDU2OC40Nzg3LC0xODAiLz4KPHRleHQgdGV4dC1hbmNob3I9Im1pZGRsZSIgeD0iNTIyLjEzMDQiIHk9Ii0xNTcuOCIgZm9udC1mYW1pbHk9IkhlbHZldGljYSxzYW5zLVNlcmlmIiBmb250LXNpemU9IjE0LjAwIiBmaWxsPSIjZmZmZmZmIj5DaGF0QXZhdGFyVmlldzwvdGV4dD4KPC9hPgo8L2c+CjwvZz4KPCEtLSBDaGF0TWVzc2FnZUNlbGwgLS0+CjxnIGlkPSJub2RlNyIgY2xhc3M9Im5vZGUiPgo8dGl0bGU+Q2hhdE1lc3NhZ2VDZWxsPC90aXRsZT4KPHBhdGggZmlsbD0iIzM0OWJlYiIgc3Ryb2tlPSIjMzQ5YmViIiBkPSJNNDA2LjU4NywtMzI0QzQwNi41ODcsLTMyNCAzMDMuNjczOCwtMzI0IDMwMy42NzM4LC0zMjQgMjk3LjY3MzgsLTMyNCAyOTEuNjczOCwtMzE4IDI5MS42NzM4LC0zMTIgMjkxLjY3MzgsLTMxMiAyOTEuNjczOCwtMzAwIDI5MS42NzM4LC0zMDAgMjkxLjY3MzgsLTI5NCAyOTcuNjczOCwtMjg4IDMwMy42NzM4LC0yODggMzAzLjY3MzgsLTI4OCA0MDYuNTg3LC0yODggNDA2LjU4NywtMjg4IDQxMi41ODcsLTI4OCA0MTguNTg3LC0yOTQgNDE4LjU4NywtMzAwIDQxOC41ODcsLTMwMCA0MTguNTg3LC0zMTIgNDE4LjU4NywtMzEyIDQxOC41ODcsLTMxOCA0MTIuNTg3LC0zMjQgNDA2LjU4NywtMzI0Ii8+Cjx0ZXh0IHRleHQtYW5jaG9yPSJtaWRkbGUiIHg9IjM1NS4xMzA0IiB5PSItMzAxLjgiIGZvbnQtZmFtaWx5PSJIZWx2ZXRpY2Esc2Fucy1TZXJpZiIgZm9udC1zaXplPSIxNC4wMCIgZmlsbD0iI2ZmZmZmZiI+Q2hhdE1lc3NhZ2VDZWxsPC90ZXh0Pgo8L2c+CjwhLS0gQ2hhdE1lc3NhZ2VMaXN0VmlldyYjNDU7Jmd0O0NoYXRNZXNzYWdlQ2VsbCAtLT4KPGcgaWQ9ImVkZ2U0IiBjbGFzcz0iZWRnZSI+Cjx0aXRsZT5DaGF0TWVzc2FnZUxpc3RWaWV3JiM0NTsmZ3Q7Q2hhdE1lc3NhZ2VDZWxsPC90aXRsZT4KPHBhdGggZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMDAwMDAwIiBkPSJNMzU1LjEzMDQsLTM1OS44MzE0QzM1NS4xMzA0LC0zNDkgMzU1LjEzMDQsLTMzNS4yODc2IDM1NS4xMzA0LC0zMjQuNDEzMyIvPgo8L2c+CjwhLS0gQ2hhdE1lc3NhZ2VEZWNvcmF0aW9uSGVhZGVyVmlldyAtLT4KPGcgaWQ9Im5vZGU4IiBjbGFzcz0ibm9kZSI+Cjx0aXRsZT5DaGF0TWVzc2FnZURlY29yYXRpb25IZWFkZXJWaWV3PC90aXRsZT4KPHBhdGggZmlsbD0iIzM0OWJlYiIgc3Ryb2tlPSIjMzQ5YmViIiBkPSJNMjM0LjM5MTMsLTI1MkMyMzQuMzkxMywtMjUyIDExLjg2OTUsLTI1MiAxMS44Njk1LC0yNTIgNS44Njk1LC0yNTIgLS4xMzA1LC0yNDYgLS4xMzA1LC0yNDAgLS4xMzA1LC0yNDAgLS4xMzA1LC0yMjggLS4xMzA1LC0yMjggLS4xMzA1LC0yMjIgNS44Njk1LC0yMTYgMTEuODY5NSwtMjE2IDExLjg2OTUsLTIxNiAyMzQuMzkxMywtMjE2IDIzNC4zOTEzLC0yMTYgMjQwLjM5MTMsLTIxNiAyNDYuMzkxMywtMjIyIDI0Ni4zOTEzLC0yMjggMjQ2LjM5MTMsLTIyOCAyNDYuMzkxMywtMjQwIDI0Ni4zOTEzLC0yNDAgMjQ2LjM5MTMsLTI0NiAyNDAuMzkxMywtMjUyIDIzNC4zOTEzLC0yNTIiLz4KPHRleHQgdGV4dC1hbmNob3I9Im1pZGRsZSIgeD0iMTIzLjEzMDQiIHk9Ii0yMjkuOCIgZm9udC1mYW1pbHk9IkhlbHZldGljYSxzYW5zLVNlcmlmIiBmb250LXNpemU9IjE0LjAwIiBmaWxsPSIjZmZmZmZmIj5DaGF0TWVzc2FnZURlY29yYXRpb25IZWFkZXJWaWV3PC90ZXh0Pgo8L2c+CjwhLS0gQ2hhdE1lc3NhZ2VDZWxsJiM0NTsmZ3Q7Q2hhdE1lc3NhZ2VEZWNvcmF0aW9uSGVhZGVyVmlldyAtLT4KPGcgaWQ9ImVkZ2U1IiBjbGFzcz0iZWRnZSI+Cjx0aXRsZT5DaGF0TWVzc2FnZUNlbGwmIzQ1OyZndDtDaGF0TWVzc2FnZURlY29yYXRpb25IZWFkZXJWaWV3PC90aXRsZT4KPHBhdGggZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMDAwMDAwIiBkPSJNMjk2Ljg4NjUsLTI4Ny45MjQzQzI2MS41ODU1LC0yNzYuOTY4OCAyMTYuNzAzNiwtMjYzLjA0IDE4MS4zOTgzLC0yNTIuMDgzMSIvPgo8L2c+CjwhLS0gQ2hhdE1lc3NhZ2VDb250ZW50VmlldyAtLT4KPGcgaWQ9Im5vZGU5IiBjbGFzcz0ibm9kZSI+Cjx0aXRsZT5DaGF0TWVzc2FnZUNvbnRlbnRWaWV3PC90aXRsZT4KPHBhdGggZmlsbD0iIzM0OWJlYiIgc3Ryb2tlPSIjMzQ5YmViIiBkPSJNNDMzLjgzNDYsLTI1MkM0MzMuODM0NiwtMjUyIDI3Ni40MjYyLC0yNTIgMjc2LjQyNjIsLTI1MiAyNzAuNDI2MiwtMjUyIDI2NC40MjYyLC0yNDYgMjY0LjQyNjIsLTI0MCAyNjQuNDI2MiwtMjQwIDI2NC40MjYyLC0yMjggMjY0LjQyNjIsLTIyOCAyNjQuNDI2MiwtMjIyIDI3MC40MjYyLC0yMTYgMjc2LjQyNjIsLTIxNiAyNzYuNDI2MiwtMjE2IDQzMy44MzQ2LC0yMTYgNDMzLjgzNDYsLTIxNiA0MzkuODM0NiwtMjE2IDQ0NS44MzQ2LC0yMjIgNDQ1LjgzNDYsLTIyOCA0NDUuODM0NiwtMjI4IDQ0NS44MzQ2LC0yNDAgNDQ1LjgzNDYsLTI0MCA0NDUuODM0NiwtMjQ2IDQzOS44MzQ2LC0yNTIgNDMzLjgzNDYsLTI1MiIvPgo8dGV4dCB0ZXh0LWFuY2hvcj0ibWlkZGxlIiB4PSIzNTUuMTMwNCIgeT0iLTIyOS44IiBmb250LWZhbWlseT0iSGVsdmV0aWNhLHNhbnMtU2VyaWYiIGZvbnQtc2l6ZT0iMTQuMDAiIGZpbGw9IiNmZmZmZmYiPkNoYXRNZXNzYWdlQ29udGVudFZpZXc8L3RleHQ+CjwvZz4KPCEtLSBDaGF0TWVzc2FnZUNlbGwmIzQ1OyZndDtDaGF0TWVzc2FnZUNvbnRlbnRWaWV3IC0tPgo8ZyBpZD0iZWRnZTYiIGNsYXNzPSJlZGdlIj4KPHRpdGxlPkNoYXRNZXNzYWdlQ2VsbCYjNDU7Jmd0O0NoYXRNZXNzYWdlQ29udGVudFZpZXc8L3RpdGxlPgo8cGF0aCBmaWxsPSJub25lIiBzdHJva2U9IiMwMDAwMDAiIGQ9Ik0zNTUuMTMwNCwtMjg3LjgzMTRDMzU1LjEzMDQsLTI3NyAzNTUuMTMwNCwtMjYzLjI4NzYgMzU1LjEzMDQsLTI1Mi40MTMzIi8+CjwvZz4KPCEtLSBDaGF0TWVzc2FnZURlY29yYXRpb25Gb290ZXJWaWV3IC0tPgo8ZyBpZD0ibm9kZTEyIiBjbGFzcz0ibm9kZSI+Cjx0aXRsZT5DaGF0TWVzc2FnZURlY29yYXRpb25Gb290ZXJWaWV3PC90aXRsZT4KPHBhdGggZmlsbD0iIzM0OWJlYiIgc3Ryb2tlPSIjMzQ5YmViIiBkPSJNNjkyLjQ0NTMsLTI1MkM2OTIuNDQ1MywtMjUyIDQ3NS44MTU1LC0yNTIgNDc1LjgxNTUsLTI1MiA0NjkuODE1NSwtMjUyIDQ2My44MTU1LC0yNDYgNDYzLjgxNTUsLTI0MCA0NjMuODE1NSwtMjQwIDQ2My44MTU1LC0yMjggNDYzLjgxNTUsLTIyOCA0NjMuODE1NSwtMjIyIDQ2OS44MTU1LC0yMTYgNDc1LjgxNTUsLTIxNiA0NzUuODE1NSwtMjE2IDY5Mi40NDUzLC0yMTYgNjkyLjQ0NTMsLTIxNiA2OTguNDQ1MywtMjE2IDcwNC40NDUzLC0yMjIgNzA0LjQ0NTMsLTIyOCA3MDQuNDQ1MywtMjI4IDcwNC40NDUzLC0yNDAgNzA0LjQ0NTMsLTI0MCA3MDQuNDQ1MywtMjQ2IDY5OC40NDUzLC0yNTIgNjkyLjQ0NTMsLTI1MiIvPgo8dGV4dCB0ZXh0LWFuY2hvcj0ibWlkZGxlIiB4PSI1ODQuMTMwNCIgeT0iLTIyOS44IiBmb250LWZhbWlseT0iSGVsdmV0aWNhLHNhbnMtU2VyaWYiIGZvbnQtc2l6ZT0iMTQuMDAiIGZpbGw9IiNmZmZmZmYiPkNoYXRNZXNzYWdlRGVjb3JhdGlvbkZvb3RlclZpZXc8L3RleHQ+CjwvZz4KPCEtLSBDaGF0TWVzc2FnZUNlbGwmIzQ1OyZndDtDaGF0TWVzc2FnZURlY29yYXRpb25Gb290ZXJWaWV3IC0tPgo8ZyBpZD0iZWRnZTEyIiBjbGFzcz0iZWRnZSI+Cjx0aXRsZT5DaGF0TWVzc2FnZUNlbGwmIzQ1OyZndDtDaGF0TWVzc2FnZURlY29yYXRpb25Gb290ZXJWaWV3PC90aXRsZT4KPHBhdGggZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMDAwMDAwIiBkPSJNNDEyLjYyMTIsLTI4Ny45MjQzQzQ0Ny40NjU3LC0yNzYuOTY4OCA0OTEuNzY3MiwtMjYzLjA0IDUyNi42MTYsLTI1Mi4wODMxIi8+CjwvZz4KPCEtLSBDaGF0TWVzc2FnZUNvbnRlbnRWaWV3JiM0NTsmZ3Q7Q2hhdE1lc3NhZ2VCdWJibGVWaWV3IC0tPgo8ZyBpZD0iZWRnZTkiIGNsYXNzPSJlZGdlIj4KPHRpdGxlPkNoYXRNZXNzYWdlQ29udGVudFZpZXcmIzQ1OyZndDtDaGF0TWVzc2FnZUJ1YmJsZVZpZXc8L3RpdGxlPgo8cGF0aCBmaWxsPSJub25lIiBzdHJva2U9IiMwMDAwMDAiIGQ9Ik0zMDUuNjczMywtMjE1LjkyNDNDMjc1LjY5NzgsLTIwNC45Njg4IDIzNy41ODcsLTE5MS4wNCAyMDcuNjA3OSwtMTgwLjA4MzEiLz4KPC9nPgo8IS0tIENoYXRNZXNzYWdlQ29udGVudFZpZXcmIzQ1OyZndDtDaGF0UmVhY3Rpb25zQnViYmxlVmlldyAtLT4KPGcgaWQ9ImVkZ2U4IiBjbGFzcz0iZWRnZSI+Cjx0aXRsZT5DaGF0TWVzc2FnZUNvbnRlbnRWaWV3JiM0NTsmZ3Q7Q2hhdFJlYWN0aW9uc0J1YmJsZVZpZXc8L3RpdGxlPgo8cGF0aCBmaWxsPSJub25lIiBzdHJva2U9IiMwMDAwMDAiIGQ9Ik0zNTUuMTMwNCwtMjE1LjgzMTRDMzU1LjEzMDQsLTIwNSAzNTUuMTMwNCwtMTkxLjI4NzYgMzU1LjEzMDQsLTE4MC40MTMzIi8+CjwvZz4KPCEtLSBDaGF0TWVzc2FnZUNvbnRlbnRWaWV3JiM0NTsmZ3Q7Q2hhdEF2YXRhclZpZXcgLS0+CjxnIGlkPSJlZGdlNyIgY2xhc3M9ImVkZ2UiPgo8dGl0bGU+Q2hhdE1lc3NhZ2VDb250ZW50VmlldyYjNDU7Jmd0O0NoYXRBdmF0YXJWaWV3PC90aXRsZT4KPHBhdGggZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMDAwMDAwIiBkPSJNMzk3LjI3MTYsLTIxNS44MzE0QzQyMi42ODQxLC0yMDQuODc1IDQ1NC45MzQzLC0xOTAuOTcwOCA0ODAuMjkyMywtMTgwLjAzOCIvPgo8L2c+CjwhLS0gQ2hhdE1lc3NhZ2VSZWFjdGlvbkl0ZW1WaWV3IC0tPgo8ZyBpZD0ibm9kZTExIiBjbGFzcz0ibm9kZSI+Cjx0aXRsZT5DaGF0TWVzc2FnZVJlYWN0aW9uSXRlbVZpZXc8L3RpdGxlPgo8cGF0aCBmaWxsPSIjMzQ5YmViIiBzdHJva2U9IiMzNDliZWIiIGQ9Ik00NTAuNzc0OCwtMzZDNDUwLjc3NDgsLTM2IDI1OS40ODYsLTM2IDI1OS40ODYsLTM2IDI1My40ODYsLTM2IDI0Ny40ODYsLTMwIDI0Ny40ODYsLTI0IDI0Ny40ODYsLTI0IDI0Ny40ODYsLTEyIDI0Ny40ODYsLTEyIDI0Ny40ODYsLTYgMjUzLjQ4NiwwIDI1OS40ODYsMCAyNTkuNDg2LDAgNDUwLjc3NDgsMCA0NTAuNzc0OCwwIDQ1Ni43NzQ4LDAgNDYyLjc3NDgsLTYgNDYyLjc3NDgsLTEyIDQ2Mi43NzQ4LC0xMiA0NjIuNzc0OCwtMjQgNDYyLjc3NDgsLTI0IDQ2Mi43NzQ4LC0zMCA0NTYuNzc0OCwtMzYgNDUwLjc3NDgsLTM2Ii8+Cjx0ZXh0IHRleHQtYW5jaG9yPSJtaWRkbGUiIHg9IjM1NS4xMzA0IiB5PSItMTMuOCIgZm9udC1mYW1pbHk9IkhlbHZldGljYSxzYW5zLVNlcmlmIiBmb250LXNpemU9IjE0LjAwIiBmaWxsPSIjZmZmZmZmIj5DaGF0TWVzc2FnZVJlYWN0aW9uSXRlbVZpZXc8L3RleHQ+CjwvZz4KPCEtLSBDaGF0TWVzc2FnZVJlYWN0aW9uc1ZpZXcmIzQ1OyZndDtDaGF0TWVzc2FnZVJlYWN0aW9uSXRlbVZpZXcgLS0+CjxnIGlkPSJlZGdlMTEiIGNsYXNzPSJlZGdlIj4KPHRpdGxlPkNoYXRNZXNzYWdlUmVhY3Rpb25zVmlldyYjNDU7Jmd0O0NoYXRNZXNzYWdlUmVhY3Rpb25JdGVtVmlldzwvdGl0bGU+CjxwYXRoIGZpbGw9Im5vbmUiIHN0cm9rZT0iIzAwMDAwMCIgZD0iTTM1NS4xMzA0LC03MS44MzE0QzM1NS4xMzA0LC02MSAzNTUuMTMwNCwtNDcuMjg3NiAzNTUuMTMwNCwtMzYuNDEzMyIvPgo8L2c+CjwvZz4KPC9zdmc+Cg==)

### Overview

1. `ChatMessageLayoutOptionsResolver` calculates the `ChatMessageLayoutOptions` for each message.
1. `ChatMessageLayoutOptions` contains all the information needed by the view to render the message.
1. `ChatMessageCell` holds the message content view and all the decorations that surround it.
1. `ChatMessageContentView` holds the entire message view and all its sub-views.
1. `ChatMessageBubbleView` wraps the message content inside a bubble. Depending on the layout options, the bubble will have different borders and colors and will show or not the user profile and name.
1. `ChatReactionsBubbleView` is a wrapper for `ChatMessageReactionsView`.
1. `ChatMessageReactionsView` is responsible for rendering all reactions attached to the message.
1. `ChatMessageReactionItemView` renders a single reaction.

## Basic Customizations

In case your application only requires minimal changes to the message view, the SDK makes it easy to apply these changes without much code.

### Changing the Bubble View

If you need to customize just one component of the message view, you can easily do it by replacing one of its components with your custom subclass. As an example of how to customize one of the components from the `ChatMessageContentView` we will replace the bubble view with a custom squared bubble view.

```swift
class CustomMessageSquaredBubbleView: ChatMessageBubbleView {
    override open func layoutSubviews() {
        super.layoutSubviews()

        layer.cornerRadius = 0
    }
}
```

```swift
Components.default.messageBubbleView = CustomMessageSquaredBubbleView.self
```

#### Result

| Before                                                                                              | After                                                                                             |
| --------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
| ![Message Basic Customization Before](@chat-sdk/ios/_assets/message-basic-customization-before.png) | ![Message Basic Customization After](@chat-sdk/ios/_assets/message-basic-customization-after.png) |

<partial id="chat-sdk/ios/_partials/common-content/components-note"></partial>

### Simple Layout Changes

The `ChatMessageLayoutOptions` are flags that the `ChatMessageLayoutOptionsResolver` injects in each message view depending on the message content (For example: does the message contain reactions? Is it coming from the same user? Etc...). When rendering the message view, the layout options will be used to know which views to show or hide, and if the message cell can be reused since different layout options combinations will produce different reuse identifiers.

By customizing the `ChatMessageLayoutOptionsResolver` it is possible to do simple layout changes, like for example always showing the timestamp (by default if the messages are sent in the same minute, only the last one shows the timestamp).

```swift
final class CustomMessageLayoutOptionsResolver: ChatMessageLayoutOptionsResolver {
    override func optionsForMessage(
        at indexPath: IndexPath,
        in channel: ChatChannel,
        with messages: AnyRandomAccessCollection<ChatMessage>,
        appearance: Appearance
    ) -> ChatMessageLayoutOptions {
        var options = super.optionsForMessage(at: indexPath, in: channel, with: messages, appearance: appearance)

        // Remove the reactions and thread info from each message.
        // Remove `.flipped` option, all messages will be rendered in the leading side
        // independent if it's the current user or not.
        options.remove([
            .flipped,
            .threadInfo,
            .reactions
        ])

        // Always show the avatar, timestamp and author name for each message.
        options.insert([.avatar, .timestamp, .authorName])

        return options
    }
}
```

```swift
Components.default.messageLayoutOptionsResolver = CustomMessageLayoutOptionsResolver()
```

#### Result

| Before                                                                                    | After                                                                                   |
| ----------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
| ![Message Basic Resolver Before](@chat-sdk/ios/_assets/message-basic-resolver-before.png) | ![Message Basic Resolver After](@chat-sdk/ios/_assets/message-basic-resolver-after.png) |

#### Decoration Views

<admonition type="note">

Decoration Views are available on SDK version 4.29.0 and above.

</admonition>

The SDK allows you to configure what will be presented above and below your message with decoration views (`ChatMessageDecorationView`). They are fully customizable, and we also use them for standard SDK components like the `Date Separators` as seen below.

In order to provide a `ChatMessageDecorationView` (either a header or a footer) for a message, you will need to implement the following methods from the `ChatMessageListVCDelegate`

```swift
func chatMessageListVC(
    _ vc: ChatMessageListVC,
    headerViewForMessage message: ChatMessage,
    at indexPath: IndexPath
) -> ChatMessageDecorationView?

func chatMessageListVC(
    _ vc: ChatMessageListVC,
    footerViewForMessage message: ChatMessage,
    at indexPath: IndexPath
) -> ChatMessageDecorationView?
```

`ChatMessageDecorationView` are following a similar flow as the `UITableViewHeaderFooterView` decorations for sections that `UITableView` is managing. During the preparation of a message cell, the `ChatMessageListVC` will ask the delegate to provide `ChatMessageDecorationView` for header and footer. If the delegate returns a non-nil value the provided `ChatMessageDecorationView` will be placed in the cell's UI according to it's decoration type. In the case where the delegate will return a nil value then the cell will be updated to release the space for this specific decoration type, if it was previously used.

#### Date Separators

The SDK groups each message from the same day and shows the day which these messages belong to, since by default each message only has the time it was sent, not the day. The StreamChat SDK provides two options out-of-the-box on how to render the grouped messages date separator that can be configured in the `Components` configuration:

```swift
Components.default.messageListDateOverlayEnabled
Components.default.messageListDateSeparatorEnabled
```

By default only the `Components.default.messageListDateOverlayEnabled` is enabled, and in this case an overlay at the top will be shown with the day which the current messages being scrolled belong to.

In case you want the separator to also be shown statically between each group of messages, you can enable that option as well, and turn off the overlay option if you so desire:

```swift
Components.default.messageListDateSeparatorEnabled = true
```

##### Result

| Overlay Enabled                                                                                       | Overlay & Static Enabled                                                                            |
| ----------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| ![Message List Date Separator Overlay](@chat-sdk/ios/_assets/message-list-date-separator-overlay.png) | ![Message List Date Separator Static](@chat-sdk/ios/_assets/message-list-date-separator-static.png) |

You can also easily customize the look of the date separators by subclassing the `ChatMessageListDateSeparatorView`:

```swift
class CustomChatMessageListDateSeparatorView: ChatMessageListDateSeparatorView {

    override func setUpAppearance() {
        super.setUpAppearance()

        backgroundColor = .systemBlue
        textLabel.textColor = .black
    }

    override func updateContent() {
        super.updateContent()

        textLabel.text = content?.uppercased()
    }

    override func setUpLayout() {
        super.setUpLayout()

        NSLayoutConstraint.activate([
            textLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 24.0),
            textLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -24.0),
            textLabel.topAnchor.constraint(equalTo: topAnchor, constant: 8.0),
            textLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8.0)
        ])
    }
}

Components.default.messageListDateSeparatorView = CustomChatMessageListDateSeparatorView.self
```

##### Result

| Before                                                                                              | After                                                                                               |
| --------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| ![Message List Date Separator Static](@chat-sdk/ios/_assets/message-list-date-separator-static.png) | ![Message List Date Separator Custom](@chat-sdk/ios/_assets/message-list-date-separator-custom.png) |

#### Thread Replies Counter

<admonition type="note">

Thread Replies Counter Decoration View is available on SDK version 4.29.0 and above.

</admonition>

The SDK provides out-of-the-box a decoration view that is being displayed in threads only and shows the number of replies in this thread. You can configure the presentation of the decoration view in the `Components` configuration:

```swift
Components.default.threadRepliesCounterEnabled = true | false
```

By default the `Components.default.threadRepliesCounterEnabled` is enabled, and in this case a decoration view will be displayed just underneath the first(source) message in a thread.

##### Result

| Thread Replies Counter Disabled                                                                                               | Thread Replies Counter Enabled                                                                                              |
| ----------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| ![Thread List Replies Counter Decoration Disabled](@chat-sdk/ios/_assets/thread-list-replies-counter-decoration-disabled.png) | ![Thread List Replies Counter Decoration Enabled](@chat-sdk/ios/_assets/thread-list-replies-counter-decoration-enabled.png) |

You can easily customize the look of the thread replies decoration by subclassing the `ChatThreadRepliesCountDecorationView`:

```swift
final class DemoChatThreadRepliesCountDecorationView: ChatThreadRepliesCountDecorationView {

    private lazy var leadingLine = UIView()
    private lazy var trailingLine = UIView()

    override func setUpLayout() {
        super.setUpLayout()

        let screenScale = UIScreen.main.scale
        let hairlineHeight = 1 / screenScale
        textLabel.removeFromSuperview()

        leadingLine.translatesAutoresizingMaskIntoConstraints = false
        textLabel.translatesAutoresizingMaskIntoConstraints = false
        trailingLine.translatesAutoresizingMaskIntoConstraints = false

        container.addSubview(leadingLine)
        container.addSubview(textLabel)
        container.addSubview(trailingLine)

        NSLayoutConstraint.activate([
            leadingLine.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 9),
            leadingLine.centerYAnchor.constraint(equalTo: container.centerYAnchor),
            leadingLine.heightAnchor.constraint(equalToConstant: hairlineHeight),

            textLabel.leadingAnchor.constraint(equalTo: leadingLine.trailingAnchor, constant: 9),
            textLabel.topAnchor.constraint(equalTo: container.topAnchor, constant: 3),
            textLabel.bottomAnchor.constraint(equalTo: container.bottomAnchor, constant: -3),
            textLabel.centerXAnchor.constraint(equalTo: container.centerXAnchor),

            trailingLine.leadingAnchor.constraint(equalTo: textLabel.trailingAnchor, constant: 9),
            trailingLine.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -9),
            trailingLine.centerYAnchor.constraint(equalTo: container.centerYAnchor),
            trailingLine.heightAnchor.constraint(equalToConstant: hairlineHeight)
        ])
    }

    override func setUpAppearance() {
        super.setUpAppearance()

        container.backgroundColor = nil

        leadingLine.backgroundColor = UIColor.gray
        trailingLine.backgroundColor = UIColor.gray
    }
}
```

| Thread Replies Counter Default                                                                                              | Thread Replies Counter Customized                                                                                                 |
| --------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| ![Thread List Replies Counter Decoration Enabled](@chat-sdk/ios/_assets/thread-list-replies-counter-decoration-enabled.png) | ![Thread List Replies Counter Decoration Customized](@chat-sdk/ios/_assets/thread-list-replies-counter-decoration-customized.png) |

## Advanced Customizations

Creating a subclass of [`ChatMessageContentView`](#chatmessagecontentview) is the best way to do more advanced customizations since you have access to all the message subviews. By customizing this component you can not only change the existing views, but add new ones and add new functionality.

<admonition type="note">

`ChatMessageContentView` sets up its own layout on the `layout(options: ChatMessageLayoutOptions)` method and not in `setupLayout()` like other regular views. This view is different from the other ones since the layout is calculated based on the `ChatMessageLayoutOptions`.

</admonition>

### Restructuring the Message Layout

In order to change the message layout we first need to understand how it is structured:

![ChatMessageContentView Layout](@chat-sdk/ios/_assets/ChatMessageContentView_documentation.default-light.png)

- `mainContainer` is a horizontal container that holds all top-hierarchy views inside the `ChatMessageContentView` - This includes the `AvatarView`, `Spacer` and `BubbleThreadMetaContainer`.
- `bubbleThreadMetaContainer` is a vertical container that holds the `bubbleView` at the top and `metadataContainer` at the bottom by default. You can switch the positions for these elements or even add your own according to your needs.
- `metadataContainer` is a horizontal container that holds `authorNameLabel` , `timestampLabel` and `onlyVisibleForYouLabel`.
- `bubbleView` is a view that embeds a `bubbleContentContainer` and is only responsible for the bubble styling. The `bubbleContentContainer` contains the `textView` and `quotedMessageView` if the message is a quote.

<admonition type="note">

`bubbleView` vs `bubbleContentContainer`
When `ChatMessageContentView`'s `options` contain `.bubble` option, the `bubbleView` is added to `bubbleThreadMetaContainer`. If the option is not contained, the hierarchy includes only `bubbleContentContainer` as subview of `bubbleThreadMetaContainer`

</admonition>

As an example on how we can restructure the layout of the message view we will do the following customization:

![Custom Message Layout](@chat-sdk/ios/_assets/messagelist-layout-custom.png)

First, we need to customize the `ChatMessageLayoutOptionsResolver` and change the message layout options according to our needs. For this specific example, let's assume our message view layout needs to respect the following conditions:

- Always include the avatar, timestamp and author name.
- All messages should be rendered on the leading side.
- Don't support reactions.
- Don't support threads.

```swift
final class CustomMessageOptionsResolver: ChatMessageLayoutOptionsResolver {
    override func optionsForMessage(
        at indexPath: IndexPath,
        in channel: ChatChannel,
        with messages: AnyRandomAccessCollection<ChatMessage>,
        appearance: Appearance
    ) -> ChatMessageLayoutOptions {
        // Call super to get the default options.
        var options = super.optionsForMessage(at: indexPath, in: channel, with: messages, appearance: appearance)

        // Remove all the options that we don't want to support.
        // By removing `.flipped` option, all messages will be rendered in the leading side.
        options.remove([.flipped, .bubble, .avatarSizePadding, .threadInfo, .reactions])

        // Insert the options that we want to support.
        options.insert([.avatar, .timestamp, .authorName])

        return options
    }
}
```

Second, we need to subclass the `ChatMessageContentView` to restructure the layout. In this case we want to change the position and margins of some views:

```swift
final class CustomChatMessageContentView: ChatMessageContentView {
    override var maxContentWidthMultiplier: CGFloat { 1 }

    override func layout(options: ChatMessageLayoutOptions) {
        super.layout(options: options)

        // To have the avatarView aligned at the top with rest of the elements,
        // we'll need to set the `mainContainer` alignment to leading.
        mainContainer.alignment = .leading

        // Set inset to zero to align it with the message author
        textView?.textContainerInset = .zero

        // Reverse the order of the views in the `bubbleThreadMetaContainer`.
        // This will reverse the order of the `textView` and `metadataContainer`
        let subviews = bubbleThreadMetaContainer.subviews
        bubbleThreadMetaContainer.removeAllArrangedSubviews()
        bubbleThreadMetaContainer.addArrangedSubviews(subviews.reversed())

        // We need to disable the layout margins of the text view
        bubbleContentContainer.directionalLayoutMargins = .zero
    }
}
```

Finally, don't forget to assign the custom subclasses to `Components`:

```swift
Components.default.messageLayoutOptionsResolver = CustomMessageOptionsResolver()
Components.default.messageContentView = CustomChatMessageContentView.self
```

#### Result

| Before                                                                                            | After                                                                                           |
| ------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
| ![Message Restructure Layout Before](@chat-sdk/ios/_assets/message-restructure-layout-before.png) | ![Message Restructure Layout After](@chat-sdk/ios/_assets/message-restructure-layout-after.png) |

### Adding new Views and Functionality

To show an example of how to add a new view and functionality to the message view, let's add a share button whenever the message has attachments, so that we can share or save the attachments to our device.

First, we need to introduce a custom message layout option:

```swift
extension ChatMessageLayoutOption {
    static let shareAttachments: Self = "shareAttachments"
}
```

The `ChatMessageLayoutOption` has a similar usage as an `enum`, but it is not an `enum`. Instead, it is a struct that holds a string raw value. The advantage of this approach is that it is extendable, while the `enum` is not.

The next step is to subclass the `ChatMessageLayoutOptionsResolver` so that we can add the new `.shareAttachments` option if the message has attachments:

```swift
final class CustomMessageLayoutOptionsResolver: ChatMessageLayoutOptionsResolver {
    override func optionsForMessage(
        at indexPath: IndexPath,
        in channel: ChatChannel,
        with messages: AnyRandomAccessCollection<ChatMessage>,
        appearance: Appearance
    ) -> ChatMessageLayoutOptions {
        var options = super.optionsForMessage(
            at: indexPath,
            in: channel,
            with: messages,
            appearance: appearance
        )

        let messageIndex = messages.index(messages.startIndex, offsetBy: indexPath.item)
        let message = messages[messageIndex]

        if !message.attachmentCounts.isEmpty {
            options.insert(.shareAttachments)
        }

        return options
    }
}
```

Now we need to customize the `ChatMessageContentView` to add the new functionality in case the new option is present:

```swift
final class CustomChatMessageContentView: ChatMessageContentView {
    /// The share button.
    private var shareAttachmentsButton: UIButton?

    /// A callback that is called when the share button is tapped.
    /// The message list can then use this callback to present an activity view controller.
    var onShareAttachments: (([URL]) -> Void)?

    override func layout(options: ChatMessageLayoutOptions) {
        super.layout(options: options)

        /// We only want to add the share button if the option is present.
        if options.contains(.shareAttachments) {
            let button = createShareAttachmentsButton()
            NSLayoutConstraint.activate([
                button.heightAnchor.constraint(equalToConstant: 40)
            ])
            /// We want the share button to be rendered in the bottom of the bubble content view.
            bubbleContentContainer.spacing = 0
            bubbleContentContainer.addArrangedSubview(button)
        }
    }

    /// Creating the share button.
    private func createShareAttachmentsButton() -> UIButton {
        if shareAttachmentsButton == nil {
            shareAttachmentsButton = UIButton(type: .system)
            shareAttachmentsButton?.tintColor = .systemBlue
            shareAttachmentsButton?.translatesAutoresizingMaskIntoConstraints = false
            shareAttachmentsButton?.setImage(UIImage(systemName: "square.and.arrow.up"), for: .normal)
            shareAttachmentsButton?.addTarget(self, action: #selector(handleTapOnShareButton(_:)), for: .touchUpInside)
        }
        return shareAttachmentsButton!
    }

    /// Handling the tap on the share button.
    @objc private func handleTapOnShareButton(_ sender: UIButton) {
        guard let message = content else { return }

        let images = message.imageAttachments.map(\.imageURL)
        let files = message.fileAttachments.map(\.assetURL)
        let gifs = message.giphyAttachments.map(\.previewURL)
        let videos = message.videoAttachments.map(\.videoURL)
        let audios = message.audioAttachments.map(\.audioURL)
        let links = message.linkAttachments.map(\.originalURL)

        let allAttachments = [images, files, gifs, videos, audios, links].reduce([], +)
        onShareAttachments?(allAttachments)
    }
}
```

Since the `ChatMessageContentView` is not a view controller it can't present an `UIActivityViewController`. So we need to subclass the `ChatMessageListVC` and handle the `onShareAttachments()` callback.

```swift
class CustomChatMessageListVC: ChatMessageListVC {
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = super.tableView(tableView, cellForRowAt: indexPath) as! ChatMessageCell
        let messageContentView = cell.messageContentView as! CustomChatMessageContentView

        messageContentView.onShareAttachments = { [weak self] attachments in
            let activityViewController = UIActivityViewController(
                activityItems: attachments,
                applicationActivities: nil
            )

            self?.present(activityViewController, animated: true, completion: nil)
        }

        return cell
    }
}
```

Finally, the last step is just to replace these custom components:

```swift
Components.default.messageLayoutOptionsResolver = CustomMessageLayoutOptionsResolver()
Components.default.messageContentView = CustomChatMessageContentView.self
Components.default.messageListVC = CustomChatMessageListVC.self
```

#### Result

| Before                                                                                              | After                                                                                             |
| --------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
| ![Message Advanced New Button Before](@chat-sdk/ios/_assets/message-advanced-new-button-before.png) | ![Message Advanced New Button After](@chat-sdk/ios/_assets/message-advanced-new-button-after.png) |

## ChatMessageContentView

`ChatMessageContentView` is the container class for a message. Internally this class uses subviews such as the message bubble, reactions, attachments, and user avatars.

### Properties and Methods

<partial id="chat-sdk/ios/_partials/common-content/reference-docs/stream-chat-ui/chat-message-list/chat-message/chat-message-content-view-properties"></partial>

---

This page was last updated at 2025-08-15T14:15:55.786Z.

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/sdk/ios/uikit/components/message/](https://getstream.io/chat/docs/sdk/ios/uikit/components/message/).