diff --git a/Ring/Ring/Bridging/ConversationsAdapter.mm b/Ring/Ring/Bridging/ConversationsAdapter.mm
index a36f3359d6e93b1db9cc4bd50a147acc1d0fafbe..510de991217af1c9a8f9f07324eb592630ae717e 100644
--- a/Ring/Ring/Bridging/ConversationsAdapter.mm
+++ b/Ring/Ring/Bridging/ConversationsAdapter.mm
@@ -88,9 +88,14 @@ static id <MessagesAdapterDelegate> _messagesDelegate;
 
     confHandlers.insert(exportable_callback<ConfigurationSignal::ComposingStatusChanged>([&](const std::string& account_id, const std::string& convId, const std::string& from, int status) {
         if (ConversationsAdapter.messagesDelegate) {
-            NSString* fromPeer =  [NSString stringWithUTF8String:from.c_str()];
-            NSString* toAccount =  [NSString stringWithUTF8String:account_id.c_str()];
-            [ConversationsAdapter.messagesDelegate detectingMessageTyping:fromPeer for:toAccount status:status];
+            NSString* fromPeer = [NSString stringWithUTF8String:from.c_str()];
+            NSString* toAccount = [NSString stringWithUTF8String:account_id.c_str()];
+            NSString* conversationId = [NSString stringWithUTF8String:convId.c_str()];
+
+            [ConversationsAdapter.messagesDelegate composingStatusChangedWithAccountId:toAccount
+                                                                   conversationId:conversationId
+                                                                              from:fromPeer
+                                                                             status:status];
         }
     }));
 
@@ -225,11 +230,11 @@ static id <MessagesAdapterDelegate> _messagesDelegate;
                            [Utils dictionnaryToMap:content], flag);
 }
 
-- (void)setComposingMessageTo:(NSString*)peer
+- (void)setComposingMessageTo:(NSString*)conversationUri
                    fromAccount:(NSString*)accountID
                    isComposing:(BOOL)isComposing {
     setIsComposing(std::string([accountID UTF8String]),
-                   std::string([peer UTF8String]),
+                   std::string([conversationUri UTF8String]),
                    isComposing);
 }
 
diff --git a/Ring/Ring/Constants/Generated/Strings.swift b/Ring/Ring/Constants/Generated/Strings.swift
index 8f869d71f027bb5b5f796ac06df255a5cd3c1559..cd29b0af871757dacd2066e7af726ebd46f12b97 100644
--- a/Ring/Ring/Constants/Generated/Strings.swift
+++ b/Ring/Ring/Constants/Generated/Strings.swift
@@ -287,6 +287,8 @@ internal enum L10n {
     internal static let changePassword = L10n.tr("Localizable", "accountPage.changePassword", fallback: "Change password")
     /// Incorrect password
     internal static let changePasswordError = L10n.tr("Localizable", "accountPage.changePasswordError", fallback: "Incorrect password")
+    /// Chats
+    internal static let chats = L10n.tr("Localizable", "accountPage.chats", fallback: "Chats")
     /// Connectivity and configurations
     internal static let connectivityAndConfiguration = L10n.tr("Localizable", "accountPage.connectivityAndConfiguration", fallback: "Connectivity and configurations")
     /// Connectivity
@@ -451,6 +453,8 @@ internal enum L10n {
     internal static let turnServer = L10n.tr("Localizable", "accountPage.turnServer", fallback: "TURN address")
     /// TURN username
     internal static let turnUsername = L10n.tr("Localizable", "accountPage.turnUsername", fallback: "TURN username")
+    /// Typing indicator
+    internal static let typingIndicator = L10n.tr("Localizable", "accountPage.typingIndicator", fallback: "Typing indicator")
     /// Unblock
     internal static let unblockContact = L10n.tr("Localizable", "accountPage.unblockContact", fallback: "Unblock")
     /// Unlink
@@ -660,6 +664,22 @@ internal enum L10n {
     }
     /// You have accepted the conversation invitation.
     internal static let synchronizationTitle = L10n.tr("Localizable", "conversation.synchronizationTitle", fallback: "You have accepted the conversation invitation.")
+    /// %@ is typing
+    internal static func typingIndicatorOneUser(_ p1: Any) -> String {
+      return L10n.tr("Localizable", "conversation.typingIndicatorOneUser", String(describing: p1), fallback: "%@ is typing")
+    }
+    /// %1@, %2@ and %3@ others are typing
+    internal static func typingIndicatorOthersUsers(_ p1: Any, _ p2: Any, _ p3: Any) -> String {
+      return L10n.tr("Localizable", "conversation.typingIndicatorOthersUsers", String(describing: p1), String(describing: p2), String(describing: p3), fallback: "%1@, %2@ and %3@ others are typing")
+    }
+    /// %1@, %2@ and %3@ other are typing
+    internal static func typingIndicatorOtherUsers(_ p1: Any, _ p2: Any, _ p3: Any) -> String {
+      return L10n.tr("Localizable", "conversation.typingIndicatorOtherUsers", String(describing: p1), String(describing: p2), String(describing: p3), fallback: "%1@, %2@ and %3@ other are typing")
+    }
+    /// %1@ and %2@ are typing
+    internal static func typingIndicatorTwoUsers(_ p1: Any, _ p2: Any) -> String {
+      return L10n.tr("Localizable", "conversation.typingIndicatorTwoUsers", String(describing: p1), String(describing: p2), fallback: "%1@ and %2@ are typing")
+    }
     /// You
     internal static let yourself = L10n.tr("Localizable", "conversation.yourself", fallback: "You")
   }
diff --git a/Ring/Ring/Features/Conversations/Conversation/ConversationViewController.swift b/Ring/Ring/Features/Conversations/Conversation/ConversationViewController.swift
index 6bdb01559cca2c54c9a933c0f87bcc0c20cadfbc..fd1ca49fb5a066539a49496f0242f4caf9d15f53 100644
--- a/Ring/Ring/Features/Conversations/Conversation/ConversationViewController.swift
+++ b/Ring/Ring/Features/Conversations/Conversation/ConversationViewController.swift
@@ -166,6 +166,8 @@ class ConversationViewController: UIViewController,
                     self.recordVideo()
                 case .sendFile:
                     self.importDocument()
+                case .registerTypingIndicator(let typingStatus):
+                    self.viewModel.setIsComposingMsg(isComposing: typingStatus)
                 }
             })
             .disposed(by: self.disposeBag)
diff --git a/Ring/Ring/Features/Conversations/Conversation/ConversationViewModel.swift b/Ring/Ring/Features/Conversations/Conversation/ConversationViewModel.swift
index 99842441309ca0ff4de6a2c350ed6ecca2c1582c..adc316ac1d47f5ae3691a9514d9ecb419c72f649 100644
--- a/Ring/Ring/Features/Conversations/Conversation/ConversationViewModel.swift
+++ b/Ring/Ring/Features/Conversations/Conversation/ConversationViewModel.swift
@@ -369,6 +369,17 @@ class ConversationViewModel: Stateable, ViewModel, ObservableObject, Identifiabl
         self.conversationsService.editSwarmMessage(conversationId: conversation.id, accountId: conversation.accountId, message: content, parentId: messageId)
     }
 
+    func setIsComposingMsg(isComposing: Bool) {
+        if let conversationx = conversation {
+            guard let uri = conversationx.getConversationURI() else { return }
+            conversationsService.setIsComposingMsg(
+                to: uri,
+                from: conversation.accountId,
+                isComposing: isComposing
+            )
+        }
+    }
+
     func sendMessage(withContent content: String, parentId: String = "", contactURI: String? = nil, conversationModel: ConversationModel? = nil) {
         let conversation = conversationModel ?? self.conversation
         guard let conversation = conversation else { return }
diff --git a/Ring/Ring/Features/Conversations/Conversation/MessageSwiftUI/ViewModels/MessagePanelVM.swift b/Ring/Ring/Features/Conversations/Conversation/MessageSwiftUI/ViewModels/MessagePanelVM.swift
index ad42356be25fb796838954f3352ce9d5d1d02462..1eb01e05ca169a3d7914aedb009b81998a3b5c1b 100644
--- a/Ring/Ring/Features/Conversations/Conversation/MessageSwiftUI/ViewModels/MessagePanelVM.swift
+++ b/Ring/Ring/Features/Conversations/Conversation/MessageSwiftUI/ViewModels/MessagePanelVM.swift
@@ -125,6 +125,14 @@ class MessagePanelVM: ObservableObject, MessageAppearanceProtocol {
         isEdit = false
     }
 
+    func handleTyping(message: String) {
+        if !message.isEmpty {
+            messagePanelState.onNext(MessagePanelState.registerTypingIndicator(typingStatus: true))
+        } else {
+            messagePanelState.onNext(MessagePanelState.registerTypingIndicator(typingStatus: false))
+        }
+    }
+
     func updateUsername(name: String, jamiId: String) {
         guard let message = messageToReply, !name.isEmpty else { return }
         if message.message.authorId == jamiId {
diff --git a/Ring/Ring/Features/Conversations/Conversation/MessageSwiftUI/ViewModels/MessagesListVM.swift b/Ring/Ring/Features/Conversations/Conversation/MessageSwiftUI/ViewModels/MessagesListVM.swift
index cf88288291782c6efe6b76ebe2c88d582f5fb17f..e5db09e91129566f8f354261a41a20135080ba2e 100644
--- a/Ring/Ring/Features/Conversations/Conversation/MessageSwiftUI/ViewModels/MessagesListVM.swift
+++ b/Ring/Ring/Features/Conversations/Conversation/MessageSwiftUI/ViewModels/MessagesListVM.swift
@@ -38,6 +38,7 @@ enum MessageInfo: State {
 enum MessagePanelState: State {
     case sendMessage(content: String, parentId: String)
     case editMessage(content: String, messageId: String)
+    case registerTypingIndicator(typingStatus: Bool)
     case sendPhoto
     case openGalery
     case shareLocation
@@ -63,6 +64,8 @@ enum MessagePanelState: State {
             return L10n.Alerts.uploadFile
         case .sendPhoto:
             return "send photo"
+        case .registerTypingIndicator:
+            return "typing indicator"
         }
     }
 
@@ -147,6 +150,7 @@ class MessagesListVM: ObservableObject {
     var presenceService: PresenceService
     var transferHelper: TransferHelper
     var messagePanel: MessagePanelVM
+    var currentlyTypingUsers: Set<String> = []
 
     // state
     private let contextStateSubject = PublishSubject<State>()
@@ -211,6 +215,8 @@ class MessagesListVM: ObservableObject {
         self.subscribeReactions()
     }
 
+    @Published var typingIndicatorText = ""
+
     init (injectionBag: InjectionBag, transferHelper: TransferHelper) {
         self.requestsService = injectionBag.requestsService
         self.conversation = ConversationModel()
@@ -231,6 +237,7 @@ class MessagesListVM: ObservableObject {
         self.subscribeReplyTarget()
         self.subscribeMessagesActions()
         self.subscribeContextMenu()
+        self.subscribeToTypingStatus()
     }
 
     func subscribeScreenTapped(screenTapped: Observable<Bool>) {
@@ -800,6 +807,54 @@ class MessagesListVM: ObservableObject {
             .disposed(by: self.disposeBag)
     }
 
+    func subscribeToTypingStatus() {
+        conversationService.typingStatusStream
+            .filter { [weak self] status in
+                guard let self = self else { return false }
+                return status.conversationId == self.conversation.id
+            }
+            .observe(on: MainScheduler.instance)
+            .subscribe(onNext: { [weak self] status in
+                guard let self = self else { return }
+
+                var typerName = status.from
+
+                if let nameObservable = self.names.get(key: status.from) as? BehaviorRelay<String> {
+                    typerName = nameObservable.value
+                }
+
+                if status.status == 1 {
+                    self.currentlyTypingUsers.insert(typerName)
+                } else {
+                    self.currentlyTypingUsers.remove(typerName)
+                }
+
+                self.updateTypingIndicatorText()
+            })
+            .disposed(by: disposeBag)
+    }
+
+    private func updateTypingIndicatorText() {
+        let users = Array(currentlyTypingUsers)
+
+        switch users.count {
+        case 0:
+            typingIndicatorText = ""
+        case 1:
+            typingIndicatorText = L10n.Conversation.typingIndicatorOneUser(users[0])
+        case 2:
+            typingIndicatorText = L10n.Conversation.typingIndicatorTwoUsers(users[0], users[1])
+        default:
+            let othersTypingCount = users.count - 2
+
+            if othersTypingCount == 1 {
+                typingIndicatorText = L10n.Conversation.typingIndicatorOtherUsers(users[0], users[1], othersTypingCount)
+            } else {
+                typingIndicatorText = L10n.Conversation.typingIndicatorOthersUsers(users[0], users[1], othersTypingCount)
+            }
+        }
+    }
+
     private func updateColorPreference() {
         guard let color = UIColor(hexString: self.conversation.preferences.color) else { return }
         DispatchQueue.main.async { [weak self] in
diff --git a/Ring/Ring/Features/Conversations/Conversation/MessageSwiftUI/Views/MessagePanelView.swift b/Ring/Ring/Features/Conversations/Conversation/MessageSwiftUI/Views/MessagePanelView.swift
index cc7c96cebe7a65d7c1718145f3c643c1eb84bb5b..e3f005e8953b94213e4f31daaae2475c819dc6a8 100644
--- a/Ring/Ring/Features/Conversations/Conversation/MessageSwiftUI/Views/MessagePanelView.swift
+++ b/Ring/Ring/Features/Conversations/Conversation/MessageSwiftUI/Views/MessagePanelView.swift
@@ -190,6 +190,9 @@ struct MessagePanelView: View {
                             .cornerRadius(18)
                             .accessibilityHidden(true)
                     }
+                    .onChange(of: text) { _ in
+                        model.handleTyping(message: text)
+                    }
                 Spacer()
                     .frame(width: 10)
                 Button(action: {
diff --git a/Ring/Ring/Features/Conversations/Conversation/MessageSwiftUI/Views/MessagesListView.swift b/Ring/Ring/Features/Conversations/Conversation/MessageSwiftUI/Views/MessagesListView.swift
index 93a373a6c14d8de8bf243d91f84801ddb1033f01..bf076f97b082ac9756f7cdc086a08d862f7e200b 100644
--- a/Ring/Ring/Features/Conversations/Conversation/MessageSwiftUI/Views/MessagesListView.swift
+++ b/Ring/Ring/Features/Conversations/Conversation/MessageSwiftUI/Views/MessagesListView.swift
@@ -65,6 +65,9 @@ struct MessagesListView: View {
     @SwiftUI.State private var reactionsForMessage: ReactionsContainerModel?
     @SwiftUI.State private var showReactionsView = false
 
+    @SwiftUI.State private var dotCount = 0
+    private let timer = Timer.publish(every: 0.5, on: .main, in: .common).autoconnect()
+
     var body: some View {
         ZStack {
             ZStack(alignment: .top) {
@@ -170,6 +173,24 @@ struct MessagesListView: View {
                     // scroll to the bottom
                     Text("")
                         .id("lastMessage")
+                    if !model.typingIndicatorText.isEmpty {
+                        HStack {
+                            Text("\(model.typingIndicatorText)\(String(repeating: ".", count: dotCount))")
+                                .font(.footnote)
+                                .foregroundColor(Color(UIColor.secondaryLabel))
+                            Spacer()
+                        }
+                        .flipped()
+                        .padding(.horizontal)
+                        .padding(.vertical, 5)
+                        .accessibilityElement()
+                        .accessibilityLabel("\(model.typingIndicatorText)")
+                        .id(model.typingIndicatorText)
+                        .transition(.opacity)
+                        .onReceive(timer) { _ in
+                            dotCount = (dotCount + 1) % 4
+                        }
+                    }
                     // messages
                     ForEach(model.messagesModels) { message in
                         createMessageRowView(for: message)
diff --git a/Ring/Ring/Resources/en.lproj/Localizable.strings b/Ring/Ring/Resources/en.lproj/Localizable.strings
index 92dc69f4ef1183da06601f6eb0f7451744e101b8..6644c7df71744afc0a626e57eb69b8b6d05f05d2 100644
--- a/Ring/Ring/Resources/en.lproj/Localizable.strings
+++ b/Ring/Ring/Resources/en.lproj/Localizable.strings
@@ -161,6 +161,11 @@
 "conversation.edited" = "Edited";
 "conversation.deletedMessage" = "%@ deleted a message";
 "conversation.contactBlocked" = "Contact blocked";
+"conversation.typingIndicatorOneUser" = "%@ is typing";
+"conversation.typingIndicatorTwoUsers" = "%1@ and %2@ are typing";
+"conversation.typingIndicatorOtherUsers" = "%1@, %2@ and %3@ other are typing";
+"conversation.typingIndicatorOthersUsers" = "%1@, %2@ and %3@ others are typing";
+
 
 // Invitations
 "invitations.noInvitations" = "No invitations";
@@ -400,6 +405,8 @@
 "accountPage.bootstrap" = "Bootstrap";
 "accountPage.dhtConfiguration" = "OpenDHT configuration";
 "accountPage.nameServer" = "Name server";
+"accountPage.chats" = "Chats";
+"accountPage.typingIndicator" = "Typing indicator";
 
 // Backup Account
 "backupAccount.explanation" = "This Jami account exists only on this device. The account will be lost if this device is lost or if the application is uninstalled. It is recommended to make a backup of this account.";
diff --git a/Ring/Ring/Services/ConversationsManager.swift b/Ring/Ring/Services/ConversationsManager.swift
index 4c96b7ff6bccec3b2c8454207a851f05b7380246..9d77b1bdade3579aebde247b0b8ca895345de6bf 100644
--- a/Ring/Ring/Services/ConversationsManager.swift
+++ b/Ring/Ring/Services/ConversationsManager.swift
@@ -515,10 +515,6 @@ class ConversationsManager {
                                                       in: conversationId)
     }
 
-    func detectingMessageTyping(_ from: String, for accountId: String, status: Int) {
-        conversationService.detectingMessageTyping(from, for: accountId, status: status)
-    }
-
     func conversationProfileUpdated(conversationId: String, accountId: String, profile: [String: String]) {
         conversationService.conversationProfileUpdated(conversationId: conversationId, accountId: accountId, profile: profile)
     }
@@ -613,6 +609,10 @@ extension  ConversationsManager: MessagesAdapterDelegate {
         self.conversationService.reactionAdded(conversationId: conversationId, accountId: accountId, messageId: messageId, reaction: reaction)
     }
 
+    func composingStatusChanged(accountId: String, conversationId: String, from: String, status: Int) {
+        self.conversationService.composingStatusChanged(accountId: accountId, conversationId: conversationId, from: from, status: status)
+    }
+
     func reactionRemoved(conversationId: String, accountId: String, messageId: String, reactionId: String) {
         self.conversationService.reactionRemoved(conversationId: conversationId, accountId: accountId, messageId: messageId, reactionId: reactionId)
     }
diff --git a/Ring/Ring/Services/ConversationsService.swift b/Ring/Ring/Services/ConversationsService.swift
index 6105eccec11be792ac531f5970d60cf629570e52..0bf55f961238001e0018ac02a26dca3a8886b8fc 100644
--- a/Ring/Ring/Services/ConversationsService.swift
+++ b/Ring/Ring/Services/ConversationsService.swift
@@ -486,6 +486,28 @@ class ConversationsService {
         conversation.reactionRemoved(messageId: messageId, reactionId: reactionId)
     }
 
+    struct TypingStatus {
+        let from: String
+        let status: Int
+        let conversationId: String
+    }
+
+    func composingStatusChanged(accountId: String, conversationId: String, from: String, status: Int) {
+        guard let conversation = self.getConversationForId(conversationId: conversationId, accountId: accountId) else {
+            return
+        }
+
+        let typingStatus = TypingStatus(from: from, status: status, conversationId: conversationId)
+
+        typingStatusSubject.onNext(typingStatus)
+    }
+
+    let typingStatusSubject = ReplaySubject<TypingStatus>.create(bufferSize: 1)
+
+    var typingStatusStream: Observable<TypingStatus> {
+        return typingStatusSubject.asObservable()
+    }
+
     func messageUpdated(conversationId: String, accountId: String, message: SwarmMessageWrap, localJamiId: String) {
         guard let conversation = self.getConversationForId(conversationId: conversationId, accountId: accountId) else { return }
         conversation.messageUpdated(swarmMessage: message, localJamiId: localJamiId)
@@ -954,17 +976,8 @@ class ConversationsService {
 
     // MARK: typing indicator
 
-    func setIsComposingMsg(to peer: String, from account: String, isComposing: Bool) {
-        conversationsAdapter.setComposingMessageTo(peer, fromAccount: account, isComposing: isComposing)
-    }
-
-    func detectingMessageTyping(_ from: String, for accountId: String, status: Int) {
-        let serviceEventType: ServiceEventType = .messageTypingIndicator
-        var serviceEvent = ServiceEvent(withEventType: serviceEventType)
-        serviceEvent.addEventInput(.peerUri, value: from)
-        serviceEvent.addEventInput(.accountId, value: accountId)
-        serviceEvent.addEventInput(.state, value: status)
-        self.responseStream.onNext(serviceEvent)
+    func setIsComposingMsg(to conversationUri: String, from accountId: String, isComposing: Bool) {
+        conversationsAdapter.setComposingMessageTo(conversationUri, fromAccount: accountId, isComposing: isComposing)
     }
 }
 
diff --git a/Ring/Ring/Services/MessagesAdapterDelegate.swift b/Ring/Ring/Services/MessagesAdapterDelegate.swift
index 1d6f399e81a750003ef7b8b61559b4ede4bc8db0..f3361761254957949003640949924ea3fe55b0c4 100644
--- a/Ring/Ring/Services/MessagesAdapterDelegate.swift
+++ b/Ring/Ring/Services/MessagesAdapterDelegate.swift
@@ -26,8 +26,6 @@
                            to receiverAccountId: String)
     func messageStatusChanged(_ status: MessageStatus, for messageId: String, from accountId: String,
                               to jamiId: String, in conversationId: String)
-    func detectingMessageTyping(_ from: String, for accountId: String, status: Int)
-
     func conversationLoaded(conversationId: String, accountId: String, messages: [SwarmMessageWrap], requestId: Int)
     func messageLoaded(conversationId: String, accountId: String, messages: [[String: String]])
     func newInteraction(conversationId: String, accountId: String, message: SwarmMessageWrap)
@@ -38,6 +36,7 @@
     func conversationProfileUpdated(conversationId: String, accountId: String, profile: [String: String])
     func conversationPreferencesUpdated(conversationId: String, accountId: String, preferences: [String: String])
     func reactionAdded(conversationId: String, accountId: String, messageId: String, reaction: [String: String])
+    func composingStatusChanged(accountId: String, conversationId: String, from: String, status: Int)
     func reactionRemoved(conversationId: String, accountId: String, messageId: String, reactionId: String)
     func messageUpdated(conversationId: String, accountId: String, message: SwarmMessageWrap)
 }
diff --git a/Ring/Ring/Services/ServiceEvent.swift b/Ring/Ring/Services/ServiceEvent.swift
index 5a65a816eb80fe49d063645d25e79e6c3360321a..a220896389a5cc125263634cdb8e2b96f341539a 100644
--- a/Ring/Ring/Services/ServiceEvent.swift
+++ b/Ring/Ring/Services/ServiceEvent.swift
@@ -49,7 +49,6 @@ enum ServiceEventType {
     case callProviderPreviewPendingCall
     case audioActivated
     case newOutgoingMessage
-    case messageTypingIndicator
     case migrationEnded
     case lastDisplayedMessageUpdated
     case presenseSubscribed