ソースを参照

nctalk with rooms branding

mex3 3 ヶ月 前
コミット
9284a7fc1c
100 ファイル変更22049 行追加0 行削除
  1. 30 0
      AUTHORS.md
  2. 42 0
      BroadcastUploadExtension/Atomic.swift
  3. 10 0
      BroadcastUploadExtension/BroadcastUploadExtension.entitlements
  4. 78 0
      BroadcastUploadExtension/DarwinNotificationCenter.swift
  5. 15 0
      BroadcastUploadExtension/Info.plist
  6. 103 0
      BroadcastUploadExtension/SampleHandler.swift
  7. 149 0
      BroadcastUploadExtension/SampleUploader.swift
  8. 201 0
      BroadcastUploadExtension/SocketConnection.swift
  9. 14 0
      COPYING.iOS
  10. 9 0
      Icons/CommentIcon.svg
  11. 8 0
      Icons/CommitIcon.svg
  12. 10 0
      Icons/IssueClosedIcon.svg
  13. 9 0
      Icons/IssueClosedNotPlannedIcon.svg
  14. 9 0
      Icons/IssueOpenIcon.svg
  15. 9 0
      Icons/PrClosedIcon.svg
  16. 9 0
      Icons/PrMergedIcon.svg
  17. 10 0
      Icons/PrOpenDraftIcon.svg
  18. 9 0
      Icons/PrOpenIcon.svg
  19. 62 0
      Icons/group-avatar.svg
  20. 64 0
      Icons/public-avatar.svg
  21. 1 0
      Icons/talk.svg
  22. 674 0
      LICENSE
  23. 73 0
      LICENSES/Apache-2.0.txt
  24. 11 0
      LICENSES/BSD-3-Clause.txt
  25. 232 0
      LICENSES/GPL-3.0-or-later.txt
  26. 9 0
      LICENSES/LicenseRef-NextcloudTrademarks.txt
  27. 49 0
      LICENSES/LicenseRef-XTrademarks.txt
  28. 9 0
      LICENSES/MIT.txt
  29. 4548 0
      NextcloudTalk.xcodeproj/project.pbxproj
  30. 10 0
      NextcloudTalk.xcworkspace/contents.xcworkspacedata
  31. 20 0
      NextcloudTalk/ABContact.h
  32. 27 0
      NextcloudTalk/ABContact.m
  33. 25 0
      NextcloudTalk/AddParticipantsTableViewController.h
  34. 476 0
      NextcloudTalk/AddParticipantsTableViewController.m
  35. 31 0
      NextcloudTalk/AddParticipantsTableViewController.xib
  36. 56 0
      NextcloudTalk/AllocationTracker.swift
  37. 22 0
      NextcloudTalk/AppDelegate.h
  38. 575 0
      NextcloudTalk/AppDelegate.m
  39. 115 0
      NextcloudTalk/AudioPlayerView.swift
  40. 67 0
      NextcloudTalk/AudioPlayerView.xib
  41. 26 0
      NextcloudTalk/AuthenticationViewController.h
  42. 200 0
      NextcloudTalk/AuthenticationViewController.m
  43. 21 0
      NextcloudTalk/AutoCompletionTableViewCell.h
  44. 122 0
      NextcloudTalk/AutoCompletionTableViewCell.m
  45. 22 0
      NextcloudTalk/AvatarBackgroundImageView.h
  46. 75 0
      NextcloudTalk/AvatarBackgroundImageView.m
  47. 85 0
      NextcloudTalk/AvatarButton.swift
  48. 77 0
      NextcloudTalk/AvatarEditView.swift
  49. 153 0
      NextcloudTalk/AvatarEditView.xib
  50. 83 0
      NextcloudTalk/AvatarImageView.swift
  51. 177 0
      NextcloudTalk/AvatarManager.swift
  52. 40 0
      NextcloudTalk/BGTaskHelper.swift
  53. 33 0
      NextcloudTalk/BannedActor.swift
  54. 89 0
      NextcloudTalk/BannedActorCell.swift
  55. 100 0
      NextcloudTalk/BannedActorCell.xib
  56. 118 0
      NextcloudTalk/BannedActorTableViewController.swift
  57. 18 0
      NextcloudTalk/BarButtonItemWithActivity.h
  58. 57 0
      NextcloudTalk/BarButtonItemWithActivity.m
  59. 38 0
      NextcloudTalk/Base.lproj/LaunchScreen.xib
  60. 209 0
      NextcloudTalk/Base.lproj/Main.storyboard
  61. 55 0
      NextcloudTalk/BaseChatTableViewCell+Audio.swift
  62. 305 0
      NextcloudTalk/BaseChatTableViewCell+File.swift
  63. 117 0
      NextcloudTalk/BaseChatTableViewCell+Location.swift
  64. 33 0
      NextcloudTalk/BaseChatTableViewCell+Message.swift
  65. 51 0
      NextcloudTalk/BaseChatTableViewCell+Poll.swift
  66. 607 0
      NextcloudTalk/BaseChatTableViewCell.swift
  67. 148 0
      NextcloudTalk/BaseChatTableViewCell.xib
  68. 3483 0
      NextcloudTalk/BaseChatViewController.swift
  69. 29 0
      NextcloudTalk/CCCertificate.h
  70. 183 0
      NextcloudTalk/CCCertificate.m
  71. 19 0
      NextcloudTalk/CallConstants.h
  72. 205 0
      NextcloudTalk/CallFlowLayout.swift
  73. 49 0
      NextcloudTalk/CallKitManager.h
  74. 654 0
      NextcloudTalk/CallKitManager.m
  75. 57 0
      NextcloudTalk/CallParticipantViewCell.h
  76. 332 0
      NextcloudTalk/CallParticipantViewCell.m
  77. 141 0
      NextcloudTalk/CallParticipantViewCell.xib
  78. 40 0
      NextcloudTalk/CallReactionView.swift
  79. 66 0
      NextcloudTalk/CallReactionView.xib
  80. 53 0
      NextcloudTalk/CallViewController.h
  81. 2654 0
      NextcloudTalk/CallViewController.m
  82. 406 0
      NextcloudTalk/CallViewController.xib
  83. 57 0
      NextcloudTalk/CallsFromOldAccountViewController.swift
  84. 74 0
      NextcloudTalk/CallsFromOldAccountViewController.xib
  85. 16 0
      NextcloudTalk/CapturerEventsDelegate.h
  86. 39 0
      NextcloudTalk/ChatTableViewCell.h
  87. 158 0
      NextcloudTalk/ChatTableViewCell.m
  88. 1813 0
      NextcloudTalk/ChatViewController.swift
  89. 18 0
      NextcloudTalk/ChatViewControllerExtension.swift
  90. 100 0
      NextcloudTalk/ColorGenerator.swift
  91. 93 0
      NextcloudTalk/ContactsSearchResultTableViewContoller.swift
  92. 27 0
      NextcloudTalk/ContactsTableViewCell.h
  93. 94 0
      NextcloudTalk/ContactsTableViewCell.m
  94. 87 0
      NextcloudTalk/ContactsTableViewCell.xib
  95. 15 0
      NextcloudTalk/ContextChatViewController.swift
  96. 48 0
      NextcloudTalk/CustomPresentable.swift
  97. 9 0
      NextcloudTalk/CustomPresentableNavigationController.swift
  98. 14 0
      NextcloudTalk/DateHeaderView.h
  99. 31 0
      NextcloudTalk/DateHeaderView.m
  100. 46 0
      NextcloudTalk/DateHeaderView.xib

+ 30 - 0
AUTHORS.md

@@ -0,0 +1,30 @@
+<!--
+  - SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+  - SPDX-License-Identifier: GPL-3.0-or-later
+-->
+# Authors
+
+- Aleksandra Lazarevic <a.lazarevic2016@gmail.com>
+- Andy Scherzinger <info@andy-scherzinger.de>
+- Björn Schießle <bjoern@schiessle.org>
+- Blacky <106263486+Black-Fox-2022@users.noreply.github.com>
+- Claudio Cambra <claudio.cambra@gmail.com>
+- Daniel Hansson <github@techandme.se>
+- e-caste-mbp <git@caste.dev>
+- fwcd <fwcdmail@gmail.com>
+- Gino <g.jongenelen@pushto.space>
+- Henrik Storch <henrik.storch@nextcloud.com>
+- Ivan Sein <ivan@nextcloud.com>
+- Jan-Christoph Borchardt <hey@jancborchardt.net>
+- Joas Schilling <coding@schilljs.com>
+- John Molakvoæ <skjnldsv@protonmail.com>
+- Lukas Lauerer <lukas.lauerer@gmx.net>
+- Marcel Müller <marcel-mueller@gmx.de>
+- Marino Faggiana <marino.faggiana@nextcloud.com>
+- Mario Danic <mario@lovelyhq.com>
+- Morris Jobke <hey@morrisjobke.de>
+- rakekniven <2069590+rakekniven@users.noreply.github.com>
+- Roman Podymov <podymfrombryansk@yandex.com>
+- sconway856 <sconway856@gmail.com>
+- Shawn Chen <swchen@swchen-mn3.linkedin.biz>
+- Valdnet <47037905+Valdnet@users.noreply.github.com>

+ 42 - 0
BroadcastUploadExtension/Atomic.swift

@@ -0,0 +1,42 @@
+//
+//  Atomic.swift
+//  Broadcast Extension
+//
+//  Created by Maksym Shcheglov.
+//  https://www.onswiftwings.com/posts/atomic-property-wrapper/
+//
+// From https://github.com/jitsi/jitsi-meet-sdk-samples (Apache 2.0 license)
+//
+// SPDX-FileCopyrightText: Maksym Shcheglov
+// SPDX-License-Identifier: Apache-2.0
+//
+
+import Foundation
+
+@propertyWrapper
+struct Atomic<Value> {
+
+    private var value: Value
+    private let lock = NSLock()
+
+    init(wrappedValue value: Value) {
+        self.value = value
+    }
+
+    var wrappedValue: Value {
+        get { load() }
+        set { store(newValue: newValue) }
+    }
+
+    func load() -> Value {
+        lock.lock()
+        defer { lock.unlock() }
+        return value
+    }
+
+    mutating func store(newValue: Value) {
+        lock.lock()
+        defer { lock.unlock() }
+        value = newValue
+    }
+}

+ 10 - 0
BroadcastUploadExtension/BroadcastUploadExtension.entitlements

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>com.apple.security.application-groups</key>
+	<array>
+		<string>group.com.sharix.sxrooms</string>
+	</array>
+</dict>
+</plist>

+ 78 - 0
BroadcastUploadExtension/DarwinNotificationCenter.swift

@@ -0,0 +1,78 @@
+//
+//  DarwinNotificationCenter.swift
+//  Broadcast Extension
+//
+//  Created by Alex-Dan Bumbu on 23/03/2021.
+//  Copyright © 2021 8x8, Inc. All rights reserved.
+//
+// From https://github.com/jitsi/jitsi-meet-sdk-samples (Apache 2.0 license)
+// SPDX-FileCopyrightText: 2021 Alex-Dan Bumbu, 8x8, Inc. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import Foundation
+
+@objcMembers public class DarwinNotificationCenter: NSObject {
+
+    static let shared = DarwinNotificationCenter()
+    static let broadcastStartedNotification = "TalkiOS_BroadcastStarted"
+    static let broadcastStoppedNotification = "TalkiOS_BroadcastStopped"
+
+    private let notificationCenter: CFNotificationCenter
+
+    internal var handlers: [String: [AnyHashable: () -> Void]] = [:]
+
+    override init() {
+        notificationCenter = CFNotificationCenterGetDarwinNotifyCenter()
+    }
+
+    func postNotification(_ name: String) {
+        CFNotificationCenterPostNotification(notificationCenter, CFNotificationName(rawValue: name as CFString), nil, nil, true)
+    }
+
+    func addHandler(notificationName: String, owner: AnyHashable, completionBlock: @escaping () -> Void) {
+        // When there are already handlers, we just add our own here
+        if handlers[notificationName] != nil {
+            handlers[notificationName]?[owner] = completionBlock
+            return
+        }
+
+        // No handler for this notification -> setup a new darwin observer for that notification
+        handlers[notificationName] = [owner: completionBlock]
+
+        // see: https://stackoverflow.com/a/33262376
+        let callback: CFNotificationCallback = { _, observer, name, _, _ in
+            if let observer, let name {
+                // Extract pointer to `self` from void pointer:
+                let mySelf = Unmanaged<DarwinNotificationCenter>.fromOpaque(observer).takeUnretainedValue()
+
+                if let handlers = mySelf.handlers[name.rawValue as String] {
+                    for handler in handlers {
+                        handler.value()
+                    }
+                }
+            }
+        }
+
+        let observer = Unmanaged.passUnretained(self).toOpaque()
+        CFNotificationCenterAddObserver(self.notificationCenter, observer, callback, notificationName as CFString, nil, .coalesce)
+    }
+
+    func removeHandler(notificationName: String, owner: AnyHashable) {
+        guard handlers[notificationName] != nil else { return }
+
+        // Remove the handler for the specified owner
+        handlers[notificationName]!.removeValue(forKey: owner)
+
+        // There are still other handlers registered for this notification, keep the darwin center observer
+        if !handlers[notificationName]!.isEmpty {
+            return
+        }
+
+        // No handlers registered for that notification anymore, remove the observer from darwin center
+        handlers.removeValue(forKey: notificationName)
+
+        let observer = Unmanaged.passUnretained(self).toOpaque()
+        let name = CFNotificationName(rawValue: notificationName as CFString)
+        CFNotificationCenterRemoveObserver(notificationCenter, observer, name, nil)
+    }
+}

+ 15 - 0
BroadcastUploadExtension/Info.plist

@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>NSExtension</key>
+	<dict>
+		<key>NSExtensionPointIdentifier</key>
+		<string>com.apple.broadcast-services-upload</string>
+		<key>NSExtensionPrincipalClass</key>
+		<string>$(PRODUCT_MODULE_NAME).SampleHandler</string>
+		<key>RPBroadcastProcessMode</key>
+		<string>RPBroadcastProcessModeSampleBuffer</string>
+	</dict>
+</dict>
+</plist>

+ 103 - 0
BroadcastUploadExtension/SampleHandler.swift

@@ -0,0 +1,103 @@
+//
+//  SampleHandler.swift
+//  Broadcast Extension
+//
+//  Created by Alex-Dan Bumbu on 04.06.2021.
+//
+// From https://github.com/jitsi/jitsi-meet-sdk-samples (Apache 2.0 license)
+// SPDX-FileCopyrightText: 2021 Alex-Dan Bumbu
+// SPDX-License-Identifier: Apache-2.0
+
+import ReplayKit
+
+class SampleHandler: RPBroadcastSampleHandler {
+
+    private var clientConnection: SocketConnection?
+    private var uploader: SampleUploader?
+
+    private var frameCount: Int = 0
+
+    var socketFilePath: String {
+        let sharedContainer = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: groupIdentifier)
+        return sharedContainer?.appendingPathComponent("rtc_SSFD").path ?? ""
+    }
+
+    override init() {
+      super.init()
+        if let connection = SocketConnection(filePath: socketFilePath) {
+          clientConnection = connection
+          setupConnection()
+
+          uploader = SampleUploader(connection: connection)
+        }
+    }
+
+    override func broadcastStarted(withSetupInfo setupInfo: [String: NSObject]?) {
+        // User has requested to start the broadcast. Setup info from the UI extension can be supplied but optional.
+        frameCount = 0
+
+        DarwinNotificationCenter.shared.postNotification(DarwinNotificationCenter.broadcastStartedNotification)
+        openConnection()
+    }
+
+    override func broadcastPaused() {
+        // User has requested to pause the broadcast. Samples will stop being delivered.
+    }
+
+    override func broadcastResumed() {
+        // User has requested to resume the broadcast. Samples delivery will resume.
+    }
+
+    override func broadcastFinished() {
+        // User has requested to finish the broadcast.
+        DarwinNotificationCenter.shared.postNotification(DarwinNotificationCenter.broadcastStoppedNotification)
+        clientConnection?.close()
+    }
+
+    override func processSampleBuffer(_ sampleBuffer: CMSampleBuffer, with sampleBufferType: RPSampleBufferType) {
+        switch sampleBufferType {
+        case RPSampleBufferType.video:
+            // very simple mechanism for adjusting frame rate by using every third frame
+            frameCount += 1
+            if frameCount % 2 == 0 {
+                uploader?.send(sample: sampleBuffer)
+            }
+        default:
+            break
+        }
+    }
+}
+
+private extension SampleHandler {
+
+    func setupConnection() {
+        clientConnection?.didClose = { [weak self] error in
+            print("client connection did close \(String(describing: error))")
+
+            if let error = error {
+                self?.finishBroadcastWithError(error)
+            } else {
+                // the displayed failure message is more user friendly when using NSError instead of Error
+                let JMScreenSharingStopped = 10001
+                let localizedError = NSLocalizedString("Screensharing stopped", comment: "")
+                let customError = NSError(domain: RPRecordingErrorDomain, code: JMScreenSharingStopped, userInfo: [NSLocalizedDescriptionKey: localizedError])
+                self?.finishBroadcastWithError(customError)
+            }
+        }
+    }
+
+    func openConnection() {
+        let queue = DispatchQueue(label: "broadcast.connectTimer")
+        let timer = DispatchSource.makeTimerSource(queue: queue)
+        timer.schedule(deadline: .now(), repeating: .milliseconds(100), leeway: .milliseconds(500))
+        timer.setEventHandler { [weak self] in
+            guard self?.clientConnection?.open() == true else {
+                return
+            }
+
+            timer.cancel()
+        }
+
+        timer.resume()
+    }
+}

+ 149 - 0
BroadcastUploadExtension/SampleUploader.swift

@@ -0,0 +1,149 @@
+//
+//  SampleUploader.swift
+//  Broadcast Extension
+//
+//  Created by Alex-Dan Bumbu on 22/03/2021.
+//  Copyright © 2021 8x8, Inc. All rights reserved.
+//
+// From https://github.com/jitsi/jitsi-meet-sdk-samples (Apache 2.0 license)
+// SPDX-FileCopyrightText: 2021 Alex-Dan Bumbu, 8x8, Inc. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import Foundation
+import ReplayKit
+
+private enum Constants {
+    static let bufferMaxLength = 10240
+}
+
+class SampleUploader {
+
+    private static var imageContext = CIContext(options: nil)
+
+    @Atomic private var isReady = false
+    private var connection: SocketConnection
+
+    private var dataToSend: Data?
+    private var byteIndex = 0
+
+    private let serialQueue: DispatchQueue
+
+    init(connection: SocketConnection) {
+        self.connection = connection
+        self.serialQueue = DispatchQueue(label: "talk.broadcast.sampleUploader")
+
+        setupConnection()
+    }
+
+    @discardableResult func send(sample buffer: CMSampleBuffer) -> Bool {
+        guard isReady else {
+            return false
+        }
+
+        isReady = false
+
+        dataToSend = prepare(sample: buffer)
+        byteIndex = 0
+
+        serialQueue.async { [weak self] in
+            self?.sendDataChunk()
+        }
+
+        return true
+    }
+}
+
+private extension SampleUploader {
+
+    func setupConnection() {
+        connection.didOpen = { [weak self] in
+            self?.isReady = true
+        }
+        connection.streamHasSpaceAvailable = { [weak self] in
+            self?.serialQueue.async {
+                if let success = self?.sendDataChunk() {
+                    self?.isReady = !success
+                }
+            }
+        }
+    }
+
+    @discardableResult func sendDataChunk() -> Bool {
+        guard let dataToSend = dataToSend else {
+            return false
+        }
+
+        var bytesLeft = dataToSend.count - byteIndex
+        var length = bytesLeft > Constants.bufferMaxLength ? Constants.bufferMaxLength : bytesLeft
+
+        length = dataToSend[byteIndex..<(byteIndex + length)].withUnsafeBytes {
+            guard let ptr = $0.bindMemory(to: UInt8.self).baseAddress else {
+                return 0
+            }
+
+            return connection.writeToStream(buffer: ptr, maxLength: length)
+        }
+
+        if length > 0 {
+            byteIndex += length
+            bytesLeft -= length
+
+            if bytesLeft == 0 {
+                self.dataToSend = nil
+                byteIndex = 0
+            }
+        } else {
+            print("writeBufferToStream failure")
+        }
+
+        return true
+    }
+
+    func prepare(sample buffer: CMSampleBuffer) -> Data? {
+        guard let imageBuffer = CMSampleBufferGetImageBuffer(buffer) else {
+            print("image buffer not available")
+            return nil
+        }
+
+        CVPixelBufferLockBaseAddress(imageBuffer, .readOnly)
+
+        let scaleFactor = 2.0
+        let width = CVPixelBufferGetWidth(imageBuffer)/Int(scaleFactor)
+        let height = CVPixelBufferGetHeight(imageBuffer)/Int(scaleFactor)
+        let orientation = CMGetAttachment(buffer, key: RPVideoSampleOrientationKey as CFString, attachmentModeOut: nil)?.uintValue ?? 0
+
+        let scaleTransform = CGAffineTransform(scaleX: CGFloat(1.0/scaleFactor), y: CGFloat(1.0/scaleFactor))
+        let bufferData = self.jpegData(from: imageBuffer, scale: scaleTransform)
+
+        CVPixelBufferUnlockBaseAddress(imageBuffer, .readOnly)
+
+        guard let messageData = bufferData else {
+            print("corrupted image buffer")
+            return nil
+        }
+
+        let httpResponse = CFHTTPMessageCreateResponse(nil, 200, nil, kCFHTTPVersion1_1).takeRetainedValue()
+        CFHTTPMessageSetHeaderFieldValue(httpResponse, "Content-Length" as CFString, String(messageData.count) as CFString)
+        CFHTTPMessageSetHeaderFieldValue(httpResponse, "Buffer-Width" as CFString, String(width) as CFString)
+        CFHTTPMessageSetHeaderFieldValue(httpResponse, "Buffer-Height" as CFString, String(height) as CFString)
+        CFHTTPMessageSetHeaderFieldValue(httpResponse, "Buffer-Orientation" as CFString, String(orientation) as CFString)
+
+        CFHTTPMessageSetBody(httpResponse, messageData as CFData)
+
+        let serializedMessage = CFHTTPMessageCopySerializedMessage(httpResponse)?.takeRetainedValue() as Data?
+
+        return serializedMessage
+    }
+
+    func jpegData(from buffer: CVPixelBuffer, scale scaleTransform: CGAffineTransform) -> Data? {
+        let image = CIImage(cvPixelBuffer: buffer).transformed(by: scaleTransform)
+
+        guard let colorSpace = image.colorSpace else {
+            return nil
+        }
+
+        let options: [CIImageRepresentationOption: Float] = [kCGImageDestinationLossyCompressionQuality as CIImageRepresentationOption: 1.0]
+
+        return SampleUploader.imageContext.jpegRepresentation(of: image, colorSpace: colorSpace, options: options)
+    }
+}

+ 201 - 0
BroadcastUploadExtension/SocketConnection.swift

@@ -0,0 +1,201 @@
+//
+//  SocketConnection.swift
+//  Broadcast Extension
+//
+//  Created by Alex-Dan Bumbu on 22/03/2021.
+//  Copyright © 2021 Atlassian Inc. All rights reserved.
+//
+// From https://github.com/jitsi/jitsi-meet-sdk-samples (Apache 2.0 license)
+// SPDX-FileCopyrightText: 2021 Alex-Dan Bumbu, Atlassian Inc. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import Foundation
+
+class SocketConnection: NSObject {
+    var didOpen: (() -> Void)?
+    var didClose: ((Error?) -> Void)?
+    var streamHasSpaceAvailable: (() -> Void)?
+
+    private let filePath: String
+    private var socketHandle: Int32 = -1
+    private var address: sockaddr_un?
+
+    private var inputStream: InputStream?
+    private var outputStream: OutputStream?
+
+    private var networkQueue: DispatchQueue?
+    private var shouldKeepRunning = false
+
+    init?(filePath path: String) {
+        filePath = path
+        socketHandle = Darwin.socket(AF_UNIX, SOCK_STREAM, 0)
+
+        guard socketHandle != -1 else {
+            print("failure: create socket")
+            return nil
+        }
+    }
+
+    func open() -> Bool {
+        print("open socket connection")
+
+        guard FileManager.default.fileExists(atPath: filePath) else {
+            print("failure: socket file missing")
+            return false
+        }
+
+        guard setupAddress() == true else {
+            return false
+        }
+
+        guard connectSocket() == true else {
+            return false
+        }
+
+        setupStreams()
+
+        inputStream?.open()
+        outputStream?.open()
+
+        return true
+    }
+
+    func close() {
+        unscheduleStreams()
+
+        inputStream?.delegate = nil
+        outputStream?.delegate = nil
+
+        inputStream?.close()
+        outputStream?.close()
+
+        inputStream = nil
+        outputStream = nil
+    }
+
+    func writeToStream(buffer: UnsafePointer<UInt8>, maxLength length: Int) -> Int {
+        outputStream?.write(buffer, maxLength: length) ?? 0
+    }
+}
+
+extension SocketConnection: StreamDelegate {
+
+    func stream(_ aStream: Stream, handle eventCode: Stream.Event) {
+        switch eventCode {
+        case .openCompleted:
+            print("client stream open completed")
+            if aStream == outputStream {
+                didOpen?()
+            }
+        case .hasBytesAvailable:
+            if aStream == inputStream {
+                var buffer: UInt8 = 0
+                let numberOfBytesRead = inputStream?.read(&buffer, maxLength: 1)
+                if numberOfBytesRead == 0 && aStream.streamStatus == .atEnd {
+                    print("server socket closed")
+                    close()
+                    notifyDidClose(error: nil)
+                }
+            }
+        case .hasSpaceAvailable:
+            if aStream == outputStream {
+                streamHasSpaceAvailable?()
+            }
+        case .errorOccurred:
+            print("client stream error occured: \(String(describing: aStream.streamError))")
+            close()
+            notifyDidClose(error: aStream.streamError)
+
+        default:
+            break
+        }
+    }
+}
+
+private extension SocketConnection {
+
+    func setupAddress() -> Bool {
+        var addr = sockaddr_un()
+        guard filePath.count < MemoryLayout.size(ofValue: addr.sun_path) else {
+            print("failure: fd path is too long")
+            return false
+        }
+
+        _ = withUnsafeMutablePointer(to: &addr.sun_path.0) { ptr in
+            filePath.withCString {
+                strncpy(ptr, $0, filePath.count)
+            }
+        }
+
+        address = addr
+        return true
+    }
+
+    func connectSocket() -> Bool {
+        guard var addr = address else {
+            return false
+        }
+
+        let status = withUnsafePointer(to: &addr) { ptr in
+            ptr.withMemoryRebound(to: sockaddr.self, capacity: 1) {
+                Darwin.connect(socketHandle, $0, socklen_t(MemoryLayout<sockaddr_un>.size))
+            }
+        }
+
+        guard status == noErr else {
+            print("failure: \(status)")
+            return false
+        }
+
+        return true
+    }
+
+    func setupStreams() {
+        var readStream: Unmanaged<CFReadStream>?
+        var writeStream: Unmanaged<CFWriteStream>?
+
+        CFStreamCreatePairWithSocket(kCFAllocatorDefault, socketHandle, &readStream, &writeStream)
+
+        inputStream = readStream?.takeRetainedValue()
+        inputStream?.delegate = self
+        inputStream?.setProperty(kCFBooleanTrue, forKey: Stream.PropertyKey(kCFStreamPropertyShouldCloseNativeSocket as String))
+
+        outputStream = writeStream?.takeRetainedValue()
+        outputStream?.delegate = self
+        outputStream?.setProperty(kCFBooleanTrue, forKey: Stream.PropertyKey(kCFStreamPropertyShouldCloseNativeSocket as String))
+
+        scheduleStreams()
+    }
+
+    func scheduleStreams() {
+        shouldKeepRunning = true
+
+        networkQueue = DispatchQueue.global(qos: .userInitiated)
+        networkQueue?.async { [weak self] in
+            self?.inputStream?.schedule(in: .current, forMode: .common)
+            self?.outputStream?.schedule(in: .current, forMode: .common)
+            RunLoop.current.run()
+
+            var isRunning = false
+
+            repeat {
+                isRunning = self?.shouldKeepRunning ?? false && RunLoop.current.run(mode: .default, before: .distantFuture)
+            } while (isRunning)
+        }
+    }
+
+    func unscheduleStreams() {
+        networkQueue?.sync { [weak self] in
+            self?.inputStream?.remove(from: .current, forMode: .common)
+            self?.outputStream?.remove(from: .current, forMode: .common)
+        }
+
+        shouldKeepRunning = false
+    }
+
+    func notifyDidClose(error: Error?) {
+        if didClose != nil {
+            didClose?(error)
+        }
+    }
+}

+ 14 - 0
COPYING.iOS

@@ -0,0 +1,14 @@
+The Nextcloud iOS developers are aware that the terms of service that
+apply to apps distributed via Apple's App Store services may conflict
+with rights granted under the Nextcloud Talk iOS license, the GNU General
+Public License, version 3 or (at your option) any later version. The
+copyright holders of the Nextcloud Talk iOS app do not wish this conflict
+to prevent the otherwise-compliant distribution of derived apps via
+the App Store. Therefore, we have committed not to pursue any license
+violation that results solely from the conflict between the GNU GPLv3
+or any later version and the Apple App Store terms of service. In
+other words, as long as you comply with the GPL in all other respects,
+including its requirements to provide users with source code and the
+text of the license, we will not object to your distribution of the
+Nextcloud Talk iOS app through the App Store.
+

+ 9 - 0
Icons/CommentIcon.svg

@@ -0,0 +1,9 @@
+
+		<svg
+			enable-background="new 0 0 16 16"
+			version="1.1"
+			viewBox="0 0 16 16"
+			xml:space="preserve"
+			xmlns="http://www.w3.org/2000/svg">
+			<path fill-rule="evenodd" d="M2.75 2.5a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h2a.75.75 0 01.75.75v2.19l2.72-2.72a.75.75 0 01.53-.22h4.5a.25.25 0 00.25-.25v-7.5a.25.25 0 00-.25-.25H2.75zM1 2.75C1 1.784 1.784 1 2.75 1h10.5c.966 0 1.75.784 1.75 1.75v7.5A1.75 1.75 0 0113.25 12H9.06l-2.573 2.573A1.457 1.457 0 014 13.543V12H2.75A1.75 1.75 0 011 10.25v-7.5z" />
+		</svg>

+ 8 - 0
Icons/CommitIcon.svg

@@ -0,0 +1,8 @@
+		<svg
+			enable-background="new 0 0 16 16"
+			version="1.1"
+			viewBox="0 0 16 16"
+			xml:space="preserve"
+			xmlns="http://www.w3.org/2000/svg">
+			<path fill-rule="evenodd" d="M10.5 7.75a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0zm1.43.75a4.002 4.002 0 01-7.86 0H.75a.75.75 0 110-1.5h3.32a4.001 4.001 0 017.86 0h3.32a.75.75 0 110 1.5h-3.32z" />
+		</svg>

+ 10 - 0
Icons/IssueClosedIcon.svg

@@ -0,0 +1,10 @@
+
+		<svg
+			enable-background="new 0 0 16 16"
+			version="1.1"
+			viewBox="0 0 16 16"
+			xml:space="preserve"
+			xmlns="http://www.w3.org/2000/svg">
+			<path d="M11.28 6.78a.75.75 0 00-1.06-1.06L7.25 8.69 5.78 7.22a.75.75 0 00-1.06 1.06l2 2a.75.75 0 001.06 0l3.5-3.5z" />
+			<path fill-rule="evenodd" d="M16 8A8 8 0 110 8a8 8 0 0116 0zm-1.5 0a6.5 6.5 0 11-13 0 6.5 6.5 0 0113 0z" />
+		</svg>

+ 9 - 0
Icons/IssueClosedNotPlannedIcon.svg

@@ -0,0 +1,9 @@
+
+		<svg
+			enable-background="new 0 0 16 16"
+			version="1.1"
+			viewBox="0 0 16 16"
+			xml:space="preserve"
+			xmlns="http://www.w3.org/2000/svg">
+			<path fill-rule="evenodd" d="M1.5 8a6.5 6.5 0 1113 0 6.5 6.5 0 01-13 0zM8 0a8 8 0 100 16A8 8 0 008 0zm3.28 5.78a.75.75 0 00-1.06-1.06l-5.5 5.5a.75.75 0 101.06 1.06l5.5-5.5z" />
+		</svg>

+ 9 - 0
Icons/IssueOpenIcon.svg

@@ -0,0 +1,9 @@
+		<svg
+			enable-background="new 0 0 16 16"
+			version="1.1"
+			viewBox="0 0 16 16"
+			xml:space="preserve"
+			xmlns="http://www.w3.org/2000/svg">
+			<path d="M8 9.5a1.5 1.5 0 100-3 1.5 1.5 0 000 3z" />
+			<path fill-rule="evenodd" d="M8 0a8 8 0 100 16A8 8 0 008 0zM1.5 8a6.5 6.5 0 1113 0 6.5 6.5 0 01-13 0z" />
+		</svg>

+ 9 - 0
Icons/PrClosedIcon.svg

@@ -0,0 +1,9 @@
+
+		<svg
+			enable-background="new 0 0 16 16"
+			version="1.1"
+			viewBox="0 0 16 16"
+			xml:space="preserve"
+			xmlns="http://www.w3.org/2000/svg">
+			<path fill-rule="evenodd" d="M10.72 1.227a.75.75 0 011.06 0l.97.97.97-.97a.75.75 0 111.06 1.061l-.97.97.97.97a.75.75 0 01-1.06 1.06l-.97-.97-.97.97a.75.75 0 11-1.06-1.06l.97-.97-.97-.97a.75.75 0 010-1.06zM12.75 6.5a.75.75 0 00-.75.75v3.378a2.251 2.251 0 101.5 0V7.25a.75.75 0 00-.75-.75zm0 5.5a.75.75 0 100 1.5.75.75 0 000-1.5zM2.5 3.25a.75.75 0 111.5 0 .75.75 0 01-1.5 0zM3.25 1a2.25 2.25 0 00-.75 4.372v5.256a2.251 2.251 0 101.5 0V5.372A2.25 2.25 0 003.25 1zm0 11a.75.75 0 100 1.5.75.75 0 000-1.5z" />
+		</svg>

+ 9 - 0
Icons/PrMergedIcon.svg

@@ -0,0 +1,9 @@
+
+		<svg
+			enable-background="new 0 0 16 16"
+			version="1.1"
+			viewBox="0 0 16 16"
+			xml:space="preserve"
+			xmlns="http://www.w3.org/2000/svg">
+			<path fill-rule="evenodd" d="M5 3.254V3.25v.005a.75.75 0 110-.005v.004zm.45 1.9a2.25 2.25 0 10-1.95.218v5.256a2.25 2.25 0 101.5 0V7.123A5.735 5.735 0 009.25 9h1.378a2.251 2.251 0 100-1.5H9.25a4.25 4.25 0 01-3.8-2.346zM12.75 9a.75.75 0 100-1.5.75.75 0 000 1.5zm-8.5 4.5a.75.75 0 100-1.5.75.75 0 000 1.5z" />
+		</svg>

+ 10 - 0
Icons/PrOpenDraftIcon.svg

@@ -0,0 +1,10 @@
+
+		<svg
+			enable-background="new 0 0 16 16"
+			version="1.1"
+			viewBox="0 0 16 16"
+			xml:space="preserve"
+			xmlns="http://www.w3.org/2000/svg">
+			<path fill-rule="evenodd" d="M2.5 3.25a.75.75 0 111.5 0 .75.75 0 01-1.5 0zM3.25 1a2.25 2.25 0 00-.75 4.372v5.256a2.251 2.251 0 101.5 0V5.372A2.25 2.25 0 003.25 1zm0 11a.75.75 0 100 1.5.75.75 0 000-1.5zm9.5 3a2.25 2.25 0 100-4.5 2.25 2.25 0 000 4.5zm0-3a.75.75 0 100 1.5.75.75 0 000-1.5z" />
+			<path d="M14 7.5a1.25 1.25 0 11-2.5 0 1.25 1.25 0 012.5 0zm0-4.25a1.25 1.25 0 11-2.5 0 1.25 1.25 0 012.5 0z" />
+		</svg>

+ 9 - 0
Icons/PrOpenIcon.svg

@@ -0,0 +1,9 @@
+
+		<svg
+			enable-background="new 0 0 16 16"
+			version="1.1"
+			viewBox="0 0 16 16"
+			xml:space="preserve"
+			xmlns="http://www.w3.org/2000/svg">
+			<path fill-rule="evenodd" d="M7.177 3.073L9.573.677A.25.25 0 0110 .854v4.792a.25.25 0 01-.427.177L7.177 3.427a.25.25 0 010-.354zM3.75 2.5a.75.75 0 100 1.5.75.75 0 000-1.5zm-2.25.75a2.25 2.25 0 113 2.122v5.256a2.251 2.251 0 11-1.5 0V5.372A2.25 2.25 0 011.5 3.25zM11 2.5h-1V4h1a1 1 0 011 1v5.628a2.251 2.251 0 101.5 0V5A2.5 2.5 0 0011 2.5zm1 10.25a.75.75 0 111.5 0 .75.75 0 01-1.5 0zM3.75 12a.75.75 0 100 1.5.75.75 0 000-1.5z" />
+		</svg>

ファイルの差分が大きいため隠しています
+ 62 - 0
Icons/group-avatar.svg


+ 64 - 0
Icons/public-avatar.svg

@@ -0,0 +1,64 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   inkscape:version="1.0beta2 (2b71d25, 2019-12-03)"
+   sodipodi:docname="public-avatar.svg"
+   version="1.1"
+   viewBox="0 0 48 48"
+   height="48"
+   width="48"
+   id="svg5">
+  <sodipodi:namedview
+     inkscape:current-layer="svg5"
+     inkscape:window-maximized="1"
+     inkscape:window-y="25"
+     inkscape:window-x="0"
+     inkscape:cy="24"
+     inkscape:cx="8.3312102"
+     inkscape:zoom="6.5416667"
+     showgrid="false"
+     id="namedview29"
+     inkscape:window-height="1027"
+     inkscape:window-width="1920"
+     inkscape:pageshadow="2"
+     inkscape:pageopacity="0"
+     guidetolerance="10"
+     gridtolerance="10"
+     objecttolerance="10"
+     borderopacity="1"
+     inkscape:document-rotation="0"
+     bordercolor="#666666"
+     pagecolor="#ffffff" />
+  <metadata
+     id="metadata11">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <defs
+     id="defs9" />
+  <rect
+     y="0"
+     x="0"
+     id="rect2"
+     height="100%"
+     width="100%"
+     style="fill:#B3B3B3" />
+  <path
+     inkscape:connector-curvature="0"
+     d="m 25.923086,14.93145 -4.940708,4.946448 c -1.098053,1.098797 -1.6116,2.514243 -1.534731,3.81751 0.07764,1.303422 0.672871,2.46126 1.534731,3.322958 l 2.192854,-2.200079 c -0.880809,-0.881275 -0.845397,-1.894305 0.0033,-2.743262 l 4.940078,-4.943497 c 0.815833,-0.816266 1.942209,-0.812879 2.751737,-0.0031 0.749199,0.863251 0.818875,1.923826 -0.0072,2.749322 l -1.273366,1.274523 c 0.861853,1.219676 1.001623,2.122859 0.920861,3.47165 l 2.548321,-2.549047 c 1.921554,-1.922583 1.921084,-5.227517 0,-7.149634 -1.919387,-1.920563 -5.189802,-1.882031 -7.13667,0.0062 z m 1.098057,6.035922 -2.195957,2.203187 c 0,0 0.0051,0 0.0072,0 0.854092,0.85455 0.787878,1.9549 -0.0072,2.749321 l -4.940712,4.943497 c -1.080814,0.919683 -2.016431,0.731883 -2.747999,0 -0.972678,-0.973193 -0.776467,-1.970282 0,-2.746991 l 1.31995,-1.316474 C 17.593021,25.582566 17.449528,24.679073 17.528423,23.33137 l -2.587136,2.58851 c -1.92451,1.925536 -1.91892,5.220527 0,7.140469 1.918298,1.919319 5.220847,1.919786 7.139613,0 l 4.940863,-4.943963 c 1.100389,-1.100506 1.614397,-2.514554 1.538303,-3.819996 -0.07456,-1.305287 -0.670851,-2.464834 -1.535352,-3.326066 z"
+     id="path3"
+     style="fill:#ffffff;stroke-width:1.55332" />
+</svg>

+ 1 - 0
Icons/talk.svg

@@ -0,0 +1 @@
+<svg width="16" height="16" version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="m7.9992 0.999a6.9993 6.9994 0 0 0-6.9992 6.9996 6.9993 6.9994 0 0 0 6.9992 6.9994 6.9993 6.9994 0 0 0 3.6308-1.024c0.86024 0.34184 2.7871 1.356 3.2457 0.91794 0.47922-0.45765-0.56261-2.6116-0.81238-3.412a6.9993 6.9994 0 0 0 0.935-3.4814 6.9993 6.9994 0 0 0-6.9991-6.9993zm8e-4 2.6611a4.34 4.3401 0 0 1 4.34 4.3401 4.34 4.3401 0 0 1-4.34 4.3398 4.34 4.3401 0 0 1-4.34-4.3398 4.34 4.3401 0 0 1 4.34-4.3401z" fill="#fff" stroke-width=".14"/></svg>

+ 674 - 0
LICENSE

@@ -0,0 +1,674 @@
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.  We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors.  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+  To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights.  Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received.  You must make sure that they, too, receive
+or can get the source code.  And you must show them these terms so they
+know their rights.
+
+  Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+  For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software.  For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+  Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so.  This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software.  The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable.  Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products.  If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+  Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary.  To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                       TERMS AND CONDITIONS
+
+  0. Definitions.
+
+  "This License" refers to version 3 of the GNU General Public License.
+
+  "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+  "The Program" refers to any copyrightable work licensed under this
+License.  Each licensee is addressed as "you".  "Licensees" and
+"recipients" may be individuals or organizations.
+
+  To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy.  The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+  A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+  To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy.  Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+  To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies.  Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+  An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License.  If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+  1. Source Code.
+
+  The "source code" for a work means the preferred form of the work
+for making modifications to it.  "Object code" means any non-source
+form of a work.
+
+  A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+  The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form.  A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+  The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities.  However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work.  For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+  The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+  The Corresponding Source for a work in source code form is that
+same work.
+
+  2. Basic Permissions.
+
+  All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met.  This License explicitly affirms your unlimited
+permission to run the unmodified Program.  The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work.  This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+  You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force.  You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright.  Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+  Conveying under any other circumstances is permitted solely under
+the conditions stated below.  Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+  No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+  When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+  4. Conveying Verbatim Copies.
+
+  You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+  You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+  5. Conveying Modified Source Versions.
+
+  You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+    a) The work must carry prominent notices stating that you modified
+    it, and giving a relevant date.
+
+    b) The work must carry prominent notices stating that it is
+    released under this License and any conditions added under section
+    7.  This requirement modifies the requirement in section 4 to
+    "keep intact all notices".
+
+    c) You must license the entire work, as a whole, under this
+    License to anyone who comes into possession of a copy.  This
+    License will therefore apply, along with any applicable section 7
+    additional terms, to the whole of the work, and all its parts,
+    regardless of how they are packaged.  This License gives no
+    permission to license the work in any other way, but it does not
+    invalidate such permission if you have separately received it.
+
+    d) If the work has interactive user interfaces, each must display
+    Appropriate Legal Notices; however, if the Program has interactive
+    interfaces that do not display Appropriate Legal Notices, your
+    work need not make them do so.
+
+  A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit.  Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+  6. Conveying Non-Source Forms.
+
+  You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+    a) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by the
+    Corresponding Source fixed on a durable physical medium
+    customarily used for software interchange.
+
+    b) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by a
+    written offer, valid for at least three years and valid for as
+    long as you offer spare parts or customer support for that product
+    model, to give anyone who possesses the object code either (1) a
+    copy of the Corresponding Source for all the software in the
+    product that is covered by this License, on a durable physical
+    medium customarily used for software interchange, for a price no
+    more than your reasonable cost of physically performing this
+    conveying of source, or (2) access to copy the
+    Corresponding Source from a network server at no charge.
+
+    c) Convey individual copies of the object code with a copy of the
+    written offer to provide the Corresponding Source.  This
+    alternative is allowed only occasionally and noncommercially, and
+    only if you received the object code with such an offer, in accord
+    with subsection 6b.
+
+    d) Convey the object code by offering access from a designated
+    place (gratis or for a charge), and offer equivalent access to the
+    Corresponding Source in the same way through the same place at no
+    further charge.  You need not require recipients to copy the
+    Corresponding Source along with the object code.  If the place to
+    copy the object code is a network server, the Corresponding Source
+    may be on a different server (operated by you or a third party)
+    that supports equivalent copying facilities, provided you maintain
+    clear directions next to the object code saying where to find the
+    Corresponding Source.  Regardless of what server hosts the
+    Corresponding Source, you remain obligated to ensure that it is
+    available for as long as needed to satisfy these requirements.
+
+    e) Convey the object code using peer-to-peer transmission, provided
+    you inform other peers where the object code and Corresponding
+    Source of the work are being offered to the general public at no
+    charge under subsection 6d.
+
+  A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+  A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling.  In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage.  For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product.  A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+  "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source.  The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+  If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information.  But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+  The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed.  Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+  Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+  7. Additional Terms.
+
+  "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law.  If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+  When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it.  (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.)  You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+  Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+    a) Disclaiming warranty or limiting liability differently from the
+    terms of sections 15 and 16 of this License; or
+
+    b) Requiring preservation of specified reasonable legal notices or
+    author attributions in that material or in the Appropriate Legal
+    Notices displayed by works containing it; or
+
+    c) Prohibiting misrepresentation of the origin of that material, or
+    requiring that modified versions of such material be marked in
+    reasonable ways as different from the original version; or
+
+    d) Limiting the use for publicity purposes of names of licensors or
+    authors of the material; or
+
+    e) Declining to grant rights under trademark law for use of some
+    trade names, trademarks, or service marks; or
+
+    f) Requiring indemnification of licensors and authors of that
+    material by anyone who conveys the material (or modified versions of
+    it) with contractual assumptions of liability to the recipient, for
+    any liability that these contractual assumptions directly impose on
+    those licensors and authors.
+
+  All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10.  If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term.  If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+  If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+  Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+  8. Termination.
+
+  You may not propagate or modify a covered work except as expressly
+provided under this License.  Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+  However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+  Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+  Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License.  If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+  9. Acceptance Not Required for Having Copies.
+
+  You are not required to accept this License in order to receive or
+run a copy of the Program.  Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance.  However,
+nothing other than this License grants you permission to propagate or
+modify any covered work.  These actions infringe copyright if you do
+not accept this License.  Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+  10. Automatic Licensing of Downstream Recipients.
+
+  Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License.  You are not responsible
+for enforcing compliance by third parties with this License.
+
+  An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations.  If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+  You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License.  For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+  11. Patents.
+
+  A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based.  The
+work thus licensed is called the contributor's "contributor version".
+
+  A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version.  For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+  Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+  In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement).  To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+  If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients.  "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+  If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+  A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License.  You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+  Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+  12. No Surrender of Others' Freedom.
+
+  If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all.  For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+  13. Use with the GNU Affero General Public License.
+
+  Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work.  The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+  Each version is given a distinguishing version number.  If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation.  If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+  If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+  Later license versions may give you additional or different
+permissions.  However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+  15. Disclaimer of Warranty.
+
+  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. Limitation of Liability.
+
+  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+  17. Interpretation of Sections 15 and 16.
+
+  If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    {one line to give the program's name and a brief idea of what it does.}
+    Copyright (C) {year}  {name of author}
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+  If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+    {project}  Copyright (C) {year}  {fullname}
+    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+  You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<http://www.gnu.org/licenses/>.
+
+  The GNU General Public License does not permit incorporating your program
+into proprietary programs.  If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.  But first, please read
+<http://www.gnu.org/philosophy/why-not-lgpl.html>.

+ 73 - 0
LICENSES/Apache-2.0.txt

@@ -0,0 +1,73 @@
+Apache License
+Version 2.0, January 2004
+http://www.apache.org/licenses/
+
+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+1. Definitions.
+
+"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
+
+"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
+
+"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
+
+"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
+
+"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
+
+"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
+
+"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
+
+"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
+
+"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
+
+"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
+
+2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
+
+3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
+
+4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
+
+     (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and
+
+     (b) You must cause any modified files to carry prominent notices stating that You changed the files; and
+
+     (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
+
+     (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
+
+     You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
+
+5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
+
+6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
+
+7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
+
+8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
+
+9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
+
+END OF TERMS AND CONDITIONS
+
+APPENDIX: How to apply the Apache License to your work.
+
+To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!)  The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives.
+
+Copyright [yyyy] [name of copyright owner]
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.

+ 11 - 0
LICENSES/BSD-3-Clause.txt

@@ -0,0 +1,11 @@
+Copyright (c) <year> <owner>. 
+
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

+ 232 - 0
LICENSES/GPL-3.0-or-later.txt

@@ -0,0 +1,232 @@
+GNU GENERAL PUBLIC LICENSE
+Version 3, 29 June 2007
+
+Copyright © 2007 Free Software Foundation, Inc. <https://fsf.org/>
+
+Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
+
+Preamble
+
+The GNU General Public License is a free, copyleft license for software and other kinds of works.
+
+The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too.
+
+When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things.
+
+To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others.
+
+For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights.
+
+Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it.
+
+For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions.
+
+Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users.
+
+Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free.
+
+The precise terms and conditions for copying, distribution and modification follow.
+
+TERMS AND CONDITIONS
+
+0. Definitions.
+
+“This License” refers to version 3 of the GNU General Public License.
+
+“Copyright” also means copyright-like laws that apply to other kinds of works, such as semiconductor masks.
+
+“The Program” refers to any copyrightable work licensed under this License. Each licensee is addressed as “you”. “Licensees” and “recipients” may be individuals or organizations.
+
+To “modify” a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a “modified version” of the earlier work or a work “based on” the earlier work.
+
+A “covered work” means either the unmodified Program or a work based on the Program.
+
+To “propagate” a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well.
+
+To “convey” a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying.
+
+An interactive user interface displays “Appropriate Legal Notices” to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion.
+
+1. Source Code.
+The “source code” for a work means the preferred form of the work for making modifications to it. “Object code” means any non-source form of a work.
+
+A “Standard Interface” means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language.
+
+The “System Libraries” of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A “Major Component”, in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it.
+
+The “Corresponding Source” for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work.
+
+The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source.
+
+The Corresponding Source for a work in source code form is that same work.
+
+2. Basic Permissions.
+All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law.
+
+You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you.
+
+Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary.
+
+3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures.
+
+When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures.
+
+4. Conveying Verbatim Copies.
+You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program.
+
+You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee.
+
+5. Conveying Modified Source Versions.
+You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions:
+
+     a) The work must carry prominent notices stating that you modified it, and giving a relevant date.
+
+     b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to “keep intact all notices”.
+
+     c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it.
+
+     d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so.
+
+A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an “aggregate” if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate.
+
+6. Conveying Non-Source Forms.
+You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways:
+
+     a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange.
+
+     b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge.
+
+     c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b.
+
+     d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements.
+
+     e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d.
+
+A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work.
+
+A “User Product” is either (1) a “consumer product”, which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, “normally used” refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product.
+
+“Installation Information” for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made.
+
+If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM).
+
+The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network.
+
+Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying.
+
+7. Additional Terms.
+“Additional permissions” are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions.
+
+When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission.
+
+Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms:
+
+     a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or
+
+     b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or
+
+     c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or
+
+     d) Limiting the use for publicity purposes of names of licensors or authors of the material; or
+
+     e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or
+
+     f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors.
+
+All other non-permissive additional terms are considered “further restrictions” within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying.
+
+If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms.
+
+Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way.
+
+8. Termination.
+You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11).
+
+However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation.
+
+Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice.
+
+Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10.
+
+9. Acceptance Not Required for Having Copies.
+You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so.
+
+10. Automatic Licensing of Downstream Recipients.
+Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License.
+
+An “entity transaction” is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts.
+
+You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it.
+
+11. Patents.
+A “contributor” is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's “contributor version”.
+
+A contributor's “essential patent claims” are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, “control” includes the right to grant patent sublicenses in a manner consistent with the requirements of this License.
+
+Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version.
+
+In the following three paragraphs, a “patent license” is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To “grant” such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party.
+
+If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. “Knowingly relying” means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid.
+
+If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it.
+
+A patent license is “discriminatory” if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007.
+
+Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law.
+
+12. No Surrender of Others' Freedom.
+If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program.
+
+13. Use with the GNU Affero General Public License.
+Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such.
+
+14. Revised Versions of this License.
+The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License “or any later version” applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation.
+
+If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program.
+
+Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version.
+
+15. Disclaimer of Warranty.
+THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM “AS IS” WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+16. Limitation of Liability.
+IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
+
+17. Interpretation of Sections 15 and 16.
+If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee.
+
+END OF TERMS AND CONDITIONS
+
+How to Apply These Terms to Your New Programs
+
+If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms.
+
+To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the “copyright” line and a pointer to where the full notice is found.
+
+     <one line to give the program's name and a brief idea of what it does.>
+     Copyright (C) <year>  <name of author>
+
+     This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+
+     This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+     You should have received a copy of the GNU General Public License along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode:
+
+     <program>  Copyright (C) <year>  <name of author>
+     This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+     This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an “about box”.
+
+You should also get your employer (if you work as a programmer) or school, if any, to sign a “copyright disclaimer” for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see <https://www.gnu.org/licenses/>.
+
+The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read <https://www.gnu.org/philosophy/why-not-lgpl.html>.

+ 9 - 0
LICENSES/LicenseRef-NextcloudTrademarks.txt

@@ -0,0 +1,9 @@
+The Nextcloud marks
+Nextcloud and the Nextcloud logo is a registered trademark of Nextcloud GmbH in Germany and/or other countries.
+These guidelines cover the following marks pertaining both to the product names and the logo: “Nextcloud”
+and the blue/white cloud logo with or without the word Nextcloud; the service “Nextcloud Enterprise”;
+and our products: “Nextcloud Files”; “Nextcloud Groupware” and “Nextcloud Talk”.
+This set of marks is collectively referred to as the “Nextcloud marks.”
+
+Use of Nextcloud logos and other marks is only permitted under the guidelines provided by the Nextcloud GmbH.
+A copy can be found at https://nextcloud.com/trademarks/

+ 49 - 0
LICENSES/LicenseRef-XTrademarks.txt

@@ -0,0 +1,49 @@
+Trademark policy
+April 2023
+
+
+You may not violate others’ intellectual property rights, including copyright and trademark.
+
+A trademark is a word, logo, phrase, or device that distinguishes a trademark holder’s good or service in the marketplace. Trademark law may prevent others from using a trademark in an unauthorized or confusing manner.  
+
+
+What is in violation of this policy?
+
+Using another’s trademark in a way that may mislead or confuse people about your affiliation may be a violation of our trademark policy.
+ 
+
+What is not a violation of this policy?
+
+Referencing another’s trademark is not automatically a violation of X's trademark policy. Examples of non-violations include:
+
+* using a trademark in a way that is outside the scope of the trademark registration e.g., in a different territory, or a different class of goods or services than that identified in the registration; and
+* using a trademark in a nominative or other fair use manner. For more information, see our Misleading and deceptive identities policy (https://help.twitter.com/en/rules-and-policies/twitter-impersonation-and-deceptive-identities-policy.html).
+ 
+
+Who can report violations of this policy?
+
+X only investigates requests that are submitted by the trademark holder or their authorized representative e.g., a legal representative or other representative for a brand. 
+ 
+
+How can I report violations of this policy?
+
+You can submit a trademark report through our trademark report form (https://help.twitter.com/forms/trademark). Please provide all the information requested in the form. If you submit an incomplete report, we’ll need to follow up about the missing information. Please note that this will result in a delay in processing your report.
+
+Note: We may provide the account holder with your name and other information included in the copy of the report.
+
+
+What happens if you violate this policy?
+
+If we determine that you violated our trademark policy, we may suspend your account. Depending on the type of violation, we may give you an opportunity to comply with our policies. In other instances, an account may be permanently suspended upon first review. If you believe that your account was suspended in error, you can submit an appeal (https://help.twitter.com/forms/general?subtopic=suspended).
+ 
+
+Additional resources
+
+Learn more about our range of enforcement options (https://help.twitter.com/rules-and-policies/enforcement-options) and our approach to policy development and enforcement (https://help.twitter.com/rules-and-policies/enforcement-philosophy).
+
+
+Legal disclaimer
+
+By using the X trademarks and resources on this site, you agree to follow the X Trademark Guidelines in our Brand Guidelines — as well as our Terms of Service and all other X rules and policies. If you have any questions, contact us at trademarks@x.com.
+
+A copy can be found at https://about.x.com/en/who-we-are/brand-toolkit and https://help.twitter.com/en/rules-and-policies/x-trademark-policy

+ 9 - 0
LICENSES/MIT.txt

@@ -0,0 +1,9 @@
+MIT License
+
+Copyright (c) <year> <copyright holders>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

+ 4548 - 0
NextcloudTalk.xcodeproj/project.pbxproj

@@ -0,0 +1,4548 @@
+// !$*UTF8*$!
+{
+	archiveVersion = 1;
+	classes = {
+	};
+	objectVersion = 54;
+	objects = {
+
+/* Begin PBXBuildFile section */
+		1F0A1D442A5F1FA800A25433 /* SwiftMarkdownObjCBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F0A1D432A5F1FA800A25433 /* SwiftMarkdownObjCBridge.swift */; };
+		1F0B0A722BA264540073FF8D /* MentionSuggestion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F0B0A712BA264540073FF8D /* MentionSuggestion.swift */; };
+		1F0B0A732BA265300073FF8D /* MentionSuggestion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F0B0A712BA264540073FF8D /* MentionSuggestion.swift */; };
+		1F0B0A742BA265310073FF8D /* MentionSuggestion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F0B0A712BA264540073FF8D /* MentionSuggestion.swift */; };
+		1F0B0A752BA265310073FF8D /* MentionSuggestion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F0B0A712BA264540073FF8D /* MentionSuggestion.swift */; };
+		1F0B0A772BA26BE10073FF8D /* UnitMentionSuggestionTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F0B0A762BA26BE10073FF8D /* UnitMentionSuggestionTest.swift */; };
+		1F0ECBF52A68274400921E90 /* CDMarkdownKit in Frameworks */ = {isa = PBXBuildFile; productRef = 1F0ECBF42A68274400921E90 /* CDMarkdownKit */; };
+		1F0ECBF72A68277000921E90 /* CDMarkdownKit in Frameworks */ = {isa = PBXBuildFile; productRef = 1F0ECBF62A68277000921E90 /* CDMarkdownKit */; };
+		1F0ECBF92A68277C00921E90 /* CDMarkdownKit in Frameworks */ = {isa = PBXBuildFile; productRef = 1F0ECBF82A68277C00921E90 /* CDMarkdownKit */; };
+		1F11FB7229C07B04001E21E7 /* NCZoomableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F11FB7129C07B04001E21E7 /* NCZoomableView.swift */; };
+		1F1B0F252BD94A0D003FD766 /* UnitDarwinCenterTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F1B0F242BD94A0D003FD766 /* UnitDarwinCenterTest.swift */; };
+		1F1B0F272BDA61C5003FD766 /* AllocationTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F1B0F262BDA61C5003FD766 /* AllocationTracker.swift */; };
+		1F1B0F2C2BDBB3AC003FD766 /* NCMediaViewerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F1B0F2B2BDBB3AC003FD766 /* NCMediaViewerViewController.swift */; };
+		1F1B0F302BDBC9D6003FD766 /* NCMediaViewerPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F1B0F2F2BDBC9D6003FD766 /* NCMediaViewerPageViewController.swift */; };
+		1F1B0F322BDC57E3003FD766 /* UIPageViewControllerExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F1B0F312BDC57E3003FD766 /* UIPageViewControllerExtension.swift */; };
+		1F1B0F362BDD8B9C003FD766 /* NCActivityIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F1B0F352BDD8B9C003FD766 /* NCActivityIndicator.swift */; };
+		1F1B0F422BE047CE003FD766 /* UIViewController+Transitions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F1B0F3B2BE047CD003FD766 /* UIViewController+Transitions.swift */; };
+		1F1B0F432BE047CE003FD766 /* ModalTransitionAnimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F1B0F3C2BE047CD003FD766 /* ModalTransitionAnimator.swift */; };
+		1F1B0F442BE047CE003FD766 /* ModalTransitionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F1B0F3D2BE047CD003FD766 /* ModalTransitionManager.swift */; };
+		1F1B0F452BE047CE003FD766 /* ModalPresentationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F1B0F3E2BE047CD003FD766 /* ModalPresentationController.swift */; };
+		1F1B0F462BE047CE003FD766 /* InteractionControlling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F1B0F3F2BE047CD003FD766 /* InteractionControlling.swift */; };
+		1F1B0F472BE047CE003FD766 /* StandardInteractionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F1B0F402BE047CE003FD766 /* StandardInteractionController.swift */; };
+		1F1B0F482BE047CE003FD766 /* CustomPresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F1B0F412BE047CE003FD766 /* CustomPresentable.swift */; };
+		1F1B0F4A2BE047D5003FD766 /* OneWayPanGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F1B0F492BE047D5003FD766 /* OneWayPanGestureRecognizer.swift */; };
+		1F1B0F4C2BE18FF3003FD766 /* CustomPresentableNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F1B0F4B2BE18FF3003FD766 /* CustomPresentableNavigationController.swift */; };
+		1F1B50342B8E069800B0F2F4 /* BaseChatTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F1B50332B8E069800B0F2F4 /* BaseChatTableViewCell.swift */; };
+		1F1B50382B8E070100B0F2F4 /* BaseChatTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 1F1B50372B8E070100B0F2F4 /* BaseChatTableViewCell.xib */; };
+		1F1B503A2B8F9E1300B0F2F4 /* BaseChatTableViewCell+File.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F1B50392B8F9E1300B0F2F4 /* BaseChatTableViewCell+File.swift */; };
+		1F1B503E2B8FB12100B0F2F4 /* BaseChatTableViewCell+Message.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F1B503D2B8FB12100B0F2F4 /* BaseChatTableViewCell+Message.swift */; };
+		1F1B50442B9095D100B0F2F4 /* FederatedCapabilities.m in Sources */ = {isa = PBXBuildFile; fileRef = 1F1B50432B9095D100B0F2F4 /* FederatedCapabilities.m */; };
+		1F1B50472B90CDF800B0F2F4 /* TalkCapabilities.m in Sources */ = {isa = PBXBuildFile; fileRef = 1F1B50462B90CDF800B0F2F4 /* TalkCapabilities.m */; };
+		1F1B50482B90CF0800B0F2F4 /* TalkCapabilities.m in Sources */ = {isa = PBXBuildFile; fileRef = 1F1B50462B90CDF800B0F2F4 /* TalkCapabilities.m */; };
+		1F1B50492B90CF0800B0F2F4 /* TalkCapabilities.m in Sources */ = {isa = PBXBuildFile; fileRef = 1F1B50462B90CDF800B0F2F4 /* TalkCapabilities.m */; };
+		1F1B504A2B90CF0800B0F2F4 /* TalkCapabilities.m in Sources */ = {isa = PBXBuildFile; fileRef = 1F1B50462B90CDF800B0F2F4 /* TalkCapabilities.m */; };
+		1F1B504B2B90CF0C00B0F2F4 /* FederatedCapabilities.m in Sources */ = {isa = PBXBuildFile; fileRef = 1F1B50432B9095D100B0F2F4 /* FederatedCapabilities.m */; };
+		1F1B504C2B90CF0C00B0F2F4 /* FederatedCapabilities.m in Sources */ = {isa = PBXBuildFile; fileRef = 1F1B50432B9095D100B0F2F4 /* FederatedCapabilities.m */; };
+		1F1B504D2B90CF0C00B0F2F4 /* FederatedCapabilities.m in Sources */ = {isa = PBXBuildFile; fileRef = 1F1B50432B9095D100B0F2F4 /* FederatedCapabilities.m */; };
+		1F1C0D7F29A7F33600D17C6D /* NCNotificationAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FA38C8F29A4B3C6008871B8 /* NCNotificationAction.swift */; };
+		1F1C0D8729AFB88800D17C6D /* VLCKitVideoViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 1F1C0D8629AFB88800D17C6D /* VLCKitVideoViewController.xib */; };
+		1F1C0D8929AFB89900D17C6D /* VLCKitVideoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F1C0D8829AFB89900D17C6D /* VLCKitVideoViewController.swift */; };
+		1F1C999D2909846400EACF02 /* BGTaskHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FD9182828C55A73009092AB /* BGTaskHelper.swift */; };
+		1F1C999E2909846400EACF02 /* BGTaskHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FD9182828C55A73009092AB /* BGTaskHelper.swift */; };
+		1F1DF83C2C5C17AF00E5EA86 /* TalkActor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F1DF83B2C5C17AF00E5EA86 /* TalkActor.swift */; };
+		1F1DF83D2C5C17AF00E5EA86 /* TalkActor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F1DF83B2C5C17AF00E5EA86 /* TalkActor.swift */; };
+		1F1DF83E2C5C17AF00E5EA86 /* TalkActor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F1DF83B2C5C17AF00E5EA86 /* TalkActor.swift */; };
+		1F1DF83F2C5C17AF00E5EA86 /* TalkActor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F1DF83B2C5C17AF00E5EA86 /* TalkActor.swift */; };
+		1F1DF8412C63C25900E5EA86 /* UnitNCDatabaseManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F1DF8402C63C25900E5EA86 /* UnitNCDatabaseManager.swift */; };
+		1F1DF8432C64006E00E5EA86 /* SignalingParticipant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F1DF8422C64006E00E5EA86 /* SignalingParticipant.swift */; };
+		1F1DF8442C64006E00E5EA86 /* SignalingParticipant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F1DF8422C64006E00E5EA86 /* SignalingParticipant.swift */; };
+		1F1DF8452C64006E00E5EA86 /* SignalingParticipant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F1DF8422C64006E00E5EA86 /* SignalingParticipant.swift */; };
+		1F1DF8462C64006E00E5EA86 /* SignalingParticipant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F1DF8422C64006E00E5EA86 /* SignalingParticipant.swift */; };
+		1F24B5A228E0648600654457 /* ReferenceGithubView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F24B5A128E0648600654457 /* ReferenceGithubView.swift */; };
+		1F24B5A428E0649200654457 /* ReferenceGithubView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 1F24B5A328E0649200654457 /* ReferenceGithubView.xib */; };
+		1F35F8E02AEEB9DE00044BDA /* ShareConfirmationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F35F8DF2AEEB9DE00044BDA /* ShareConfirmationViewController.swift */; };
+		1F35F8E12AEEB9DE00044BDA /* ShareConfirmationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F35F8DF2AEEB9DE00044BDA /* ShareConfirmationViewController.swift */; };
+		1F35F8E22AEEBAF900044BDA /* InputbarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F5A24322ADA77DA009939FE /* InputbarViewController.swift */; };
+		1F35F8E32AEEBBE000044BDA /* NCChatTitleView.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C7381552106136000CDB8DB /* NCChatTitleView.m */; };
+		1F35F8E42AEEBBE500044BDA /* NCChatTitleView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2C738157210613A200CDB8DB /* NCChatTitleView.xib */; };
+		1F35F8E52AEEBC0100044BDA /* SLKInputAccessoryView.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CB3039D2264775E0053078A /* SLKInputAccessoryView.m */; };
+		1F35F8E62AEEBC0300044BDA /* SLKTextInput+Implementation.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CB3039E2264775E0053078A /* SLKTextInput+Implementation.m */; };
+		1F35F8E72AEEBC0600044BDA /* SLKTextInputbar.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CB303A12264775E0053078A /* SLKTextInputbar.m */; };
+		1F35F8E82AEEBC0800044BDA /* SLKTextView+SLKAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CB303A32264775E0053078A /* SLKTextView+SLKAdditions.m */; };
+		1F35F8E92AEEBC0C00044BDA /* SLKTextView.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CB303A52264775E0053078A /* SLKTextView.m */; };
+		1F35F8EA2AEEBC0E00044BDA /* SLKTextViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CB303A72264775E0053078A /* SLKTextViewController.m */; };
+		1F35F8EB2AEEBC1100044BDA /* UIResponder+SLKAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CB303AD2264775E0053078A /* UIResponder+SLKAdditions.m */; };
+		1F35F8EC2AEEBC1400044BDA /* UIScrollView+SLKAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CB303AF2264775E0053078A /* UIScrollView+SLKAdditions.m */; };
+		1F35F8ED2AEEBC1600044BDA /* UIView+SLKAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CB303B12264775E0053078A /* UIView+SLKAdditions.m */; };
+		1F35F8EF2AEEBC1A00044BDA /* SLKDefaultReplyView.m in Sources */ = {isa = PBXBuildFile; fileRef = 1F66B72829FA936E003FB168 /* SLKDefaultReplyView.m */; };
+		1F35F8F02AEEBC1D00044BDA /* SLKDefaultTypingIndicatorView.m in Sources */ = {isa = PBXBuildFile; fileRef = 1F66B72B29FA9414003FB168 /* SLKDefaultTypingIndicatorView.m */; };
+		1F35F8F12AEEC25B00044BDA /* TypingIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F66B71E29FA703B003FB168 /* TypingIndicatorView.swift */; };
+		1F35F8F22AEEC25E00044BDA /* TypingIndicatorView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 1F66B72029FA7089003FB168 /* TypingIndicatorView.xib */; };
+		1F35F8F32AEEC29A00044BDA /* AvatarButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FDCC3EF29ECB4CE00DEB39B /* AvatarButton.swift */; };
+		1F35F8F52AEEDA9800044BDA /* SwiftyAttributes in Frameworks */ = {isa = PBXBuildFile; productRef = 1F35F8F42AEEDA9800044BDA /* SwiftyAttributes */; };
+		1F35F8FB2AEEDBC600044BDA /* ChatViewControllerExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F35F8FA2AEEDBC600044BDA /* ChatViewControllerExtension.swift */; };
+		1F35F8FC2AEEDBC600044BDA /* ChatViewControllerExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F35F8FA2AEEDBC600044BDA /* ChatViewControllerExtension.swift */; };
+		1F35F9042AEEDF0E00044BDA /* AutoCompletionTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 1F35F9032AEEDF0E00044BDA /* AutoCompletionTableViewCell.m */; };
+		1F35F9052AEEDF0E00044BDA /* AutoCompletionTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 1F35F9032AEEDF0E00044BDA /* AutoCompletionTableViewCell.m */; };
+		1F35F9062AEEE3C400044BDA /* NCMessageTextView.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CA1554A208F2E5700CE8EF0 /* NCMessageTextView.m */; };
+		1F35F9072AEEE3EC00044BDA /* NCUserStatus.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C78E9E225120DE500E3D4CA /* NCUserStatus.m */; };
+		1F35F90A2AEEE76A00044BDA /* QuotedMessageView.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C6E7448238C1A0800AE396C /* QuotedMessageView.m */; };
+		1F35F90B2AEEE76C00044BDA /* ReplyMessageView.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C6E74452386D33200AE396C /* ReplyMessageView.m */; };
+		1F371A372A7B921A006CBFB3 /* DatePickerTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F371A362A7B921A006CBFB3 /* DatePickerTextField.swift */; };
+		1F3C419F29EDAC7D00F58435 /* RoomAvatarInfoTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F3C419E29EDAC7D00F58435 /* RoomAvatarInfoTableViewController.swift */; };
+		1F3C41A129EDAC8800F58435 /* RoomAvatarInfoTableViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 1F3C41A029EDAC8800F58435 /* RoomAvatarInfoTableViewController.xib */; };
+		1F3C41A329EDF05700F58435 /* AvatarEditView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F3C41A229EDF05700F58435 /* AvatarEditView.swift */; };
+		1F3C41A529EDF0B800F58435 /* AvatarEditView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 1F3C41A429EDF0B800F58435 /* AvatarEditView.xib */; };
+		1F3D3B22255F109E00230DAE /* BarButtonItemWithActivity.m in Sources */ = {isa = PBXBuildFile; fileRef = 1F3D3B20255F109E00230DAE /* BarButtonItemWithActivity.m */; };
+		1F45A1162A01D6EC005FE87D /* SDWebImage in Frameworks */ = {isa = PBXBuildFile; productRef = 1F45A1152A01D6EC005FE87D /* SDWebImage */; };
+		1F45A11A2A01D70E005FE87D /* SDWebImage in Frameworks */ = {isa = PBXBuildFile; productRef = 1F45A1192A01D70E005FE87D /* SDWebImage */; };
+		1F45A11E2A01D719005FE87D /* SDWebImage in Frameworks */ = {isa = PBXBuildFile; productRef = 1F45A11D2A01D719005FE87D /* SDWebImage */; };
+		1F45A1212A01D8BA005FE87D /* SDWebImageSVGKitPlugin in Frameworks */ = {isa = PBXBuildFile; productRef = 1F45A1202A01D8BA005FE87D /* SDWebImageSVGKitPlugin */; };
+		1F45A1232A01D8F1005FE87D /* SDWebImageSVGKitPlugin in Frameworks */ = {isa = PBXBuildFile; productRef = 1F45A1222A01D8F1005FE87D /* SDWebImageSVGKitPlugin */; };
+		1F45A1252A01D8F7005FE87D /* SDWebImageSVGKitPlugin in Frameworks */ = {isa = PBXBuildFile; productRef = 1F45A1242A01D8F7005FE87D /* SDWebImageSVGKitPlugin */; };
+		1F468E7628DCC6C60099597B /* Dynamic in Frameworks */ = {isa = PBXBuildFile; productRef = 1F468E7528DCC6C60099597B /* Dynamic */; };
+		1F468E7828DCC7310099597B /* EmojiTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F468E7728DCC7310099597B /* EmojiTextField.swift */; };
+		1F46CE2928E05B3200E7D88E /* ReferenceDefaultView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F46CE2828E05B3200E7D88E /* ReferenceDefaultView.swift */; };
+		1F46CE2B28E05B3C00E7D88E /* ReferenceDefaultView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 1F46CE2A28E05B3C00E7D88E /* ReferenceDefaultView.xib */; };
+		1F4DD3EB2571C688007DC98E /* EmojiUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F4DD3EA2571C688007DC98E /* EmojiUtils.swift */; };
+		1F4DD3EC2571C688007DC98E /* EmojiUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F4DD3EA2571C688007DC98E /* EmojiUtils.swift */; };
+		1F4DD3ED2571C688007DC98E /* EmojiUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F4DD3EA2571C688007DC98E /* EmojiUtils.swift */; };
+		1F53819129195FA4003DA6B7 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2CA1CCAB1F067F35002FE6A2 /* Images.xcassets */; };
+		1F5683CF2BA7980C0023E151 /* FilePreviewImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F5683CE2BA7980C0023E151 /* FilePreviewImageView.swift */; };
+		1F5813F828EB23EF00318FC3 /* NCSplitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F5813F628EB23EF00318FC3 /* NCSplitViewController.swift */; };
+		1F5813F928EB23EF00318FC3 /* NCSplitViewPlaceholderViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F5813F728EB23EF00318FC3 /* NCSplitViewPlaceholderViewController.swift */; };
+		1F59446225B8EDF5002AD65F /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 2C7F47AC20289B9600081CC7 /* Localizable.strings */; };
+		1F59446625B8EDF5002AD65F /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 2C7F47AC20289B9600081CC7 /* Localizable.strings */; };
+		1F5A24332ADA77DA009939FE /* InputbarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F5A24322ADA77DA009939FE /* InputbarViewController.swift */; };
+		1F5CDAE72B3B05110040ECC0 /* UnitColorGeneratorTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F5CDAE62B3B05110040ECC0 /* UnitColorGeneratorTest.swift */; };
+		1F61C767285E35A6004D74D8 /* DiagnosticsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F61C766285E35A6004D74D8 /* DiagnosticsTableViewController.swift */; };
+		1F61C76B285F65E1004D74D8 /* SimpleTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F61C76A285F65E1004D74D8 /* SimpleTableViewController.swift */; };
+		1F628CBA2842BAAF0083A425 /* QRCodeReader in Frameworks */ = {isa = PBXBuildFile; productRef = 1F628CB92842BAAF0083A425 /* QRCodeReader */; };
+		1F6629FA2C17700E001C6C0E /* IntegrationRoomsManagerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F6629F92C17700E001C6C0E /* IntegrationRoomsManagerTest.swift */; };
+		1F66B71F29FA703B003FB168 /* TypingIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F66B71E29FA703B003FB168 /* TypingIndicatorView.swift */; };
+		1F66B72129FA7089003FB168 /* TypingIndicatorView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 1F66B72029FA7089003FB168 /* TypingIndicatorView.xib */; };
+		1F66B72929FA936E003FB168 /* SLKDefaultReplyView.m in Sources */ = {isa = PBXBuildFile; fileRef = 1F66B72829FA936E003FB168 /* SLKDefaultReplyView.m */; };
+		1F66B72C29FA9414003FB168 /* SLKDefaultTypingIndicatorView.m in Sources */ = {isa = PBXBuildFile; fileRef = 1F66B72B29FA9414003FB168 /* SLKDefaultTypingIndicatorView.m */; };
+		1F66B72F29FABD01003FB168 /* SwiftyAttributes in Frameworks */ = {isa = PBXBuildFile; productRef = 1F66B72E29FABD01003FB168 /* SwiftyAttributes */; };
+		1F6D8C332B2E3756004376B8 /* IntegrationRoomTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F6D8C322B2E3756004376B8 /* IntegrationRoomTest.swift */; };
+		1F6D8C3D2B2F23C4004376B8 /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F6D8C3C2B2F23C4004376B8 /* Helpers.swift */; };
+		1F6D8C412B2F26D5004376B8 /* TestConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F6D8C402B2F26D5004376B8 /* TestConstants.swift */; };
+		1F6D8C432B2F26EE004376B8 /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F6D8C422B2F26EE004376B8 /* Helpers.swift */; };
+		1F6D8C442B2F2791004376B8 /* TestConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F6D8C402B2F26D5004376B8 /* TestConstants.swift */; };
+		1F6D8C492B2F2FB7004376B8 /* AAAALoginTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F6D8C472B2F2F69004376B8 /* AAAALoginTest.swift */; };
+		1F6D8C4B2B2F5B61004376B8 /* TestBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F6D8C4A2B2F5B61004376B8 /* TestBase.swift */; };
+		1F6D8C4D2B2F8FE5004376B8 /* IntegrationChatTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F6D8C4C2B2F8FE5004376B8 /* IntegrationChatTest.swift */; };
+		1F759C092B63B9A7000534AB /* SDWebImage in Frameworks */ = {isa = PBXBuildFile; productRef = 1F759C082B63B9A7000534AB /* SDWebImage */; };
+		1F759C0B2B63B9A7000534AB /* SDWebImageSVGKitPlugin in Frameworks */ = {isa = PBXBuildFile; productRef = 1F759C0A2B63B9A7000534AB /* SDWebImageSVGKitPlugin */; };
+		1F759C0E2B63B9BA000534AB /* WebRTC in Frameworks */ = {isa = PBXBuildFile; productRef = 1F759C0D2B63B9BA000534AB /* WebRTC */; };
+		1F759C102B63B9D9000534AB /* OpenSSL in Frameworks */ = {isa = PBXBuildFile; productRef = 1F759C0F2B63B9D9000534AB /* OpenSSL */; };
+		1F759C142B63B9D9000534AB /* QRCodeReader in Frameworks */ = {isa = PBXBuildFile; productRef = 1F759C132B63B9D9000534AB /* QRCodeReader */; };
+		1F759C162B63B9D9000534AB /* NextcloudKit in Frameworks */ = {isa = PBXBuildFile; productRef = 1F759C152B63B9D9000534AB /* NextcloudKit */; };
+		1F759C182B63B9D9000534AB /* SwiftyAttributes in Frameworks */ = {isa = PBXBuildFile; productRef = 1F759C172B63B9D9000534AB /* SwiftyAttributes */; };
+		1F759C1A2B63B9D9000534AB /* CDMarkdownKit in Frameworks */ = {isa = PBXBuildFile; productRef = 1F759C192B63B9D9000534AB /* CDMarkdownKit */; };
+		1F759C1C2B63B9D9000534AB /* TOCropViewController in Frameworks */ = {isa = PBXBuildFile; productRef = 1F759C1B2B63B9D9000534AB /* TOCropViewController */; };
+		1F759C1E2B63B9D9000534AB /* SwiftUIIntrospect in Frameworks */ = {isa = PBXBuildFile; productRef = 1F759C1D2B63B9D9000534AB /* SwiftUIIntrospect */; };
+		1F759C2C2B63CB93000534AB /* Realm in Frameworks */ = {isa = PBXBuildFile; productRef = 1F759C2B2B63CB93000534AB /* Realm */; };
+		1F759C2E2B63CB9A000534AB /* Realm in Frameworks */ = {isa = PBXBuildFile; productRef = 1F759C2D2B63CB9A000534AB /* Realm */; };
+		1F759C302B63CBA0000534AB /* Realm in Frameworks */ = {isa = PBXBuildFile; productRef = 1F759C2F2B63CBA0000534AB /* Realm */; };
+		1F759C322B63CBA5000534AB /* Realm in Frameworks */ = {isa = PBXBuildFile; productRef = 1F759C312B63CBA5000534AB /* Realm */; };
+		1F759C342B63CBAA000534AB /* Realm in Frameworks */ = {isa = PBXBuildFile; productRef = 1F759C332B63CBAA000534AB /* Realm */; };
+		1F7625E52901B0DB00834869 /* CallsFromOldAccountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F7625E42901B0DB00834869 /* CallsFromOldAccountViewController.swift */; };
+		1F7625E72901B0E800834869 /* CallsFromOldAccountViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 1F7625E62901B0E800834869 /* CallsFromOldAccountViewController.xib */; };
+		1F77A5EB2AB9A3EE007B6037 /* BGTaskHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FD9182828C55A73009092AB /* BGTaskHelper.swift */; };
+		1F77A5EC2AB9A405007B6037 /* NCChatBlock.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C4446DC2658158000DF1DBC /* NCChatBlock.m */; };
+		1F77A5ED2AB9A408007B6037 /* NCChatMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CA15540208E350300CE8EF0 /* NCChatMessage.m */; };
+		1F77A5EF2AB9A41E007B6037 /* SDWebImage in Frameworks */ = {isa = PBXBuildFile; productRef = 1F77A5EE2AB9A41E007B6037 /* SDWebImage */; };
+		1F77A5F12AB9A423007B6037 /* SDWebImageSVGKitPlugin in Frameworks */ = {isa = PBXBuildFile; productRef = 1F77A5F02AB9A423007B6037 /* SDWebImageSVGKitPlugin */; };
+		1F77A5F22AB9A436007B6037 /* EmojiUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F4DD3EA2571C688007DC98E /* EmojiUtils.swift */; };
+		1F77A5F32AB9A43B007B6037 /* SwiftMarkdownObjCBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F0A1D432A5F1FA800A25433 /* SwiftMarkdownObjCBridge.swift */; };
+		1F77A5F42AB9A4B2007B6037 /* ABContact.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C1ABDE4257F883400AEDFB6 /* ABContact.m */; };
+		1F77A5F52AB9A4B9007B6037 /* NCAPIController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CA1CCA91F02D1A4002FE6A2 /* NCAPIController.m */; };
+		1F77A5F62AB9A4BF007B6037 /* NCChatReaction.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CC32E9727F5D9BD00BB8C39 /* NCChatReaction.m */; };
+		1F77A5F72AB9A4C5007B6037 /* NCContact.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C1ABDCD257E939600AEDFB6 /* NCContact.m */; };
+		1F77A5F82AB9A4CD007B6037 /* NCDeckCardParameter.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CC1FF4728183958009F7288 /* NCDeckCardParameter.m */; };
+		1F77A5F92AB9A4D9007B6037 /* NCMessageFileParameter.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FEDE3CC257D43AB00853F79 /* NCMessageFileParameter.m */; };
+		1F77A5FA2AB9A4DF007B6037 /* NCMessageLocationParameter.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CB6ACD92641483800D3D641 /* NCMessageLocationParameter.m */; };
+		1F77A5FB2AB9A4E6007B6037 /* NCMessageParameter.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C43BA7521309A1000B3068A /* NCMessageParameter.m */; };
+		1F77A5FC2AB9A4ED007B6037 /* NCRoom.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CA1CCC21F166CC5002FE6A2 /* NCRoom.m */; };
+		1F77A5FD2AB9A4F3007B6037 /* ServerCapabilities.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C4446D7265814D100DF1DBC /* ServerCapabilities.m */; };
+		1F77A5FE2AB9A4F9007B6037 /* TalkAccount.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C4446D22658147900DF1DBC /* TalkAccount.m */; };
+		1F77A6002AB9A50D007B6037 /* NextcloudKit in Frameworks */ = {isa = PBXBuildFile; productRef = 1F77A5FF2AB9A50D007B6037 /* NextcloudKit */; };
+		1F77A6012AB9A51D007B6037 /* NCNotificationAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FA38C8F29A4B3C6008871B8 /* NCNotificationAction.swift */; };
+		1F77A6022AB9A532007B6037 /* CCCertificate.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CBF82B11FCC7DBA00636459 /* CCCertificate.m */; };
+		1F77A6032AB9A56D007B6037 /* NotificationCenterNotifications.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C4446EF265D454200DF1DBC /* NotificationCenterNotifications.m */; };
+		1F77A6062AB9A581007B6037 /* NCKeyChainController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C4446EB265D25BA00DF1DBC /* NCKeyChainController.m */; };
+		1F77A6082AB9A58D007B6037 /* NCRoomParticipants.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C3780BC2107209C003F9AE8 /* NCRoomParticipants.m */; };
+		1F77A60A2AB9A5AE007B6037 /* NCUser.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CA1CCCC1F181741002FE6A2 /* NCUser.m */; };
+		1F77A60C2AB9A5BE007B6037 /* CDMarkdownKit in Frameworks */ = {isa = PBXBuildFile; productRef = 1F77A60B2AB9A5BE007B6037 /* CDMarkdownKit */; };
+		1F77A60D2AB9A5CC007B6037 /* NCPoll.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C5BFBF1288A97D800E75118 /* NCPoll.m */; };
+		1F77A6162AB9B161007B6037 /* ScreenCaptureController.m in Sources */ = {isa = PBXBuildFile; fileRef = 1F77A6112AB9B161007B6037 /* ScreenCaptureController.m */; };
+		1F77A6172AB9B161007B6037 /* ScreenCapturer.m in Sources */ = {isa = PBXBuildFile; fileRef = 1F77A6132AB9B161007B6037 /* ScreenCapturer.m */; };
+		1F77A6222AB9EB06007B6037 /* SocketConnection.m in Sources */ = {isa = PBXBuildFile; fileRef = 1F77A6212AB9EB06007B6037 /* SocketConnection.m */; };
+		1F77A6242ABA0003007B6037 /* SampleHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F77A6232ABA0003007B6037 /* SampleHandler.swift */; };
+		1F77A6272ABA0CD9007B6037 /* NCScreensharingController.m in Sources */ = {isa = PBXBuildFile; fileRef = 1F77A6262ABA0CD9007B6037 /* NCScreensharingController.m */; };
+		1F77A62E2ABAFCC0007B6037 /* DarwinNotificationCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF2FD792AB99E4D000C9905 /* DarwinNotificationCenter.swift */; };
+		1F77A62F2ABAFCEB007B6037 /* DarwinNotificationCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF2FD792AB99E4D000C9905 /* DarwinNotificationCenter.swift */; };
+		1F785DDD2707865F00AC4B40 /* VoiceMessageTranscribeViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 1F785DDA2707865F00AC4B40 /* VoiceMessageTranscribeViewController.m */; };
+		1F785DDE2707865F00AC4B40 /* VoiceMessageTranscribeViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 1F785DDB2707865F00AC4B40 /* VoiceMessageTranscribeViewController.xib */; };
+		1F7AE07829142CA1009F72AD /* NextcloudKit in Frameworks */ = {isa = PBXBuildFile; productRef = 1F7AE07729142CA1009F72AD /* NextcloudKit */; };
+		1F7AE07A29142E62009F72AD /* NextcloudKit in Frameworks */ = {isa = PBXBuildFile; productRef = 1F7AE07929142E62009F72AD /* NextcloudKit */; };
+		1F7AE07C29142E6A009F72AD /* NextcloudKit in Frameworks */ = {isa = PBXBuildFile; productRef = 1F7AE07B29142E6A009F72AD /* NextcloudKit */; };
+		1F7AE07D29158878009F72AD /* IntentsUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1F90EFC225FE489B00F3FA55 /* IntentsUI.framework */; };
+		1F8995B32970644C00CABA33 /* ColorGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F8995B22970644C00CABA33 /* ColorGenerator.swift */; };
+		1F8995B52973547700CABA33 /* WebRTCCommon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F8995B42973547700CABA33 /* WebRTCCommon.swift */; };
+		1F8AAC322C518759004DA20A /* SignalingSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F8AAC312C518759004DA20A /* SignalingSettings.swift */; };
+		1F8AAC332C518B8A004DA20A /* SignalingSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F8AAC312C518759004DA20A /* SignalingSettings.swift */; };
+		1F8AAC342C518B8A004DA20A /* SignalingSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F8AAC312C518759004DA20A /* SignalingSettings.swift */; };
+		1F8AAC352C518B8B004DA20A /* SignalingSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F8AAC312C518759004DA20A /* SignalingSettings.swift */; };
+		1F8AAC372C519577004DA20A /* TurnServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F8AAC362C519577004DA20A /* TurnServer.swift */; };
+		1F8AAC382C519577004DA20A /* TurnServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F8AAC362C519577004DA20A /* TurnServer.swift */; };
+		1F8AAC392C519577004DA20A /* TurnServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F8AAC362C519577004DA20A /* TurnServer.swift */; };
+		1F8AAC3A2C519577004DA20A /* TurnServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F8AAC362C519577004DA20A /* TurnServer.swift */; };
+		1F8AAC3C2C519689004DA20A /* StunServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F8AAC3B2C519689004DA20A /* StunServer.swift */; };
+		1F8AAC3D2C519689004DA20A /* StunServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F8AAC3B2C519689004DA20A /* StunServer.swift */; };
+		1F8AAC3E2C519689004DA20A /* StunServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F8AAC3B2C519689004DA20A /* StunServer.swift */; };
+		1F8AAC3F2C519689004DA20A /* StunServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F8AAC3B2C519689004DA20A /* StunServer.swift */; };
+		1F8AAC622C596308004DA20A /* UnitSignalingSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F8AAC612C596308004DA20A /* UnitSignalingSettings.swift */; };
+		1F90DA0429E9A28E00E81E3D /* AvatarManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F90DA0329E9A28E00E81E3D /* AvatarManager.swift */; };
+		1F90EFBC25FE39F800F3FA55 /* NCIntentController.m in Sources */ = {isa = PBXBuildFile; fileRef = 1F90EFBB25FE39F800F3FA55 /* NCIntentController.m */; };
+		1F90EFBD25FE39F800F3FA55 /* NCIntentController.m in Sources */ = {isa = PBXBuildFile; fileRef = 1F90EFBB25FE39F800F3FA55 /* NCIntentController.m */; };
+		1F90EFBE25FE39F800F3FA55 /* NCIntentController.m in Sources */ = {isa = PBXBuildFile; fileRef = 1F90EFBB25FE39F800F3FA55 /* NCIntentController.m */; };
+		1F90EFC725FE4BE700F3FA55 /* IntentsUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1F90EFC225FE489B00F3FA55 /* IntentsUI.framework */; };
+		1F98DF9C28E7484700E05174 /* ReferenceDeckView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F98DF9B28E7484700E05174 /* ReferenceDeckView.swift */; };
+		1F98DF9E28E7485000E05174 /* ReferenceDeckView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 1F98DF9D28E7485000E05174 /* ReferenceDeckView.xib */; };
+		1FA20C8A284001D80062B4F3 /* DebounceWebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FA20C89284001D80062B4F3 /* DebounceWebView.swift */; };
+		1FA38C9029A4B3C6008871B8 /* NCNotificationAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FA38C8F29A4B3C6008871B8 /* NCNotificationAction.swift */; };
+		1FA38C9129A4B3C6008871B8 /* NCNotificationAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FA38C8F29A4B3C6008871B8 /* NCNotificationAction.swift */; };
+		1FA732FC2966CBB7003D2103 /* CallFlowLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FA732FB2966CBB7003D2103 /* CallFlowLayout.swift */; };
+		1FAB2E7D2AC99326001214EB /* TOCropViewController in Frameworks */ = {isa = PBXBuildFile; productRef = 1FAB2E7C2AC99326001214EB /* TOCropViewController */; };
+		1FAB2E7F2AC99367001214EB /* TOCropViewController in Frameworks */ = {isa = PBXBuildFile; productRef = 1FAB2E7E2AC99367001214EB /* TOCropViewController */; };
+		1FAB2E832AC9EC3F001214EB /* BaseChatViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FAB2E822AC9EC3F001214EB /* BaseChatViewController.swift */; };
+		1FAB2E852ACB482B001214EB /* ChatViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FAB2E842ACB482B001214EB /* ChatViewController.swift */; };
+		1FAB2E882ACD44D0001214EB /* WebRTC in Frameworks */ = {isa = PBXBuildFile; productRef = 1FAB2E872ACD44D0001214EB /* WebRTC */; };
+		1FAB2EEE2AD1BC1B001214EB /* UIControlExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FAB2EED2AD1BC1B001214EB /* UIControlExtensions.swift */; };
+		1FAB2EF02AD1EAA3001214EB /* RLMSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FAB2EEF2AD1EAA3001214EB /* RLMSupport.swift */; };
+		1FAB2EF12AD1EAA3001214EB /* RLMSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FAB2EEF2AD1EAA3001214EB /* RLMSupport.swift */; };
+		1FAB2EF22AD1EAA3001214EB /* RLMSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FAB2EEF2AD1EAA3001214EB /* RLMSupport.swift */; };
+		1FADECD62B821E24007AD94B /* FederationInvitationTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FADECD52B821E24007AD94B /* FederationInvitationTableViewController.swift */; };
+		1FADECD82B82269E007AD94B /* FederationInvitationCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FADECD72B82269E007AD94B /* FederationInvitationCell.swift */; };
+		1FADECDA2B8227B1007AD94B /* FederationInvitationCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 1FADECD92B8227B1007AD94B /* FederationInvitationCell.xib */; };
+		1FB52E762842C75E00AC741B /* QRCodeLoginController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FB52E752842C75E00AC741B /* QRCodeLoginController.swift */; };
+		1FB6678F28CE381300D29F8D /* SubtitleTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FB6678E28CE381300D29F8D /* SubtitleTableViewCell.swift */; };
+		1FB78E1F2B6ADBAA00B0D69D /* NCAPIControllerExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FB78E1E2B6ADBAA00B0D69D /* NCAPIControllerExtensions.swift */; };
+		1FB78E202B6ADBB600B0D69D /* NCAPIControllerExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FB78E1E2B6ADBAA00B0D69D /* NCAPIControllerExtensions.swift */; };
+		1FB78E212B6ADBB700B0D69D /* NCAPIControllerExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FB78E1E2B6ADBAA00B0D69D /* NCAPIControllerExtensions.swift */; };
+		1FB78E222B6ADBB700B0D69D /* NCAPIControllerExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FB78E1E2B6ADBAA00B0D69D /* NCAPIControllerExtensions.swift */; };
+		1FB78E262B6AE5A600B0D69D /* FederationInvitation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FB78E252B6AE5A600B0D69D /* FederationInvitation.swift */; };
+		1FB78E272B6AE8C900B0D69D /* FederationInvitation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FB78E252B6AE5A600B0D69D /* FederationInvitation.swift */; };
+		1FB78E282B6AE8C900B0D69D /* FederationInvitation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FB78E252B6AE5A600B0D69D /* FederationInvitation.swift */; };
+		1FB78E292B6AE8CA00B0D69D /* FederationInvitation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FB78E252B6AE5A600B0D69D /* FederationInvitation.swift */; };
+		1FB7B9852BE2EE020093CE98 /* UnitChatViewControllerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FB7B9842BE2EE020093CE98 /* UnitChatViewControllerTest.swift */; };
+		1FB7B9872BE441450093CE98 /* UIViewExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FB7B9862BE441450093CE98 /* UIViewExtensions.swift */; };
+		1FB7B9892BE442400093CE98 /* UnitBaseChatTableViewCellTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FB7B9882BE442400093CE98 /* UnitBaseChatTableViewCellTest.swift */; };
+		1FB7B98E2BF0CBA60093CE98 /* BannedActor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FB7B98D2BF0CBA60093CE98 /* BannedActor.swift */; };
+		1FB7B9902BF0CDF80093CE98 /* BannedActor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FB7B98D2BF0CBA60093CE98 /* BannedActor.swift */; };
+		1FB7B9912BF0CDF80093CE98 /* BannedActor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FB7B98D2BF0CBA60093CE98 /* BannedActor.swift */; };
+		1FB7B9922BF0CDF90093CE98 /* BannedActor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FB7B98D2BF0CBA60093CE98 /* BannedActor.swift */; };
+		1FB7B9952BF0DF1C0093CE98 /* BannedActorTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FB7B9942BF0DF1C0093CE98 /* BannedActorTableViewController.swift */; };
+		1FB7B99A2BF0DF290093CE98 /* BannedActorCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FB7B9992BF0DF290093CE98 /* BannedActorCell.swift */; };
+		1FB7B99C2BF0DF360093CE98 /* BannedActorCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 1FB7B99B2BF0DF360093CE98 /* BannedActorCell.xib */; };
+		1FBC3BE52B61ACD5003909E0 /* UnitBaseChatViewControllerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FBC3BE42B61ACD5003909E0 /* UnitBaseChatViewControllerTest.swift */; };
+		1FBC3BE92B61BD09003909E0 /* TestBaseRealm.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FBC3BE82B61BD09003909E0 /* TestBaseRealm.swift */; };
+		1FC940B92A5F21FC00FFFADE /* SwiftMarkdownObjCBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F0A1D432A5F1FA800A25433 /* SwiftMarkdownObjCBridge.swift */; };
+		1FC940BA2A5F21FD00FFFADE /* SwiftMarkdownObjCBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F0A1D432A5F1FA800A25433 /* SwiftMarkdownObjCBridge.swift */; };
+		1FCE3D532C9B5918009C68A9 /* SwiftyGif in Frameworks */ = {isa = PBXBuildFile; productRef = 1FCE3D522C9B5918009C68A9 /* SwiftyGif */; };
+		1FCE3D552C9C189D009C68A9 /* NCChatFileControllerWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FCE3D542C9C189D009C68A9 /* NCChatFileControllerWrapper.swift */; };
+		1FCE3D572C9C4D18009C68A9 /* ReferenceGiphyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FCE3D562C9C4D18009C68A9 /* ReferenceGiphyView.swift */; };
+		1FCE3D592C9C4D21009C68A9 /* ReferenceGiphyView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 1FCE3D582C9C4D21009C68A9 /* ReferenceGiphyView.xib */; };
+		1FD6F83C2B825069004048AB /* NCRoomsManagerExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FD6F83B2B825069004048AB /* NCRoomsManagerExtensions.swift */; };
+		1FD6F83E2B87B712004048AB /* NCUserStatusExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FD6F83D2B87B712004048AB /* NCUserStatusExtensions.swift */; };
+		1FD8AE6B2A3A216300787C16 /* UIRoomTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FD8AD8C2A3A162100787C16 /* UIRoomTest.swift */; };
+		1FD9182928C55A73009092AB /* BGTaskHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FD9182828C55A73009092AB /* BGTaskHelper.swift */; };
+		1FDB47F62C9C71CE00D6F423 /* TalkAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FDB47F52C9C71CE00D6F423 /* TalkAccount.swift */; };
+		1FDB47F82C9C7E3F00D6F423 /* NCDatabaseManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FDB47F72C9C7E3F00D6F423 /* NCDatabaseManager.swift */; };
+		1FDCC3D429EBF6E700DEB39B /* AvatarImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FDCC3D329EBF6E700DEB39B /* AvatarImageView.swift */; };
+		1FDCC3E329EC787400DEB39B /* AvatarManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F90DA0329E9A28E00E81E3D /* AvatarManager.swift */; };
+		1FDCC3ED29EC7E6700DEB39B /* AvatarImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FDCC3D329EBF6E700DEB39B /* AvatarImageView.swift */; };
+		1FDCC3EE29EC7E8500DEB39B /* AvatarManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F90DA0329E9A28E00E81E3D /* AvatarManager.swift */; };
+		1FDCC3F029ECB4CE00DEB39B /* AvatarButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FDCC3EF29ECB4CE00DEB39B /* AvatarButton.swift */; };
+		1FDDB0D92AF440DD00FBAFB7 /* BoundsChangedFlowLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FDDB0D82AF440DD00FBAFB7 /* BoundsChangedFlowLayout.swift */; };
+		1FDDB0DB2AF440E100FBAFB7 /* BoundsChangedFlowLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FDDB0D82AF440DD00FBAFB7 /* BoundsChangedFlowLayout.swift */; };
+		1FDE7C9A28DE14A200CB718E /* ReferenceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FDE7C9928DE14A200CB718E /* ReferenceView.swift */; };
+		1FDE7C9C28DE14B000CB718E /* ReferenceView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 1FDE7C9B28DE14B000CB718E /* ReferenceView.xib */; };
+		1FDFC94D2BA50B9100670DF4 /* UIFontExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FDFC94C2BA50B9100670DF4 /* UIFontExtension.swift */; };
+		1FDFC94E2BA50B9100670DF4 /* UIFontExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FDFC94C2BA50B9100670DF4 /* UIFontExtension.swift */; };
+		1FDFC94F2BA50B9100670DF4 /* UIFontExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FDFC94C2BA50B9100670DF4 /* UIFontExtension.swift */; };
+		1FDFC9502BA50B9100670DF4 /* UIFontExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FDFC94C2BA50B9100670DF4 /* UIFontExtension.swift */; };
+		1FE0C56C2A0531200083576A /* ReferenceTalkView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 1FE0C56B2A0531200083576A /* ReferenceTalkView.xib */; };
+		1FE0C56E2A0531270083576A /* ReferenceTalkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FE0C56D2A0531270083576A /* ReferenceTalkView.swift */; };
+		1FE7DE302BB4598F0040EE12 /* RoomInvitationViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FE7DE2F2BB4598F0040EE12 /* RoomInvitationViewCell.swift */; };
+		1FE7DE322BB459B10040EE12 /* RoomInvitationViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 1FE7DE312BB459B10040EE12 /* RoomInvitationViewCell.xib */; };
+		1FE7DE332BBC8FA00040EE12 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 1FADECD42B7EACCB007AD94B /* PrivacyInfo.xcprivacy */; };
+		1FE7DE342BBC8FA10040EE12 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 1FADECD42B7EACCB007AD94B /* PrivacyInfo.xcprivacy */; };
+		1FE7DE352BBC8FA10040EE12 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 1FADECD42B7EACCB007AD94B /* PrivacyInfo.xcprivacy */; };
+		1FE7DE362BBC8FA10040EE12 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 1FADECD42B7EACCB007AD94B /* PrivacyInfo.xcprivacy */; };
+		1FE94734293CE55600D6584C /* NCCameraController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FE94733293CE55600D6584C /* NCCameraController.swift */; };
+		1FEC459C2A02BCAE00A636AA /* ReferenceGithubPermalinkView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 1FEC459B2A02BCAE00A636AA /* ReferenceGithubPermalinkView.xib */; };
+		1FEC459E2A02BCB900A636AA /* ReferenceGithubPermalinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FEC459D2A02BCB900A636AA /* ReferenceGithubPermalinkView.swift */; };
+		1FEC45A32A02F92700A636AA /* GithubPermalinkViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FEC45A22A02F92700A636AA /* GithubPermalinkViewController.swift */; };
+		1FEC45A52A02F92B00A636AA /* GithubPermalinkViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 1FEC45A42A02F92B00A636AA /* GithubPermalinkViewController.xib */; };
+		1FEDE3C6257D439500853F79 /* NCChatFileController.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FEDE3C4257D439500853F79 /* NCChatFileController.m */; };
+		1FEDE3CE257D43AB00853F79 /* NCMessageFileParameter.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FEDE3CC257D43AB00853F79 /* NCMessageFileParameter.m */; };
+		1FEDE3CF257D43AB00853F79 /* NCMessageFileParameter.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FEDE3CC257D43AB00853F79 /* NCMessageFileParameter.m */; };
+		1FEDE3D0257D43AB00853F79 /* NCMessageFileParameter.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FEDE3CC257D43AB00853F79 /* NCMessageFileParameter.m */; };
+		1FF1360F2BFB4F8C006A6101 /* NCRoom.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF1360E2BFB4F8C006A6101 /* NCRoom.swift */; };
+		1FF136102BFB4F8C006A6101 /* NCRoom.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF1360E2BFB4F8C006A6101 /* NCRoom.swift */; };
+		1FF136112BFB4F8C006A6101 /* NCRoom.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF1360E2BFB4F8C006A6101 /* NCRoom.swift */; };
+		1FF136122BFB4F8C006A6101 /* NCRoom.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF1360E2BFB4F8C006A6101 /* NCRoom.swift */; };
+		1FF136132BFB6FCD006A6101 /* RLMSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FAB2EEF2AD1EAA3001214EB /* RLMSupport.swift */; };
+		1FF136152BFB74C3006A6101 /* NCChatMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF136142BFB74C3006A6101 /* NCChatMessage.swift */; };
+		1FF136162BFB74CF006A6101 /* NCChatMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF136142BFB74C3006A6101 /* NCChatMessage.swift */; };
+		1FF136172BFB74CF006A6101 /* NCChatMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF136142BFB74C3006A6101 /* NCChatMessage.swift */; };
+		1FF136182BFB74D0006A6101 /* NCChatMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF136142BFB74C3006A6101 /* NCChatMessage.swift */; };
+		1FF1361A2BFBC841006A6101 /* SwiftyAttributes in Frameworks */ = {isa = PBXBuildFile; productRef = 1FF136192BFBC841006A6101 /* SwiftyAttributes */; };
+		1FF1361C2BFBC86A006A6101 /* SwiftyAttributes in Frameworks */ = {isa = PBXBuildFile; productRef = 1FF1361B2BFBC86A006A6101 /* SwiftyAttributes */; };
+		1FF2FD7F2AB99E4D000C9905 /* Atomic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF2FD7A2AB99E4D000C9905 /* Atomic.swift */; };
+		1FF2FD802AB99E4D000C9905 /* SampleUploader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF2FD7B2AB99E4D000C9905 /* SampleUploader.swift */; };
+		1FF2FD822AB99E4D000C9905 /* SocketConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF2FD7D2AB99E4D000C9905 /* SocketConnection.swift */; };
+		1FF2FD832AB99F3B000C9905 /* NCAppBranding.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C2A788D2359CC8800EEB797 /* NCAppBranding.m */; };
+		1FF2FD852AB99F51000C9905 /* NCUserStatus.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C78E9E225120DE500E3D4CA /* NCUserStatus.m */; };
+		1FF2FD862AB99F5B000C9905 /* NCDatabaseManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C40281422832EED0000DDFC /* NCDatabaseManager.m */; };
+		1FF4DA7E2C0237D000C1B952 /* DirectoryTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF4DA7D2C0237D000C1B952 /* DirectoryTableViewCell.swift */; };
+		1FF4DA802C023FF300C1B952 /* NCChatFileStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF4DA7F2C023FF300C1B952 /* NCChatFileStatus.swift */; };
+		1FF4DA822C025DB900C1B952 /* NCAPISessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF4DA812C025DB900C1B952 /* NCAPISessionManager.swift */; };
+		1FF4DA832C025DBF00C1B952 /* NCAPISessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF4DA812C025DB900C1B952 /* NCAPISessionManager.swift */; };
+		1FF4DA842C025DC000C1B952 /* NCAPISessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF4DA812C025DB900C1B952 /* NCAPISessionManager.swift */; };
+		1FF4DA852C025DC000C1B952 /* NCAPISessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF4DA812C025DB900C1B952 /* NCAPISessionManager.swift */; };
+		1FF4DA872C02626D00C1B952 /* NCBaseSessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF4DA862C02626D00C1B952 /* NCBaseSessionManager.swift */; };
+		1FF4DA882C0262BA00C1B952 /* NCBaseSessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF4DA862C02626D00C1B952 /* NCBaseSessionManager.swift */; };
+		1FF4DA892C0262BB00C1B952 /* NCBaseSessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF4DA862C02626D00C1B952 /* NCBaseSessionManager.swift */; };
+		1FF4DA8A2C0262BB00C1B952 /* NCBaseSessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF4DA862C02626D00C1B952 /* NCBaseSessionManager.swift */; };
+		1FF4DA8C2C0263A200C1B952 /* NCPushProxySessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF4DA8B2C0263A200C1B952 /* NCPushProxySessionManager.swift */; };
+		1FF4DA8D2C0264B100C1B952 /* NCPushProxySessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF4DA8B2C0263A200C1B952 /* NCPushProxySessionManager.swift */; };
+		1FF4DA8E2C0264B200C1B952 /* NCPushProxySessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF4DA8B2C0263A200C1B952 /* NCPushProxySessionManager.swift */; };
+		1FF4DA8F2C0264B200C1B952 /* NCPushProxySessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF4DA8B2C0263A200C1B952 /* NCPushProxySessionManager.swift */; };
+		1FF4DA912C02677C00C1B952 /* NCImageSessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF4DA902C02677C00C1B952 /* NCImageSessionManager.swift */; };
+		1FF4DA922C02677F00C1B952 /* NCImageSessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF4DA902C02677C00C1B952 /* NCImageSessionManager.swift */; };
+		1FF4DA932C02678000C1B952 /* NCImageSessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF4DA902C02677C00C1B952 /* NCImageSessionManager.swift */; };
+		1FF4DA942C02678000C1B952 /* NCImageSessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF4DA902C02677C00C1B952 /* NCImageSessionManager.swift */; };
+		1FF4DA962C0327FF00C1B952 /* NCWebImageDownloaderOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF4DA952C0327FF00C1B952 /* NCWebImageDownloaderOperation.swift */; };
+		1FF4DA972C0327FF00C1B952 /* NCWebImageDownloaderOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF4DA952C0327FF00C1B952 /* NCWebImageDownloaderOperation.swift */; };
+		1FF4DA982C0327FF00C1B952 /* NCWebImageDownloaderOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF4DA952C0327FF00C1B952 /* NCWebImageDownloaderOperation.swift */; };
+		1FF4DA992C0327FF00C1B952 /* NCWebImageDownloaderOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF4DA952C0327FF00C1B952 /* NCWebImageDownloaderOperation.swift */; };
+		1FF4DA9B2C032AAC00C1B952 /* RoomTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF4DA9A2C032AAC00C1B952 /* RoomTableViewCell.swift */; };
+		1FF4DAA02C03351E00C1B952 /* RoomNameTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF4DA9F2C03351E00C1B952 /* RoomNameTableViewCell.swift */; };
+		1FF4DAA22C0338D000C1B952 /* RoomDescriptionTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF4DAA12C0338D000C1B952 /* RoomDescriptionTableViewCell.swift */; };
+		1FF4DAA62C08D81D00C1B952 /* UnitNCChatMessageTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF4DAA52C08D81D00C1B952 /* UnitNCChatMessageTest.swift */; };
+		1FF4DAA82C08DE3A00C1B952 /* UnitNCRoomsManagerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF4DAA72C08DE3A00C1B952 /* UnitNCRoomsManagerTest.swift */; };
+		1FF4DAAA2C0A114900C1B952 /* OcsResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF4DAA92C0A114900C1B952 /* OcsResponse.swift */; };
+		1FF4DAAB2C0A114900C1B952 /* OcsResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF4DAA92C0A114900C1B952 /* OcsResponse.swift */; };
+		1FF4DAAC2C0A114900C1B952 /* OcsResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF4DAA92C0A114900C1B952 /* OcsResponse.swift */; };
+		1FF4DAAD2C0A114900C1B952 /* OcsResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF4DAA92C0A114900C1B952 /* OcsResponse.swift */; };
+		1FFF41622C70937B00162F4D /* ReferenceZammadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FFF41612C70937B00162F4D /* ReferenceZammadView.swift */; };
+		1FFF41642C70938700162F4D /* ReferenceZammadView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 1FFF41632C70938700162F4D /* ReferenceZammadView.xib */; };
+		2C0424902CA32D45004772F6 /* BaseChatTableViewCell+Audio.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C04248F2CA32D45004772F6 /* BaseChatTableViewCell+Audio.swift */; };
+		2C0424982CA335C4004772F6 /* AudioPlayerView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2C0424962CA335C4004772F6 /* AudioPlayerView.xib */; };
+		2C04249B2CA33681004772F6 /* AudioPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C0424992CA33681004772F6 /* AudioPlayerView.swift */; };
+		2C0574821EDD9E8E00D9E7F2 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C0574811EDD9E8E00D9E7F2 /* main.m */; };
+		2C0574851EDD9E8E00D9E7F2 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C0574841EDD9E8E00D9E7F2 /* AppDelegate.m */; };
+		2C05748E1EDD9E8E00D9E7F2 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 2C05748C1EDD9E8E00D9E7F2 /* Main.storyboard */; };
+		2C0574A41EDDA2E300D9E7F2 /* LoginViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C0574A21EDDA2E300D9E7F2 /* LoginViewController.m */; };
+		2C0574A51EDDA2E300D9E7F2 /* LoginViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2C0574A31EDDA2E300D9E7F2 /* LoginViewController.xib */; };
+		2C06330F2046CC8B0043481A /* NCUserInterfaceController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C06330E2046CC8B0043481A /* NCUserInterfaceController.m */; };
+		2C06BF5D20A89F510031EB46 /* NCRoomsManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C06BF5C20A89F510031EB46 /* NCRoomsManager.m */; };
+		2C06BF6420AC64370031EB46 /* DateHeaderView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2C06BF6320AC64370031EB46 /* DateHeaderView.xib */; };
+		2C06BF6720AC647A0031EB46 /* DateHeaderView.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C06BF6620AC647A0031EB46 /* DateHeaderView.m */; };
+		2C06BF6C20AEB0030031EB46 /* RoundedNumberView.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C06BF6A20AEB0030031EB46 /* RoundedNumberView.m */; };
+		2C16A82C28E7284D00EDE523 /* NCButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C16A82B28E7284D00EDE523 /* NCButton.swift */; };
+		2C1ABD8625769E7D00AEDFB6 /* ShareConfirmationCollectionViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C1ABD8025769E7D00AEDFB6 /* ShareConfirmationCollectionViewCell.m */; };
+		2C1ABD8725769E7D00AEDFB6 /* ShareItemController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C1ABD8225769E7D00AEDFB6 /* ShareItemController.m */; };
+		2C1ABD8825769E7D00AEDFB6 /* ShareConfirmationCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2C1ABD8425769E7D00AEDFB6 /* ShareConfirmationCollectionViewCell.xib */; };
+		2C1ABD8925769E7D00AEDFB6 /* ShareItem.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C1ABD8525769E7D00AEDFB6 /* ShareItem.m */; };
+		2C1ABD9925769F7500AEDFB6 /* ShareItem.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C1ABD8525769E7D00AEDFB6 /* ShareItem.m */; };
+		2C1ABDC6257A7CF000AEDFB6 /* NCContactsManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C1ABDC5257A7CF000AEDFB6 /* NCContactsManager.m */; };
+		2C1ABDCE257E939600AEDFB6 /* NCContact.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C1ABDCD257E939600AEDFB6 /* NCContact.m */; };
+		2C1ABDCF257E939600AEDFB6 /* NCContact.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C1ABDCD257E939600AEDFB6 /* NCContact.m */; };
+		2C1ABDD0257E939600AEDFB6 /* NCContact.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C1ABDCD257E939600AEDFB6 /* NCContact.m */; };
+		2C1ABDE5257F883400AEDFB6 /* ABContact.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C1ABDE4257F883400AEDFB6 /* ABContact.m */; };
+		2C1D13A3253760EE00EC0533 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2C1D13A1253760EE00EC0533 /* LaunchScreen.xib */; };
+		2C1EF36B25505DCE007C9768 /* NCNavigationController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C1EF36A25505DCE007C9768 /* NCNavigationController.m */; };
+		2C1EF36D25505DCE007C9768 /* NCNavigationController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C1EF36A25505DCE007C9768 /* NCNavigationController.m */; };
+		2C21446E2BB5B54D005A6537 /* BaseChatTableViewCell+Location.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C21446D2BB5B54D005A6537 /* BaseChatTableViewCell+Location.swift */; };
+		2C2145682BF6B8E900470C0C /* NewRoomTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C2145672BF6B8E900470C0C /* NewRoomTableViewController.swift */; };
+		2C2A788E2359CC8800EEB797 /* NCAppBranding.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C2A788D2359CC8800EEB797 /* NCAppBranding.m */; };
+		2C2D7A172B8C9C0000642373 /* RoomCreationTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C2D7A162B8C9C0000642373 /* RoomCreationTableViewController.swift */; };
+		2C2E64251F3462AF00D39CE8 /* NCSignalingMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C2E64241F3462AF00D39CE8 /* NCSignalingMessage.m */; };
+		2C3195BC24C599130066F221 /* PlaceholderView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2CC7158820B837140045C789 /* PlaceholderView.xib */; };
+		2C3195BE24C5A7410066F221 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2CA1CCAB1F067F35002FE6A2 /* Images.xcassets */; };
+		2C3195C224C5E2100066F221 /* ShareTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C3195C024C5E2100066F221 /* ShareTableViewCell.m */; };
+		2C3195C324C5E2100066F221 /* ShareTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2C3195C124C5E2100066F221 /* ShareTableViewCell.xib */; };
+		2C330372255E6EBC00BDB4E4 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 2C330374255E6EBC00BDB4E4 /* InfoPlist.strings */; };
+		2C36A04A261487BC0026F04A /* DetailedOptionsSelectorTableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C36A049261487BC0026F04A /* DetailedOptionsSelectorTableViewController.m */; };
+		2C3780BD2107209C003F9AE8 /* NCRoomParticipants.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C3780BC2107209C003F9AE8 /* NCRoomParticipants.m */; };
+		2C3780C3210F49DC003F9AE8 /* HeaderWithButton.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C3780C2210F49DC003F9AE8 /* HeaderWithButton.m */; };
+		2C3780C5210F4A26003F9AE8 /* HeaderWithButton.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2C3780C4210F4A26003F9AE8 /* HeaderWithButton.xib */; };
+		2C40281522832EED0000DDFC /* NCDatabaseManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C40281422832EED0000DDFC /* NCDatabaseManager.m */; };
+		2C4230F72B207AB00013E1FA /* ContextChatViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C4230F62B207AB00013E1FA /* ContextChatViewController.swift */; };
+		2C42ADB420B58E6300296DEA /* NCChatController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C42ADB320B58E6300296DEA /* NCChatController.m */; };
+		2C43BA7621309A1000B3068A /* NCMessageParameter.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C43BA7521309A1000B3068A /* NCMessageParameter.m */; };
+		2C440D1120EA4A770005F9BB /* RoomInfoTableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C440D0F20EA4A770005F9BB /* RoomInfoTableViewController.m */; };
+		2C440D1220EA4A770005F9BB /* RoomInfoTableViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2C440D1020EA4A770005F9BB /* RoomInfoTableViewController.xib */; };
+		2C4446D32658147900DF1DBC /* TalkAccount.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C4446D22658147900DF1DBC /* TalkAccount.m */; };
+		2C4446D42658147900DF1DBC /* TalkAccount.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C4446D22658147900DF1DBC /* TalkAccount.m */; };
+		2C4446D52658147900DF1DBC /* TalkAccount.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C4446D22658147900DF1DBC /* TalkAccount.m */; };
+		2C4446D8265814D100DF1DBC /* ServerCapabilities.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C4446D7265814D100DF1DBC /* ServerCapabilities.m */; };
+		2C4446D9265814D100DF1DBC /* ServerCapabilities.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C4446D7265814D100DF1DBC /* ServerCapabilities.m */; };
+		2C4446DA265814D100DF1DBC /* ServerCapabilities.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C4446D7265814D100DF1DBC /* ServerCapabilities.m */; };
+		2C4446DD2658158000DF1DBC /* NCChatBlock.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C4446DC2658158000DF1DBC /* NCChatBlock.m */; };
+		2C4446DE2658158000DF1DBC /* NCChatBlock.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C4446DC2658158000DF1DBC /* NCChatBlock.m */; };
+		2C4446DF2658158000DF1DBC /* NCChatBlock.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C4446DC2658158000DF1DBC /* NCChatBlock.m */; };
+		2C4446EC265D25BA00DF1DBC /* NCKeyChainController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C4446EB265D25BA00DF1DBC /* NCKeyChainController.m */; };
+		2C4446ED265D25BA00DF1DBC /* NCKeyChainController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C4446EB265D25BA00DF1DBC /* NCKeyChainController.m */; };
+		2C4446F0265D454200DF1DBC /* NotificationCenterNotifications.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C4446EF265D454200DF1DBC /* NotificationCenterNotifications.m */; };
+		2C4446F3265D51A600DF1DBC /* NCPushNotificationsUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C4446F2265D51A600DF1DBC /* NCPushNotificationsUtils.m */; };
+		2C4446F4265D51A600DF1DBC /* NCPushNotificationsUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C4446F2265D51A600DF1DBC /* NCPushNotificationsUtils.m */; };
+		2C4446F5265D583200DF1DBC /* NCKeyChainController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C4446EB265D25BA00DF1DBC /* NCKeyChainController.m */; };
+		2C4446F8265D5A0700DF1DBC /* NotificationCenterNotifications.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C4446EF265D454200DF1DBC /* NotificationCenterNotifications.m */; };
+		2C4446F9265D5A0700DF1DBC /* NotificationCenterNotifications.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C4446EF265D454200DF1DBC /* NotificationCenterNotifications.m */; };
+		2C4446FB265D5C5700DF1DBC /* NCRoomParticipants.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C3780BC2107209C003F9AE8 /* NCRoomParticipants.m */; };
+		2C4446FC265D5C5800DF1DBC /* NCRoomParticipants.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C3780BC2107209C003F9AE8 /* NCRoomParticipants.m */; };
+		2C4446FD265D5DFA00DF1DBC /* ABContact.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C1ABDE4257F883400AEDFB6 /* ABContact.m */; };
+		2C4446FE265D5DFA00DF1DBC /* ABContact.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C1ABDE4257F883400AEDFB6 /* ABContact.m */; };
+		2C444703265D641300DF1DBC /* NCUserDefaults.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C444702265D641300DF1DBC /* NCUserDefaults.m */; };
+		2C444704265D641300DF1DBC /* NCUserDefaults.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C444702265D641300DF1DBC /* NCUserDefaults.m */; };
+		2C444705265D641300DF1DBC /* NCUserDefaults.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C444702265D641300DF1DBC /* NCUserDefaults.m */; };
+		2C444706265E59B100DF1DBC /* ShareConfirmationCollectionViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C1ABD8025769E7D00AEDFB6 /* ShareConfirmationCollectionViewCell.m */; };
+		2C444707265E59B500DF1DBC /* ShareConfirmationCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2C1ABD8425769E7D00AEDFB6 /* ShareConfirmationCollectionViewCell.xib */; };
+		2C444708265E59BC00DF1DBC /* ShareItemController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C1ABD8225769E7D00AEDFB6 /* ShareItemController.m */; };
+		2C44B4D127FF05A000AD1C86 /* ReactionsSummaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C44B4D027FF05A000AD1C86 /* ReactionsSummaryView.swift */; };
+		2C4747E22CB58FD2002828F2 /* PollMessageView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2C4747E12CB58FD2002828F2 /* PollMessageView.xib */; };
+		2C4747E62CB6711F002828F2 /* BaseChatTableViewCell+Poll.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C4747E52CB6710F002828F2 /* BaseChatTableViewCell+Poll.swift */; };
+		2C4747E92CB67177002828F2 /* PollMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C4747E82CB67177002828F2 /* PollMessageView.swift */; };
+		2C477C1628B79D980044DEB4 /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 2C477C1828B79D980044DEB4 /* Localizable.stringsdict */; };
+		2C4987BD21E640E20060AC27 /* CallKitManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C4987BC21E640E20060AC27 /* CallKitManager.m */; };
+		2C4CDCCD269618240023F403 /* RoomDescriptionTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2C4CDCCB269618230023F403 /* RoomDescriptionTableViewCell.xib */; };
+		2C4CDCD026A84AEA0023F403 /* ShareViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C62AFB824C1A4E6007E460A /* ShareViewController.m */; };
+		2C4CDCD126A84E500023F403 /* ShareTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C3195C024C5E2100066F221 /* ShareTableViewCell.m */; };
+		2C4CDCD226A84E550023F403 /* ShareTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2C3195C124C5E2100066F221 /* ShareTableViewCell.xib */; };
+		2C4D7D631F2F7C2C00FF4A0D /* ARDCaptureController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C4D7D621F2F7C2C00FF4A0D /* ARDCaptureController.m */; };
+		2C4D7D691F2F7DBC00FF4A0D /* ARDSettingsModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C4D7D651F2F7DBC00FF4A0D /* ARDSettingsModel.m */; };
+		2C4D7D6A1F2F7DBC00FF4A0D /* ARDSettingsStore.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C4D7D681F2F7DBC00FF4A0D /* ARDSettingsStore.m */; };
+		2C4D7D721F309DA500FF4A0D /* RTCIceCandidate+JSON.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C4D7D6E1F309DA500FF4A0D /* RTCIceCandidate+JSON.m */; };
+		2C4D7D731F309DA500FF4A0D /* RTCSessionDescription+JSON.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C4D7D701F309DA500FF4A0D /* RTCSessionDescription+JSON.m */; };
+		2C4D7D761F30F7B600FF4A0D /* ARDUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C4D7D751F30F7B600FF4A0D /* ARDUtilities.m */; };
+		2C4DE9F221F732B40096940D /* NCAudioController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C4DE9F121F732B40096940D /* NCAudioController.m */; };
+		2C57CD8428C2255000B22E03 /* PollCreationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C57CD8328C2255000B22E03 /* PollCreationViewController.swift */; };
+		2C5BFBEA28772A9A00E75118 /* NCUnifiedSearchController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C5BFBE928772A9A00E75118 /* NCUnifiedSearchController.swift */; };
+		2C5BFBEF288A947900E75118 /* PollVotingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C5BFBEE288A947800E75118 /* PollVotingView.swift */; };
+		2C5BFBF2288A97D800E75118 /* NCPoll.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C5BFBF1288A97D800E75118 /* NCPoll.m */; };
+		2C5BFBF3288AA37F00E75118 /* NCPoll.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C5BFBF1288A97D800E75118 /* NCPoll.m */; };
+		2C5BFBF4288AA37F00E75118 /* NCPoll.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C5BFBF1288A97D800E75118 /* NCPoll.m */; };
+		2C5BFBF628902E0300E75118 /* PollFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C5BFBF528902E0300E75118 /* PollFooterView.swift */; };
+		2C5BFBF828902E3700E75118 /* PollFooterView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2C5BFBF728902E3700E75118 /* PollFooterView.xib */; };
+		2C5BFBFB2891598A00E75118 /* PollResultTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C5BFBF92891598900E75118 /* PollResultTableViewCell.swift */; };
+		2C5BFBFC2891598A00E75118 /* PollResultTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2C5BFBFA2891598900E75118 /* PollResultTableViewCell.xib */; };
+		2C5BFBFE2891C3DF00E75118 /* PollResultsDetailsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C5BFBFD2891C3DF00E75118 /* PollResultsDetailsViewController.swift */; };
+		2C604BD9211988A700D34DCD /* SystemMessageTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C604BD8211988A700D34DCD /* SystemMessageTableViewCell.m */; };
+		2C62AFB624C1A449007E460A /* Share.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 2C62AFB524C1A449007E460A /* Share.storyboard */; };
+		2C62AFB924C1A4E6007E460A /* ShareViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C62AFB824C1A4E6007E460A /* ShareViewController.m */; };
+		2C62AFBB24C1B7B1007E460A /* NCDatabaseManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C40281422832EED0000DDFC /* NCDatabaseManager.m */; };
+		2C62AFFA24C1BDA5007E460A /* NCChatMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CA15540208E350300CE8EF0 /* NCChatMessage.m */; };
+		2C62AFFD24C1BDA5007E460A /* NCMessageParameter.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C43BA7521309A1000B3068A /* NCMessageParameter.m */; };
+		2C62AFFF24C1BDAA007E460A /* NCUser.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CA1CCCC1F181741002FE6A2 /* NCUser.m */; };
+		2C62B00724C1BDBD007E460A /* NCAPIController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CA1CCA91F02D1A4002FE6A2 /* NCAPIController.m */; };
+		2C62B00C24C1BDC1007E460A /* NCNotification.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C9B0B9B217F756B00A4752C /* NCNotification.m */; };
+		2C62B00D24C1BDC1007E460A /* NCPushNotification.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CBF82AD1FC888FC00636459 /* NCPushNotification.m */; };
+		2C62B01024C1BDC5007E460A /* NCRoom.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CA1CCC21F166CC5002FE6A2 /* NCRoom.m */; };
+		2C62B01C24C1BDC9007E460A /* CCCertificate.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CBF82B11FCC7DBA00636459 /* CCCertificate.m */; };
+		2C62B02424C1BDCF007E460A /* NCAppBranding.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C2A788D2359CC8800EEB797 /* NCAppBranding.m */; };
+		2C62B02E24C1BDD7007E460A /* PlaceholderView.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CC7158B20B8394A0045C789 /* PlaceholderView.m */; };
+		2C69323D2923ECAA00017AD2 /* WSMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C69323C2923ECAA00017AD2 /* WSMessage.m */; };
+		2C6955122B0CE1A10070F6E1 /* NCUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FDDB0E42AFD046600FBAFB7 /* NCUtils.swift */; };
+		2C6955132B0CE1A20070F6E1 /* NCUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FDDB0E42AFD046600FBAFB7 /* NCUtils.swift */; };
+		2C6955142B0CE1A20070F6E1 /* NCUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FDDB0E42AFD046600FBAFB7 /* NCUtils.swift */; };
+		2C6955152B0CE1A30070F6E1 /* NCUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FDDB0E42AFD046600FBAFB7 /* NCUtils.swift */; };
+		2C6E74462386D33200AE396C /* ReplyMessageView.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C6E74452386D33200AE396C /* ReplyMessageView.m */; };
+		2C6E7449238C1A0800AE396C /* QuotedMessageView.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C6E7448238C1A0800AE396C /* QuotedMessageView.m */; };
+		2C7381562106136000CDB8DB /* NCChatTitleView.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C7381552106136000CDB8DB /* NCChatTitleView.m */; };
+		2C738158210613A200CDB8DB /* NCChatTitleView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2C738157210613A200CDB8DB /* NCChatTitleView.xib */; };
+		2C78E9E325120DE600E3D4CA /* NCUserStatus.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C78E9E225120DE500E3D4CA /* NCUserStatus.m */; };
+		2C78EF951F7E70EB008AFA74 /* NCPeerConnection.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C78EF941F7E70EB008AFA74 /* NCPeerConnection.m */; };
+		2C78EF991F80F81E008AFA74 /* NCSignalingController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C78EF981F80F81E008AFA74 /* NCSignalingController.m */; };
+		2C78EF9C1F826B22008AFA74 /* NCCallController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C78EF9B1F826B22008AFA74 /* NCCallController.m */; };
+		2C78EFA01F828C41008AFA74 /* CallViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C78EF9E1F828C41008AFA74 /* CallViewController.m */; };
+		2C78EFA11F828C41008AFA74 /* CallViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2C78EF9F1F828C41008AFA74 /* CallViewController.xib */; };
+		2C78EFA51F86FF4A008AFA74 /* CallParticipantViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C78EFA31F86FF4A008AFA74 /* CallParticipantViewCell.m */; };
+		2C78EFA61F86FF4A008AFA74 /* CallParticipantViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2C78EFA41F86FF4A008AFA74 /* CallParticipantViewCell.xib */; };
+		2C7A1237200E0A5700864818 /* UserSettingsTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2C7A1235200E0A5700864818 /* UserSettingsTableViewCell.xib */; };
+		2C7A12422017872600864818 /* AddParticipantsTableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C7A12402017872600864818 /* AddParticipantsTableViewController.m */; };
+		2C7A12432017872600864818 /* AddParticipantsTableViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2C7A12412017872600864818 /* AddParticipantsTableViewController.xib */; };
+		2C7F47AA20289B9600081CC7 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 2C7F47AC20289B9600081CC7 /* Localizable.strings */; };
+		2C84BCCC29EEB9C6001BA6DA /* CallReactionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C84BCCB29EEB9C6001BA6DA /* CallReactionView.swift */; };
+		2C84BCCE29EEDCE8001BA6DA /* CallReactionView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2C84BCCD29EEDCE8001BA6DA /* CallReactionView.xib */; };
+		2C8A2BC9221F094F00DE6D2C /* DirectoryTableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C8A2BC8221F094F00DE6D2C /* DirectoryTableViewController.m */; };
+		2C8A2BCF221FEEFE00DE6D2C /* DirectoryTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2C8A2BCD221FEEFE00DE6D2C /* DirectoryTableViewCell.xib */; };
+		2C8CDD0621C2EDE8004E2997 /* AvatarBackgroundImageView.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C8CDD0521C2EDE8004E2997 /* AvatarBackgroundImageView.m */; };
+		2C8E2A1B232174C20022BFC9 /* MessageSeparatorTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C8E2A1A232174C20022BFC9 /* MessageSeparatorTableViewCell.m */; };
+		2C90E5641EDDE0FB0093D85A /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2C90E5631EDDE0FB0093D85A /* Foundation.framework */; };
+		2C90E5671EDDE1340093D85A /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2C90E5661EDDE1340093D85A /* CoreGraphics.framework */; };
+		2C90E5691EDDE13A0093D85A /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2C90E5681EDDE13A0093D85A /* UIKit.framework */; };
+		2C90E5CF1EDF23A00093D85A /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2C90E5CE1EDF23A00093D85A /* WebKit.framework */; };
+		2C90E5D31EE80C870093D85A /* AuthenticationViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C90E5D11EE80C870093D85A /* AuthenticationViewController.m */; };
+		2C9200C32474262C0050084F /* UIBarButtonItem+Badge.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C9200C22474262C0050084F /* UIBarButtonItem+Badge.m */; };
+		2C98F77921622445001A6A73 /* RoomSearchTableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C98F77821622445001A6A73 /* RoomSearchTableViewController.m */; };
+		2C98F77D216231D3001A6A73 /* RoomTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2C98F77C216231D3001A6A73 /* RoomTableViewCell.xib */; };
+		2C9B0B98217F6DBA00A4752C /* NCNotificationController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C9B0B97217F6DBA00A4752C /* NCNotificationController.m */; };
+		2C9B0B9C217F756B00A4752C /* NCNotification.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C9B0B9B217F756B00A4752C /* NCNotification.m */; };
+		2C9E6CCE1F6F34F000399B7A /* ARDSDPUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C9E6CCD1F6F34F000399B7A /* ARDSDPUtils.m */; };
+		2CA15541208E350300CE8EF0 /* NCChatMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CA15540208E350300CE8EF0 /* NCChatMessage.m */; };
+		2CA1554B208F2E5700CE8EF0 /* NCMessageTextView.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CA1554A208F2E5700CE8EF0 /* NCMessageTextView.m */; };
+		2CA1CC911F014354002FE6A2 /* NCConnectionController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CA1CC901F014354002FE6A2 /* NCConnectionController.m */; };
+		2CA1CC951F014EF9002FE6A2 /* NCSettingsController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CA1CC941F014EF9002FE6A2 /* NCSettingsController.m */; };
+		2CA1CC971F016117002FE6A2 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2CA1CC961F016117002FE6A2 /* Security.framework */; };
+		2CA1CCA41F025F64002FE6A2 /* RoomsTableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CA1CCA31F025F64002FE6A2 /* RoomsTableViewController.m */; };
+		2CA1CCAA1F02D1A4002FE6A2 /* NCAPIController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CA1CCA91F02D1A4002FE6A2 /* NCAPIController.m */; };
+		2CA1CCAC1F067F35002FE6A2 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2CA1CCAB1F067F35002FE6A2 /* Images.xcassets */; };
+		2CA1CCC31F166CC5002FE6A2 /* NCRoom.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CA1CCC21F166CC5002FE6A2 /* NCRoom.m */; };
+		2CA1CCCA1F17C503002FE6A2 /* AudioToolbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2CA1CCC91F17C503002FE6A2 /* AudioToolbox.framework */; };
+		2CA1CCCD1F181741002FE6A2 /* NCUser.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CA1CCCC1F181741002FE6A2 /* NCUser.m */; };
+		2CA1CCD01F1E1779002FE6A2 /* SearchTableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CA1CCCF1F1E1779002FE6A2 /* SearchTableViewController.m */; };
+		2CA1CCD61F1E664C002FE6A2 /* ContactsTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CA1CCD41F1E664C002FE6A2 /* ContactsTableViewCell.m */; };
+		2CA1CCD71F1E664C002FE6A2 /* ContactsTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2CA1CCD51F1E664C002FE6A2 /* ContactsTableViewCell.xib */; };
+		2CA52ACB2670D02800619610 /* VoiceMessageRecordingView.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CA52ACA2670D02800619610 /* VoiceMessageRecordingView.m */; };
+		2CA52ACD2670D07900619610 /* VoiceMessageRecordingView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2CA52ACC2670D07900619610 /* VoiceMessageRecordingView.xib */; };
+		2CB052A12BF2297500191349 /* connecting.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 2CB052A02BF2297500191349 /* connecting.mp3 */; };
+		2CB304192264775E0053078A /* SLKInputAccessoryView.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CB3039D2264775E0053078A /* SLKInputAccessoryView.m */; };
+		2CB3041A2264775E0053078A /* SLKTextInput+Implementation.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CB3039E2264775E0053078A /* SLKTextInput+Implementation.m */; };
+		2CB3041B2264775E0053078A /* SLKTextInputbar.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CB303A12264775E0053078A /* SLKTextInputbar.m */; };
+		2CB3041C2264775E0053078A /* SLKTextView+SLKAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CB303A32264775E0053078A /* SLKTextView+SLKAdditions.m */; };
+		2CB3041D2264775E0053078A /* SLKTextView.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CB303A52264775E0053078A /* SLKTextView.m */; };
+		2CB3041E2264775E0053078A /* SLKTextViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CB303A72264775E0053078A /* SLKTextViewController.m */; };
+		2CB304202264775E0053078A /* UIResponder+SLKAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CB303AD2264775E0053078A /* UIResponder+SLKAdditions.m */; };
+		2CB304212264775E0053078A /* UIScrollView+SLKAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CB303AF2264775E0053078A /* UIScrollView+SLKAdditions.m */; };
+		2CB304222264775E0053078A /* UIView+SLKAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CB303B12264775E0053078A /* UIView+SLKAdditions.m */; };
+		2CB6ACBC26385A3800D3D641 /* ShareLocationViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CB6ACBA26385A3800D3D641 /* ShareLocationViewController.m */; };
+		2CB6ACBF26385A3800D3D641 /* ShareLocationViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2CB6ACBB26385A3800D3D641 /* ShareLocationViewController.xib */; };
+		2CB6ACCA26401D5200D3D641 /* GeoLocationRichObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CB6ACC926401D5100D3D641 /* GeoLocationRichObject.m */; };
+		2CB6ACDA2641483800D3D641 /* NCMessageLocationParameter.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CB6ACD92641483800D3D641 /* NCMessageLocationParameter.m */; };
+		2CB6ACDB2641483800D3D641 /* NCMessageLocationParameter.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CB6ACD92641483800D3D641 /* NCMessageLocationParameter.m */; };
+		2CB6ACDC2641483800D3D641 /* NCMessageLocationParameter.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CB6ACD92641483800D3D641 /* NCMessageLocationParameter.m */; };
+		2CB6ACE92641954700D3D641 /* MapViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CB6ACE72641954700D3D641 /* MapViewController.m */; };
+		2CB6ACEC2641954700D3D641 /* MapViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2CB6ACE82641954700D3D641 /* MapViewController.xib */; };
+		2CB6ACED2641954700D3D641 /* MapViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2CB6ACE82641954700D3D641 /* MapViewController.xib */; };
+		2CB6ACEE2641954700D3D641 /* MapViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2CB6ACE82641954700D3D641 /* MapViewController.xib */; };
+		2CB997C52A052449003C41AC /* EmojiAvatarPickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CB997C32A052449003C41AC /* EmojiAvatarPickerViewController.swift */; };
+		2CB997C62A052449003C41AC /* EmojiAvatarPickerViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2CB997C42A052449003C41AC /* EmojiAvatarPickerViewController.xib */; };
+		2CBD0D5A2C8770A40013C089 /* UIImageExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CBD0D592C8770A40013C089 /* UIImageExtension.swift */; };
+		2CBF82AE1FC888FC00636459 /* NCPushNotification.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CBF82AD1FC888FC00636459 /* NCPushNotification.m */; };
+		2CBF82B21FCC7DBA00636459 /* CCCertificate.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CBF82B11FCC7DBA00636459 /* CCCertificate.m */; };
+		2CC0015324A1F0E900A20167 /* NotificationService.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CC0015224A1F0E900A20167 /* NotificationService.m */; };
+		2CC0016124A25B5500A20167 /* NCAPIController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CA1CCA91F02D1A4002FE6A2 /* NCAPIController.m */; };
+		2CC0016324A25B7400A20167 /* NCDatabaseManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C40281422832EED0000DDFC /* NCDatabaseManager.m */; };
+		2CC0016724A25BE100A20167 /* NCChatMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CA15540208E350300CE8EF0 /* NCChatMessage.m */; };
+		2CC0016924A25C3400A20167 /* NCMessageParameter.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C43BA7521309A1000B3068A /* NCMessageParameter.m */; };
+		2CC001B724A37A9A00A20167 /* NCUser.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CA1CCCC1F181741002FE6A2 /* NCUser.m */; };
+		2CC001C124A37AC500A20167 /* NCNotification.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C9B0B9B217F756B00A4752C /* NCNotification.m */; };
+		2CC001C224A37AC500A20167 /* NCPushNotification.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CBF82AD1FC888FC00636459 /* NCPushNotification.m */; };
+		2CC001CE24A37ACA00A20167 /* NCRoom.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CA1CCC21F166CC5002FE6A2 /* NCRoom.m */; };
+		2CC001DB24A37AD000A20167 /* CCCertificate.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CBF82B11FCC7DBA00636459 /* CCCertificate.m */; };
+		2CC001DC24A37AD400A20167 /* NCAppBranding.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C2A788D2359CC8800EEB797 /* NCAppBranding.m */; };
+		2CC007B420D7AE990096D91F /* ResultMultiSelectionTableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CC007B320D7AE990096D91F /* ResultMultiSelectionTableViewController.m */; };
+		2CC007C620D90AE50096D91F /* RoomNameTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2CC007C420D90AE50096D91F /* RoomNameTableViewCell.xib */; };
+		2CC007CE20E50B0A0096D91F /* MessageBodyTextView.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CC007CD20E50B0A0096D91F /* MessageBodyTextView.m */; };
+		2CC1C38629C0945700C8436B /* DRCellSlideGestureRecognizer.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CC1C38029C0945600C8436B /* DRCellSlideGestureRecognizer.m */; };
+		2CC1C38729C0945700C8436B /* DRCellSlideAction.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CC1C38429C0945700C8436B /* DRCellSlideAction.m */; };
+		2CC1C38829C0945700C8436B /* DRCellSlideActionView.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CC1C38529C0945700C8436B /* DRCellSlideActionView.m */; };
+		2CC1FF4428147F11009F7288 /* RoomSharedItemsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CC1FF4228147F10009F7288 /* RoomSharedItemsTableViewController.swift */; };
+		2CC1FF4528147F11009F7288 /* RoomSharedItemsTableViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2CC1FF4328147F10009F7288 /* RoomSharedItemsTableViewController.xib */; };
+		2CC1FF4828183958009F7288 /* NCDeckCardParameter.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CC1FF4728183958009F7288 /* NCDeckCardParameter.m */; };
+		2CC1FF492818395E009F7288 /* NCDeckCardParameter.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CC1FF4728183958009F7288 /* NCDeckCardParameter.m */; };
+		2CC1FF4A2818395F009F7288 /* NCDeckCardParameter.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CC1FF4728183958009F7288 /* NCDeckCardParameter.m */; };
+		2CC32E8D27F4540E00BB8C39 /* ReactionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CC32E8C27F4540E00BB8C39 /* ReactionsView.swift */; };
+		2CC32E9227F45AE000BB8C39 /* ReactionsViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CC32E9027F45AE000BB8C39 /* ReactionsViewCell.swift */; };
+		2CC32E9327F45AE000BB8C39 /* ReactionsViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2CC32E9127F45AE000BB8C39 /* ReactionsViewCell.xib */; };
+		2CC32E9827F5D9BD00BB8C39 /* NCChatReaction.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CC32E9727F5D9BD00BB8C39 /* NCChatReaction.m */; };
+		2CC32E9927F5DADA00BB8C39 /* NCChatReaction.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CC32E9727F5D9BD00BB8C39 /* NCChatReaction.m */; };
+		2CC32E9A27F5DADB00BB8C39 /* NCChatReaction.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CC32E9727F5D9BD00BB8C39 /* NCChatReaction.m */; };
+		2CC7158920B837140045C789 /* PlaceholderView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2CC7158820B837140045C789 /* PlaceholderView.xib */; };
+		2CC7158C20B8394A0045C789 /* PlaceholderView.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CC7158B20B8394A0045C789 /* PlaceholderView.m */; };
+		2CC7159420C54D080045C789 /* ChatTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CC7159320C54D080045C789 /* ChatTableViewCell.m */; };
+		2CCCD21D2835088F00F076CE /* OpenSSL in Frameworks */ = {isa = PBXBuildFile; productRef = 2CCCD21C2835088F00F076CE /* OpenSSL */; };
+		2CD4F6B72C11C80600ED594F /* ContactsSearchResultTableViewContoller.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD4F6B62C11C80600ED594F /* ContactsSearchResultTableViewContoller.swift */; };
+		2CD5F3242142781A006B71BF /* NCExternalSignalingController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CD5F3232142781A006B71BF /* NCExternalSignalingController.m */; };
+		2CD80F482A4304AD00919057 /* OpenConversationsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD80F472A4304AD00919057 /* OpenConversationsTableViewController.swift */; };
+		2CEDA88C26F492610044552B /* NSMutableAttributedString+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CEDA88B26F492610044552B /* NSMutableAttributedString+Extensions.swift */; };
+		2CF8AD3F2A0010FB00A4D3E6 /* MessageTranslationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CF8AD3D2A0010FB00A4D3E6 /* MessageTranslationViewController.swift */; };
+		2CF8AD402A0010FB00A4D3E6 /* MessageTranslationViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2CF8AD3E2A0010FB00A4D3E6 /* MessageTranslationViewController.xib */; };
+		2CF9CBFF26025F65002246EF /* TextInputTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2CF9CBFB26025F64002246EF /* TextInputTableViewCell.xib */; };
+		3FCA62550CD1442D28E8A7C6 /* libPods-NotificationServiceExtension.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 9B81BB7A4920C391CC2CACFD /* libPods-NotificationServiceExtension.a */; };
+		4890175925A0D7FC2EC76CC0 /* libPods-NextcloudTalkTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7005E22D6C2896927FC3AEEC /* libPods-NextcloudTalkTests.a */; };
+		5EE5ACBB2CF371E7004D7EDB /* AudioToolbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2CA1CCC91F17C503002FE6A2 /* AudioToolbox.framework */; };
+		5EE5ACBE2CF371E9004D7EDB /* IntentsUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1F90EFC225FE489B00F3FA55 /* IntentsUI.framework */; };
+		5EE5ACC02CF372AD004D7EDB /* ReplayKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1FF2FD5C2AB99CCB000C9905 /* ReplayKit.framework */; };
+		5EE5ACC32CF48BCA004D7EDB /* BroadcastUploadExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 1FF2FD5B2AB99CCB000C9905 /* BroadcastUploadExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
+		5EE5ACC72CF48BDA004D7EDB /* NotificationServiceExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 2CC0014F24A1F0E900A20167 /* NotificationServiceExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
+		5EE5ACCA2CF48BDF004D7EDB /* ShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 2C62AFA324C08845007E460A /* ShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
+		807E30762A83A90F00089D28 /* UserStatusOptionsSwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 807E30752A83A90F00089D28 /* UserStatusOptionsSwiftUI.swift */; };
+		80832B762A822E5100195A97 /* UserStatusSwiftUIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80832B752A822E5100195A97 /* UserStatusSwiftUIView.swift */; };
+		80832B782A823D0700195A97 /* UserStatusMessageSwiftUIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80832B772A823D0700195A97 /* UserStatusMessageSwiftUIView.swift */; };
+		80CDF8C42A8E098900CB57AE /* SwiftUIIntrospect in Frameworks */ = {isa = PBXBuildFile; productRef = 80CDF8C32A8E098900CB57AE /* SwiftUIIntrospect */; };
+		847EFC7236336B67A1A89358 /* libPods-BroadcastUploadExtension.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 9A3D305FCD7BF7E727A62F35 /* libPods-BroadcastUploadExtension.a */; };
+		8789AE73BFCAA413B43319C0 /* libPods-ShareExtension.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 684807120F4439797973DF73 /* libPods-ShareExtension.a */; };
+		9993261EDAC77481FF4EF58A /* libPods-NextcloudTalk.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F7C31E9D74F550EAF89931B /* libPods-NextcloudTalk.a */; };
+		DA1AEFC3270F1FA90088E519 /* DateLabelCustom.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA1AEFC2270F1FA90088E519 /* DateLabelCustom.swift */; };
+		DA66582B27B6992F00B46B11 /* UserProfileTableViewController+AvatarSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA66582A27B6992F00B46B11 /* UserProfileTableViewController+AvatarSetup.swift */; };
+		DA66582D27B6A73800B46B11 /* UserProfileTableViewController+DelegateMethods.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA66582C27B6A73800B46B11 /* UserProfileTableViewController+DelegateMethods.swift */; };
+		DA66582F27B6B19C00B46B11 /* UserProfileTableViewController+Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA66582E27B6B19C00B46B11 /* UserProfileTableViewController+Actions.swift */; };
+		DA66583127B6B24E00B46B11 /* UserProfileTableViewController+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA66583027B6B24E00B46B11 /* UserProfileTableViewController+Utils.swift */; };
+		DA75580F278EEA1000A48A1B /* SettingsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA75580E278EEA1000A48A1B /* SettingsTableViewController.swift */; };
+		DA755811278EF3EF00A48A1B /* UserSettingsTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA755810278EF3EF00A48A1B /* UserSettingsTableViewCell.swift */; };
+		DA8801A227A2DA00009EF248 /* UserProfileTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA8801A127A2DA00009EF248 /* UserProfileTableViewController.swift */; };
+		DA8801A427AC52AC009EF248 /* TextInputTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA8801A327AC52AC009EF248 /* TextInputTableViewCell.swift */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXContainerItemProxy section */
+		1F6D8C342B2E3756004376B8 /* PBXContainerItemProxy */ = {
+			isa = PBXContainerItemProxy;
+			containerPortal = 2C0574751EDD9E8E00D9E7F2 /* Project object */;
+			proxyType = 1;
+			remoteGlobalIDString = 2C05747C1EDD9E8E00D9E7F2;
+			remoteInfo = NextcloudTalk;
+		};
+		1FD8AD902A3A162100787C16 /* PBXContainerItemProxy */ = {
+			isa = PBXContainerItemProxy;
+			containerPortal = 2C0574751EDD9E8E00D9E7F2 /* Project object */;
+			proxyType = 1;
+			remoteGlobalIDString = 2C05747C1EDD9E8E00D9E7F2;
+			remoteInfo = NextcloudTalk;
+		};
+		5EE5ACC42CF48BCA004D7EDB /* PBXContainerItemProxy */ = {
+			isa = PBXContainerItemProxy;
+			containerPortal = 2C0574751EDD9E8E00D9E7F2 /* Project object */;
+			proxyType = 1;
+			remoteGlobalIDString = 1FF2FD5A2AB99CCB000C9905;
+			remoteInfo = BroadcastUploadExtension;
+		};
+		5EE5ACC82CF48BDA004D7EDB /* PBXContainerItemProxy */ = {
+			isa = PBXContainerItemProxy;
+			containerPortal = 2C0574751EDD9E8E00D9E7F2 /* Project object */;
+			proxyType = 1;
+			remoteGlobalIDString = 2CC0014E24A1F0E900A20167;
+			remoteInfo = NotificationServiceExtension;
+		};
+		5EE5ACCB2CF48BDF004D7EDB /* PBXContainerItemProxy */ = {
+			isa = PBXContainerItemProxy;
+			containerPortal = 2C0574751EDD9E8E00D9E7F2 /* Project object */;
+			proxyType = 1;
+			remoteGlobalIDString = 2C62AFA224C08845007E460A;
+			remoteInfo = ShareExtension;
+		};
+/* End PBXContainerItemProxy section */
+
+/* Begin PBXCopyFilesBuildPhase section */
+		5EE5ACC62CF48BCA004D7EDB /* Embed Foundation Extensions */ = {
+			isa = PBXCopyFilesBuildPhase;
+			buildActionMask = 2147483647;
+			dstPath = "";
+			dstSubfolderSpec = 13;
+			files = (
+				5EE5ACCA2CF48BDF004D7EDB /* ShareExtension.appex in Embed Foundation Extensions */,
+				5EE5ACC32CF48BCA004D7EDB /* BroadcastUploadExtension.appex in Embed Foundation Extensions */,
+				5EE5ACC72CF48BDA004D7EDB /* NotificationServiceExtension.appex in Embed Foundation Extensions */,
+			);
+			name = "Embed Foundation Extensions";
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXCopyFilesBuildPhase section */
+
+/* Begin PBXFileReference section */
+		1F0A1D432A5F1FA800A25433 /* SwiftMarkdownObjCBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftMarkdownObjCBridge.swift; sourceTree = "<group>"; };
+		1F0B0A712BA264540073FF8D /* MentionSuggestion.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MentionSuggestion.swift; sourceTree = "<group>"; };
+		1F0B0A762BA26BE10073FF8D /* UnitMentionSuggestionTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnitMentionSuggestionTest.swift; sourceTree = "<group>"; };
+		1F11FB7129C07B04001E21E7 /* NCZoomableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCZoomableView.swift; sourceTree = "<group>"; };
+		1F1B0F242BD94A0D003FD766 /* UnitDarwinCenterTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnitDarwinCenterTest.swift; sourceTree = "<group>"; };
+		1F1B0F262BDA61C5003FD766 /* AllocationTracker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AllocationTracker.swift; sourceTree = "<group>"; };
+		1F1B0F2B2BDBB3AC003FD766 /* NCMediaViewerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCMediaViewerViewController.swift; sourceTree = "<group>"; };
+		1F1B0F2F2BDBC9D6003FD766 /* NCMediaViewerPageViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCMediaViewerPageViewController.swift; sourceTree = "<group>"; };
+		1F1B0F312BDC57E3003FD766 /* UIPageViewControllerExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIPageViewControllerExtension.swift; sourceTree = "<group>"; };
+		1F1B0F352BDD8B9C003FD766 /* NCActivityIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCActivityIndicator.swift; sourceTree = "<group>"; };
+		1F1B0F3B2BE047CD003FD766 /* UIViewController+Transitions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIViewController+Transitions.swift"; sourceTree = "<group>"; };
+		1F1B0F3C2BE047CD003FD766 /* ModalTransitionAnimator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ModalTransitionAnimator.swift; sourceTree = "<group>"; };
+		1F1B0F3D2BE047CD003FD766 /* ModalTransitionManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ModalTransitionManager.swift; sourceTree = "<group>"; };
+		1F1B0F3E2BE047CD003FD766 /* ModalPresentationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ModalPresentationController.swift; sourceTree = "<group>"; };
+		1F1B0F3F2BE047CD003FD766 /* InteractionControlling.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InteractionControlling.swift; sourceTree = "<group>"; };
+		1F1B0F402BE047CE003FD766 /* StandardInteractionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StandardInteractionController.swift; sourceTree = "<group>"; };
+		1F1B0F412BE047CE003FD766 /* CustomPresentable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomPresentable.swift; sourceTree = "<group>"; };
+		1F1B0F492BE047D5003FD766 /* OneWayPanGestureRecognizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OneWayPanGestureRecognizer.swift; sourceTree = "<group>"; };
+		1F1B0F4B2BE18FF3003FD766 /* CustomPresentableNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomPresentableNavigationController.swift; sourceTree = "<group>"; };
+		1F1B50332B8E069800B0F2F4 /* BaseChatTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseChatTableViewCell.swift; sourceTree = "<group>"; };
+		1F1B50372B8E070100B0F2F4 /* BaseChatTableViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = BaseChatTableViewCell.xib; sourceTree = "<group>"; };
+		1F1B50392B8F9E1300B0F2F4 /* BaseChatTableViewCell+File.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "BaseChatTableViewCell+File.swift"; sourceTree = "<group>"; };
+		1F1B503D2B8FB12100B0F2F4 /* BaseChatTableViewCell+Message.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "BaseChatTableViewCell+Message.swift"; sourceTree = "<group>"; };
+		1F1B50422B9095C900B0F2F4 /* FederatedCapabilities.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FederatedCapabilities.h; sourceTree = "<group>"; };
+		1F1B50432B9095D100B0F2F4 /* FederatedCapabilities.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FederatedCapabilities.m; sourceTree = "<group>"; };
+		1F1B50452B90CDE600B0F2F4 /* TalkCapabilities.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TalkCapabilities.h; sourceTree = "<group>"; };
+		1F1B50462B90CDF800B0F2F4 /* TalkCapabilities.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TalkCapabilities.m; sourceTree = "<group>"; };
+		1F1C0D8629AFB88800D17C6D /* VLCKitVideoViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = VLCKitVideoViewController.xib; sourceTree = "<group>"; };
+		1F1C0D8829AFB89900D17C6D /* VLCKitVideoViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VLCKitVideoViewController.swift; sourceTree = "<group>"; };
+		1F1DF83B2C5C17AF00E5EA86 /* TalkActor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TalkActor.swift; sourceTree = "<group>"; };
+		1F1DF8402C63C25900E5EA86 /* UnitNCDatabaseManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnitNCDatabaseManager.swift; sourceTree = "<group>"; };
+		1F1DF8422C64006E00E5EA86 /* SignalingParticipant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SignalingParticipant.swift; sourceTree = "<group>"; };
+		1F21A0622C77863500ED8C0C /* nb-NO */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "nb-NO"; path = "nb-NO.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
+		1F21A0632C77863500ED8C0C /* nb-NO */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "nb-NO"; path = "nb-NO.lproj/Localizable.strings"; sourceTree = "<group>"; };
+		1F21A0642C77863500ED8C0C /* nb-NO */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "nb-NO"; path = "nb-NO.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
+		1F21A0652C77865D00ED8C0C /* ga */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ga; path = ga.lproj/InfoPlist.strings; sourceTree = "<group>"; };
+		1F21A0662C77865D00ED8C0C /* ga */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ga; path = ga.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
+		1F21A0672C77865D00ED8C0C /* ga */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ga; path = ga.lproj/Localizable.strings; sourceTree = "<group>"; };
+		1F21A0682C77868000ED8C0C /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = sv; path = sv.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
+		1F21A0692C77868000ED8C0C /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/Localizable.strings; sourceTree = "<group>"; };
+		1F21A06A2C77868000ED8C0C /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/InfoPlist.strings; sourceTree = "<group>"; };
+		1F21A06B2C77869600ED8C0C /* sr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sr; path = sr.lproj/InfoPlist.strings; sourceTree = "<group>"; };
+		1F21A06C2C77869600ED8C0C /* sr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sr; path = sr.lproj/Localizable.strings; sourceTree = "<group>"; };
+		1F21A06D2C77869600ED8C0C /* sr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = sr; path = sr.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
+		1F24B5A128E0648600654457 /* ReferenceGithubView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReferenceGithubView.swift; sourceTree = "<group>"; };
+		1F24B5A328E0649200654457 /* ReferenceGithubView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ReferenceGithubView.xib; sourceTree = "<group>"; };
+		1F35F8DF2AEEB9DE00044BDA /* ShareConfirmationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareConfirmationViewController.swift; sourceTree = "<group>"; };
+		1F35F8FA2AEEDBC600044BDA /* ChatViewControllerExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatViewControllerExtension.swift; sourceTree = "<group>"; };
+		1F35F9022AEEDEE800044BDA /* AutoCompletionTableViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AutoCompletionTableViewCell.h; sourceTree = "<group>"; };
+		1F35F9032AEEDF0E00044BDA /* AutoCompletionTableViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AutoCompletionTableViewCell.m; sourceTree = "<group>"; };
+		1F371A362A7B921A006CBFB3 /* DatePickerTextField.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatePickerTextField.swift; sourceTree = "<group>"; };
+		1F3C419E29EDAC7D00F58435 /* RoomAvatarInfoTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RoomAvatarInfoTableViewController.swift; sourceTree = "<group>"; };
+		1F3C41A029EDAC8800F58435 /* RoomAvatarInfoTableViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = RoomAvatarInfoTableViewController.xib; sourceTree = "<group>"; };
+		1F3C41A229EDF05700F58435 /* AvatarEditView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarEditView.swift; sourceTree = "<group>"; };
+		1F3C41A429EDF0B800F58435 /* AvatarEditView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = AvatarEditView.xib; sourceTree = "<group>"; };
+		1F3D3B20255F109E00230DAE /* BarButtonItemWithActivity.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BarButtonItemWithActivity.m; sourceTree = "<group>"; };
+		1F3D3B21255F109E00230DAE /* BarButtonItemWithActivity.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BarButtonItemWithActivity.h; sourceTree = "<group>"; };
+		1F468E7728DCC7310099597B /* EmojiTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiTextField.swift; sourceTree = "<group>"; };
+		1F46CE2828E05B3200E7D88E /* ReferenceDefaultView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReferenceDefaultView.swift; sourceTree = "<group>"; };
+		1F46CE2A28E05B3C00E7D88E /* ReferenceDefaultView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ReferenceDefaultView.xib; sourceTree = "<group>"; };
+		1F4DD3EA2571C688007DC98E /* EmojiUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiUtils.swift; sourceTree = "<group>"; };
+		1F5683CE2BA7980C0023E151 /* FilePreviewImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilePreviewImageView.swift; sourceTree = "<group>"; };
+		1F5813F628EB23EF00318FC3 /* NCSplitViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCSplitViewController.swift; sourceTree = "<group>"; };
+		1F5813F728EB23EF00318FC3 /* NCSplitViewPlaceholderViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCSplitViewPlaceholderViewController.swift; sourceTree = "<group>"; };
+		1F5A24322ADA77DA009939FE /* InputbarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputbarViewController.swift; sourceTree = "<group>"; };
+		1F5CDAE62B3B05110040ECC0 /* UnitColorGeneratorTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = UnitColorGeneratorTest.swift; path = NextcloudTalkTests/Unit/UnitColorGeneratorTest.swift; sourceTree = SOURCE_ROOT; };
+		1F61C766285E35A6004D74D8 /* DiagnosticsTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiagnosticsTableViewController.swift; sourceTree = "<group>"; };
+		1F61C76A285F65E1004D74D8 /* SimpleTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleTableViewController.swift; sourceTree = "<group>"; };
+		1F6629F92C17700E001C6C0E /* IntegrationRoomsManagerTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IntegrationRoomsManagerTest.swift; sourceTree = "<group>"; };
+		1F66B71E29FA703B003FB168 /* TypingIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypingIndicatorView.swift; sourceTree = "<group>"; };
+		1F66B72029FA7089003FB168 /* TypingIndicatorView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TypingIndicatorView.xib; sourceTree = "<group>"; };
+		1F66B72729FA936E003FB168 /* SLKDefaultReplyView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SLKDefaultReplyView.h; sourceTree = "<group>"; };
+		1F66B72829FA936E003FB168 /* SLKDefaultReplyView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SLKDefaultReplyView.m; sourceTree = "<group>"; };
+		1F66B72A29FA9414003FB168 /* SLKDefaultTypingIndicatorView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SLKDefaultTypingIndicatorView.h; sourceTree = "<group>"; };
+		1F66B72B29FA9414003FB168 /* SLKDefaultTypingIndicatorView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SLKDefaultTypingIndicatorView.m; sourceTree = "<group>"; };
+		1F6D8C302B2E3756004376B8 /* NextcloudTalkTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = NextcloudTalkTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+		1F6D8C322B2E3756004376B8 /* IntegrationRoomTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntegrationRoomTest.swift; sourceTree = "<group>"; };
+		1F6D8C3C2B2F23C4004376B8 /* Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Helpers.swift; sourceTree = "<group>"; };
+		1F6D8C402B2F26D5004376B8 /* TestConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestConstants.swift; sourceTree = "<group>"; };
+		1F6D8C422B2F26EE004376B8 /* Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Helpers.swift; sourceTree = "<group>"; };
+		1F6D8C472B2F2F69004376B8 /* AAAALoginTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AAAALoginTest.swift; sourceTree = "<group>"; };
+		1F6D8C4A2B2F5B61004376B8 /* TestBase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestBase.swift; sourceTree = "<group>"; };
+		1F6D8C4C2B2F8FE5004376B8 /* IntegrationChatTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IntegrationChatTest.swift; sourceTree = "<group>"; };
+		1F7625E42901B0DB00834869 /* CallsFromOldAccountViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallsFromOldAccountViewController.swift; sourceTree = "<group>"; };
+		1F7625E62901B0E800834869 /* CallsFromOldAccountViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = CallsFromOldAccountViewController.xib; sourceTree = "<group>"; };
+		1F77A6112AB9B161007B6037 /* ScreenCaptureController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ScreenCaptureController.m; sourceTree = "<group>"; };
+		1F77A6122AB9B161007B6037 /* ScreenCaptureController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ScreenCaptureController.h; sourceTree = "<group>"; };
+		1F77A6132AB9B161007B6037 /* ScreenCapturer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ScreenCapturer.m; sourceTree = "<group>"; };
+		1F77A6142AB9B161007B6037 /* ScreenCapturer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ScreenCapturer.h; sourceTree = "<group>"; };
+		1F77A61C2AB9B301007B6037 /* CapturerEventsDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CapturerEventsDelegate.h; sourceTree = "<group>"; };
+		1F77A61F2AB9D82B007B6037 /* BroadcastUploadExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = BroadcastUploadExtension.entitlements; sourceTree = "<group>"; };
+		1F77A6202AB9EB06007B6037 /* SocketConnection.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SocketConnection.h; sourceTree = "<group>"; };
+		1F77A6212AB9EB06007B6037 /* SocketConnection.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SocketConnection.m; sourceTree = "<group>"; };
+		1F77A6232ABA0003007B6037 /* SampleHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SampleHandler.swift; sourceTree = "<group>"; };
+		1F77A6252ABA0CD9007B6037 /* NCScreensharingController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NCScreensharingController.h; sourceTree = "<group>"; };
+		1F77A6262ABA0CD9007B6037 /* NCScreensharingController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NCScreensharingController.m; sourceTree = "<group>"; };
+		1F785DDA2707865F00AC4B40 /* VoiceMessageTranscribeViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = VoiceMessageTranscribeViewController.m; sourceTree = "<group>"; };
+		1F785DDB2707865F00AC4B40 /* VoiceMessageTranscribeViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = VoiceMessageTranscribeViewController.xib; sourceTree = "<group>"; };
+		1F785DDC2707865F00AC4B40 /* VoiceMessageTranscribeViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = VoiceMessageTranscribeViewController.h; sourceTree = "<group>"; };
+		1F8995B22970644C00CABA33 /* ColorGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorGenerator.swift; sourceTree = "<group>"; };
+		1F8995B42973547700CABA33 /* WebRTCCommon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebRTCCommon.swift; sourceTree = "<group>"; };
+		1F8AAC312C518759004DA20A /* SignalingSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignalingSettings.swift; sourceTree = "<group>"; };
+		1F8AAC362C519577004DA20A /* TurnServer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TurnServer.swift; sourceTree = "<group>"; };
+		1F8AAC3B2C519689004DA20A /* StunServer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StunServer.swift; sourceTree = "<group>"; };
+		1F8AAC612C596308004DA20A /* UnitSignalingSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnitSignalingSettings.swift; sourceTree = "<group>"; };
+		1F90DA0329E9A28E00E81E3D /* AvatarManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarManager.swift; sourceTree = "<group>"; };
+		1F90EFBA25FE39F800F3FA55 /* NCIntentController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NCIntentController.h; sourceTree = "<group>"; };
+		1F90EFBB25FE39F800F3FA55 /* NCIntentController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NCIntentController.m; sourceTree = "<group>"; };
+		1F90EFC225FE489B00F3FA55 /* IntentsUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = IntentsUI.framework; path = System/Library/Frameworks/IntentsUI.framework; sourceTree = SDKROOT; };
+		1F98DF9B28E7484700E05174 /* ReferenceDeckView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReferenceDeckView.swift; sourceTree = "<group>"; };
+		1F98DF9D28E7485000E05174 /* ReferenceDeckView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ReferenceDeckView.xib; sourceTree = "<group>"; };
+		1FA20C89284001D80062B4F3 /* DebounceWebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebounceWebView.swift; sourceTree = "<group>"; };
+		1FA38C8F29A4B3C6008871B8 /* NCNotificationAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCNotificationAction.swift; sourceTree = "<group>"; };
+		1FA732FB2966CBB7003D2103 /* CallFlowLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallFlowLayout.swift; sourceTree = "<group>"; };
+		1FAB2E822AC9EC3F001214EB /* BaseChatViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseChatViewController.swift; sourceTree = "<group>"; };
+		1FAB2E842ACB482B001214EB /* ChatViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatViewController.swift; sourceTree = "<group>"; };
+		1FAB2EED2AD1BC1B001214EB /* UIControlExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIControlExtensions.swift; sourceTree = "<group>"; };
+		1FAB2EEF2AD1EAA3001214EB /* RLMSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RLMSupport.swift; sourceTree = "<group>"; };
+		1FADECD42B7EACCB007AD94B /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
+		1FADECD52B821E24007AD94B /* FederationInvitationTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FederationInvitationTableViewController.swift; sourceTree = "<group>"; };
+		1FADECD72B82269E007AD94B /* FederationInvitationCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FederationInvitationCell.swift; sourceTree = "<group>"; };
+		1FADECD92B8227B1007AD94B /* FederationInvitationCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = FederationInvitationCell.xib; sourceTree = "<group>"; };
+		1FB52E752842C75E00AC741B /* QRCodeLoginController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeLoginController.swift; sourceTree = "<group>"; };
+		1FB6678E28CE381300D29F8D /* SubtitleTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubtitleTableViewCell.swift; sourceTree = "<group>"; };
+		1FB78E1E2B6ADBAA00B0D69D /* NCAPIControllerExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCAPIControllerExtensions.swift; sourceTree = "<group>"; };
+		1FB78E252B6AE5A600B0D69D /* FederationInvitation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FederationInvitation.swift; sourceTree = "<group>"; };
+		1FB7B9842BE2EE020093CE98 /* UnitChatViewControllerTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnitChatViewControllerTest.swift; sourceTree = "<group>"; };
+		1FB7B9862BE441450093CE98 /* UIViewExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIViewExtensions.swift; sourceTree = "<group>"; };
+		1FB7B9882BE442400093CE98 /* UnitBaseChatTableViewCellTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnitBaseChatTableViewCellTest.swift; sourceTree = "<group>"; };
+		1FB7B98D2BF0CBA60093CE98 /* BannedActor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BannedActor.swift; sourceTree = "<group>"; };
+		1FB7B9942BF0DF1C0093CE98 /* BannedActorTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BannedActorTableViewController.swift; sourceTree = "<group>"; };
+		1FB7B9992BF0DF290093CE98 /* BannedActorCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BannedActorCell.swift; sourceTree = "<group>"; };
+		1FB7B99B2BF0DF360093CE98 /* BannedActorCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = BannedActorCell.xib; sourceTree = "<group>"; };
+		1FBC3BE42B61ACD5003909E0 /* UnitBaseChatViewControllerTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnitBaseChatViewControllerTest.swift; sourceTree = "<group>"; };
+		1FBC3BE82B61BD09003909E0 /* TestBaseRealm.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestBaseRealm.swift; sourceTree = "<group>"; };
+		1FCE3D542C9C189D009C68A9 /* NCChatFileControllerWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCChatFileControllerWrapper.swift; sourceTree = "<group>"; };
+		1FCE3D562C9C4D18009C68A9 /* ReferenceGiphyView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReferenceGiphyView.swift; sourceTree = "<group>"; };
+		1FCE3D582C9C4D21009C68A9 /* ReferenceGiphyView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ReferenceGiphyView.xib; sourceTree = "<group>"; };
+		1FD6F83B2B825069004048AB /* NCRoomsManagerExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCRoomsManagerExtensions.swift; sourceTree = "<group>"; };
+		1FD6F83D2B87B712004048AB /* NCUserStatusExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCUserStatusExtensions.swift; sourceTree = "<group>"; };
+		1FD8AD8A2A3A162100787C16 /* NextcloudTalkUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = NextcloudTalkUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+		1FD8AD8C2A3A162100787C16 /* UIRoomTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIRoomTest.swift; sourceTree = "<group>"; };
+		1FD9182828C55A73009092AB /* BGTaskHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGTaskHelper.swift; sourceTree = "<group>"; };
+		1FDB47F52C9C71CE00D6F423 /* TalkAccount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TalkAccount.swift; sourceTree = "<group>"; };
+		1FDB47F72C9C7E3F00D6F423 /* NCDatabaseManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCDatabaseManager.swift; sourceTree = "<group>"; };
+		1FDCC3D329EBF6E700DEB39B /* AvatarImageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvatarImageView.swift; sourceTree = "<group>"; };
+		1FDCC3EC29EC7DD400DEB39B /* NextcloudTalk-Bridging-Header-Extensions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NextcloudTalk-Bridging-Header-Extensions.h"; sourceTree = "<group>"; };
+		1FDCC3EF29ECB4CE00DEB39B /* AvatarButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvatarButton.swift; sourceTree = "<group>"; };
+		1FDDB0D82AF440DD00FBAFB7 /* BoundsChangedFlowLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoundsChangedFlowLayout.swift; sourceTree = "<group>"; };
+		1FDDB0E42AFD046600FBAFB7 /* NCUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCUtils.swift; sourceTree = "<group>"; };
+		1FDDB0E82AFE8F5C00FBAFB7 /* MobileCoreServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MobileCoreServices.framework; path = System/Library/Frameworks/MobileCoreServices.framework; sourceTree = SDKROOT; };
+		1FDE7C9928DE14A200CB718E /* ReferenceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReferenceView.swift; sourceTree = "<group>"; };
+		1FDE7C9B28DE14B000CB718E /* ReferenceView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ReferenceView.xib; sourceTree = "<group>"; };
+		1FDFC94C2BA50B9100670DF4 /* UIFontExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIFontExtension.swift; sourceTree = "<group>"; };
+		1FE0C56B2A0531200083576A /* ReferenceTalkView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ReferenceTalkView.xib; sourceTree = "<group>"; };
+		1FE0C56D2A0531270083576A /* ReferenceTalkView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReferenceTalkView.swift; sourceTree = "<group>"; };
+		1FE7DE2F2BB4598F0040EE12 /* RoomInvitationViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RoomInvitationViewCell.swift; sourceTree = "<group>"; };
+		1FE7DE312BB459B10040EE12 /* RoomInvitationViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = RoomInvitationViewCell.xib; sourceTree = "<group>"; };
+		1FE94733293CE55600D6584C /* NCCameraController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCCameraController.swift; sourceTree = "<group>"; };
+		1FEC459B2A02BCAE00A636AA /* ReferenceGithubPermalinkView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ReferenceGithubPermalinkView.xib; sourceTree = "<group>"; };
+		1FEC459D2A02BCB900A636AA /* ReferenceGithubPermalinkView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReferenceGithubPermalinkView.swift; sourceTree = "<group>"; };
+		1FEC45A22A02F92700A636AA /* GithubPermalinkViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GithubPermalinkViewController.swift; sourceTree = "<group>"; };
+		1FEC45A42A02F92B00A636AA /* GithubPermalinkViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = GithubPermalinkViewController.xib; sourceTree = "<group>"; };
+		1FEDE3C4257D439500853F79 /* NCChatFileController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NCChatFileController.m; sourceTree = "<group>"; };
+		1FEDE3C5257D439500853F79 /* NCChatFileController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NCChatFileController.h; sourceTree = "<group>"; };
+		1FEDE3CC257D43AB00853F79 /* NCMessageFileParameter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NCMessageFileParameter.m; sourceTree = "<group>"; };
+		1FEDE3CD257D43AB00853F79 /* NCMessageFileParameter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NCMessageFileParameter.h; sourceTree = "<group>"; };
+		1FF1360E2BFB4F8C006A6101 /* NCRoom.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCRoom.swift; sourceTree = "<group>"; };
+		1FF136142BFB74C3006A6101 /* NCChatMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCChatMessage.swift; sourceTree = "<group>"; };
+		1FF2FD5B2AB99CCB000C9905 /* BroadcastUploadExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = BroadcastUploadExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
+		1FF2FD5C2AB99CCB000C9905 /* ReplayKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ReplayKit.framework; path = System/Library/Frameworks/ReplayKit.framework; sourceTree = SDKROOT; };
+		1FF2FD612AB99CCB000C9905 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
+		1FF2FD792AB99E4D000C9905 /* DarwinNotificationCenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DarwinNotificationCenter.swift; sourceTree = "<group>"; };
+		1FF2FD7A2AB99E4D000C9905 /* Atomic.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Atomic.swift; sourceTree = "<group>"; };
+		1FF2FD7B2AB99E4D000C9905 /* SampleUploader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SampleUploader.swift; sourceTree = "<group>"; };
+		1FF2FD7D2AB99E4D000C9905 /* SocketConnection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketConnection.swift; sourceTree = "<group>"; };
+		1FF4DA7D2C0237D000C1B952 /* DirectoryTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectoryTableViewCell.swift; sourceTree = "<group>"; };
+		1FF4DA7F2C023FF300C1B952 /* NCChatFileStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCChatFileStatus.swift; sourceTree = "<group>"; };
+		1FF4DA812C025DB900C1B952 /* NCAPISessionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCAPISessionManager.swift; sourceTree = "<group>"; };
+		1FF4DA862C02626D00C1B952 /* NCBaseSessionManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCBaseSessionManager.swift; sourceTree = "<group>"; };
+		1FF4DA8B2C0263A200C1B952 /* NCPushProxySessionManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCPushProxySessionManager.swift; sourceTree = "<group>"; };
+		1FF4DA902C02677C00C1B952 /* NCImageSessionManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCImageSessionManager.swift; sourceTree = "<group>"; };
+		1FF4DA952C0327FF00C1B952 /* NCWebImageDownloaderOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCWebImageDownloaderOperation.swift; sourceTree = "<group>"; };
+		1FF4DA9A2C032AAC00C1B952 /* RoomTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RoomTableViewCell.swift; sourceTree = "<group>"; };
+		1FF4DA9F2C03351E00C1B952 /* RoomNameTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RoomNameTableViewCell.swift; sourceTree = "<group>"; };
+		1FF4DAA12C0338D000C1B952 /* RoomDescriptionTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RoomDescriptionTableViewCell.swift; sourceTree = "<group>"; };
+		1FF4DAA52C08D81D00C1B952 /* UnitNCChatMessageTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnitNCChatMessageTest.swift; sourceTree = "<group>"; };
+		1FF4DAA72C08DE3A00C1B952 /* UnitNCRoomsManagerTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnitNCRoomsManagerTest.swift; sourceTree = "<group>"; };
+		1FF4DAA92C0A114900C1B952 /* OcsResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OcsResponse.swift; sourceTree = "<group>"; };
+		1FFF41612C70937B00162F4D /* ReferenceZammadView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReferenceZammadView.swift; sourceTree = "<group>"; };
+		1FFF41632C70938700162F4D /* ReferenceZammadView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ReferenceZammadView.xib; sourceTree = "<group>"; };
+		2C04248F2CA32D45004772F6 /* BaseChatTableViewCell+Audio.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BaseChatTableViewCell+Audio.swift"; sourceTree = "<group>"; };
+		2C0424962CA335C4004772F6 /* AudioPlayerView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AudioPlayerView.xib; sourceTree = "<group>"; };
+		2C0424992CA33681004772F6 /* AudioPlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerView.swift; sourceTree = "<group>"; };
+		2C05747D1EDD9E8E00D9E7F2 /* NextcloudTalk.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = NextcloudTalk.app; sourceTree = BUILT_PRODUCTS_DIR; };
+		2C0574811EDD9E8E00D9E7F2 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; };
+		2C0574831EDD9E8E00D9E7F2 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = "<group>"; };
+		2C0574841EDD9E8E00D9E7F2 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = "<group>"; };
+		2C05748D1EDD9E8E00D9E7F2 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
+		2C0574941EDD9E8E00D9E7F2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
+		2C0574A11EDDA2E300D9E7F2 /* LoginViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LoginViewController.h; sourceTree = "<group>"; };
+		2C0574A21EDDA2E300D9E7F2 /* LoginViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LoginViewController.m; sourceTree = "<group>"; };
+		2C0574A31EDDA2E300D9E7F2 /* LoginViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = LoginViewController.xib; sourceTree = "<group>"; };
+		2C06330D2046CC8B0043481A /* NCUserInterfaceController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NCUserInterfaceController.h; sourceTree = "<group>"; };
+		2C06330E2046CC8B0043481A /* NCUserInterfaceController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NCUserInterfaceController.m; sourceTree = "<group>"; };
+		2C06BF5B20A89F510031EB46 /* NCRoomsManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NCRoomsManager.h; sourceTree = "<group>"; };
+		2C06BF5C20A89F510031EB46 /* NCRoomsManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NCRoomsManager.m; sourceTree = "<group>"; };
+		2C06BF6320AC64370031EB46 /* DateHeaderView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = DateHeaderView.xib; sourceTree = "<group>"; };
+		2C06BF6520AC647A0031EB46 /* DateHeaderView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DateHeaderView.h; sourceTree = "<group>"; };
+		2C06BF6620AC647A0031EB46 /* DateHeaderView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DateHeaderView.m; sourceTree = "<group>"; };
+		2C06BF6A20AEB0030031EB46 /* RoundedNumberView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RoundedNumberView.m; sourceTree = "<group>"; };
+		2C06BF6B20AEB0030031EB46 /* RoundedNumberView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RoundedNumberView.h; sourceTree = "<group>"; };
+		2C16A82B28E7284D00EDE523 /* NCButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCButton.swift; sourceTree = "<group>"; };
+		2C1ABD7F25769E7C00AEDFB6 /* ShareItemController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ShareItemController.h; sourceTree = "<group>"; };
+		2C1ABD8025769E7D00AEDFB6 /* ShareConfirmationCollectionViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ShareConfirmationCollectionViewCell.m; sourceTree = "<group>"; };
+		2C1ABD8125769E7D00AEDFB6 /* ShareItem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ShareItem.h; sourceTree = "<group>"; };
+		2C1ABD8225769E7D00AEDFB6 /* ShareItemController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ShareItemController.m; sourceTree = "<group>"; };
+		2C1ABD8325769E7D00AEDFB6 /* ShareConfirmationCollectionViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ShareConfirmationCollectionViewCell.h; sourceTree = "<group>"; };
+		2C1ABD8425769E7D00AEDFB6 /* ShareConfirmationCollectionViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ShareConfirmationCollectionViewCell.xib; sourceTree = "<group>"; };
+		2C1ABD8525769E7D00AEDFB6 /* ShareItem.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ShareItem.m; sourceTree = "<group>"; };
+		2C1ABDC4257A7CF000AEDFB6 /* NCContactsManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NCContactsManager.h; sourceTree = "<group>"; };
+		2C1ABDC5257A7CF000AEDFB6 /* NCContactsManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NCContactsManager.m; sourceTree = "<group>"; };
+		2C1ABDCC257E939600AEDFB6 /* NCContact.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NCContact.h; sourceTree = "<group>"; };
+		2C1ABDCD257E939600AEDFB6 /* NCContact.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NCContact.m; sourceTree = "<group>"; };
+		2C1ABDE3257F883400AEDFB6 /* ABContact.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ABContact.h; sourceTree = "<group>"; };
+		2C1ABDE4257F883400AEDFB6 /* ABContact.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ABContact.m; sourceTree = "<group>"; };
+		2C1D13A2253760EE00EC0533 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = "<group>"; };
+		2C1EF36925505DCE007C9768 /* NCNavigationController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NCNavigationController.h; sourceTree = "<group>"; };
+		2C1EF36A25505DCE007C9768 /* NCNavigationController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NCNavigationController.m; sourceTree = "<group>"; };
+		2C21446D2BB5B54D005A6537 /* BaseChatTableViewCell+Location.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BaseChatTableViewCell+Location.swift"; sourceTree = "<group>"; };
+		2C2145672BF6B8E900470C0C /* NewRoomTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewRoomTableViewController.swift; sourceTree = "<group>"; };
+		2C2A788C2359CC8800EEB797 /* NCAppBranding.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NCAppBranding.h; sourceTree = "<group>"; };
+		2C2A788D2359CC8800EEB797 /* NCAppBranding.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NCAppBranding.m; sourceTree = "<group>"; };
+		2C2D7A162B8C9C0000642373 /* RoomCreationTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomCreationTableViewController.swift; sourceTree = "<group>"; };
+		2C2E64231F3462AF00D39CE8 /* NCSignalingMessage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NCSignalingMessage.h; sourceTree = "<group>"; };
+		2C2E64241F3462AF00D39CE8 /* NCSignalingMessage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NCSignalingMessage.m; sourceTree = "<group>"; };
+		2C3195BB24C1F58A0066F221 /* ShareExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ShareExtension.entitlements; sourceTree = "<group>"; };
+		2C3195BF24C5E2100066F221 /* ShareTableViewCell.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ShareTableViewCell.h; sourceTree = "<group>"; };
+		2C3195C024C5E2100066F221 /* ShareTableViewCell.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ShareTableViewCell.m; sourceTree = "<group>"; };
+		2C3195C124C5E2100066F221 /* ShareTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ShareTableViewCell.xib; sourceTree = "<group>"; };
+		2C330373255E6EBC00BDB4E4 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = "<group>"; };
+		2C36A048261487BC0026F04A /* DetailedOptionsSelectorTableViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DetailedOptionsSelectorTableViewController.h; sourceTree = "<group>"; };
+		2C36A049261487BC0026F04A /* DetailedOptionsSelectorTableViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DetailedOptionsSelectorTableViewController.m; sourceTree = "<group>"; };
+		2C3780BB2107209C003F9AE8 /* NCRoomParticipant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NCRoomParticipant.h; sourceTree = "<group>"; };
+		2C3780BC2107209C003F9AE8 /* NCRoomParticipants.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NCRoomParticipants.m; sourceTree = "<group>"; };
+		2C3780C1210F49DC003F9AE8 /* HeaderWithButton.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = HeaderWithButton.h; sourceTree = "<group>"; };
+		2C3780C2210F49DC003F9AE8 /* HeaderWithButton.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = HeaderWithButton.m; sourceTree = "<group>"; };
+		2C3780C4210F4A26003F9AE8 /* HeaderWithButton.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = HeaderWithButton.xib; sourceTree = "<group>"; };
+		2C40281322832EED0000DDFC /* NCDatabaseManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NCDatabaseManager.h; sourceTree = "<group>"; };
+		2C40281422832EED0000DDFC /* NCDatabaseManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NCDatabaseManager.m; sourceTree = "<group>"; };
+		2C4230F62B207AB00013E1FA /* ContextChatViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextChatViewController.swift; sourceTree = "<group>"; };
+		2C42ADB220B58E6300296DEA /* NCChatController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NCChatController.h; sourceTree = "<group>"; };
+		2C42ADB320B58E6300296DEA /* NCChatController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NCChatController.m; sourceTree = "<group>"; };
+		2C43BA7421309A1000B3068A /* NCMessageParameter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NCMessageParameter.h; sourceTree = "<group>"; };
+		2C43BA7521309A1000B3068A /* NCMessageParameter.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NCMessageParameter.m; sourceTree = "<group>"; };
+		2C440D0E20EA4A770005F9BB /* RoomInfoTableViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RoomInfoTableViewController.h; sourceTree = "<group>"; };
+		2C440D0F20EA4A770005F9BB /* RoomInfoTableViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RoomInfoTableViewController.m; sourceTree = "<group>"; };
+		2C440D1020EA4A770005F9BB /* RoomInfoTableViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = RoomInfoTableViewController.xib; sourceTree = "<group>"; };
+		2C4446D12658147900DF1DBC /* TalkAccount.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TalkAccount.h; sourceTree = "<group>"; };
+		2C4446D22658147900DF1DBC /* TalkAccount.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TalkAccount.m; sourceTree = "<group>"; };
+		2C4446D6265814D100DF1DBC /* ServerCapabilities.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ServerCapabilities.h; sourceTree = "<group>"; };
+		2C4446D7265814D100DF1DBC /* ServerCapabilities.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ServerCapabilities.m; sourceTree = "<group>"; };
+		2C4446DB2658158000DF1DBC /* NCChatBlock.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NCChatBlock.h; sourceTree = "<group>"; };
+		2C4446DC2658158000DF1DBC /* NCChatBlock.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NCChatBlock.m; sourceTree = "<group>"; };
+		2C4446EA265D25BA00DF1DBC /* NCKeyChainController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NCKeyChainController.h; sourceTree = "<group>"; };
+		2C4446EB265D25BA00DF1DBC /* NCKeyChainController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NCKeyChainController.m; sourceTree = "<group>"; };
+		2C4446EE265D454200DF1DBC /* NotificationCenterNotifications.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NotificationCenterNotifications.h; sourceTree = "<group>"; };
+		2C4446EF265D454200DF1DBC /* NotificationCenterNotifications.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NotificationCenterNotifications.m; sourceTree = "<group>"; };
+		2C4446F1265D51A600DF1DBC /* NCPushNotificationsUtils.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NCPushNotificationsUtils.h; sourceTree = "<group>"; };
+		2C4446F2265D51A600DF1DBC /* NCPushNotificationsUtils.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NCPushNotificationsUtils.m; sourceTree = "<group>"; };
+		2C4446FA265D5BEF00DF1DBC /* CallConstants.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CallConstants.h; sourceTree = "<group>"; };
+		2C444701265D641300DF1DBC /* NCUserDefaults.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NCUserDefaults.h; sourceTree = "<group>"; };
+		2C444702265D641300DF1DBC /* NCUserDefaults.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NCUserDefaults.m; sourceTree = "<group>"; };
+		2C44B4D027FF05A000AD1C86 /* ReactionsSummaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionsSummaryView.swift; sourceTree = "<group>"; };
+		2C4747E12CB58FD2002828F2 /* PollMessageView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = PollMessageView.xib; sourceTree = "<group>"; };
+		2C4747E52CB6710F002828F2 /* BaseChatTableViewCell+Poll.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BaseChatTableViewCell+Poll.swift"; sourceTree = "<group>"; };
+		2C4747E82CB67177002828F2 /* PollMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollMessageView.swift; sourceTree = "<group>"; };
+		2C477C1728B79D980044DEB4 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
+		2C4987BB21E640E20060AC27 /* CallKitManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CallKitManager.h; sourceTree = "<group>"; };
+		2C4987BC21E640E20060AC27 /* CallKitManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CallKitManager.m; sourceTree = "<group>"; };
+		2C4CDCCB269618230023F403 /* RoomDescriptionTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = RoomDescriptionTableViewCell.xib; sourceTree = "<group>"; };
+		2C4CDCD326AF16650023F403 /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/Localizable.strings; sourceTree = "<group>"; };
+		2C4CDCD426AF16650023F403 /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/InfoPlist.strings; sourceTree = "<group>"; };
+		2C4D7D611F2F7C2C00FF4A0D /* ARDCaptureController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARDCaptureController.h; sourceTree = "<group>"; };
+		2C4D7D621F2F7C2C00FF4A0D /* ARDCaptureController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARDCaptureController.m; sourceTree = "<group>"; };
+		2C4D7D641F2F7DBC00FF4A0D /* ARDSettingsModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARDSettingsModel.h; sourceTree = "<group>"; };
+		2C4D7D651F2F7DBC00FF4A0D /* ARDSettingsModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARDSettingsModel.m; sourceTree = "<group>"; };
+		2C4D7D661F2F7DBC00FF4A0D /* ARDSettingsModel+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ARDSettingsModel+Private.h"; sourceTree = "<group>"; };
+		2C4D7D671F2F7DBC00FF4A0D /* ARDSettingsStore.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARDSettingsStore.h; sourceTree = "<group>"; };
+		2C4D7D681F2F7DBC00FF4A0D /* ARDSettingsStore.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARDSettingsStore.m; sourceTree = "<group>"; };
+		2C4D7D6D1F309DA500FF4A0D /* RTCIceCandidate+JSON.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "RTCIceCandidate+JSON.h"; sourceTree = "<group>"; };
+		2C4D7D6E1F309DA500FF4A0D /* RTCIceCandidate+JSON.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "RTCIceCandidate+JSON.m"; sourceTree = "<group>"; };
+		2C4D7D6F1F309DA500FF4A0D /* RTCSessionDescription+JSON.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "RTCSessionDescription+JSON.h"; sourceTree = "<group>"; };
+		2C4D7D701F309DA500FF4A0D /* RTCSessionDescription+JSON.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "RTCSessionDescription+JSON.m"; sourceTree = "<group>"; };
+		2C4D7D741F30F7B600FF4A0D /* ARDUtilities.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARDUtilities.h; sourceTree = "<group>"; };
+		2C4D7D751F30F7B600FF4A0D /* ARDUtilities.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARDUtilities.m; sourceTree = "<group>"; };
+		2C4DE9F021F732B40096940D /* NCAudioController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NCAudioController.h; sourceTree = "<group>"; };
+		2C4DE9F121F732B40096940D /* NCAudioController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NCAudioController.m; sourceTree = "<group>"; };
+		2C57CD8228C204C600B22E03 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = es; path = es.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
+		2C57CD8328C2255000B22E03 /* PollCreationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollCreationViewController.swift; sourceTree = "<group>"; };
+		2C57CD8528CB3FAF00B22E03 /* eu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = eu; path = eu.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
+		2C5BFBE928772A9A00E75118 /* NCUnifiedSearchController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCUnifiedSearchController.swift; sourceTree = "<group>"; };
+		2C5BFBEE288A947800E75118 /* PollVotingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollVotingView.swift; sourceTree = "<group>"; };
+		2C5BFBF0288A97D800E75118 /* NCPoll.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NCPoll.h; sourceTree = "<group>"; };
+		2C5BFBF1288A97D800E75118 /* NCPoll.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NCPoll.m; sourceTree = "<group>"; };
+		2C5BFBF528902E0300E75118 /* PollFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollFooterView.swift; sourceTree = "<group>"; };
+		2C5BFBF728902E3700E75118 /* PollFooterView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = PollFooterView.xib; sourceTree = "<group>"; };
+		2C5BFBF92891598900E75118 /* PollResultTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollResultTableViewCell.swift; sourceTree = "<group>"; };
+		2C5BFBFA2891598900E75118 /* PollResultTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = PollResultTableViewCell.xib; sourceTree = "<group>"; };
+		2C5BFBFD2891C3DF00E75118 /* PollResultsDetailsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollResultsDetailsViewController.swift; sourceTree = "<group>"; };
+		2C604A2A25E4556E00F23615 /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/Localizable.strings; sourceTree = "<group>"; };
+		2C604A2B25E4556E00F23615 /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/InfoPlist.strings; sourceTree = "<group>"; };
+		2C604A2C25E455AC00F23615 /* sl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sl; path = sl.lproj/Localizable.strings; sourceTree = "<group>"; };
+		2C604A2D25E455AC00F23615 /* sl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sl; path = sl.lproj/InfoPlist.strings; sourceTree = "<group>"; };
+		2C604A2E25E455C500F23615 /* hr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hr; path = hr.lproj/Localizable.strings; sourceTree = "<group>"; };
+		2C604A2F25E455C500F23615 /* hr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hr; path = hr.lproj/InfoPlist.strings; sourceTree = "<group>"; };
+		2C604A3025E455D900F23615 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/Localizable.strings; sourceTree = "<group>"; };
+		2C604A3125E455D900F23615 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/InfoPlist.strings; sourceTree = "<group>"; };
+		2C604A3225E455ED00F23615 /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/Localizable.strings"; sourceTree = "<group>"; };
+		2C604A3325E455ED00F23615 /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
+		2C604A3825E4568400F23615 /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = "<group>"; };
+		2C604A3925E4568400F23615 /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/InfoPlist.strings; sourceTree = "<group>"; };
+		2C604A3A25E4569300F23615 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = "<group>"; };
+		2C604A3B25E4569300F23615 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/InfoPlist.strings; sourceTree = "<group>"; };
+		2C604A3C25E4569F00F23615 /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/Localizable.strings; sourceTree = "<group>"; };
+		2C604A3D25E4569F00F23615 /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/InfoPlist.strings; sourceTree = "<group>"; };
+		2C604A4025E45A9400F23615 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = "<group>"; };
+		2C604A4125E45A9400F23615 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
+		2C604A4225E45BAE00F23615 /* gl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = gl; path = gl.lproj/Localizable.strings; sourceTree = "<group>"; };
+		2C604A4325E45BAE00F23615 /* gl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = gl; path = gl.lproj/InfoPlist.strings; sourceTree = "<group>"; };
+		2C604BD7211988A700D34DCD /* SystemMessageTableViewCell.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SystemMessageTableViewCell.h; sourceTree = "<group>"; };
+		2C604BD8211988A700D34DCD /* SystemMessageTableViewCell.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SystemMessageTableViewCell.m; sourceTree = "<group>"; };
+		2C6085C11FB1063700B36A6E /* NextcloudTalk.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NextcloudTalk.entitlements; sourceTree = "<group>"; };
+		2C62AFA324C08845007E460A /* ShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
+		2C62AFAB24C08845007E460A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
+		2C62AFB524C1A449007E460A /* Share.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Share.storyboard; sourceTree = "<group>"; };
+		2C62AFB724C1A4E6007E460A /* ShareViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ShareViewController.h; sourceTree = "<group>"; };
+		2C62AFB824C1A4E6007E460A /* ShareViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ShareViewController.m; sourceTree = "<group>"; };
+		2C67905128D35BEB00762744 /* sl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = sl; path = sl.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
+		2C69323B2923ECAA00017AD2 /* WSMessage.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = WSMessage.h; sourceTree = "<group>"; };
+		2C69323C2923ECAA00017AD2 /* WSMessage.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = WSMessage.m; sourceTree = "<group>"; };
+		2C6E74442386D33200AE396C /* ReplyMessageView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ReplyMessageView.h; sourceTree = "<group>"; };
+		2C6E74452386D33200AE396C /* ReplyMessageView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ReplyMessageView.m; sourceTree = "<group>"; };
+		2C6E7447238C1A0800AE396C /* QuotedMessageView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QuotedMessageView.h; sourceTree = "<group>"; };
+		2C6E7448238C1A0800AE396C /* QuotedMessageView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QuotedMessageView.m; sourceTree = "<group>"; };
+		2C7381542106136000CDB8DB /* NCChatTitleView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NCChatTitleView.h; sourceTree = "<group>"; };
+		2C7381552106136000CDB8DB /* NCChatTitleView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NCChatTitleView.m; sourceTree = "<group>"; };
+		2C738157210613A200CDB8DB /* NCChatTitleView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = NCChatTitleView.xib; sourceTree = "<group>"; };
+		2C78E9E125120DE500E3D4CA /* NCUserStatus.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NCUserStatus.h; sourceTree = "<group>"; };
+		2C78E9E225120DE500E3D4CA /* NCUserStatus.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NCUserStatus.m; sourceTree = "<group>"; };
+		2C78EF931F7E70EB008AFA74 /* NCPeerConnection.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NCPeerConnection.h; sourceTree = "<group>"; };
+		2C78EF941F7E70EB008AFA74 /* NCPeerConnection.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NCPeerConnection.m; sourceTree = "<group>"; };
+		2C78EF971F80F81E008AFA74 /* NCSignalingController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NCSignalingController.h; sourceTree = "<group>"; };
+		2C78EF981F80F81E008AFA74 /* NCSignalingController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NCSignalingController.m; sourceTree = "<group>"; };
+		2C78EF9A1F826B22008AFA74 /* NCCallController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NCCallController.h; sourceTree = "<group>"; };
+		2C78EF9B1F826B22008AFA74 /* NCCallController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NCCallController.m; sourceTree = "<group>"; };
+		2C78EF9D1F828C41008AFA74 /* CallViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CallViewController.h; sourceTree = "<group>"; };
+		2C78EF9E1F828C41008AFA74 /* CallViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CallViewController.m; sourceTree = "<group>"; };
+		2C78EF9F1F828C41008AFA74 /* CallViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = CallViewController.xib; sourceTree = "<group>"; };
+		2C78EFA21F86FF4A008AFA74 /* CallParticipantViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CallParticipantViewCell.h; sourceTree = "<group>"; };
+		2C78EFA31F86FF4A008AFA74 /* CallParticipantViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CallParticipantViewCell.m; sourceTree = "<group>"; };
+		2C78EFA41F86FF4A008AFA74 /* CallParticipantViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = CallParticipantViewCell.xib; sourceTree = "<group>"; };
+		2C7A1235200E0A5700864818 /* UserSettingsTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = UserSettingsTableViewCell.xib; sourceTree = "<group>"; };
+		2C7A123F2017872600864818 /* AddParticipantsTableViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AddParticipantsTableViewController.h; sourceTree = "<group>"; };
+		2C7A12402017872600864818 /* AddParticipantsTableViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AddParticipantsTableViewController.m; sourceTree = "<group>"; };
+		2C7A12412017872600864818 /* AddParticipantsTableViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AddParticipantsTableViewController.xib; sourceTree = "<group>"; };
+		2C7F47AB20289B9600081CC7 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
+		2C84BCCB29EEB9C6001BA6DA /* CallReactionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallReactionView.swift; sourceTree = "<group>"; };
+		2C84BCCD29EEDCE8001BA6DA /* CallReactionView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = CallReactionView.xib; sourceTree = "<group>"; };
+		2C8A2BC7221F094F00DE6D2C /* DirectoryTableViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DirectoryTableViewController.h; sourceTree = "<group>"; };
+		2C8A2BC8221F094F00DE6D2C /* DirectoryTableViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DirectoryTableViewController.m; sourceTree = "<group>"; };
+		2C8A2BCD221FEEFE00DE6D2C /* DirectoryTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = DirectoryTableViewCell.xib; sourceTree = "<group>"; };
+		2C8CDD0421C2EDE8004E2997 /* AvatarBackgroundImageView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AvatarBackgroundImageView.h; sourceTree = "<group>"; };
+		2C8CDD0521C2EDE8004E2997 /* AvatarBackgroundImageView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AvatarBackgroundImageView.m; sourceTree = "<group>"; };
+		2C8E2A19232174C20022BFC9 /* MessageSeparatorTableViewCell.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MessageSeparatorTableViewCell.h; sourceTree = "<group>"; };
+		2C8E2A1A232174C20022BFC9 /* MessageSeparatorTableViewCell.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MessageSeparatorTableViewCell.m; sourceTree = "<group>"; };
+		2C90E5631EDDE0FB0093D85A /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; };
+		2C90E5661EDDE1340093D85A /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; };
+		2C90E5681EDDE13A0093D85A /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; };
+		2C90E5CE1EDF23A00093D85A /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = System/Library/Frameworks/WebKit.framework; sourceTree = SDKROOT; };
+		2C90E5D01EE80C870093D85A /* AuthenticationViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AuthenticationViewController.h; sourceTree = "<group>"; };
+		2C90E5D11EE80C870093D85A /* AuthenticationViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AuthenticationViewController.m; sourceTree = "<group>"; };
+		2C9200C12474262C0050084F /* UIBarButtonItem+Badge.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIBarButtonItem+Badge.h"; sourceTree = "<group>"; };
+		2C9200C22474262C0050084F /* UIBarButtonItem+Badge.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIBarButtonItem+Badge.m"; sourceTree = "<group>"; };
+		2C928BD2268A06BB00729332 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = "<group>"; };
+		2C928BD3268A06BB00729332 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/InfoPlist.strings; sourceTree = "<group>"; };
+		2C928BD4268A0AAD00729332 /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/Localizable.strings; sourceTree = "<group>"; };
+		2C928BD5268A0AAD00729332 /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/InfoPlist.strings; sourceTree = "<group>"; };
+		2C928BD6268A0B2800729332 /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ko; path = ko.lproj/Localizable.strings; sourceTree = "<group>"; };
+		2C928BD7268A0B2800729332 /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ko; path = ko.lproj/InfoPlist.strings; sourceTree = "<group>"; };
+		2C928BD8268A0BC000729332 /* eu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = eu; path = eu.lproj/Localizable.strings; sourceTree = "<group>"; };
+		2C928BD9268A0BC000729332 /* eu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = eu; path = eu.lproj/InfoPlist.strings; sourceTree = "<group>"; };
+		2C928BDA268A103600729332 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = "<group>"; };
+		2C98F77721622445001A6A73 /* RoomSearchTableViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RoomSearchTableViewController.h; sourceTree = "<group>"; };
+		2C98F77821622445001A6A73 /* RoomSearchTableViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RoomSearchTableViewController.m; sourceTree = "<group>"; };
+		2C98F77C216231D3001A6A73 /* RoomTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = RoomTableViewCell.xib; sourceTree = "<group>"; };
+		2C9B0B96217F6DBA00A4752C /* NCNotificationController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NCNotificationController.h; sourceTree = "<group>"; };
+		2C9B0B97217F6DBA00A4752C /* NCNotificationController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NCNotificationController.m; sourceTree = "<group>"; };
+		2C9B0B9A217F756B00A4752C /* NCNotification.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NCNotification.h; sourceTree = "<group>"; };
+		2C9B0B9B217F756B00A4752C /* NCNotification.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NCNotification.m; sourceTree = "<group>"; };
+		2C9E6CCC1F6F34F000399B7A /* ARDSDPUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARDSDPUtils.h; sourceTree = "<group>"; };
+		2C9E6CCD1F6F34F000399B7A /* ARDSDPUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARDSDPUtils.m; sourceTree = "<group>"; };
+		2CA1553F208E350300CE8EF0 /* NCChatMessage.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NCChatMessage.h; sourceTree = "<group>"; };
+		2CA15540208E350300CE8EF0 /* NCChatMessage.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NCChatMessage.m; sourceTree = "<group>"; };
+		2CA15549208F2E5700CE8EF0 /* NCMessageTextView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NCMessageTextView.h; sourceTree = "<group>"; };
+		2CA1554A208F2E5700CE8EF0 /* NCMessageTextView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NCMessageTextView.m; sourceTree = "<group>"; };
+		2CA1CC8F1F014354002FE6A2 /* NCConnectionController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NCConnectionController.h; sourceTree = "<group>"; };
+		2CA1CC901F014354002FE6A2 /* NCConnectionController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NCConnectionController.m; sourceTree = "<group>"; };
+		2CA1CC931F014EF9002FE6A2 /* NCSettingsController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NCSettingsController.h; sourceTree = "<group>"; };
+		2CA1CC941F014EF9002FE6A2 /* NCSettingsController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NCSettingsController.m; sourceTree = "<group>"; };
+		2CA1CC961F016117002FE6A2 /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; };
+		2CA1CCA21F025F64002FE6A2 /* RoomsTableViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RoomsTableViewController.h; sourceTree = "<group>"; };
+		2CA1CCA31F025F64002FE6A2 /* RoomsTableViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RoomsTableViewController.m; sourceTree = "<group>"; };
+		2CA1CCA81F02D1A4002FE6A2 /* NCAPIController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NCAPIController.h; sourceTree = "<group>"; };
+		2CA1CCA91F02D1A4002FE6A2 /* NCAPIController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NCAPIController.m; sourceTree = "<group>"; };
+		2CA1CCAB1F067F35002FE6A2 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = "<group>"; };
+		2CA1CCC11F166CC5002FE6A2 /* NCRoom.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NCRoom.h; sourceTree = "<group>"; };
+		2CA1CCC21F166CC5002FE6A2 /* NCRoom.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NCRoom.m; sourceTree = "<group>"; };
+		2CA1CCC91F17C503002FE6A2 /* AudioToolbox.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AudioToolbox.framework; path = System/Library/Frameworks/AudioToolbox.framework; sourceTree = SDKROOT; };
+		2CA1CCCB1F181741002FE6A2 /* NCUser.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NCUser.h; sourceTree = "<group>"; };
+		2CA1CCCC1F181741002FE6A2 /* NCUser.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NCUser.m; sourceTree = "<group>"; };
+		2CA1CCCE1F1E1779002FE6A2 /* SearchTableViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SearchTableViewController.h; sourceTree = "<group>"; };
+		2CA1CCCF1F1E1779002FE6A2 /* SearchTableViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SearchTableViewController.m; sourceTree = "<group>"; };
+		2CA1CCD31F1E664C002FE6A2 /* ContactsTableViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ContactsTableViewCell.h; sourceTree = "<group>"; };
+		2CA1CCD41F1E664C002FE6A2 /* ContactsTableViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ContactsTableViewCell.m; sourceTree = "<group>"; };
+		2CA1CCD51F1E664C002FE6A2 /* ContactsTableViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ContactsTableViewCell.xib; sourceTree = "<group>"; };
+		2CA52AC92670D02800619610 /* VoiceMessageRecordingView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = VoiceMessageRecordingView.h; sourceTree = "<group>"; };
+		2CA52ACA2670D02800619610 /* VoiceMessageRecordingView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = VoiceMessageRecordingView.m; sourceTree = "<group>"; };
+		2CA52ACC2670D07900619610 /* VoiceMessageRecordingView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = VoiceMessageRecordingView.xib; sourceTree = "<group>"; };
+		2CA80EDB256C1249006BA449 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = "<group>"; };
+		2CA80EDC256C1249006BA449 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/InfoPlist.strings; sourceTree = "<group>"; };
+		2CA80EDD256C1296006BA449 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = "<group>"; };
+		2CA80EDE256C1296006BA449 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/InfoPlist.strings; sourceTree = "<group>"; };
+		2CA80EDF256C12E7006BA449 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Localizable.strings; sourceTree = "<group>"; };
+		2CA80EE0256C12E7006BA449 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/InfoPlist.strings; sourceTree = "<group>"; };
+		2CB052A02BF2297500191349 /* connecting.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; name = connecting.mp3; path = Sounds/connecting.mp3; sourceTree = SOURCE_ROOT; };
+		2CB2B24D28BCB9D900A9D606 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = cs; path = cs.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
+		2CB2B24E28BCB9E800A9D606 /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = hu; path = hu.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
+		2CB2B24F28BCBABC00A9D606 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = pl; path = pl.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
+		2CB2B25128BF957100A9D606 /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = tr; path = tr.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
+		2CB3039C2264775E0053078A /* SLKInputAccessoryView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SLKInputAccessoryView.h; sourceTree = "<group>"; };
+		2CB3039D2264775E0053078A /* SLKInputAccessoryView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SLKInputAccessoryView.m; sourceTree = "<group>"; };
+		2CB3039E2264775E0053078A /* SLKTextInput+Implementation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "SLKTextInput+Implementation.m"; sourceTree = "<group>"; };
+		2CB3039F2264775E0053078A /* SLKTextInput.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SLKTextInput.h; sourceTree = "<group>"; };
+		2CB303A02264775E0053078A /* SLKTextInputbar.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SLKTextInputbar.h; sourceTree = "<group>"; };
+		2CB303A12264775E0053078A /* SLKTextInputbar.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SLKTextInputbar.m; sourceTree = "<group>"; };
+		2CB303A22264775E0053078A /* SLKTextView+SLKAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "SLKTextView+SLKAdditions.h"; sourceTree = "<group>"; };
+		2CB303A32264775E0053078A /* SLKTextView+SLKAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "SLKTextView+SLKAdditions.m"; sourceTree = "<group>"; };
+		2CB303A42264775E0053078A /* SLKTextView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SLKTextView.h; sourceTree = "<group>"; };
+		2CB303A52264775E0053078A /* SLKTextView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SLKTextView.m; sourceTree = "<group>"; };
+		2CB303A62264775E0053078A /* SLKTextViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SLKTextViewController.h; sourceTree = "<group>"; };
+		2CB303A72264775E0053078A /* SLKTextViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SLKTextViewController.m; sourceTree = "<group>"; };
+		2CB303A82264775E0053078A /* SLKVisibleViewProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SLKVisibleViewProtocol.h; sourceTree = "<group>"; };
+		2CB303AB2264775E0053078A /* SLKUIConstants.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SLKUIConstants.h; sourceTree = "<group>"; };
+		2CB303AC2264775E0053078A /* UIResponder+SLKAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIResponder+SLKAdditions.h"; sourceTree = "<group>"; };
+		2CB303AD2264775E0053078A /* UIResponder+SLKAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIResponder+SLKAdditions.m"; sourceTree = "<group>"; };
+		2CB303AE2264775E0053078A /* UIScrollView+SLKAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIScrollView+SLKAdditions.h"; sourceTree = "<group>"; };
+		2CB303AF2264775E0053078A /* UIScrollView+SLKAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIScrollView+SLKAdditions.m"; sourceTree = "<group>"; };
+		2CB303B02264775E0053078A /* UIView+SLKAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIView+SLKAdditions.h"; sourceTree = "<group>"; };
+		2CB303B12264775E0053078A /* UIView+SLKAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIView+SLKAdditions.m"; sourceTree = "<group>"; };
+		2CB6ACB926385A3800D3D641 /* ShareLocationViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ShareLocationViewController.h; sourceTree = "<group>"; };
+		2CB6ACBA26385A3800D3D641 /* ShareLocationViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ShareLocationViewController.m; sourceTree = "<group>"; };
+		2CB6ACBB26385A3800D3D641 /* ShareLocationViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ShareLocationViewController.xib; sourceTree = "<group>"; };
+		2CB6ACC826401D5100D3D641 /* GeoLocationRichObject.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeoLocationRichObject.h; sourceTree = "<group>"; };
+		2CB6ACC926401D5100D3D641 /* GeoLocationRichObject.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = GeoLocationRichObject.m; sourceTree = "<group>"; };
+		2CB6ACD82641483800D3D641 /* NCMessageLocationParameter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NCMessageLocationParameter.h; sourceTree = "<group>"; };
+		2CB6ACD92641483800D3D641 /* NCMessageLocationParameter.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NCMessageLocationParameter.m; sourceTree = "<group>"; };
+		2CB6ACE62641954700D3D641 /* MapViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MapViewController.h; sourceTree = "<group>"; };
+		2CB6ACE72641954700D3D641 /* MapViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MapViewController.m; sourceTree = "<group>"; };
+		2CB6ACE82641954700D3D641 /* MapViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = MapViewController.xib; sourceTree = "<group>"; };
+		2CB997C32A052449003C41AC /* EmojiAvatarPickerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiAvatarPickerViewController.swift; sourceTree = "<group>"; };
+		2CB997C42A052449003C41AC /* EmojiAvatarPickerViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = EmojiAvatarPickerViewController.xib; sourceTree = "<group>"; };
+		2CBD0D592C8770A40013C089 /* UIImageExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIImageExtension.swift; sourceTree = "<group>"; };
+		2CBF82AC1FC888FC00636459 /* NCPushNotification.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NCPushNotification.h; sourceTree = "<group>"; };
+		2CBF82AD1FC888FC00636459 /* NCPushNotification.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NCPushNotification.m; sourceTree = "<group>"; };
+		2CBF82B01FCC7DBA00636459 /* CCCertificate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CCCertificate.h; sourceTree = "<group>"; };
+		2CBF82B11FCC7DBA00636459 /* CCCertificate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CCCertificate.m; sourceTree = "<group>"; };
+		2CC0014F24A1F0E900A20167 /* NotificationServiceExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = NotificationServiceExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
+		2CC0015124A1F0E900A20167 /* NotificationService.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NotificationService.h; sourceTree = "<group>"; };
+		2CC0015224A1F0E900A20167 /* NotificationService.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NotificationService.m; sourceTree = "<group>"; };
+		2CC0015424A1F0E900A20167 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
+		2CC0015B24A1F1D700A20167 /* NotificationServiceExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NotificationServiceExtension.entitlements; sourceTree = "<group>"; };
+		2CC007B220D7AE990096D91F /* ResultMultiSelectionTableViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ResultMultiSelectionTableViewController.h; sourceTree = "<group>"; };
+		2CC007B320D7AE990096D91F /* ResultMultiSelectionTableViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ResultMultiSelectionTableViewController.m; sourceTree = "<group>"; };
+		2CC007C420D90AE50096D91F /* RoomNameTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = RoomNameTableViewCell.xib; sourceTree = "<group>"; };
+		2CC007CC20E50B0A0096D91F /* MessageBodyTextView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MessageBodyTextView.h; sourceTree = "<group>"; };
+		2CC007CD20E50B0A0096D91F /* MessageBodyTextView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MessageBodyTextView.m; sourceTree = "<group>"; };
+		2CC1C38029C0945600C8436B /* DRCellSlideGestureRecognizer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DRCellSlideGestureRecognizer.m; sourceTree = "<group>"; };
+		2CC1C38129C0945600C8436B /* DRCellSlideAction.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DRCellSlideAction.h; sourceTree = "<group>"; };
+		2CC1C38229C0945600C8436B /* DRCellSlideActionView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DRCellSlideActionView.h; sourceTree = "<group>"; };
+		2CC1C38329C0945600C8436B /* DRCellSlideGestureRecognizer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DRCellSlideGestureRecognizer.h; sourceTree = "<group>"; };
+		2CC1C38429C0945700C8436B /* DRCellSlideAction.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DRCellSlideAction.m; sourceTree = "<group>"; };
+		2CC1C38529C0945700C8436B /* DRCellSlideActionView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DRCellSlideActionView.m; sourceTree = "<group>"; };
+		2CC1FF4228147F10009F7288 /* RoomSharedItemsTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSharedItemsTableViewController.swift; sourceTree = "<group>"; };
+		2CC1FF4328147F10009F7288 /* RoomSharedItemsTableViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = RoomSharedItemsTableViewController.xib; sourceTree = "<group>"; };
+		2CC1FF4628183958009F7288 /* NCDeckCardParameter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NCDeckCardParameter.h; sourceTree = "<group>"; };
+		2CC1FF4728183958009F7288 /* NCDeckCardParameter.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NCDeckCardParameter.m; sourceTree = "<group>"; };
+		2CC32E8C27F4540E00BB8C39 /* ReactionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionsView.swift; sourceTree = "<group>"; };
+		2CC32E9027F45AE000BB8C39 /* ReactionsViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionsViewCell.swift; sourceTree = "<group>"; };
+		2CC32E9127F45AE000BB8C39 /* ReactionsViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ReactionsViewCell.xib; sourceTree = "<group>"; };
+		2CC32E9627F5D9BD00BB8C39 /* NCChatReaction.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NCChatReaction.h; sourceTree = "<group>"; };
+		2CC32E9727F5D9BD00BB8C39 /* NCChatReaction.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NCChatReaction.m; sourceTree = "<group>"; };
+		2CC7158820B837140045C789 /* PlaceholderView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = PlaceholderView.xib; sourceTree = "<group>"; };
+		2CC7158A20B8394A0045C789 /* PlaceholderView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PlaceholderView.h; sourceTree = "<group>"; };
+		2CC7158B20B8394A0045C789 /* PlaceholderView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PlaceholderView.m; sourceTree = "<group>"; };
+		2CC7159220C54D080045C789 /* ChatTableViewCell.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ChatTableViewCell.h; sourceTree = "<group>"; };
+		2CC7159320C54D080045C789 /* ChatTableViewCell.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ChatTableViewCell.m; sourceTree = "<group>"; };
+		2CD4F6B62C11C80600ED594F /* ContactsSearchResultTableViewContoller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactsSearchResultTableViewContoller.swift; sourceTree = "<group>"; };
+		2CD5F3222142781A006B71BF /* NCExternalSignalingController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NCExternalSignalingController.h; sourceTree = "<group>"; };
+		2CD5F3232142781A006B71BF /* NCExternalSignalingController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NCExternalSignalingController.m; sourceTree = "<group>"; };
+		2CD80F472A4304AD00919057 /* OpenConversationsTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenConversationsTableViewController.swift; sourceTree = "<group>"; };
+		2CEA990828B8B5780029216A /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = de; path = de.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
+		2CEDA87E26EF91460044552B /* NextcloudTalk-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NextcloudTalk-Bridging-Header.h"; sourceTree = "<group>"; };
+		2CEDA88B26F492610044552B /* NSMutableAttributedString+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSMutableAttributedString+Extensions.swift"; sourceTree = "<group>"; };
+		2CF8AD3D2A0010FB00A4D3E6 /* MessageTranslationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageTranslationViewController.swift; sourceTree = "<group>"; };
+		2CF8AD3E2A0010FB00A4D3E6 /* MessageTranslationViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = MessageTranslationViewController.xib; sourceTree = "<group>"; };
+		2CF9CBFB26025F64002246EF /* TextInputTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TextInputTableViewCell.xib; sourceTree = "<group>"; };
+		342600BABD1AD1FCA48B5E59 /* Pods-NextcloudTalkTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NextcloudTalkTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-NextcloudTalkTests/Pods-NextcloudTalkTests.debug.xcconfig"; sourceTree = "<group>"; };
+		4202C63030F0FFBB1C16D75E /* Pods-NextcloudTalk.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NextcloudTalk.debug.xcconfig"; path = "Pods/Target Support Files/Pods-NextcloudTalk/Pods-NextcloudTalk.debug.xcconfig"; sourceTree = "<group>"; };
+		4D4C7BF2F97F47B0D9094618 /* Pods-NextcloudTalk.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NextcloudTalk.release.xcconfig"; path = "Pods/Target Support Files/Pods-NextcloudTalk/Pods-NextcloudTalk.release.xcconfig"; sourceTree = "<group>"; };
+		4F7C31E9D74F550EAF89931B /* libPods-NextcloudTalk.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-NextcloudTalk.a"; sourceTree = BUILT_PRODUCTS_DIR; };
+		584BF273DF09DE4D5EE0DA0F /* Pods-ShareExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.debug.xcconfig"; path = "Pods/Target Support Files/Pods-ShareExtension/Pods-ShareExtension.debug.xcconfig"; sourceTree = "<group>"; };
+		684807120F4439797973DF73 /* libPods-ShareExtension.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-ShareExtension.a"; sourceTree = BUILT_PRODUCTS_DIR; };
+		7005E22D6C2896927FC3AEEC /* libPods-NextcloudTalkTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-NextcloudTalkTests.a"; sourceTree = BUILT_PRODUCTS_DIR; };
+		807E30752A83A90F00089D28 /* UserStatusOptionsSwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserStatusOptionsSwiftUI.swift; sourceTree = "<group>"; };
+		80832B752A822E5100195A97 /* UserStatusSwiftUIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserStatusSwiftUIView.swift; sourceTree = "<group>"; };
+		80832B772A823D0700195A97 /* UserStatusMessageSwiftUIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserStatusMessageSwiftUIView.swift; sourceTree = "<group>"; };
+		82CD0527E04B844CAD762ADE /* Pods-BroadcastUploadExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-BroadcastUploadExtension.debug.xcconfig"; path = "Pods/Target Support Files/Pods-BroadcastUploadExtension/Pods-BroadcastUploadExtension.debug.xcconfig"; sourceTree = "<group>"; };
+		95D756208A81284B975853EC /* Pods-ShareExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.release.xcconfig"; path = "Pods/Target Support Files/Pods-ShareExtension/Pods-ShareExtension.release.xcconfig"; sourceTree = "<group>"; };
+		9A3D305FCD7BF7E727A62F35 /* libPods-BroadcastUploadExtension.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-BroadcastUploadExtension.a"; sourceTree = BUILT_PRODUCTS_DIR; };
+		9B81BB7A4920C391CC2CACFD /* libPods-NotificationServiceExtension.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-NotificationServiceExtension.a"; sourceTree = BUILT_PRODUCTS_DIR; };
+		A8F95DE6635ABC1E64CA8E4A /* Pods-BroadcastUploadExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-BroadcastUploadExtension.release.xcconfig"; path = "Pods/Target Support Files/Pods-BroadcastUploadExtension/Pods-BroadcastUploadExtension.release.xcconfig"; sourceTree = "<group>"; };
+		B7874918820589BF8FD69BED /* Pods-NextcloudTalkTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NextcloudTalkTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-NextcloudTalkTests/Pods-NextcloudTalkTests.release.xcconfig"; sourceTree = "<group>"; };
+		D6DF51D976DC0F681FF83F7B /* Pods-NotificationServiceExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationServiceExtension.debug.xcconfig"; path = "Pods/Target Support Files/Pods-NotificationServiceExtension/Pods-NotificationServiceExtension.debug.xcconfig"; sourceTree = "<group>"; };
+		D86091EC1125C3057B9A299B /* Pods-NotificationServiceExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationServiceExtension.release.xcconfig"; path = "Pods/Target Support Files/Pods-NotificationServiceExtension/Pods-NotificationServiceExtension.release.xcconfig"; sourceTree = "<group>"; };
+		DA1AEFC2270F1FA90088E519 /* DateLabelCustom.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateLabelCustom.swift; sourceTree = "<group>"; };
+		DA66582A27B6992F00B46B11 /* UserProfileTableViewController+AvatarSetup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserProfileTableViewController+AvatarSetup.swift"; sourceTree = "<group>"; };
+		DA66582C27B6A73800B46B11 /* UserProfileTableViewController+DelegateMethods.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserProfileTableViewController+DelegateMethods.swift"; sourceTree = "<group>"; };
+		DA66582E27B6B19C00B46B11 /* UserProfileTableViewController+Actions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserProfileTableViewController+Actions.swift"; sourceTree = "<group>"; };
+		DA66583027B6B24E00B46B11 /* UserProfileTableViewController+Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserProfileTableViewController+Utils.swift"; sourceTree = "<group>"; };
+		DA75580E278EEA1000A48A1B /* SettingsTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsTableViewController.swift; sourceTree = "<group>"; };
+		DA755810278EF3EF00A48A1B /* UserSettingsTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSettingsTableViewCell.swift; sourceTree = "<group>"; };
+		DA8801A127A2DA00009EF248 /* UserProfileTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileTableViewController.swift; sourceTree = "<group>"; };
+		DA8801A327AC52AC009EF248 /* TextInputTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextInputTableViewCell.swift; sourceTree = "<group>"; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+		1F6D8C2D2B2E3756004376B8 /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				1F759C0E2B63B9BA000534AB /* WebRTC in Frameworks */,
+				1F759C092B63B9A7000534AB /* SDWebImage in Frameworks */,
+				1F759C1A2B63B9D9000534AB /* CDMarkdownKit in Frameworks */,
+				1F759C0B2B63B9A7000534AB /* SDWebImageSVGKitPlugin in Frameworks */,
+				1F759C102B63B9D9000534AB /* OpenSSL in Frameworks */,
+				1F759C142B63B9D9000534AB /* QRCodeReader in Frameworks */,
+				1F759C342B63CBAA000534AB /* Realm in Frameworks */,
+				1F759C162B63B9D9000534AB /* NextcloudKit in Frameworks */,
+				1F759C182B63B9D9000534AB /* SwiftyAttributes in Frameworks */,
+				1F759C1C2B63B9D9000534AB /* TOCropViewController in Frameworks */,
+				1F759C1E2B63B9D9000534AB /* SwiftUIIntrospect in Frameworks */,
+				4890175925A0D7FC2EC76CC0 /* libPods-NextcloudTalkTests.a in Frameworks */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		1FD8AD872A3A162100787C16 /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		1FF2FD582AB99CCB000C9905 /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				1F759C322B63CBA5000534AB /* Realm in Frameworks */,
+				1F77A6002AB9A50D007B6037 /* NextcloudKit in Frameworks */,
+				1F77A60C2AB9A5BE007B6037 /* CDMarkdownKit in Frameworks */,
+				1F77A5EF2AB9A41E007B6037 /* SDWebImage in Frameworks */,
+				5EE5ACC02CF372AD004D7EDB /* ReplayKit.framework in Frameworks */,
+				1FF1361A2BFBC841006A6101 /* SwiftyAttributes in Frameworks */,
+				847EFC7236336B67A1A89358 /* libPods-BroadcastUploadExtension.a in Frameworks */,
+				1F77A5F12AB9A423007B6037 /* SDWebImageSVGKitPlugin in Frameworks */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		2C05747A1EDD9E8E00D9E7F2 /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				2CA1CCCA1F17C503002FE6A2 /* AudioToolbox.framework in Frameworks */,
+				2CCCD21D2835088F00F076CE /* OpenSSL in Frameworks */,
+				1FCE3D532C9B5918009C68A9 /* SwiftyGif in Frameworks */,
+				1F90EFC725FE4BE700F3FA55 /* IntentsUI.framework in Frameworks */,
+				2CA1CC971F016117002FE6A2 /* Security.framework in Frameworks */,
+				1F45A1212A01D8BA005FE87D /* SDWebImageSVGKitPlugin in Frameworks */,
+				1F628CBA2842BAAF0083A425 /* QRCodeReader in Frameworks */,
+				1FAB2E7D2AC99326001214EB /* TOCropViewController in Frameworks */,
+				2C90E5CF1EDF23A00093D85A /* WebKit.framework in Frameworks */,
+				2C90E5691EDDE13A0093D85A /* UIKit.framework in Frameworks */,
+				80CDF8C42A8E098900CB57AE /* SwiftUIIntrospect in Frameworks */,
+				2C90E5671EDDE1340093D85A /* CoreGraphics.framework in Frameworks */,
+				1F45A1162A01D6EC005FE87D /* SDWebImage in Frameworks */,
+				2C90E5641EDDE0FB0093D85A /* Foundation.framework in Frameworks */,
+				1F468E7628DCC6C60099597B /* Dynamic in Frameworks */,
+				1F759C2C2B63CB93000534AB /* Realm in Frameworks */,
+				1FAB2E882ACD44D0001214EB /* WebRTC in Frameworks */,
+				1F0ECBF52A68274400921E90 /* CDMarkdownKit in Frameworks */,
+				1F66B72F29FABD01003FB168 /* SwiftyAttributes in Frameworks */,
+				1F7AE07829142CA1009F72AD /* NextcloudKit in Frameworks */,
+				9993261EDAC77481FF4EF58A /* libPods-NextcloudTalk.a in Frameworks */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		2C62AFA024C08845007E460A /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				1F7AE07C29142E6A009F72AD /* NextcloudKit in Frameworks */,
+				1FAB2E7F2AC99367001214EB /* TOCropViewController in Frameworks */,
+				1F0ECBF92A68277C00921E90 /* CDMarkdownKit in Frameworks */,
+				8789AE73BFCAA413B43319C0 /* libPods-ShareExtension.a in Frameworks */,
+				1F759C302B63CBA0000534AB /* Realm in Frameworks */,
+				1F45A11A2A01D70E005FE87D /* SDWebImage in Frameworks */,
+				5EE5ACBB2CF371E7004D7EDB /* AudioToolbox.framework in Frameworks */,
+				1F45A1252A01D8F7005FE87D /* SDWebImageSVGKitPlugin in Frameworks */,
+				5EE5ACBE2CF371E9004D7EDB /* IntentsUI.framework in Frameworks */,
+				1F35F8F52AEEDA9800044BDA /* SwiftyAttributes in Frameworks */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		2CC0014C24A1F0E900A20167 /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				1F759C2E2B63CB9A000534AB /* Realm in Frameworks */,
+				1F45A1232A01D8F1005FE87D /* SDWebImageSVGKitPlugin in Frameworks */,
+				1F0ECBF72A68277000921E90 /* CDMarkdownKit in Frameworks */,
+				1F7AE07A29142E62009F72AD /* NextcloudKit in Frameworks */,
+				1F7AE07D29158878009F72AD /* IntentsUI.framework in Frameworks */,
+				1FF1361C2BFBC86A006A6101 /* SwiftyAttributes in Frameworks */,
+				3FCA62550CD1442D28E8A7C6 /* libPods-NotificationServiceExtension.a in Frameworks */,
+				1F45A11E2A01D719005FE87D /* SDWebImage in Frameworks */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+		1F1B0F3A2BE0476C003FD766 /* Media Viewer */ = {
+			isa = PBXGroup;
+			children = (
+				1F1B0F2B2BDBB3AC003FD766 /* NCMediaViewerViewController.swift */,
+				1F1B0F2F2BDBC9D6003FD766 /* NCMediaViewerPageViewController.swift */,
+				1F1B0F492BE047D5003FD766 /* OneWayPanGestureRecognizer.swift */,
+				1F1B0F412BE047CE003FD766 /* CustomPresentable.swift */,
+				1F1B0F3F2BE047CD003FD766 /* InteractionControlling.swift */,
+				1F1B0F3E2BE047CD003FD766 /* ModalPresentationController.swift */,
+				1F1B0F3C2BE047CD003FD766 /* ModalTransitionAnimator.swift */,
+				1F1B0F3D2BE047CD003FD766 /* ModalTransitionManager.swift */,
+				1F1B0F402BE047CE003FD766 /* StandardInteractionController.swift */,
+				1F1B0F3B2BE047CD003FD766 /* UIViewController+Transitions.swift */,
+				1F1B0F4B2BE18FF3003FD766 /* CustomPresentableNavigationController.swift */,
+			);
+			name = "Media Viewer";
+			sourceTree = "<group>";
+		};
+		1F46CE2528E05ABB00E7D88E /* Reactions */ = {
+			isa = PBXGroup;
+			children = (
+				2CC32E8C27F4540E00BB8C39 /* ReactionsView.swift */,
+				2CC32E9027F45AE000BB8C39 /* ReactionsViewCell.swift */,
+				2CC32E9127F45AE000BB8C39 /* ReactionsViewCell.xib */,
+				2C44B4D027FF05A000AD1C86 /* ReactionsSummaryView.swift */,
+			);
+			name = Reactions;
+			sourceTree = "<group>";
+		};
+		1F46CE2628E05AC100E7D88E /* Polls */ = {
+			isa = PBXGroup;
+			children = (
+				2C57CD8328C2255000B22E03 /* PollCreationViewController.swift */,
+				2C5BFBF528902E0300E75118 /* PollFooterView.swift */,
+				2C5BFBF728902E3700E75118 /* PollFooterView.xib */,
+				2C4747E82CB67177002828F2 /* PollMessageView.swift */,
+				2C4747E12CB58FD2002828F2 /* PollMessageView.xib */,
+				2C5BFBFD2891C3DF00E75118 /* PollResultsDetailsViewController.swift */,
+				2C5BFBF92891598900E75118 /* PollResultTableViewCell.swift */,
+				2C5BFBFA2891598900E75118 /* PollResultTableViewCell.xib */,
+				2C5BFBEE288A947800E75118 /* PollVotingView.swift */,
+			);
+			name = Polls;
+			sourceTree = "<group>";
+		};
+		1F46CE2728E05ACF00E7D88E /* References */ = {
+			isa = PBXGroup;
+			children = (
+				1FDE7C9928DE14A200CB718E /* ReferenceView.swift */,
+				1FDE7C9B28DE14B000CB718E /* ReferenceView.xib */,
+				1F46CE2828E05B3200E7D88E /* ReferenceDefaultView.swift */,
+				1F46CE2A28E05B3C00E7D88E /* ReferenceDefaultView.xib */,
+				1FCE3D562C9C4D18009C68A9 /* ReferenceGiphyView.swift */,
+				1FCE3D582C9C4D21009C68A9 /* ReferenceGiphyView.xib */,
+				1F24B5A128E0648600654457 /* ReferenceGithubView.swift */,
+				1F24B5A328E0649200654457 /* ReferenceGithubView.xib */,
+				1FFF41612C70937B00162F4D /* ReferenceZammadView.swift */,
+				1FFF41632C70938700162F4D /* ReferenceZammadView.xib */,
+				1FEC459D2A02BCB900A636AA /* ReferenceGithubPermalinkView.swift */,
+				1FEC459B2A02BCAE00A636AA /* ReferenceGithubPermalinkView.xib */,
+				1FE0C56D2A0531270083576A /* ReferenceTalkView.swift */,
+				1FE0C56B2A0531200083576A /* ReferenceTalkView.xib */,
+				1FEC45A22A02F92700A636AA /* GithubPermalinkViewController.swift */,
+				1FEC45A42A02F92B00A636AA /* GithubPermalinkViewController.xib */,
+				1F98DF9B28E7484700E05174 /* ReferenceDeckView.swift */,
+				1F98DF9D28E7485000E05174 /* ReferenceDeckView.xib */,
+			);
+			name = References;
+			sourceTree = "<group>";
+		};
+		1F6D8C312B2E3756004376B8 /* NextcloudTalkTests */ = {
+			isa = PBXGroup;
+			children = (
+				1F6D8C452B2F27A3004376B8 /* Common */,
+				1F6D8C462B2F27BB004376B8 /* UI */,
+				1F6D8C3A2B2F236D004376B8 /* Integration */,
+				1F6D8C3B2B2F237F004376B8 /* Unit */,
+			);
+			path = NextcloudTalkTests;
+			sourceTree = "<group>";
+		};
+		1F6D8C3A2B2F236D004376B8 /* Integration */ = {
+			isa = PBXGroup;
+			children = (
+				1F6D8C422B2F26EE004376B8 /* Helpers.swift */,
+				1F6D8C4A2B2F5B61004376B8 /* TestBase.swift */,
+				1F6D8C322B2E3756004376B8 /* IntegrationRoomTest.swift */,
+				1F6629F92C17700E001C6C0E /* IntegrationRoomsManagerTest.swift */,
+				1F6D8C4C2B2F8FE5004376B8 /* IntegrationChatTest.swift */,
+			);
+			path = Integration;
+			sourceTree = "<group>";
+		};
+		1F6D8C3B2B2F237F004376B8 /* Unit */ = {
+			isa = PBXGroup;
+			children = (
+				1F8AAC602C5962BE004DA20A /* Chat */,
+				1FBC3BE82B61BD09003909E0 /* TestBaseRealm.swift */,
+				1FB7B9862BE441450093CE98 /* UIViewExtensions.swift */,
+				1F5CDAE62B3B05110040ECC0 /* UnitColorGeneratorTest.swift */,
+				1F1B0F242BD94A0D003FD766 /* UnitDarwinCenterTest.swift */,
+				1F0B0A762BA26BE10073FF8D /* UnitMentionSuggestionTest.swift */,
+				1FF4DAA72C08DE3A00C1B952 /* UnitNCRoomsManagerTest.swift */,
+				1F1DF8402C63C25900E5EA86 /* UnitNCDatabaseManager.swift */,
+				1F8AAC612C596308004DA20A /* UnitSignalingSettings.swift */,
+			);
+			path = Unit;
+			sourceTree = "<group>";
+		};
+		1F6D8C452B2F27A3004376B8 /* Common */ = {
+			isa = PBXGroup;
+			children = (
+				1F6D8C402B2F26D5004376B8 /* TestConstants.swift */,
+			);
+			path = Common;
+			sourceTree = "<group>";
+		};
+		1F6D8C462B2F27BB004376B8 /* UI */ = {
+			isa = PBXGroup;
+			children = (
+				1F6D8C3C2B2F23C4004376B8 /* Helpers.swift */,
+				1F6D8C472B2F2F69004376B8 /* AAAALoginTest.swift */,
+				1FD8AD8C2A3A162100787C16 /* UIRoomTest.swift */,
+			);
+			path = UI;
+			sourceTree = "<group>";
+		};
+		1F77A60E2AB9B14D007B6037 /* Screensharing */ = {
+			isa = PBXGroup;
+			children = (
+				1F77A6202AB9EB06007B6037 /* SocketConnection.h */,
+				1F77A6212AB9EB06007B6037 /* SocketConnection.m */,
+				1F77A61C2AB9B301007B6037 /* CapturerEventsDelegate.h */,
+				1F77A6122AB9B161007B6037 /* ScreenCaptureController.h */,
+				1F77A6112AB9B161007B6037 /* ScreenCaptureController.m */,
+				1F77A6142AB9B161007B6037 /* ScreenCapturer.h */,
+				1F77A6132AB9B161007B6037 /* ScreenCapturer.m */,
+				1F77A6252ABA0CD9007B6037 /* NCScreensharingController.h */,
+				1F77A6262ABA0CD9007B6037 /* NCScreensharingController.m */,
+			);
+			name = Screensharing;
+			sourceTree = "<group>";
+		};
+		1F8AAC602C5962BE004DA20A /* Chat */ = {
+			isa = PBXGroup;
+			children = (
+				1FB7B9882BE442400093CE98 /* UnitBaseChatTableViewCellTest.swift */,
+				1FBC3BE42B61ACD5003909E0 /* UnitBaseChatViewControllerTest.swift */,
+				1FB7B9842BE2EE020093CE98 /* UnitChatViewControllerTest.swift */,
+				1FF4DAA52C08D81D00C1B952 /* UnitNCChatMessageTest.swift */,
+			);
+			path = Chat;
+			sourceTree = "<group>";
+		};
+		1F90EFB925FE398B00F3FA55 /* Siri */ = {
+			isa = PBXGroup;
+			children = (
+				1F90EFBA25FE39F800F3FA55 /* NCIntentController.h */,
+				1F90EFBB25FE39F800F3FA55 /* NCIntentController.m */,
+			);
+			name = Siri;
+			sourceTree = "<group>";
+		};
+		1FB78E242B6AE58100B0D69D /* Federation */ = {
+			isa = PBXGroup;
+			children = (
+				1FADECD92B8227B1007AD94B /* FederationInvitationCell.xib */,
+				1FADECD72B82269E007AD94B /* FederationInvitationCell.swift */,
+				1FADECD52B821E24007AD94B /* FederationInvitationTableViewController.swift */,
+				1FB78E252B6AE5A600B0D69D /* FederationInvitation.swift */,
+			);
+			name = Federation;
+			sourceTree = "<group>";
+		};
+		1FB7B9932BF0DE4A0093CE98 /* Banned Actors */ = {
+			isa = PBXGroup;
+			children = (
+				1FB7B99B2BF0DF360093CE98 /* BannedActorCell.xib */,
+				1FB7B9992BF0DF290093CE98 /* BannedActorCell.swift */,
+				1FB7B9942BF0DF1C0093CE98 /* BannedActorTableViewController.swift */,
+				1FB7B98D2BF0CBA60093CE98 /* BannedActor.swift */,
+			);
+			name = "Banned Actors";
+			sourceTree = "<group>";
+		};
+		1FF2FD5E2AB99CCB000C9905 /* BroadcastUploadExtension */ = {
+			isa = PBXGroup;
+			children = (
+				1F77A61F2AB9D82B007B6037 /* BroadcastUploadExtension.entitlements */,
+				1FF2FD7A2AB99E4D000C9905 /* Atomic.swift */,
+				1FF2FD792AB99E4D000C9905 /* DarwinNotificationCenter.swift */,
+				1F77A6232ABA0003007B6037 /* SampleHandler.swift */,
+				1FF2FD7B2AB99E4D000C9905 /* SampleUploader.swift */,
+				1FF2FD7D2AB99E4D000C9905 /* SocketConnection.swift */,
+				1FF2FD612AB99CCB000C9905 /* Info.plist */,
+			);
+			path = BroadcastUploadExtension;
+			sourceTree = "<group>";
+		};
+		2C0574741EDD9E8E00D9E7F2 = {
+			isa = PBXGroup;
+			children = (
+				2CC0015024A1F0E900A20167 /* NotificationServiceExtension */,
+				2C62AFA424C08845007E460A /* ShareExtension */,
+				1FF2FD5E2AB99CCB000C9905 /* BroadcastUploadExtension */,
+				1F6D8C312B2E3756004376B8 /* NextcloudTalkTests */,
+				2C05747E1EDD9E8E00D9E7F2 /* Products */,
+				2C05749C1EDDA01700D9E7F2 /* ThirdParty */,
+				2C05747F1EDD9E8E00D9E7F2 /* NextcloudTalk */,
+				2C90E5621EDDE0FB0093D85A /* Frameworks */,
+				926177EBCFB97EA1273DEDB9 /* Pods */,
+			);
+			sourceTree = "<group>";
+		};
+		2C05747E1EDD9E8E00D9E7F2 /* Products */ = {
+			isa = PBXGroup;
+			children = (
+				2C05747D1EDD9E8E00D9E7F2 /* NextcloudTalk.app */,
+				2CC0014F24A1F0E900A20167 /* NotificationServiceExtension.appex */,
+				2C62AFA324C08845007E460A /* ShareExtension.appex */,
+				1FD8AD8A2A3A162100787C16 /* NextcloudTalkUITests.xctest */,
+				1FF2FD5B2AB99CCB000C9905 /* BroadcastUploadExtension.appex */,
+				1F6D8C302B2E3756004376B8 /* NextcloudTalkTests.xctest */,
+			);
+			name = Products;
+			sourceTree = "<group>";
+		};
+		2C05747F1EDD9E8E00D9E7F2 /* NextcloudTalk */ = {
+			isa = PBXGroup;
+			children = (
+				2C0574831EDD9E8E00D9E7F2 /* AppDelegate.h */,
+				2C0574841EDD9E8E00D9E7F2 /* AppDelegate.m */,
+				2C5521691F7D48480077E587 /* Calls */,
+				2CF0679E208A2A430070A79B /* Chat */,
+				2C5521671F7D47BA0077E587 /* Contacts */,
+				2C40281622832EF90000DDFC /* Database */,
+				1FB78E242B6AE58100B0D69D /* Federation */,
+				2C8A2BCA221F096200DE6D2C /* File sharing */,
+				2C5521681F7D481C0077E587 /* Login */,
+				2CB6ACA72638489300D3D641 /* Maps */,
+				1F1B0F3A2BE0476C003FD766 /* Media Viewer */,
+				2C55216B1F7D48970077E587 /* Network */,
+				2C9B0B99217F6E3400A4752C /* Notifications */,
+				2C5521661F7D47A50077E587 /* Rooms */,
+				1F77A60E2AB9B14D007B6037 /* Screensharing */,
+				2CBF82B31FCC7DC100636459 /* Security */,
+				2C55216A1F7D48850077E587 /* Settings */,
+				1F90EFB925FE398B00F3FA55 /* Siri */,
+				2C0574801EDD9E8E00D9E7F2 /* Supporting Files */,
+				2C0633102046CCA60043481A /* User Interface */,
+				2C78EF961F7E720A008AFA74 /* WebRTC */,
+				2C6085C11FB1063700B36A6E /* NextcloudTalk.entitlements */,
+				2CA1CCAB1F067F35002FE6A2 /* Images.xcassets */,
+				2C0574941EDD9E8E00D9E7F2 /* Info.plist */,
+				1FADECD42B7EACCB007AD94B /* PrivacyInfo.xcprivacy */,
+				2C05748C1EDD9E8E00D9E7F2 /* Main.storyboard */,
+				2C1D13A1253760EE00EC0533 /* LaunchScreen.xib */,
+				2CB052A02BF2297500191349 /* connecting.mp3 */,
+				2C7F47AC20289B9600081CC7 /* Localizable.strings */,
+				2C330374255E6EBC00BDB4E4 /* InfoPlist.strings */,
+				2C4446EE265D454200DF1DBC /* NotificationCenterNotifications.h */,
+				2C4446EF265D454200DF1DBC /* NotificationCenterNotifications.m */,
+				2CEDA87E26EF91460044552B /* NextcloudTalk-Bridging-Header.h */,
+				1FDCC3EC29EC7DD400DEB39B /* NextcloudTalk-Bridging-Header-Extensions.h */,
+				2C477C1828B79D980044DEB4 /* Localizable.stringsdict */,
+				1FAB2EEF2AD1EAA3001214EB /* RLMSupport.swift */,
+			);
+			path = NextcloudTalk;
+			sourceTree = "<group>";
+		};
+		2C0574801EDD9E8E00D9E7F2 /* Supporting Files */ = {
+			isa = PBXGroup;
+			children = (
+				2C0574811EDD9E8E00D9E7F2 /* main.m */,
+			);
+			name = "Supporting Files";
+			sourceTree = "<group>";
+		};
+		2C05749C1EDDA01700D9E7F2 /* ThirdParty */ = {
+			isa = PBXGroup;
+			children = (
+				2CC1C37F29C0945600C8436B /* DRCellSlideGestureRecognizer */,
+				2CB302F92264775E0053078A /* SlackTextViewController */,
+				2C4D7D601F2F7C2C00FF4A0D /* AppRTC */,
+			);
+			name = ThirdParty;
+			sourceTree = "<group>";
+		};
+		2C0633102046CCA60043481A /* User Interface */ = {
+			isa = PBXGroup;
+			children = (
+				1F1B0F262BDA61C5003FD766 /* AllocationTracker.swift */,
+				1F5813F628EB23EF00318FC3 /* NCSplitViewController.swift */,
+				1F5813F728EB23EF00318FC3 /* NCSplitViewPlaceholderViewController.swift */,
+				2C06330D2046CC8B0043481A /* NCUserInterfaceController.h */,
+				2C06330E2046CC8B0043481A /* NCUserInterfaceController.m */,
+				2C3780C1210F49DC003F9AE8 /* HeaderWithButton.h */,
+				2C3780C2210F49DC003F9AE8 /* HeaderWithButton.m */,
+				2C3780C4210F4A26003F9AE8 /* HeaderWithButton.xib */,
+				2CC7158A20B8394A0045C789 /* PlaceholderView.h */,
+				2CC7158B20B8394A0045C789 /* PlaceholderView.m */,
+				2CC7158820B837140045C789 /* PlaceholderView.xib */,
+				2C9200C12474262C0050084F /* UIBarButtonItem+Badge.h */,
+				2C9200C22474262C0050084F /* UIBarButtonItem+Badge.m */,
+				2C1EF36925505DCE007C9768 /* NCNavigationController.h */,
+				2C1EF36A25505DCE007C9768 /* NCNavigationController.m */,
+				1F3D3B21255F109E00230DAE /* BarButtonItemWithActivity.h */,
+				1F3D3B20255F109E00230DAE /* BarButtonItemWithActivity.m */,
+				2C36A048261487BC0026F04A /* DetailedOptionsSelectorTableViewController.h */,
+				2C36A049261487BC0026F04A /* DetailedOptionsSelectorTableViewController.m */,
+				2CEDA88B26F492610044552B /* NSMutableAttributedString+Extensions.swift */,
+				1F61C76A285F65E1004D74D8 /* SimpleTableViewController.swift */,
+				1FB6678E28CE381300D29F8D /* SubtitleTableViewCell.swift */,
+				1F468E7728DCC7310099597B /* EmojiTextField.swift */,
+				1F371A362A7B921A006CBFB3 /* DatePickerTextField.swift */,
+				2C16A82B28E7284D00EDE523 /* NCButton.swift */,
+				1F11FB7129C07B04001E21E7 /* NCZoomableView.swift */,
+				1F8995B22970644C00CABA33 /* ColorGenerator.swift */,
+				1F1C0D8829AFB89900D17C6D /* VLCKitVideoViewController.swift */,
+				1F1C0D8629AFB88800D17C6D /* VLCKitVideoViewController.xib */,
+				1F90DA0329E9A28E00E81E3D /* AvatarManager.swift */,
+				1FDCC3D329EBF6E700DEB39B /* AvatarImageView.swift */,
+				1FDCC3EF29ECB4CE00DEB39B /* AvatarButton.swift */,
+				1F3C41A229EDF05700F58435 /* AvatarEditView.swift */,
+				1F3C41A429EDF0B800F58435 /* AvatarEditView.xib */,
+				2CB997C32A052449003C41AC /* EmojiAvatarPickerViewController.swift */,
+				2CB997C42A052449003C41AC /* EmojiAvatarPickerViewController.xib */,
+				1FAB2EED2AD1BC1B001214EB /* UIControlExtensions.swift */,
+				1FDFC94C2BA50B9100670DF4 /* UIFontExtension.swift */,
+				2CBD0D592C8770A40013C089 /* UIImageExtension.swift */,
+				1F1B0F312BDC57E3003FD766 /* UIPageViewControllerExtension.swift */,
+				1F1B0F352BDD8B9C003FD766 /* NCActivityIndicator.swift */,
+			);
+			name = "User Interface";
+			sourceTree = "<group>";
+		};
+		2C40281622832EF90000DDFC /* Database */ = {
+			isa = PBXGroup;
+			children = (
+				2C40281322832EED0000DDFC /* NCDatabaseManager.h */,
+				2C40281422832EED0000DDFC /* NCDatabaseManager.m */,
+				1FDB47F72C9C7E3F00D6F423 /* NCDatabaseManager.swift */,
+				2C4446D12658147900DF1DBC /* TalkAccount.h */,
+				2C4446D22658147900DF1DBC /* TalkAccount.m */,
+				1FDB47F52C9C71CE00D6F423 /* TalkAccount.swift */,
+				1F1B50452B90CDE600B0F2F4 /* TalkCapabilities.h */,
+				1F1B50462B90CDF800B0F2F4 /* TalkCapabilities.m */,
+				2C4446D6265814D100DF1DBC /* ServerCapabilities.h */,
+				2C4446D7265814D100DF1DBC /* ServerCapabilities.m */,
+				1F1B50422B9095C900B0F2F4 /* FederatedCapabilities.h */,
+				1F1B50432B9095D100B0F2F4 /* FederatedCapabilities.m */,
+				2C4446DB2658158000DF1DBC /* NCChatBlock.h */,
+				2C4446DC2658158000DF1DBC /* NCChatBlock.m */,
+				1FD9182828C55A73009092AB /* BGTaskHelper.swift */,
+			);
+			name = Database;
+			sourceTree = "<group>";
+		};
+		2C4D7D601F2F7C2C00FF4A0D /* AppRTC */ = {
+			isa = PBXGroup;
+			children = (
+				2C4D7D611F2F7C2C00FF4A0D /* ARDCaptureController.h */,
+				2C4D7D621F2F7C2C00FF4A0D /* ARDCaptureController.m */,
+				2C9E6CCC1F6F34F000399B7A /* ARDSDPUtils.h */,
+				2C9E6CCD1F6F34F000399B7A /* ARDSDPUtils.m */,
+				2C4D7D641F2F7DBC00FF4A0D /* ARDSettingsModel.h */,
+				2C4D7D651F2F7DBC00FF4A0D /* ARDSettingsModel.m */,
+				2C4D7D661F2F7DBC00FF4A0D /* ARDSettingsModel+Private.h */,
+				2C4D7D671F2F7DBC00FF4A0D /* ARDSettingsStore.h */,
+				2C4D7D681F2F7DBC00FF4A0D /* ARDSettingsStore.m */,
+				2C4D7D741F30F7B600FF4A0D /* ARDUtilities.h */,
+				2C4D7D751F30F7B600FF4A0D /* ARDUtilities.m */,
+				2C4D7D6D1F309DA500FF4A0D /* RTCIceCandidate+JSON.h */,
+				2C4D7D6E1F309DA500FF4A0D /* RTCIceCandidate+JSON.m */,
+				2C4D7D6F1F309DA500FF4A0D /* RTCSessionDescription+JSON.h */,
+				2C4D7D701F309DA500FF4A0D /* RTCSessionDescription+JSON.m */,
+			);
+			name = AppRTC;
+			path = ThirdParty/AppRTC;
+			sourceTree = "<group>";
+		};
+		2C5521661F7D47A50077E587 /* Rooms */ = {
+			isa = PBXGroup;
+			children = (
+				1FB7B9932BF0DE4A0093CE98 /* Banned Actors */,
+				1FE7DE312BB459B10040EE12 /* RoomInvitationViewCell.xib */,
+				1FE7DE2F2BB4598F0040EE12 /* RoomInvitationViewCell.swift */,
+				2C06BF6B20AEB0030031EB46 /* RoundedNumberView.h */,
+				2C06BF6A20AEB0030031EB46 /* RoundedNumberView.m */,
+				2CA1CCC11F166CC5002FE6A2 /* NCRoom.h */,
+				2CA1CCC21F166CC5002FE6A2 /* NCRoom.m */,
+				1FF1360E2BFB4F8C006A6101 /* NCRoom.swift */,
+				2CA1CCA21F025F64002FE6A2 /* RoomsTableViewController.h */,
+				2CA1CCA31F025F64002FE6A2 /* RoomsTableViewController.m */,
+				1FF4DA9A2C032AAC00C1B952 /* RoomTableViewCell.swift */,
+				2C98F77C216231D3001A6A73 /* RoomTableViewCell.xib */,
+				2C06BF5B20A89F510031EB46 /* NCRoomsManager.h */,
+				2C06BF5C20A89F510031EB46 /* NCRoomsManager.m */,
+				1FD6F83B2B825069004048AB /* NCRoomsManagerExtensions.swift */,
+				1FF4DA9F2C03351E00C1B952 /* RoomNameTableViewCell.swift */,
+				2CC007C420D90AE50096D91F /* RoomNameTableViewCell.xib */,
+				1FF4DAA12C0338D000C1B952 /* RoomDescriptionTableViewCell.swift */,
+				2C4CDCCB269618230023F403 /* RoomDescriptionTableViewCell.xib */,
+				2C440D0E20EA4A770005F9BB /* RoomInfoTableViewController.h */,
+				2C440D0F20EA4A770005F9BB /* RoomInfoTableViewController.m */,
+				2C440D1020EA4A770005F9BB /* RoomInfoTableViewController.xib */,
+				2C3780BB2107209C003F9AE8 /* NCRoomParticipant.h */,
+				2C3780BC2107209C003F9AE8 /* NCRoomParticipants.m */,
+				2C98F77721622445001A6A73 /* RoomSearchTableViewController.h */,
+				2C98F77821622445001A6A73 /* RoomSearchTableViewController.m */,
+				2CC1FF4228147F10009F7288 /* RoomSharedItemsTableViewController.swift */,
+				2CC1FF4328147F10009F7288 /* RoomSharedItemsTableViewController.xib */,
+				1F3C419E29EDAC7D00F58435 /* RoomAvatarInfoTableViewController.swift */,
+				1F3C41A029EDAC8800F58435 /* RoomAvatarInfoTableViewController.xib */,
+				2CD80F472A4304AD00919057 /* OpenConversationsTableViewController.swift */,
+				2C2D7A162B8C9C0000642373 /* RoomCreationTableViewController.swift */,
+				2C2145672BF6B8E900470C0C /* NewRoomTableViewController.swift */,
+			);
+			name = Rooms;
+			sourceTree = "<group>";
+		};
+		2C5521671F7D47BA0077E587 /* Contacts */ = {
+			isa = PBXGroup;
+			children = (
+				2CA1CCD31F1E664C002FE6A2 /* ContactsTableViewCell.h */,
+				2CA1CCD41F1E664C002FE6A2 /* ContactsTableViewCell.m */,
+				2CA1CCD51F1E664C002FE6A2 /* ContactsTableViewCell.xib */,
+				2CA1CCCB1F181741002FE6A2 /* NCUser.h */,
+				2CA1CCCC1F181741002FE6A2 /* NCUser.m */,
+				2CA1CCCE1F1E1779002FE6A2 /* SearchTableViewController.h */,
+				2CA1CCCF1F1E1779002FE6A2 /* SearchTableViewController.m */,
+				2C7A123F2017872600864818 /* AddParticipantsTableViewController.h */,
+				2C7A12402017872600864818 /* AddParticipantsTableViewController.m */,
+				2C7A12412017872600864818 /* AddParticipantsTableViewController.xib */,
+				2CC007B220D7AE990096D91F /* ResultMultiSelectionTableViewController.h */,
+				2CC007B320D7AE990096D91F /* ResultMultiSelectionTableViewController.m */,
+				2C1ABDCC257E939600AEDFB6 /* NCContact.h */,
+				2C1ABDCD257E939600AEDFB6 /* NCContact.m */,
+				2C1ABDC4257A7CF000AEDFB6 /* NCContactsManager.h */,
+				2C1ABDC5257A7CF000AEDFB6 /* NCContactsManager.m */,
+				2C1ABDE3257F883400AEDFB6 /* ABContact.h */,
+				2C1ABDE4257F883400AEDFB6 /* ABContact.m */,
+				2CD4F6B62C11C80600ED594F /* ContactsSearchResultTableViewContoller.swift */,
+			);
+			name = Contacts;
+			sourceTree = "<group>";
+		};
+		2C5521681F7D481C0077E587 /* Login */ = {
+			isa = PBXGroup;
+			children = (
+				2C90E5D01EE80C870093D85A /* AuthenticationViewController.h */,
+				2C90E5D11EE80C870093D85A /* AuthenticationViewController.m */,
+				2C0574A11EDDA2E300D9E7F2 /* LoginViewController.h */,
+				2C0574A21EDDA2E300D9E7F2 /* LoginViewController.m */,
+				2C0574A31EDDA2E300D9E7F2 /* LoginViewController.xib */,
+				1FA20C89284001D80062B4F3 /* DebounceWebView.swift */,
+				1FB52E752842C75E00AC741B /* QRCodeLoginController.swift */,
+			);
+			name = Login;
+			sourceTree = "<group>";
+		};
+		2C5521691F7D48480077E587 /* Calls */ = {
+			isa = PBXGroup;
+			children = (
+				2C78EFA21F86FF4A008AFA74 /* CallParticipantViewCell.h */,
+				2C78EFA31F86FF4A008AFA74 /* CallParticipantViewCell.m */,
+				2C78EFA41F86FF4A008AFA74 /* CallParticipantViewCell.xib */,
+				2C78EF9D1F828C41008AFA74 /* CallViewController.h */,
+				2C78EF9E1F828C41008AFA74 /* CallViewController.m */,
+				2C78EF9F1F828C41008AFA74 /* CallViewController.xib */,
+				2C78EF9A1F826B22008AFA74 /* NCCallController.h */,
+				2C78EF9B1F826B22008AFA74 /* NCCallController.m */,
+				2C8CDD0421C2EDE8004E2997 /* AvatarBackgroundImageView.h */,
+				2C8CDD0521C2EDE8004E2997 /* AvatarBackgroundImageView.m */,
+				2C4987BB21E640E20060AC27 /* CallKitManager.h */,
+				2C4987BC21E640E20060AC27 /* CallKitManager.m */,
+				2C4DE9F021F732B40096940D /* NCAudioController.h */,
+				2C4DE9F121F732B40096940D /* NCAudioController.m */,
+				2C4446FA265D5BEF00DF1DBC /* CallConstants.h */,
+				1FA732FB2966CBB7003D2103 /* CallFlowLayout.swift */,
+				2C84BCCB29EEB9C6001BA6DA /* CallReactionView.swift */,
+				2C84BCCD29EEDCE8001BA6DA /* CallReactionView.xib */,
+				1FE94733293CE55600D6584C /* NCCameraController.swift */,
+			);
+			name = Calls;
+			sourceTree = "<group>";
+		};
+		2C55216A1F7D48850077E587 /* Settings */ = {
+			isa = PBXGroup;
+			children = (
+				2C2A788C2359CC8800EEB797 /* NCAppBranding.h */,
+				2C2A788D2359CC8800EEB797 /* NCAppBranding.m */,
+				2CA1CC931F014EF9002FE6A2 /* NCSettingsController.h */,
+				2CA1CC941F014EF9002FE6A2 /* NCSettingsController.m */,
+				1FDDB0E42AFD046600FBAFB7 /* NCUtils.swift */,
+				DA75580E278EEA1000A48A1B /* SettingsTableViewController.swift */,
+				1F61C766285E35A6004D74D8 /* DiagnosticsTableViewController.swift */,
+				1F7625E42901B0DB00834869 /* CallsFromOldAccountViewController.swift */,
+				1F7625E62901B0E800834869 /* CallsFromOldAccountViewController.xib */,
+				2C7A1235200E0A5700864818 /* UserSettingsTableViewCell.xib */,
+				DA755810278EF3EF00A48A1B /* UserSettingsTableViewCell.swift */,
+				2C78E9E125120DE500E3D4CA /* NCUserStatus.h */,
+				2C78E9E225120DE500E3D4CA /* NCUserStatus.m */,
+				1FD6F83D2B87B712004048AB /* NCUserStatusExtensions.swift */,
+				807E30752A83A90F00089D28 /* UserStatusOptionsSwiftUI.swift */,
+				80832B752A822E5100195A97 /* UserStatusSwiftUIView.swift */,
+				80832B772A823D0700195A97 /* UserStatusMessageSwiftUIView.swift */,
+				DA8801A127A2DA00009EF248 /* UserProfileTableViewController.swift */,
+				DA66582A27B6992F00B46B11 /* UserProfileTableViewController+AvatarSetup.swift */,
+				DA66582C27B6A73800B46B11 /* UserProfileTableViewController+DelegateMethods.swift */,
+				DA66582E27B6B19C00B46B11 /* UserProfileTableViewController+Actions.swift */,
+				DA66583027B6B24E00B46B11 /* UserProfileTableViewController+Utils.swift */,
+				DA8801A327AC52AC009EF248 /* TextInputTableViewCell.swift */,
+				2CF9CBFB26025F64002246EF /* TextInputTableViewCell.xib */,
+				2C4446EA265D25BA00DF1DBC /* NCKeyChainController.h */,
+				2C4446EB265D25BA00DF1DBC /* NCKeyChainController.m */,
+				2C444701265D641300DF1DBC /* NCUserDefaults.h */,
+				2C444702265D641300DF1DBC /* NCUserDefaults.m */,
+			);
+			name = Settings;
+			sourceTree = "<group>";
+		};
+		2C55216B1F7D48970077E587 /* Network */ = {
+			isa = PBXGroup;
+			children = (
+				2CA1CCA81F02D1A4002FE6A2 /* NCAPIController.h */,
+				2CA1CCA91F02D1A4002FE6A2 /* NCAPIController.m */,
+				1FB78E1E2B6ADBAA00B0D69D /* NCAPIControllerExtensions.swift */,
+				2CA1CC8F1F014354002FE6A2 /* NCConnectionController.h */,
+				2CA1CC901F014354002FE6A2 /* NCConnectionController.m */,
+				1FF4DA862C02626D00C1B952 /* NCBaseSessionManager.swift */,
+				1FF4DA812C025DB900C1B952 /* NCAPISessionManager.swift */,
+				1FF4DA8B2C0263A200C1B952 /* NCPushProxySessionManager.swift */,
+				1FF4DA902C02677C00C1B952 /* NCImageSessionManager.swift */,
+				2C5BFBE928772A9A00E75118 /* NCUnifiedSearchController.swift */,
+				1FF4DA952C0327FF00C1B952 /* NCWebImageDownloaderOperation.swift */,
+				1FF4DAA92C0A114900C1B952 /* OcsResponse.swift */,
+			);
+			name = Network;
+			sourceTree = "<group>";
+		};
+		2C62AFA424C08845007E460A /* ShareExtension */ = {
+			isa = PBXGroup;
+			children = (
+				2C1ABD8325769E7D00AEDFB6 /* ShareConfirmationCollectionViewCell.h */,
+				2C1ABD8025769E7D00AEDFB6 /* ShareConfirmationCollectionViewCell.m */,
+				2C1ABD8425769E7D00AEDFB6 /* ShareConfirmationCollectionViewCell.xib */,
+				2C1ABD8125769E7D00AEDFB6 /* ShareItem.h */,
+				2C1ABD8525769E7D00AEDFB6 /* ShareItem.m */,
+				2C1ABD7F25769E7C00AEDFB6 /* ShareItemController.h */,
+				2C1ABD8225769E7D00AEDFB6 /* ShareItemController.m */,
+				2C3195BB24C1F58A0066F221 /* ShareExtension.entitlements */,
+				2C62AFAB24C08845007E460A /* Info.plist */,
+				2C62AFB524C1A449007E460A /* Share.storyboard */,
+				2C62AFB724C1A4E6007E460A /* ShareViewController.h */,
+				2C62AFB824C1A4E6007E460A /* ShareViewController.m */,
+				2C3195BF24C5E2100066F221 /* ShareTableViewCell.h */,
+				2C3195C024C5E2100066F221 /* ShareTableViewCell.m */,
+				2C3195C124C5E2100066F221 /* ShareTableViewCell.xib */,
+				1F35F8DF2AEEB9DE00044BDA /* ShareConfirmationViewController.swift */,
+				1FDDB0D82AF440DD00FBAFB7 /* BoundsChangedFlowLayout.swift */,
+			);
+			path = ShareExtension;
+			sourceTree = "<group>";
+		};
+		2C6DEAB2243CCC7F00AE8437 /* Chat cells */ = {
+			isa = PBXGroup;
+			children = (
+				1F4DD3EA2571C688007DC98E /* EmojiUtils.swift */,
+				1F1B50332B8E069800B0F2F4 /* BaseChatTableViewCell.swift */,
+				1F1B50372B8E070100B0F2F4 /* BaseChatTableViewCell.xib */,
+				1F1B503D2B8FB12100B0F2F4 /* BaseChatTableViewCell+Message.swift */,
+				1F1B50392B8F9E1300B0F2F4 /* BaseChatTableViewCell+File.swift */,
+				2C21446D2BB5B54D005A6537 /* BaseChatTableViewCell+Location.swift */,
+				2C04248F2CA32D45004772F6 /* BaseChatTableViewCell+Audio.swift */,
+				2C4747E52CB6710F002828F2 /* BaseChatTableViewCell+Poll.swift */,
+				2CC7159220C54D080045C789 /* ChatTableViewCell.h */,
+				2CC7159320C54D080045C789 /* ChatTableViewCell.m */,
+				1F35F9022AEEDEE800044BDA /* AutoCompletionTableViewCell.h */,
+				1F35F9032AEEDF0E00044BDA /* AutoCompletionTableViewCell.m */,
+				2C604BD7211988A700D34DCD /* SystemMessageTableViewCell.h */,
+				2C604BD8211988A700D34DCD /* SystemMessageTableViewCell.m */,
+				2C8E2A19232174C20022BFC9 /* MessageSeparatorTableViewCell.h */,
+				2C8E2A1A232174C20022BFC9 /* MessageSeparatorTableViewCell.m */,
+				1F0A1D432A5F1FA800A25433 /* SwiftMarkdownObjCBridge.swift */,
+				1F5683CE2BA7980C0023E151 /* FilePreviewImageView.swift */,
+			);
+			name = "Chat cells";
+			sourceTree = "<group>";
+		};
+		2C6DEAB3243CCCCA00AE8437 /* Chat views */ = {
+			isa = PBXGroup;
+			children = (
+				1F46CE2728E05ACF00E7D88E /* References */,
+				1F46CE2628E05AC100E7D88E /* Polls */,
+				1F46CE2528E05ABB00E7D88E /* Reactions */,
+				2C06BF6520AC647A0031EB46 /* DateHeaderView.h */,
+				2C06BF6620AC647A0031EB46 /* DateHeaderView.m */,
+				2C06BF6320AC64370031EB46 /* DateHeaderView.xib */,
+				DA1AEFC2270F1FA90088E519 /* DateLabelCustom.swift */,
+				2CC007CC20E50B0A0096D91F /* MessageBodyTextView.h */,
+				2CC007CD20E50B0A0096D91F /* MessageBodyTextView.m */,
+				2C7381542106136000CDB8DB /* NCChatTitleView.h */,
+				2C7381552106136000CDB8DB /* NCChatTitleView.m */,
+				2C738157210613A200CDB8DB /* NCChatTitleView.xib */,
+				2CA15549208F2E5700CE8EF0 /* NCMessageTextView.h */,
+				2CA1554A208F2E5700CE8EF0 /* NCMessageTextView.m */,
+				2C6E7447238C1A0800AE396C /* QuotedMessageView.h */,
+				2C6E7448238C1A0800AE396C /* QuotedMessageView.m */,
+				2C6E74442386D33200AE396C /* ReplyMessageView.h */,
+				2C6E74452386D33200AE396C /* ReplyMessageView.m */,
+				2CA52AC92670D02800619610 /* VoiceMessageRecordingView.h */,
+				2CA52ACA2670D02800619610 /* VoiceMessageRecordingView.m */,
+				2CA52ACC2670D07900619610 /* VoiceMessageRecordingView.xib */,
+				1F66B71E29FA703B003FB168 /* TypingIndicatorView.swift */,
+				1F66B72029FA7089003FB168 /* TypingIndicatorView.xib */,
+				2C0424992CA33681004772F6 /* AudioPlayerView.swift */,
+				2C0424962CA335C4004772F6 /* AudioPlayerView.xib */,
+			);
+			name = "Chat views";
+			sourceTree = "<group>";
+		};
+		2C78EF961F7E720A008AFA74 /* WebRTC */ = {
+			isa = PBXGroup;
+			children = (
+				2C78EF931F7E70EB008AFA74 /* NCPeerConnection.h */,
+				2C78EF941F7E70EB008AFA74 /* NCPeerConnection.m */,
+				2C78EF971F80F81E008AFA74 /* NCSignalingController.h */,
+				2C78EF981F80F81E008AFA74 /* NCSignalingController.m */,
+				2C2E64231F3462AF00D39CE8 /* NCSignalingMessage.h */,
+				2C2E64241F3462AF00D39CE8 /* NCSignalingMessage.m */,
+				2CD5F3222142781A006B71BF /* NCExternalSignalingController.h */,
+				2CD5F3232142781A006B71BF /* NCExternalSignalingController.m */,
+				2C69323B2923ECAA00017AD2 /* WSMessage.h */,
+				2C69323C2923ECAA00017AD2 /* WSMessage.m */,
+				1F1DF83B2C5C17AF00E5EA86 /* TalkActor.swift */,
+				1F1DF8422C64006E00E5EA86 /* SignalingParticipant.swift */,
+				1F8AAC312C518759004DA20A /* SignalingSettings.swift */,
+				1F8AAC362C519577004DA20A /* TurnServer.swift */,
+				1F8AAC3B2C519689004DA20A /* StunServer.swift */,
+				1F8995B42973547700CABA33 /* WebRTCCommon.swift */,
+			);
+			name = WebRTC;
+			sourceTree = "<group>";
+		};
+		2C8A2BCA221F096200DE6D2C /* File sharing */ = {
+			isa = PBXGroup;
+			children = (
+				1FF4DA7D2C0237D000C1B952 /* DirectoryTableViewCell.swift */,
+				2C8A2BCD221FEEFE00DE6D2C /* DirectoryTableViewCell.xib */,
+				2C8A2BC7221F094F00DE6D2C /* DirectoryTableViewController.h */,
+				2C8A2BC8221F094F00DE6D2C /* DirectoryTableViewController.m */,
+			);
+			name = "File sharing";
+			sourceTree = "<group>";
+		};
+		2C90E5621EDDE0FB0093D85A /* Frameworks */ = {
+			isa = PBXGroup;
+			children = (
+				1FDDB0E82AFE8F5C00FBAFB7 /* MobileCoreServices.framework */,
+				1F90EFC225FE489B00F3FA55 /* IntentsUI.framework */,
+				2CA1CCC91F17C503002FE6A2 /* AudioToolbox.framework */,
+				2CA1CC961F016117002FE6A2 /* Security.framework */,
+				2C90E5CE1EDF23A00093D85A /* WebKit.framework */,
+				2C90E5681EDDE13A0093D85A /* UIKit.framework */,
+				2C90E5661EDDE1340093D85A /* CoreGraphics.framework */,
+				2C90E5631EDDE0FB0093D85A /* Foundation.framework */,
+				9B81BB7A4920C391CC2CACFD /* libPods-NotificationServiceExtension.a */,
+				684807120F4439797973DF73 /* libPods-ShareExtension.a */,
+				4F7C31E9D74F550EAF89931B /* libPods-NextcloudTalk.a */,
+				1FF2FD5C2AB99CCB000C9905 /* ReplayKit.framework */,
+				9A3D305FCD7BF7E727A62F35 /* libPods-BroadcastUploadExtension.a */,
+				7005E22D6C2896927FC3AEEC /* libPods-NextcloudTalkTests.a */,
+			);
+			name = Frameworks;
+			sourceTree = "<group>";
+		};
+		2C9B0B99217F6E3400A4752C /* Notifications */ = {
+			isa = PBXGroup;
+			children = (
+				1FA38C8F29A4B3C6008871B8 /* NCNotificationAction.swift */,
+				2C9B0B9A217F756B00A4752C /* NCNotification.h */,
+				2C9B0B9B217F756B00A4752C /* NCNotification.m */,
+				2CBF82AC1FC888FC00636459 /* NCPushNotification.h */,
+				2CBF82AD1FC888FC00636459 /* NCPushNotification.m */,
+				2C9B0B96217F6DBA00A4752C /* NCNotificationController.h */,
+				2C9B0B97217F6DBA00A4752C /* NCNotificationController.m */,
+				2C4446F1265D51A600DF1DBC /* NCPushNotificationsUtils.h */,
+				2C4446F2265D51A600DF1DBC /* NCPushNotificationsUtils.m */,
+			);
+			name = Notifications;
+			sourceTree = "<group>";
+		};
+		2CB302F92264775E0053078A /* SlackTextViewController */ = {
+			isa = PBXGroup;
+			children = (
+				2CB3039B2264775E0053078A /* Source */,
+			);
+			name = SlackTextViewController;
+			path = ThirdParty/SlackTextViewController;
+			sourceTree = "<group>";
+		};
+		2CB3039B2264775E0053078A /* Source */ = {
+			isa = PBXGroup;
+			children = (
+				2CB3039C2264775E0053078A /* SLKInputAccessoryView.h */,
+				2CB3039D2264775E0053078A /* SLKInputAccessoryView.m */,
+				2CB3039E2264775E0053078A /* SLKTextInput+Implementation.m */,
+				2CB3039F2264775E0053078A /* SLKTextInput.h */,
+				2CB303A02264775E0053078A /* SLKTextInputbar.h */,
+				2CB303A12264775E0053078A /* SLKTextInputbar.m */,
+				2CB303A22264775E0053078A /* SLKTextView+SLKAdditions.h */,
+				2CB303A32264775E0053078A /* SLKTextView+SLKAdditions.m */,
+				2CB303A42264775E0053078A /* SLKTextView.h */,
+				2CB303A52264775E0053078A /* SLKTextView.m */,
+				2CB303A62264775E0053078A /* SLKTextViewController.h */,
+				2CB303A72264775E0053078A /* SLKTextViewController.m */,
+				2CB303A82264775E0053078A /* SLKVisibleViewProtocol.h */,
+				2CB303AB2264775E0053078A /* SLKUIConstants.h */,
+				2CB303AC2264775E0053078A /* UIResponder+SLKAdditions.h */,
+				2CB303AD2264775E0053078A /* UIResponder+SLKAdditions.m */,
+				2CB303AE2264775E0053078A /* UIScrollView+SLKAdditions.h */,
+				2CB303AF2264775E0053078A /* UIScrollView+SLKAdditions.m */,
+				2CB303B02264775E0053078A /* UIView+SLKAdditions.h */,
+				2CB303B12264775E0053078A /* UIView+SLKAdditions.m */,
+				1F66B72729FA936E003FB168 /* SLKDefaultReplyView.h */,
+				1F66B72829FA936E003FB168 /* SLKDefaultReplyView.m */,
+				1F66B72A29FA9414003FB168 /* SLKDefaultTypingIndicatorView.h */,
+				1F66B72B29FA9414003FB168 /* SLKDefaultTypingIndicatorView.m */,
+			);
+			path = Source;
+			sourceTree = "<group>";
+		};
+		2CB6ACA72638489300D3D641 /* Maps */ = {
+			isa = PBXGroup;
+			children = (
+				2CB6ACB926385A3800D3D641 /* ShareLocationViewController.h */,
+				2CB6ACBA26385A3800D3D641 /* ShareLocationViewController.m */,
+				2CB6ACBB26385A3800D3D641 /* ShareLocationViewController.xib */,
+				2CB6ACC826401D5100D3D641 /* GeoLocationRichObject.h */,
+				2CB6ACC926401D5100D3D641 /* GeoLocationRichObject.m */,
+				2CB6ACE62641954700D3D641 /* MapViewController.h */,
+				2CB6ACE72641954700D3D641 /* MapViewController.m */,
+				2CB6ACE82641954700D3D641 /* MapViewController.xib */,
+			);
+			name = Maps;
+			sourceTree = "<group>";
+		};
+		2CBF82B31FCC7DC100636459 /* Security */ = {
+			isa = PBXGroup;
+			children = (
+				2CBF82B01FCC7DBA00636459 /* CCCertificate.h */,
+				2CBF82B11FCC7DBA00636459 /* CCCertificate.m */,
+			);
+			name = Security;
+			sourceTree = "<group>";
+		};
+		2CC0015024A1F0E900A20167 /* NotificationServiceExtension */ = {
+			isa = PBXGroup;
+			children = (
+				2CC0015B24A1F1D700A20167 /* NotificationServiceExtension.entitlements */,
+				2CC0015124A1F0E900A20167 /* NotificationService.h */,
+				2CC0015224A1F0E900A20167 /* NotificationService.m */,
+				2CC0015424A1F0E900A20167 /* Info.plist */,
+			);
+			path = NotificationServiceExtension;
+			sourceTree = "<group>";
+		};
+		2CC1C37F29C0945600C8436B /* DRCellSlideGestureRecognizer */ = {
+			isa = PBXGroup;
+			children = (
+				2CC1C38029C0945600C8436B /* DRCellSlideGestureRecognizer.m */,
+				2CC1C38129C0945600C8436B /* DRCellSlideAction.h */,
+				2CC1C38229C0945600C8436B /* DRCellSlideActionView.h */,
+				2CC1C38329C0945600C8436B /* DRCellSlideGestureRecognizer.h */,
+				2CC1C38429C0945700C8436B /* DRCellSlideAction.m */,
+				2CC1C38529C0945700C8436B /* DRCellSlideActionView.m */,
+			);
+			name = DRCellSlideGestureRecognizer;
+			path = ThirdParty/DRCellSlideGestureRecognizer/DRCellSlideGestureRecognizer;
+			sourceTree = "<group>";
+		};
+		2CF0679E208A2A430070A79B /* Chat */ = {
+			isa = PBXGroup;
+			children = (
+				2C6DEAB2243CCC7F00AE8437 /* Chat cells */,
+				2C6DEAB3243CCCCA00AE8437 /* Chat views */,
+				2CA1553F208E350300CE8EF0 /* NCChatMessage.h */,
+				2CA15540208E350300CE8EF0 /* NCChatMessage.m */,
+				1FF136142BFB74C3006A6101 /* NCChatMessage.swift */,
+				2CC32E9627F5D9BD00BB8C39 /* NCChatReaction.h */,
+				2CC32E9727F5D9BD00BB8C39 /* NCChatReaction.m */,
+				2C42ADB220B58E6300296DEA /* NCChatController.h */,
+				2C42ADB320B58E6300296DEA /* NCChatController.m */,
+				1FEDE3C5257D439500853F79 /* NCChatFileController.h */,
+				1FEDE3C4257D439500853F79 /* NCChatFileController.m */,
+				1FCE3D542C9C189D009C68A9 /* NCChatFileControllerWrapper.swift */,
+				1FF4DA7F2C023FF300C1B952 /* NCChatFileStatus.swift */,
+				2C5BFBF0288A97D800E75118 /* NCPoll.h */,
+				2C5BFBF1288A97D800E75118 /* NCPoll.m */,
+				2C43BA7421309A1000B3068A /* NCMessageParameter.h */,
+				2C43BA7521309A1000B3068A /* NCMessageParameter.m */,
+				1FEDE3CD257D43AB00853F79 /* NCMessageFileParameter.h */,
+				1FEDE3CC257D43AB00853F79 /* NCMessageFileParameter.m */,
+				2CB6ACD82641483800D3D641 /* NCMessageLocationParameter.h */,
+				2CB6ACD92641483800D3D641 /* NCMessageLocationParameter.m */,
+				2CC1FF4628183958009F7288 /* NCDeckCardParameter.h */,
+				2CC1FF4728183958009F7288 /* NCDeckCardParameter.m */,
+				1F785DDC2707865F00AC4B40 /* VoiceMessageTranscribeViewController.h */,
+				1F785DDA2707865F00AC4B40 /* VoiceMessageTranscribeViewController.m */,
+				1F785DDB2707865F00AC4B40 /* VoiceMessageTranscribeViewController.xib */,
+				2CF8AD3D2A0010FB00A4D3E6 /* MessageTranslationViewController.swift */,
+				2CF8AD3E2A0010FB00A4D3E6 /* MessageTranslationViewController.xib */,
+				1F5A24322ADA77DA009939FE /* InputbarViewController.swift */,
+				1FAB2E822AC9EC3F001214EB /* BaseChatViewController.swift */,
+				1FAB2E842ACB482B001214EB /* ChatViewController.swift */,
+				1F35F8FA2AEEDBC600044BDA /* ChatViewControllerExtension.swift */,
+				2C4230F62B207AB00013E1FA /* ContextChatViewController.swift */,
+				1F0B0A712BA264540073FF8D /* MentionSuggestion.swift */,
+			);
+			name = Chat;
+			sourceTree = "<group>";
+		};
+		926177EBCFB97EA1273DEDB9 /* Pods */ = {
+			isa = PBXGroup;
+			children = (
+				D6DF51D976DC0F681FF83F7B /* Pods-NotificationServiceExtension.debug.xcconfig */,
+				D86091EC1125C3057B9A299B /* Pods-NotificationServiceExtension.release.xcconfig */,
+				584BF273DF09DE4D5EE0DA0F /* Pods-ShareExtension.debug.xcconfig */,
+				95D756208A81284B975853EC /* Pods-ShareExtension.release.xcconfig */,
+				4202C63030F0FFBB1C16D75E /* Pods-NextcloudTalk.debug.xcconfig */,
+				4D4C7BF2F97F47B0D9094618 /* Pods-NextcloudTalk.release.xcconfig */,
+				82CD0527E04B844CAD762ADE /* Pods-BroadcastUploadExtension.debug.xcconfig */,
+				A8F95DE6635ABC1E64CA8E4A /* Pods-BroadcastUploadExtension.release.xcconfig */,
+				342600BABD1AD1FCA48B5E59 /* Pods-NextcloudTalkTests.debug.xcconfig */,
+				B7874918820589BF8FD69BED /* Pods-NextcloudTalkTests.release.xcconfig */,
+			);
+			name = Pods;
+			sourceTree = "<group>";
+		};
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+		1F6D8C2F2B2E3756004376B8 /* NextcloudTalkTests */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = 1F6D8C362B2E3756004376B8 /* Build configuration list for PBXNativeTarget "NextcloudTalkTests" */;
+			buildPhases = (
+				3A811BF7A761836E61C82B80 /* [CP] Check Pods Manifest.lock */,
+				1F6D8C2C2B2E3756004376B8 /* Sources */,
+				1F6D8C2D2B2E3756004376B8 /* Frameworks */,
+				1F6D8C2E2B2E3756004376B8 /* Resources */,
+			);
+			buildRules = (
+			);
+			dependencies = (
+				1F6D8C352B2E3756004376B8 /* PBXTargetDependency */,
+			);
+			name = NextcloudTalkTests;
+			packageProductDependencies = (
+				1F759C082B63B9A7000534AB /* SDWebImage */,
+				1F759C0A2B63B9A7000534AB /* SDWebImageSVGKitPlugin */,
+				1F759C0D2B63B9BA000534AB /* WebRTC */,
+				1F759C0F2B63B9D9000534AB /* OpenSSL */,
+				1F759C132B63B9D9000534AB /* QRCodeReader */,
+				1F759C152B63B9D9000534AB /* NextcloudKit */,
+				1F759C172B63B9D9000534AB /* SwiftyAttributes */,
+				1F759C192B63B9D9000534AB /* CDMarkdownKit */,
+				1F759C1B2B63B9D9000534AB /* TOCropViewController */,
+				1F759C1D2B63B9D9000534AB /* SwiftUIIntrospect */,
+				1F759C332B63CBAA000534AB /* Realm */,
+			);
+			productName = NextcloudTalkTests;
+			productReference = 1F6D8C302B2E3756004376B8 /* NextcloudTalkTests.xctest */;
+			productType = "com.apple.product-type.bundle.unit-test";
+		};
+		1FD8AD892A3A162100787C16 /* NextcloudTalkUITests */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = 1FD8AD922A3A162100787C16 /* Build configuration list for PBXNativeTarget "NextcloudTalkUITests" */;
+			buildPhases = (
+				1FD8AD862A3A162100787C16 /* Sources */,
+				1FD8AD872A3A162100787C16 /* Frameworks */,
+				1FD8AD882A3A162100787C16 /* Resources */,
+			);
+			buildRules = (
+			);
+			dependencies = (
+				1FD8AD912A3A162100787C16 /* PBXTargetDependency */,
+			);
+			name = NextcloudTalkUITests;
+			productName = NextcloudTalkUITests;
+			productReference = 1FD8AD8A2A3A162100787C16 /* NextcloudTalkUITests.xctest */;
+			productType = "com.apple.product-type.bundle.ui-testing";
+		};
+		1FF2FD5A2AB99CCB000C9905 /* BroadcastUploadExtension */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = 1FF2FD782AB99CCC000C9905 /* Build configuration list for PBXNativeTarget "BroadcastUploadExtension" */;
+			buildPhases = (
+				CBF95D503F715CE6BE80B113 /* [CP] Check Pods Manifest.lock */,
+				1FF2FD572AB99CCB000C9905 /* Sources */,
+				1FF2FD582AB99CCB000C9905 /* Frameworks */,
+				1FF2FD592AB99CCB000C9905 /* Resources */,
+			);
+			buildRules = (
+			);
+			dependencies = (
+			);
+			name = BroadcastUploadExtension;
+			packageProductDependencies = (
+				1F77A5EE2AB9A41E007B6037 /* SDWebImage */,
+				1F77A5F02AB9A423007B6037 /* SDWebImageSVGKitPlugin */,
+				1F77A5FF2AB9A50D007B6037 /* NextcloudKit */,
+				1F77A60B2AB9A5BE007B6037 /* CDMarkdownKit */,
+				1F759C312B63CBA5000534AB /* Realm */,
+				1FF136192BFBC841006A6101 /* SwiftyAttributes */,
+			);
+			productName = BroadcastUploadExtension;
+			productReference = 1FF2FD5B2AB99CCB000C9905 /* BroadcastUploadExtension.appex */;
+			productType = "com.apple.product-type.app-extension";
+		};
+		2C05747C1EDD9E8E00D9E7F2 /* NextcloudTalk */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = 2C0574971EDD9E8E00D9E7F2 /* Build configuration list for PBXNativeTarget "NextcloudTalk" */;
+			buildPhases = (
+				902A7A3EC0BDCC947AEF3EBF /* [CP] Check Pods Manifest.lock */,
+				2C0574791EDD9E8E00D9E7F2 /* Sources */,
+				2C05747A1EDD9E8E00D9E7F2 /* Frameworks */,
+				2C05747B1EDD9E8E00D9E7F2 /* Resources */,
+				A3C686B1B84C4462F93441AB /* [CP] Copy Pods Resources */,
+				2C8035721F950BA800501B5C /* ShellScript */,
+				2C5E72BC27957FCA004ED7FB /* ShellScript */,
+				C21100AE204AFC213989DA96 /* [CP] Embed Pods Frameworks */,
+				5EE5ACC62CF48BCA004D7EDB /* Embed Foundation Extensions */,
+			);
+			buildRules = (
+			);
+			dependencies = (
+				5EE5ACC52CF48BCA004D7EDB /* PBXTargetDependency */,
+				5EE5ACC92CF48BDA004D7EDB /* PBXTargetDependency */,
+				5EE5ACCC2CF48BDF004D7EDB /* PBXTargetDependency */,
+			);
+			name = NextcloudTalk;
+			packageProductDependencies = (
+				2CCCD21C2835088F00F076CE /* OpenSSL */,
+				1F468E7528DCC6C60099597B /* Dynamic */,
+				1F628CB92842BAAF0083A425 /* QRCodeReader */,
+				1F7AE07729142CA1009F72AD /* NextcloudKit */,
+				1F66B72E29FABD01003FB168 /* SwiftyAttributes */,
+				1F45A1152A01D6EC005FE87D /* SDWebImage */,
+				1F45A1202A01D8BA005FE87D /* SDWebImageSVGKitPlugin */,
+				1F0ECBF42A68274400921E90 /* CDMarkdownKit */,
+				1FAB2E7C2AC99326001214EB /* TOCropViewController */,
+				1FAB2E872ACD44D0001214EB /* WebRTC */,
+				80CDF8C32A8E098900CB57AE /* SwiftUIIntrospect */,
+				1F759C2B2B63CB93000534AB /* Realm */,
+				1FCE3D522C9B5918009C68A9 /* SwiftyGif */,
+			);
+			productName = NextcloudTalk;
+			productReference = 2C05747D1EDD9E8E00D9E7F2 /* NextcloudTalk.app */;
+			productType = "com.apple.product-type.application";
+		};
+		2C62AFA224C08845007E460A /* ShareExtension */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = 2C62AFB124C08845007E460A /* Build configuration list for PBXNativeTarget "ShareExtension" */;
+			buildPhases = (
+				25F3EB565BD21EF2FF15F197 /* [CP] Check Pods Manifest.lock */,
+				2C62AF9F24C08845007E460A /* Sources */,
+				2C62AFA024C08845007E460A /* Frameworks */,
+				2C62AFA124C08845007E460A /* Resources */,
+			);
+			buildRules = (
+			);
+			dependencies = (
+			);
+			name = ShareExtension;
+			packageProductDependencies = (
+				1F7AE07B29142E6A009F72AD /* NextcloudKit */,
+				1F45A1192A01D70E005FE87D /* SDWebImage */,
+				1F45A1242A01D8F7005FE87D /* SDWebImageSVGKitPlugin */,
+				1F0ECBF82A68277C00921E90 /* CDMarkdownKit */,
+				1FAB2E7E2AC99367001214EB /* TOCropViewController */,
+				1F35F8F42AEEDA9800044BDA /* SwiftyAttributes */,
+				1F759C2F2B63CBA0000534AB /* Realm */,
+			);
+			productName = ShareExtension;
+			productReference = 2C62AFA324C08845007E460A /* ShareExtension.appex */;
+			productType = "com.apple.product-type.app-extension";
+		};
+		2CC0014E24A1F0E900A20167 /* NotificationServiceExtension */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = 2CC0015A24A1F0E900A20167 /* Build configuration list for PBXNativeTarget "NotificationServiceExtension" */;
+			buildPhases = (
+				E75968B5C5288222BC8FCA99 /* [CP] Check Pods Manifest.lock */,
+				2CC0014B24A1F0E900A20167 /* Sources */,
+				2CC0014C24A1F0E900A20167 /* Frameworks */,
+				2CC0014D24A1F0E900A20167 /* Resources */,
+			);
+			buildRules = (
+			);
+			dependencies = (
+			);
+			name = NotificationServiceExtension;
+			packageProductDependencies = (
+				1F7AE07929142E62009F72AD /* NextcloudKit */,
+				1F45A11D2A01D719005FE87D /* SDWebImage */,
+				1F45A1222A01D8F1005FE87D /* SDWebImageSVGKitPlugin */,
+				1F0ECBF62A68277000921E90 /* CDMarkdownKit */,
+				1F759C2D2B63CB9A000534AB /* Realm */,
+				1FF1361B2BFBC86A006A6101 /* SwiftyAttributes */,
+			);
+			productName = NotificationServiceExtension;
+			productReference = 2CC0014F24A1F0E900A20167 /* NotificationServiceExtension.appex */;
+			productType = "com.apple.product-type.app-extension";
+		};
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+		2C0574751EDD9E8E00D9E7F2 /* Project object */ = {
+			isa = PBXProject;
+			attributes = {
+				LastSwiftUpdateCheck = 1510;
+				LastUpgradeCheck = 1320;
+				ORGANIZATIONNAME = "";
+				TargetAttributes = {
+					1F6D8C2F2B2E3756004376B8 = {
+						CreatedOnToolsVersion = 15.1;
+						LastSwiftMigration = 1520;
+						TestTargetID = 2C05747C1EDD9E8E00D9E7F2;
+					};
+					1FD8AD892A3A162100787C16 = {
+						CreatedOnToolsVersion = 14.3;
+						TestTargetID = 2C05747C1EDD9E8E00D9E7F2;
+					};
+					1FF2FD5A2AB99CCB000C9905 = {
+						CreatedOnToolsVersion = 14.3.1;
+					};
+					2C05747C1EDD9E8E00D9E7F2 = {
+						CreatedOnToolsVersion = 8.3.2;
+						LastSwiftMigration = 1220;
+						ProvisioningStyle = Automatic;
+						SystemCapabilities = {
+							com.apple.ApplicationGroups.iOS = {
+								enabled = 1;
+							};
+							com.apple.BackgroundModes = {
+								enabled = 1;
+							};
+							com.apple.Keychain = {
+								enabled = 0;
+							};
+							com.apple.Push = {
+								enabled = 1;
+							};
+						};
+					};
+					2C62AFA224C08845007E460A = {
+						CreatedOnToolsVersion = 11.5;
+						LastSwiftMigration = 1220;
+						ProvisioningStyle = Automatic;
+					};
+					2CC0014E24A1F0E900A20167 = {
+						CreatedOnToolsVersion = 11.5;
+						LastSwiftMigration = 1220;
+						ProvisioningStyle = Automatic;
+					};
+				};
+			};
+			buildConfigurationList = 2C0574781EDD9E8E00D9E7F2 /* Build configuration list for PBXProject "NextcloudTalk" */;
+			compatibilityVersion = "Xcode 3.2";
+			developmentRegion = English;
+			hasScannedForEncodings = 0;
+			knownRegions = (
+				English,
+				en,
+				Base,
+				es,
+				de,
+				it,
+				tr,
+				sl,
+				hr,
+				pl,
+				"pt-BR",
+				nl,
+				fr,
+				el,
+				"zh-Hans",
+				gl,
+				cs,
+				hu,
+				ko,
+				eu,
+				ja,
+				ar,
+				"nb-NO",
+				ga,
+				sv,
+				sr,
+			);
+			mainGroup = 2C0574741EDD9E8E00D9E7F2;
+			packageReferences = (
+				2CCCD21B2835088F00F076CE /* XCRemoteSwiftPackageReference "OpenSSL" */,
+				1F468E7428DCC6C60099597B /* XCRemoteSwiftPackageReference "Dynamic" */,
+				1F628CB82842BAAF0083A425 /* XCRemoteSwiftPackageReference "QRCodeReader" */,
+				1F7AE07629142CA1009F72AD /* XCRemoteSwiftPackageReference "NextcloudKit" */,
+				1F66B72D29FABD01003FB168 /* XCRemoteSwiftPackageReference "SwiftyAttributes" */,
+				1F45A1142A01D6EC005FE87D /* XCRemoteSwiftPackageReference "SDWebImage" */,
+				1F45A11F2A01D8BA005FE87D /* XCRemoteSwiftPackageReference "SDWebImageSVGKitPlugin" */,
+				1F0ECBF32A68274400921E90 /* XCRemoteSwiftPackageReference "CDMarkdownKit" */,
+				1FAB2E7B2AC99326001214EB /* XCRemoteSwiftPackageReference "TOCropViewController" */,
+				1FAB2E862ACD44CF001214EB /* XCRemoteSwiftPackageReference "talk-clients-webrtc" */,
+				80CDF8C22A8E098900CB57AE /* XCRemoteSwiftPackageReference "swiftui-introspect" */,
+				1F759C2A2B63CB93000534AB /* XCRemoteSwiftPackageReference "realm-swift-binary" */,
+				1FCE3D512C9B5918009C68A9 /* XCRemoteSwiftPackageReference "SwiftyGif" */,
+			);
+			productRefGroup = 2C05747E1EDD9E8E00D9E7F2 /* Products */;
+			projectDirPath = "";
+			projectRoot = "";
+			targets = (
+				2C05747C1EDD9E8E00D9E7F2 /* NextcloudTalk */,
+				2CC0014E24A1F0E900A20167 /* NotificationServiceExtension */,
+				2C62AFA224C08845007E460A /* ShareExtension */,
+				1FF2FD5A2AB99CCB000C9905 /* BroadcastUploadExtension */,
+				1FD8AD892A3A162100787C16 /* NextcloudTalkUITests */,
+				1F6D8C2F2B2E3756004376B8 /* NextcloudTalkTests */,
+			);
+		};
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+		1F6D8C2E2B2E3756004376B8 /* Resources */ = {
+			isa = PBXResourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		1FD8AD882A3A162100787C16 /* Resources */ = {
+			isa = PBXResourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		1FF2FD592AB99CCB000C9905 /* Resources */ = {
+			isa = PBXResourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				1FE7DE332BBC8FA00040EE12 /* PrivacyInfo.xcprivacy in Resources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		2C05747B1EDD9E8E00D9E7F2 /* Resources */ = {
+			isa = PBXResourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				2CB052A12BF2297500191349 /* connecting.mp3 in Resources */,
+				2C84BCCE29EEDCE8001BA6DA /* CallReactionView.xib in Resources */,
+				2CC32E9327F45AE000BB8C39 /* ReactionsViewCell.xib in Resources */,
+				2C330372255E6EBC00BDB4E4 /* InfoPlist.strings in Resources */,
+				2C78EFA11F828C41008AFA74 /* CallViewController.xib in Resources */,
+				2C3780C5210F4A26003F9AE8 /* HeaderWithButton.xib in Resources */,
+				1FCE3D592C9C4D21009C68A9 /* ReferenceGiphyView.xib in Resources */,
+				2C7F47AA20289B9600081CC7 /* Localizable.strings in Resources */,
+				2CB997C62A052449003C41AC /* EmojiAvatarPickerViewController.xib in Resources */,
+				2C0574A51EDDA2E300D9E7F2 /* LoginViewController.xib in Resources */,
+				1F46CE2B28E05B3C00E7D88E /* ReferenceDefaultView.xib in Resources */,
+				1F98DF9E28E7485000E05174 /* ReferenceDeckView.xib in Resources */,
+				1F66B72129FA7089003FB168 /* TypingIndicatorView.xib in Resources */,
+				2C738158210613A200CDB8DB /* NCChatTitleView.xib in Resources */,
+				2CA52ACD2670D07900619610 /* VoiceMessageRecordingView.xib in Resources */,
+				2C8A2BCF221FEEFE00DE6D2C /* DirectoryTableViewCell.xib in Resources */,
+				2CC007C620D90AE50096D91F /* RoomNameTableViewCell.xib in Resources */,
+				2C78EFA61F86FF4A008AFA74 /* CallParticipantViewCell.xib in Resources */,
+				2C98F77D216231D3001A6A73 /* RoomTableViewCell.xib in Resources */,
+				1F7625E72901B0E800834869 /* CallsFromOldAccountViewController.xib in Resources */,
+				2CF9CBFF26025F65002246EF /* TextInputTableViewCell.xib in Resources */,
+				1F24B5A428E0649200654457 /* ReferenceGithubView.xib in Resources */,
+				2C0424982CA335C4004772F6 /* AudioPlayerView.xib in Resources */,
+				2CC1FF4528147F11009F7288 /* RoomSharedItemsTableViewController.xib in Resources */,
+				2C06BF6420AC64370031EB46 /* DateHeaderView.xib in Resources */,
+				2C440D1220EA4A770005F9BB /* RoomInfoTableViewController.xib in Resources */,
+				2C7A1237200E0A5700864818 /* UserSettingsTableViewCell.xib in Resources */,
+				2C1D13A3253760EE00EC0533 /* LaunchScreen.xib in Resources */,
+				1FB7B99C2BF0DF360093CE98 /* BannedActorCell.xib in Resources */,
+				2C4747E22CB58FD2002828F2 /* PollMessageView.xib in Resources */,
+				2CA1CCAC1F067F35002FE6A2 /* Images.xcassets in Resources */,
+				2CA1CCD71F1E664C002FE6A2 /* ContactsTableViewCell.xib in Resources */,
+				2C05748E1EDD9E8E00D9E7F2 /* Main.storyboard in Resources */,
+				1F785DDE2707865F00AC4B40 /* VoiceMessageTranscribeViewController.xib in Resources */,
+				2C4CDCCD269618240023F403 /* RoomDescriptionTableViewCell.xib in Resources */,
+				1FFF41642C70938700162F4D /* ReferenceZammadView.xib in Resources */,
+				2CB6ACBF26385A3800D3D641 /* ShareLocationViewController.xib in Resources */,
+				2C5BFBFC2891598A00E75118 /* PollResultTableViewCell.xib in Resources */,
+				1FADECDA2B8227B1007AD94B /* FederationInvitationCell.xib in Resources */,
+				1FEC459C2A02BCAE00A636AA /* ReferenceGithubPermalinkView.xib in Resources */,
+				1FE0C56C2A0531200083576A /* ReferenceTalkView.xib in Resources */,
+				1F1B50382B8E070100B0F2F4 /* BaseChatTableViewCell.xib in Resources */,
+				2C5BFBF828902E3700E75118 /* PollFooterView.xib in Resources */,
+				1FDE7C9C28DE14B000CB718E /* ReferenceView.xib in Resources */,
+				2C444707265E59B500DF1DBC /* ShareConfirmationCollectionViewCell.xib in Resources */,
+				2CC7158920B837140045C789 /* PlaceholderView.xib in Resources */,
+				1F1C0D8729AFB88800D17C6D /* VLCKitVideoViewController.xib in Resources */,
+				2CF8AD402A0010FB00A4D3E6 /* MessageTranslationViewController.xib in Resources */,
+				1FE7DE362BBC8FA10040EE12 /* PrivacyInfo.xcprivacy in Resources */,
+				2C477C1628B79D980044DEB4 /* Localizable.stringsdict in Resources */,
+				2CB6ACEC2641954700D3D641 /* MapViewController.xib in Resources */,
+				1FE7DE322BB459B10040EE12 /* RoomInvitationViewCell.xib in Resources */,
+				2C4CDCD226A84E550023F403 /* ShareTableViewCell.xib in Resources */,
+				1FEC45A52A02F92B00A636AA /* GithubPermalinkViewController.xib in Resources */,
+				2C7A12432017872600864818 /* AddParticipantsTableViewController.xib in Resources */,
+				1F3C41A129EDAC8800F58435 /* RoomAvatarInfoTableViewController.xib in Resources */,
+				1F3C41A529EDF0B800F58435 /* AvatarEditView.xib in Resources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		2C62AFA124C08845007E460A /* Resources */ = {
+			isa = PBXResourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				1F35F8E42AEEBBE500044BDA /* NCChatTitleView.xib in Resources */,
+				2C3195BC24C599130066F221 /* PlaceholderView.xib in Resources */,
+				2C1ABD8825769E7D00AEDFB6 /* ShareConfirmationCollectionViewCell.xib in Resources */,
+				2C3195C324C5E2100066F221 /* ShareTableViewCell.xib in Resources */,
+				2C3195BE24C5A7410066F221 /* Images.xcassets in Resources */,
+				1F59446625B8EDF5002AD65F /* Localizable.strings in Resources */,
+				2CB6ACEE2641954700D3D641 /* MapViewController.xib in Resources */,
+				2C62AFB624C1A449007E460A /* Share.storyboard in Resources */,
+				1FE7DE342BBC8FA10040EE12 /* PrivacyInfo.xcprivacy in Resources */,
+				1F35F8F22AEEC25E00044BDA /* TypingIndicatorView.xib in Resources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		2CC0014D24A1F0E900A20167 /* Resources */ = {
+			isa = PBXResourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				1F53819129195FA4003DA6B7 /* Images.xcassets in Resources */,
+				1F59446225B8EDF5002AD65F /* Localizable.strings in Resources */,
+				1FE7DE352BBC8FA10040EE12 /* PrivacyInfo.xcprivacy in Resources */,
+				2CB6ACED2641954700D3D641 /* MapViewController.xib in Resources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXShellScriptBuildPhase section */
+		25F3EB565BD21EF2FF15F197 /* [CP] Check Pods Manifest.lock */ = {
+			isa = PBXShellScriptBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			inputFileListPaths = (
+			);
+			inputPaths = (
+				"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
+				"${PODS_ROOT}/Manifest.lock",
+			);
+			name = "[CP] Check Pods Manifest.lock";
+			outputFileListPaths = (
+			);
+			outputPaths = (
+				"$(DERIVED_FILE_DIR)/Pods-ShareExtension-checkManifestLockResult.txt",
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+			shellPath = /bin/sh;
+			shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n    # print error to STDERR\n    echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n    exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
+			showEnvVarsInLog = 0;
+		};
+		2C5E72BC27957FCA004ED7FB /* ShellScript */ = {
+			isa = PBXShellScriptBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			inputFileListPaths = (
+			);
+			inputPaths = (
+			);
+			outputFileListPaths = (
+			);
+			outputPaths = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+			shellPath = /bin/sh;
+			shellScript = "if [ -z \"$CI\" ]; then\n    export PATH=\"$PATH:/opt/homebrew/bin\"\n    if which swiftlint >/dev/null; then\n      swiftlint\n    else\n      echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\n    fi\nfi\n";
+		};
+		2C8035721F950BA800501B5C /* ShellScript */ = {
+			isa = PBXShellScriptBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			inputPaths = (
+			);
+			outputPaths = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+			shellPath = /bin/sh;
+			shellScript = "# See: https://stackoverflow.com/a/41416964\n\necho \"Target architectures: $ARCHS\"\n\nAPP_PATH=\"${TARGET_BUILD_DIR}/${WRAPPER_NAME}\"\n\nfind \"$APP_PATH\" -name '*.framework' -type d | while read -r FRAMEWORK\ndo\nFRAMEWORK_EXECUTABLE_NAME=$(defaults read \"$FRAMEWORK/Info.plist\" CFBundleExecutable)\nFRAMEWORK_EXECUTABLE_PATH=\"$FRAMEWORK/$FRAMEWORK_EXECUTABLE_NAME\"\necho \"Executable is $FRAMEWORK_EXECUTABLE_PATH\"\necho $(lipo -info \"$FRAMEWORK_EXECUTABLE_PATH\")\n\nFRAMEWORK_TMP_PATH=\"$FRAMEWORK_EXECUTABLE_PATH-tmp\"\n\n# remove simulator's archs if location is not simulator's directory\ncase \"${TARGET_BUILD_DIR}\" in\n*\"iphonesimulator\")\n    echo \"No need to remove archs\"\n    ;;\n*)\n    if $(lipo \"$FRAMEWORK_EXECUTABLE_PATH\" -verify_arch \"i386\") ; then\n    lipo -output \"$FRAMEWORK_TMP_PATH\" -remove \"i386\" \"$FRAMEWORK_EXECUTABLE_PATH\"\n    echo \"i386 architecture removed\"\n    rm \"$FRAMEWORK_EXECUTABLE_PATH\"\n    mv \"$FRAMEWORK_TMP_PATH\" \"$FRAMEWORK_EXECUTABLE_PATH\"\n    fi\n    if $(lipo \"$FRAMEWORK_EXECUTABLE_PATH\" -verify_arch \"x86_64\") ; then\n    lipo -output \"$FRAMEWORK_TMP_PATH\" -remove \"x86_64\" \"$FRAMEWORK_EXECUTABLE_PATH\"\n    echo \"x86_64 architecture removed\"\n    rm \"$FRAMEWORK_EXECUTABLE_PATH\"\n    mv \"$FRAMEWORK_TMP_PATH\" \"$FRAMEWORK_EXECUTABLE_PATH\"\n    fi\n    ;;\nesac\n\necho \"Completed for executable $FRAMEWORK_EXECUTABLE_PATH\"\necho $(lipo -info \"$FRAMEWORK_EXECUTABLE_PATH\")\n\ndone\n";
+		};
+		3A811BF7A761836E61C82B80 /* [CP] Check Pods Manifest.lock */ = {
+			isa = PBXShellScriptBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			inputFileListPaths = (
+			);
+			inputPaths = (
+				"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
+				"${PODS_ROOT}/Manifest.lock",
+			);
+			name = "[CP] Check Pods Manifest.lock";
+			outputFileListPaths = (
+			);
+			outputPaths = (
+				"$(DERIVED_FILE_DIR)/Pods-NextcloudTalkTests-checkManifestLockResult.txt",
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+			shellPath = /bin/sh;
+			shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n    # print error to STDERR\n    echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n    exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
+			showEnvVarsInLog = 0;
+		};
+		902A7A3EC0BDCC947AEF3EBF /* [CP] Check Pods Manifest.lock */ = {
+			isa = PBXShellScriptBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			inputPaths = (
+				"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
+				"${PODS_ROOT}/Manifest.lock",
+			);
+			name = "[CP] Check Pods Manifest.lock";
+			outputPaths = (
+				"$(DERIVED_FILE_DIR)/Pods-NextcloudTalk-checkManifestLockResult.txt",
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+			shellPath = /bin/sh;
+			shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n    # print error to STDERR\n    echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n    exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
+			showEnvVarsInLog = 0;
+		};
+		A3C686B1B84C4462F93441AB /* [CP] Copy Pods Resources */ = {
+			isa = PBXShellScriptBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			inputPaths = (
+				"${PODS_ROOT}/Target Support Files/Pods-NextcloudTalk/Pods-NextcloudTalk-resources.sh",
+				"${PODS_ROOT}/DateTools/DateTools/DateTools/DateTools.bundle",
+				"${PODS_ROOT}/MaterialComponents/components/ActivityIndicator/src/MaterialActivityIndicator.bundle",
+			);
+			name = "[CP] Copy Pods Resources";
+			outputPaths = (
+				"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/DateTools.bundle",
+				"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/MaterialActivityIndicator.bundle",
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+			shellPath = /bin/sh;
+			shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-NextcloudTalk/Pods-NextcloudTalk-resources.sh\"\n";
+			showEnvVarsInLog = 0;
+		};
+		C21100AE204AFC213989DA96 /* [CP] Embed Pods Frameworks */ = {
+			isa = PBXShellScriptBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			inputPaths = (
+				"${PODS_ROOT}/Target Support Files/Pods-NextcloudTalk/Pods-NextcloudTalk-frameworks.sh",
+				"${PODS_XCFRAMEWORKS_BUILD_DIR}/MobileVLCKit/MobileVLCKit.framework/MobileVLCKit",
+			);
+			name = "[CP] Embed Pods Frameworks";
+			outputPaths = (
+				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MobileVLCKit.framework",
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+			shellPath = /bin/sh;
+			shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-NextcloudTalk/Pods-NextcloudTalk-frameworks.sh\"\n";
+			showEnvVarsInLog = 0;
+		};
+		CBF95D503F715CE6BE80B113 /* [CP] Check Pods Manifest.lock */ = {
+			isa = PBXShellScriptBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			inputFileListPaths = (
+			);
+			inputPaths = (
+				"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
+				"${PODS_ROOT}/Manifest.lock",
+			);
+			name = "[CP] Check Pods Manifest.lock";
+			outputFileListPaths = (
+			);
+			outputPaths = (
+				"$(DERIVED_FILE_DIR)/Pods-BroadcastUploadExtension-checkManifestLockResult.txt",
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+			shellPath = /bin/sh;
+			shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n    # print error to STDERR\n    echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n    exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
+			showEnvVarsInLog = 0;
+		};
+		E75968B5C5288222BC8FCA99 /* [CP] Check Pods Manifest.lock */ = {
+			isa = PBXShellScriptBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			inputFileListPaths = (
+			);
+			inputPaths = (
+				"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
+				"${PODS_ROOT}/Manifest.lock",
+			);
+			name = "[CP] Check Pods Manifest.lock";
+			outputFileListPaths = (
+			);
+			outputPaths = (
+				"$(DERIVED_FILE_DIR)/Pods-NotificationServiceExtension-checkManifestLockResult.txt",
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+			shellPath = /bin/sh;
+			shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n    # print error to STDERR\n    echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n    exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
+			showEnvVarsInLog = 0;
+		};
+/* End PBXShellScriptBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+		1F6D8C2C2B2E3756004376B8 /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				1FF4DAA62C08D81D00C1B952 /* UnitNCChatMessageTest.swift in Sources */,
+				1F6629FA2C17700E001C6C0E /* IntegrationRoomsManagerTest.swift in Sources */,
+				1FB7B9852BE2EE020093CE98 /* UnitChatViewControllerTest.swift in Sources */,
+				1F6D8C4B2B2F5B61004376B8 /* TestBase.swift in Sources */,
+				1F6D8C332B2E3756004376B8 /* IntegrationRoomTest.swift in Sources */,
+				1FBC3BE92B61BD09003909E0 /* TestBaseRealm.swift in Sources */,
+				1F6D8C4D2B2F8FE5004376B8 /* IntegrationChatTest.swift in Sources */,
+				1FB7B9892BE442400093CE98 /* UnitBaseChatTableViewCellTest.swift in Sources */,
+				1F5CDAE72B3B05110040ECC0 /* UnitColorGeneratorTest.swift in Sources */,
+				1F1DF8412C63C25900E5EA86 /* UnitNCDatabaseManager.swift in Sources */,
+				1FF4DAA82C08DE3A00C1B952 /* UnitNCRoomsManagerTest.swift in Sources */,
+				1F1B0F252BD94A0D003FD766 /* UnitDarwinCenterTest.swift in Sources */,
+				1F6D8C412B2F26D5004376B8 /* TestConstants.swift in Sources */,
+				1FB7B9872BE441450093CE98 /* UIViewExtensions.swift in Sources */,
+				1FBC3BE52B61ACD5003909E0 /* UnitBaseChatViewControllerTest.swift in Sources */,
+				1F8AAC622C596308004DA20A /* UnitSignalingSettings.swift in Sources */,
+				1F0B0A772BA26BE10073FF8D /* UnitMentionSuggestionTest.swift in Sources */,
+				1F6D8C432B2F26EE004376B8 /* Helpers.swift in Sources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		1FD8AD862A3A162100787C16 /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				1FD8AE6B2A3A216300787C16 /* UIRoomTest.swift in Sources */,
+				1F6D8C3D2B2F23C4004376B8 /* Helpers.swift in Sources */,
+				1F6D8C442B2F2791004376B8 /* TestConstants.swift in Sources */,
+				1F6D8C492B2F2FB7004376B8 /* AAAALoginTest.swift in Sources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		1FF2FD572AB99CCB000C9905 /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				1F1B504B2B90CF0C00B0F2F4 /* FederatedCapabilities.m in Sources */,
+				1F77A5F22AB9A436007B6037 /* EmojiUtils.swift in Sources */,
+				1F77A5FA2AB9A4DF007B6037 /* NCMessageLocationParameter.m in Sources */,
+				1F77A6022AB9A532007B6037 /* CCCertificate.m in Sources */,
+				1F77A5EC2AB9A405007B6037 /* NCChatBlock.m in Sources */,
+				1F77A5F42AB9A4B2007B6037 /* ABContact.m in Sources */,
+				1F77A6012AB9A51D007B6037 /* NCNotificationAction.swift in Sources */,
+				1F77A6242ABA0003007B6037 /* SampleHandler.swift in Sources */,
+				1FB7B9902BF0CDF80093CE98 /* BannedActor.swift in Sources */,
+				1F77A5F32AB9A43B007B6037 /* SwiftMarkdownObjCBridge.swift in Sources */,
+				1FF4DA832C025DBF00C1B952 /* NCAPISessionManager.swift in Sources */,
+				1FDFC9502BA50B9100670DF4 /* UIFontExtension.swift in Sources */,
+				1F1DF8462C64006E00E5EA86 /* SignalingParticipant.swift in Sources */,
+				1FF2FD832AB99F3B000C9905 /* NCAppBranding.m in Sources */,
+				1F77A5F82AB9A4CD007B6037 /* NCDeckCardParameter.m in Sources */,
+				1F77A5FD2AB9A4F3007B6037 /* ServerCapabilities.m in Sources */,
+				1FF4DA892C0262BB00C1B952 /* NCBaseSessionManager.swift in Sources */,
+				1F8AAC352C518B8B004DA20A /* SignalingSettings.swift in Sources */,
+				1F77A6062AB9A581007B6037 /* NCKeyChainController.m in Sources */,
+				1F0B0A732BA265300073FF8D /* MentionSuggestion.swift in Sources */,
+				1F77A5FB2AB9A4E6007B6037 /* NCMessageParameter.m in Sources */,
+				1F77A5F62AB9A4BF007B6037 /* NCChatReaction.m in Sources */,
+				1FF4DA922C02677F00C1B952 /* NCImageSessionManager.swift in Sources */,
+				1FB78E272B6AE8C900B0D69D /* FederationInvitation.swift in Sources */,
+				1F77A5FE2AB9A4F9007B6037 /* TalkAccount.m in Sources */,
+				1FF136132BFB6FCD006A6101 /* RLMSupport.swift in Sources */,
+				1F77A5ED2AB9A408007B6037 /* NCChatMessage.m in Sources */,
+				1F77A5EB2AB9A3EE007B6037 /* BGTaskHelper.swift in Sources */,
+				1FF136182BFB74D0006A6101 /* NCChatMessage.swift in Sources */,
+				1F77A5FC2AB9A4ED007B6037 /* NCRoom.m in Sources */,
+				1F77A62E2ABAFCC0007B6037 /* DarwinNotificationCenter.swift in Sources */,
+				1F77A60D2AB9A5CC007B6037 /* NCPoll.m in Sources */,
+				1F77A6032AB9A56D007B6037 /* NotificationCenterNotifications.m in Sources */,
+				1F77A60A2AB9A5AE007B6037 /* NCUser.m in Sources */,
+				1F8AAC3F2C519689004DA20A /* StunServer.swift in Sources */,
+				1FF4DA992C0327FF00C1B952 /* NCWebImageDownloaderOperation.swift in Sources */,
+				1F1DF83F2C5C17AF00E5EA86 /* TalkActor.swift in Sources */,
+				1F1B504A2B90CF0800B0F2F4 /* TalkCapabilities.m in Sources */,
+				2C6955122B0CE1A10070F6E1 /* NCUtils.swift in Sources */,
+				1FF4DA8D2C0264B100C1B952 /* NCPushProxySessionManager.swift in Sources */,
+				1FF2FD802AB99E4D000C9905 /* SampleUploader.swift in Sources */,
+				1F77A5F52AB9A4B9007B6037 /* NCAPIController.m in Sources */,
+				1F77A5F92AB9A4D9007B6037 /* NCMessageFileParameter.m in Sources */,
+				1FF2FD852AB99F51000C9905 /* NCUserStatus.m in Sources */,
+				1F8AAC3A2C519577004DA20A /* TurnServer.swift in Sources */,
+				1FB78E202B6ADBB600B0D69D /* NCAPIControllerExtensions.swift in Sources */,
+				1FF136122BFB4F8C006A6101 /* NCRoom.swift in Sources */,
+				1FF2FD7F2AB99E4D000C9905 /* Atomic.swift in Sources */,
+				1F77A5F72AB9A4C5007B6037 /* NCContact.m in Sources */,
+				1FF2FD822AB99E4D000C9905 /* SocketConnection.swift in Sources */,
+				1F77A6082AB9A58D007B6037 /* NCRoomParticipants.m in Sources */,
+				1FF4DAAD2C0A114900C1B952 /* OcsResponse.swift in Sources */,
+				1FF2FD862AB99F5B000C9905 /* NCDatabaseManager.m in Sources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		2C0574791EDD9E8E00D9E7F2 /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				2C444703265D641300DF1DBC /* NCUserDefaults.m in Sources */,
+				2C4747E92CB67177002828F2 /* PollMessageView.swift in Sources */,
+				2CD80F482A4304AD00919057 /* OpenConversationsTableViewController.swift in Sources */,
+				1FEC459E2A02BCB900A636AA /* ReferenceGithubPermalinkView.swift in Sources */,
+				2CC1C38729C0945700C8436B /* DRCellSlideAction.m in Sources */,
+				1FEDE3C6257D439500853F79 /* NCChatFileController.m in Sources */,
+				1FEDE3CE257D43AB00853F79 /* NCMessageFileParameter.m in Sources */,
+				2C4446DD2658158000DF1DBC /* NCChatBlock.m in Sources */,
+				1FDCC3F029ECB4CE00DEB39B /* AvatarButton.swift in Sources */,
+				2C06BF6720AC647A0031EB46 /* DateHeaderView.m in Sources */,
+				2CB6ACDA2641483800D3D641 /* NCMessageLocationParameter.m in Sources */,
+				1F61C767285E35A6004D74D8 /* DiagnosticsTableViewController.swift in Sources */,
+				1F1B0F472BE047CE003FD766 /* StandardInteractionController.swift in Sources */,
+				2CD5F3242142781A006B71BF /* NCExternalSignalingController.m in Sources */,
+				2C0574851EDD9E8E00D9E7F2 /* AppDelegate.m in Sources */,
+				2C4987BD21E640E20060AC27 /* CallKitManager.m in Sources */,
+				1F1B0F4C2BE18FF3003FD766 /* CustomPresentableNavigationController.swift in Sources */,
+				2C4446F3265D51A600DF1DBC /* NCPushNotificationsUtils.m in Sources */,
+				2C0424902CA32D45004772F6 /* BaseChatTableViewCell+Audio.swift in Sources */,
+				2C1ABDE5257F883400AEDFB6 /* ABContact.m in Sources */,
+				2C5BFBEF288A947900E75118 /* PollVotingView.swift in Sources */,
+				1FAB2EF02AD1EAA3001214EB /* RLMSupport.swift in Sources */,
+				1F1B50342B8E069800B0F2F4 /* BaseChatTableViewCell.swift in Sources */,
+				2C1EF36B25505DCE007C9768 /* NCNavigationController.m in Sources */,
+				DA755811278EF3EF00A48A1B /* UserSettingsTableViewCell.swift in Sources */,
+				1FA38C9029A4B3C6008871B8 /* NCNotificationAction.swift in Sources */,
+				2C44B4D127FF05A000AD1C86 /* ReactionsSummaryView.swift in Sources */,
+				2CD4F6B72C11C80600ED594F /* ContactsSearchResultTableViewContoller.swift in Sources */,
+				1FB7B99A2BF0DF290093CE98 /* BannedActorCell.swift in Sources */,
+				1F35F8FB2AEEDBC600044BDA /* ChatViewControllerExtension.swift in Sources */,
+				2CC007B420D7AE990096D91F /* ResultMultiSelectionTableViewController.m in Sources */,
+				2CA1CCC31F166CC5002FE6A2 /* NCRoom.m in Sources */,
+				2C06BF5D20A89F510031EB46 /* NCRoomsManager.m in Sources */,
+				2CB6ACCA26401D5200D3D641 /* GeoLocationRichObject.m in Sources */,
+				2C78EF9C1F826B22008AFA74 /* NCCallController.m in Sources */,
+				1F1B50442B9095D100B0F2F4 /* FederatedCapabilities.m in Sources */,
+				1FF4DAA22C0338D000C1B952 /* RoomDescriptionTableViewCell.swift in Sources */,
+				2C5BFBF628902E0300E75118 /* PollFooterView.swift in Sources */,
+				2C4D7D761F30F7B600FF4A0D /* ARDUtilities.m in Sources */,
+				1FF4DA7E2C0237D000C1B952 /* DirectoryTableViewCell.swift in Sources */,
+				2CB6ACE92641954700D3D641 /* MapViewController.m in Sources */,
+				1FF1360F2BFB4F8C006A6101 /* NCRoom.swift in Sources */,
+				1F8995B32970644C00CABA33 /* ColorGenerator.swift in Sources */,
+				1F1B0F2C2BDBB3AC003FD766 /* NCMediaViewerViewController.swift in Sources */,
+				1FB7B98E2BF0CBA60093CE98 /* BannedActor.swift in Sources */,
+				1F1B503A2B8F9E1300B0F2F4 /* BaseChatTableViewCell+File.swift in Sources */,
+				1F5813F928EB23EF00318FC3 /* NCSplitViewPlaceholderViewController.swift in Sources */,
+				2CC32E9227F45AE000BB8C39 /* ReactionsViewCell.swift in Sources */,
+				1F1B0F452BE047CE003FD766 /* ModalPresentationController.swift in Sources */,
+				2CBF82AE1FC888FC00636459 /* NCPushNotification.m in Sources */,
+				2CC7159420C54D080045C789 /* ChatTableViewCell.m in Sources */,
+				1F8AAC322C518759004DA20A /* SignalingSettings.swift in Sources */,
+				1F1B0F272BDA61C5003FD766 /* AllocationTracker.swift in Sources */,
+				2CA1CCAA1F02D1A4002FE6A2 /* NCAPIController.m in Sources */,
+				1F1B0F302BDBC9D6003FD766 /* NCMediaViewerPageViewController.swift in Sources */,
+				DA66582B27B6992F00B46B11 /* UserProfileTableViewController+AvatarSetup.swift in Sources */,
+				2C69323D2923ECAA00017AD2 /* WSMessage.m in Sources */,
+				2C3780C3210F49DC003F9AE8 /* HeaderWithButton.m in Sources */,
+				1FB7B9952BF0DF1C0093CE98 /* BannedActorTableViewController.swift in Sources */,
+				2C1ABDC6257A7CF000AEDFB6 /* NCContactsManager.m in Sources */,
+				2C5BFBEA28772A9A00E75118 /* NCUnifiedSearchController.swift in Sources */,
+				1F4DD3EB2571C688007DC98E /* EmojiUtils.swift in Sources */,
+				2C4D7D731F309DA500FF4A0D /* RTCSessionDescription+JSON.m in Sources */,
+				2CB3041C2264775E0053078A /* SLKTextView+SLKAdditions.m in Sources */,
+				2C57CD8428C2255000B22E03 /* PollCreationViewController.swift in Sources */,
+				1F77A6272ABA0CD9007B6037 /* NCScreensharingController.m in Sources */,
+				2CEDA88C26F492610044552B /* NSMutableAttributedString+Extensions.swift in Sources */,
+				2C5BFBFB2891598A00E75118 /* PollResultTableViewCell.swift in Sources */,
+				1FA20C8A284001D80062B4F3 /* DebounceWebView.swift in Sources */,
+				1F1B0F322BDC57E3003FD766 /* UIPageViewControllerExtension.swift in Sources */,
+				2C4CDCD126A84E500023F403 /* ShareTableViewCell.m in Sources */,
+				2C8CDD0621C2EDE8004E2997 /* AvatarBackgroundImageView.m in Sources */,
+				2C9B0B9C217F756B00A4752C /* NCNotification.m in Sources */,
+				2C2D7A172B8C9C0000642373 /* RoomCreationTableViewController.swift in Sources */,
+				1F8995B52973547700CABA33 /* WebRTCCommon.swift in Sources */,
+				1F8AAC3C2C519689004DA20A /* StunServer.swift in Sources */,
+				2C2145682BF6B8E900470C0C /* NewRoomTableViewController.swift in Sources */,
+				1F1B503E2B8FB12100B0F2F4 /* BaseChatTableViewCell+Message.swift in Sources */,
+				DA66583127B6B24E00B46B11 /* UserProfileTableViewController+Utils.swift in Sources */,
+				1F1B0F422BE047CE003FD766 /* UIViewController+Transitions.swift in Sources */,
+				1F90EFBC25FE39F800F3FA55 /* NCIntentController.m in Sources */,
+				2C1ABD9925769F7500AEDFB6 /* ShareItem.m in Sources */,
+				2C2E64251F3462AF00D39CE8 /* NCSignalingMessage.m in Sources */,
+				2CA1554B208F2E5700CE8EF0 /* NCMessageTextView.m in Sources */,
+				2C4D7D721F309DA500FF4A0D /* RTCIceCandidate+JSON.m in Sources */,
+				1F77A62F2ABAFCEB007B6037 /* DarwinNotificationCenter.swift in Sources */,
+				2C9E6CCE1F6F34F000399B7A /* ARDSDPUtils.m in Sources */,
+				2C06330F2046CC8B0043481A /* NCUserInterfaceController.m in Sources */,
+				2CB304222264775E0053078A /* UIView+SLKAdditions.m in Sources */,
+				1F11FB7229C07B04001E21E7 /* NCZoomableView.swift in Sources */,
+				2C4446F0265D454200DF1DBC /* NotificationCenterNotifications.m in Sources */,
+				2C6955152B0CE1A30070F6E1 /* NCUtils.swift in Sources */,
+				1FB78E1F2B6ADBAA00B0D69D /* NCAPIControllerExtensions.swift in Sources */,
+				1F3D3B22255F109E00230DAE /* BarButtonItemWithActivity.m in Sources */,
+				2C0574821EDD9E8E00D9E7F2 /* main.m in Sources */,
+				1FDE7C9A28DE14A200CB718E /* ReferenceView.swift in Sources */,
+				2CC32E9827F5D9BD00BB8C39 /* NCChatReaction.m in Sources */,
+				2C40281522832EED0000DDFC /* NCDatabaseManager.m in Sources */,
+				1F1B0F362BDD8B9C003FD766 /* NCActivityIndicator.swift in Sources */,
+				1F3C419F29EDAC7D00F58435 /* RoomAvatarInfoTableViewController.swift in Sources */,
+				1FF4DAAA2C0A114900C1B952 /* OcsResponse.swift in Sources */,
+				1FAB2E852ACB482B001214EB /* ChatViewController.swift in Sources */,
+				1F5813F828EB23EF00318FC3 /* NCSplitViewController.swift in Sources */,
+				2C5BFBFE2891C3DF00E75118 /* PollResultsDetailsViewController.swift in Sources */,
+				1F8AAC372C519577004DA20A /* TurnServer.swift in Sources */,
+				2CA1CCA41F025F64002FE6A2 /* RoomsTableViewController.m in Sources */,
+				2CB3041A2264775E0053078A /* SLKTextInput+Implementation.m in Sources */,
+				2C90E5D31EE80C870093D85A /* AuthenticationViewController.m in Sources */,
+				2C604BD9211988A700D34DCD /* SystemMessageTableViewCell.m in Sources */,
+				1F1B50472B90CDF800B0F2F4 /* TalkCapabilities.m in Sources */,
+				2CA1CCD01F1E1779002FE6A2 /* SearchTableViewController.m in Sources */,
+				1F1C0D8929AFB89900D17C6D /* VLCKitVideoViewController.swift in Sources */,
+				2C9B0B98217F6DBA00A4752C /* NCNotificationController.m in Sources */,
+				2C36A04A261487BC0026F04A /* DetailedOptionsSelectorTableViewController.m in Sources */,
+				2CA52ACB2670D02800619610 /* VoiceMessageRecordingView.m in Sources */,
+				2C7381562106136000CDB8DB /* NCChatTitleView.m in Sources */,
+				1FF4DA872C02626D00C1B952 /* NCBaseSessionManager.swift in Sources */,
+				1FADECD62B821E24007AD94B /* FederationInvitationTableViewController.swift in Sources */,
+				2C98F77921622445001A6A73 /* RoomSearchTableViewController.m in Sources */,
+				2CB3041B2264775E0053078A /* SLKTextInputbar.m in Sources */,
+				1F1B0F432BE047CE003FD766 /* ModalTransitionAnimator.swift in Sources */,
+				2C78E9E325120DE600E3D4CA /* NCUserStatus.m in Sources */,
+				2C0574A41EDDA2E300D9E7F2 /* LoginViewController.m in Sources */,
+				2C78EFA51F86FF4A008AFA74 /* CallParticipantViewCell.m in Sources */,
+				1F66B72C29FA9414003FB168 /* SLKDefaultTypingIndicatorView.m in Sources */,
+				1F46CE2928E05B3200E7D88E /* ReferenceDefaultView.swift in Sources */,
+				2C444706265E59B100DF1DBC /* ShareConfirmationCollectionViewCell.m in Sources */,
+				1FCE3D552C9C189D009C68A9 /* NCChatFileControllerWrapper.swift in Sources */,
+				2C78EF991F80F81E008AFA74 /* NCSignalingController.m in Sources */,
+				1FDB47F82C9C7E3F00D6F423 /* NCDatabaseManager.swift in Sources */,
+				2CB304202264775E0053078A /* UIResponder+SLKAdditions.m in Sources */,
+				2C8E2A1B232174C20022BFC9 /* MessageSeparatorTableViewCell.m in Sources */,
+				1FAB2EEE2AD1BC1B001214EB /* UIControlExtensions.swift in Sources */,
+				1F7625E52901B0DB00834869 /* CallsFromOldAccountViewController.swift in Sources */,
+				2CB3041E2264775E0053078A /* SLKTextViewController.m in Sources */,
+				2CC007CE20E50B0A0096D91F /* MessageBodyTextView.m in Sources */,
+				2CBF82B21FCC7DBA00636459 /* CCCertificate.m in Sources */,
+				2CC1FF4828183958009F7288 /* NCDeckCardParameter.m in Sources */,
+				2C3780BD2107209C003F9AE8 /* NCRoomParticipants.m in Sources */,
+				1F5683CF2BA7980C0023E151 /* FilePreviewImageView.swift in Sources */,
+				1F0B0A722BA264540073FF8D /* MentionSuggestion.swift in Sources */,
+				2CA1CCCD1F181741002FE6A2 /* NCUser.m in Sources */,
+				1F77A6162AB9B161007B6037 /* ScreenCaptureController.m in Sources */,
+				2CF8AD3F2A0010FB00A4D3E6 /* MessageTranslationViewController.swift in Sources */,
+				2C21446E2BB5B54D005A6537 /* BaseChatTableViewCell+Location.swift in Sources */,
+				2C4230F72B207AB00013E1FA /* ContextChatViewController.swift in Sources */,
+				2CBD0D5A2C8770A40013C089 /* UIImageExtension.swift in Sources */,
+				1F90DA0429E9A28E00E81E3D /* AvatarManager.swift in Sources */,
+				1F1DF8432C64006E00E5EA86 /* SignalingParticipant.swift in Sources */,
+				2CC1FF4428147F11009F7288 /* RoomSharedItemsTableViewController.swift in Sources */,
+				2CC1C38629C0945700C8436B /* DRCellSlideGestureRecognizer.m in Sources */,
+				1FF4DA9B2C032AAC00C1B952 /* RoomTableViewCell.swift in Sources */,
+				1FB52E762842C75E00AC741B /* QRCodeLoginController.swift in Sources */,
+				1F5A24332ADA77DA009939FE /* InputbarViewController.swift in Sources */,
+				807E30762A83A90F00089D28 /* UserStatusOptionsSwiftUI.swift in Sources */,
+				2C4446D8265814D100DF1DBC /* ServerCapabilities.m in Sources */,
+				DA66582D27B6A73800B46B11 /* UserProfileTableViewController+DelegateMethods.swift in Sources */,
+				1F371A372A7B921A006CBFB3 /* DatePickerTextField.swift in Sources */,
+				1F1B0F482BE047CE003FD766 /* CustomPresentable.swift in Sources */,
+				1FE0C56E2A0531270083576A /* ReferenceTalkView.swift in Sources */,
+				1FE7DE302BB4598F0040EE12 /* RoomInvitationViewCell.swift in Sources */,
+				1F77A6222AB9EB06007B6037 /* SocketConnection.m in Sources */,
+				1FF136152BFB74C3006A6101 /* NCChatMessage.swift in Sources */,
+				1FDB47F62C9C71CE00D6F423 /* TalkAccount.swift in Sources */,
+				2CC1C38829C0945700C8436B /* DRCellSlideActionView.m in Sources */,
+				1FA732FC2966CBB7003D2103 /* CallFlowLayout.swift in Sources */,
+				2C78EF951F7E70EB008AFA74 /* NCPeerConnection.m in Sources */,
+				2C06BF6C20AEB0030031EB46 /* RoundedNumberView.m in Sources */,
+				2C78EFA01F828C41008AFA74 /* CallViewController.m in Sources */,
+				1FDDB0DB2AF440E100FBAFB7 /* BoundsChangedFlowLayout.swift in Sources */,
+				DA1AEFC3270F1FA90088E519 /* DateLabelCustom.swift in Sources */,
+				2C6E74462386D33200AE396C /* ReplyMessageView.m in Sources */,
+				DA8801A227A2DA00009EF248 /* UserProfileTableViewController.swift in Sources */,
+				1F0A1D442A5F1FA800A25433 /* SwiftMarkdownObjCBridge.swift in Sources */,
+				2C16A82C28E7284D00EDE523 /* NCButton.swift in Sources */,
+				DA75580F278EEA1000A48A1B /* SettingsTableViewController.swift in Sources */,
+				1FF4DA962C0327FF00C1B952 /* NCWebImageDownloaderOperation.swift in Sources */,
+				2CB3041D2264775E0053078A /* SLKTextView.m in Sources */,
+				1F1B0F4A2BE047D5003FD766 /* OneWayPanGestureRecognizer.swift in Sources */,
+				1FE94734293CE55600D6584C /* NCCameraController.swift in Sources */,
+				2CB304212264775E0053078A /* UIScrollView+SLKAdditions.m in Sources */,
+				2C4446D32658147900DF1DBC /* TalkAccount.m in Sources */,
+				2CA1CCD61F1E664C002FE6A2 /* ContactsTableViewCell.m in Sources */,
+				1F77A6172AB9B161007B6037 /* ScreenCapturer.m in Sources */,
+				1FCE3D572C9C4D18009C68A9 /* ReferenceGiphyView.swift in Sources */,
+				1FFF41622C70937B00162F4D /* ReferenceZammadView.swift in Sources */,
+				1F98DF9C28E7484700E05174 /* ReferenceDeckView.swift in Sources */,
+				DA66582F27B6B19C00B46B11 /* UserProfileTableViewController+Actions.swift in Sources */,
+				2C6E7449238C1A0800AE396C /* QuotedMessageView.m in Sources */,
+				2C1ABDCE257E939600AEDFB6 /* NCContact.m in Sources */,
+				2C7A12422017872600864818 /* AddParticipantsTableViewController.m in Sources */,
+				2C84BCCC29EEB9C6001BA6DA /* CallReactionView.swift in Sources */,
+				2C43BA7621309A1000B3068A /* NCMessageParameter.m in Sources */,
+				2CC32E8D27F4540E00BB8C39 /* ReactionsView.swift in Sources */,
+				2C4DE9F221F732B40096940D /* NCAudioController.m in Sources */,
+				80832B782A823D0700195A97 /* UserStatusMessageSwiftUIView.swift in Sources */,
+				2C8A2BC9221F094F00DE6D2C /* DirectoryTableViewController.m in Sources */,
+				1FADECD82B82269E007AD94B /* FederationInvitationCell.swift in Sources */,
+				1F61C76B285F65E1004D74D8 /* SimpleTableViewController.swift in Sources */,
+				2CB997C52A052449003C41AC /* EmojiAvatarPickerViewController.swift in Sources */,
+				1F1B0F442BE047CE003FD766 /* ModalTransitionManager.swift in Sources */,
+				2C5BFBF2288A97D800E75118 /* NCPoll.m in Sources */,
+				1F66B71F29FA703B003FB168 /* TypingIndicatorView.swift in Sources */,
+				1F35F9042AEEDF0E00044BDA /* AutoCompletionTableViewCell.m in Sources */,
+				2C42ADB420B58E6300296DEA /* NCChatController.m in Sources */,
+				2C4CDCD026A84AEA0023F403 /* ShareViewController.m in Sources */,
+				1FD9182928C55A73009092AB /* BGTaskHelper.swift in Sources */,
+				1F66B72929FA936E003FB168 /* SLKDefaultReplyView.m in Sources */,
+				1F785DDD2707865F00AC4B40 /* VoiceMessageTranscribeViewController.m in Sources */,
+				2C4446EC265D25BA00DF1DBC /* NCKeyChainController.m in Sources */,
+				1FD6F83C2B825069004048AB /* NCRoomsManagerExtensions.swift in Sources */,
+				1FF4DA802C023FF300C1B952 /* NCChatFileStatus.swift in Sources */,
+				DA8801A427AC52AC009EF248 /* TextInputTableViewCell.swift in Sources */,
+				2C2A788E2359CC8800EEB797 /* NCAppBranding.m in Sources */,
+				1F3C41A329EDF05700F58435 /* AvatarEditView.swift in Sources */,
+				2CA15541208E350300CE8EF0 /* NCChatMessage.m in Sources */,
+				2CB304192264775E0053078A /* SLKInputAccessoryView.m in Sources */,
+				2CA1CC911F014354002FE6A2 /* NCConnectionController.m in Sources */,
+				1FB6678F28CE381300D29F8D /* SubtitleTableViewCell.swift in Sources */,
+				1FF4DA912C02677C00C1B952 /* NCImageSessionManager.swift in Sources */,
+				1F24B5A228E0648600654457 /* ReferenceGithubView.swift in Sources */,
+				2C4D7D691F2F7DBC00FF4A0D /* ARDSettingsModel.m in Sources */,
+				1FF4DA8C2C0263A200C1B952 /* NCPushProxySessionManager.swift in Sources */,
+				2CB6ACBC26385A3800D3D641 /* ShareLocationViewController.m in Sources */,
+				2C04249B2CA33681004772F6 /* AudioPlayerView.swift in Sources */,
+				1F1B0F462BE047CE003FD766 /* InteractionControlling.swift in Sources */,
+				1FDCC3D429EBF6E700DEB39B /* AvatarImageView.swift in Sources */,
+				1FB78E262B6AE5A600B0D69D /* FederationInvitation.swift in Sources */,
+				1FDFC94D2BA50B9100670DF4 /* UIFontExtension.swift in Sources */,
+				1F468E7828DCC7310099597B /* EmojiTextField.swift in Sources */,
+				80832B762A822E5100195A97 /* UserStatusSwiftUIView.swift in Sources */,
+				1FF4DA822C025DB900C1B952 /* NCAPISessionManager.swift in Sources */,
+				1F1DF83C2C5C17AF00E5EA86 /* TalkActor.swift in Sources */,
+				2C4747E62CB6711F002828F2 /* BaseChatTableViewCell+Poll.swift in Sources */,
+				2C444708265E59BC00DF1DBC /* ShareItemController.m in Sources */,
+				2CA1CC951F014EF9002FE6A2 /* NCSettingsController.m in Sources */,
+				2C440D1120EA4A770005F9BB /* RoomInfoTableViewController.m in Sources */,
+				1FAB2E832AC9EC3F001214EB /* BaseChatViewController.swift in Sources */,
+				2C4D7D631F2F7C2C00FF4A0D /* ARDCaptureController.m in Sources */,
+				2C4D7D6A1F2F7DBC00FF4A0D /* ARDSettingsStore.m in Sources */,
+				1F35F8E02AEEB9DE00044BDA /* ShareConfirmationViewController.swift in Sources */,
+				1FF4DAA02C03351E00C1B952 /* RoomNameTableViewCell.swift in Sources */,
+				2C9200C32474262C0050084F /* UIBarButtonItem+Badge.m in Sources */,
+				1FD6F83E2B87B712004048AB /* NCUserStatusExtensions.swift in Sources */,
+				1FEC45A32A02F92700A636AA /* GithubPermalinkViewController.swift in Sources */,
+				2CC7158C20B8394A0045C789 /* PlaceholderView.m in Sources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		2C62AF9F24C08845007E460A /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				1F4DD3ED2571C688007DC98E /* EmojiUtils.swift in Sources */,
+				1F35F8EB2AEEBC1100044BDA /* UIResponder+SLKAdditions.m in Sources */,
+				2C62B02424C1BDCF007E460A /* NCAppBranding.m in Sources */,
+				2C1ABD8625769E7D00AEDFB6 /* ShareConfirmationCollectionViewCell.m in Sources */,
+				1F35F90B2AEEE76C00044BDA /* ReplyMessageView.m in Sources */,
+				1F1C999E2909846400EACF02 /* BGTaskHelper.swift in Sources */,
+				1F35F8F12AEEC25B00044BDA /* TypingIndicatorView.swift in Sources */,
+				2C62AFFD24C1BDA5007E460A /* NCMessageParameter.m in Sources */,
+				1F35F8EC2AEEBC1400044BDA /* UIScrollView+SLKAdditions.m in Sources */,
+				1FF136172BFB74CF006A6101 /* NCChatMessage.swift in Sources */,
+				1FF4DA8A2C0262BB00C1B952 /* NCBaseSessionManager.swift in Sources */,
+				2C62B00C24C1BDC1007E460A /* NCNotification.m in Sources */,
+				1F8AAC3E2C519689004DA20A /* StunServer.swift in Sources */,
+				1F1C0D7F29A7F33600D17C6D /* NCNotificationAction.swift in Sources */,
+				1F35F8E62AEEBC0300044BDA /* SLKTextInput+Implementation.m in Sources */,
+				1FF4DA982C0327FF00C1B952 /* NCWebImageDownloaderOperation.swift in Sources */,
+				1F35F8E72AEEBC0600044BDA /* SLKTextInputbar.m in Sources */,
+				1F35F8E52AEEBC0100044BDA /* SLKInputAccessoryView.m in Sources */,
+				1F35F8E92AEEBC0C00044BDA /* SLKTextView.m in Sources */,
+				2C4446FC265D5C5800DF1DBC /* NCRoomParticipants.m in Sources */,
+				2CB6ACDC2641483800D3D641 /* NCMessageLocationParameter.m in Sources */,
+				1F35F8E82AEEBC0800044BDA /* SLKTextView+SLKAdditions.m in Sources */,
+				1F35F9052AEEDF0E00044BDA /* AutoCompletionTableViewCell.m in Sources */,
+				2C1ABD8925769E7D00AEDFB6 /* ShareItem.m in Sources */,
+				1F35F90A2AEEE76A00044BDA /* QuotedMessageView.m in Sources */,
+				2C62B02E24C1BDD7007E460A /* PlaceholderView.m in Sources */,
+				2C62B01024C1BDC5007E460A /* NCRoom.m in Sources */,
+				1FDCC3ED29EC7E6700DEB39B /* AvatarImageView.swift in Sources */,
+				1F35F8E32AEEBBE000044BDA /* NCChatTitleView.m in Sources */,
+				1FDCC3EE29EC7E8500DEB39B /* AvatarManager.swift in Sources */,
+				1F0B0A742BA265310073FF8D /* MentionSuggestion.swift in Sources */,
+				2C62B00D24C1BDC1007E460A /* NCPushNotification.m in Sources */,
+				1F1B50492B90CF0800B0F2F4 /* TalkCapabilities.m in Sources */,
+				1F1DF83E2C5C17AF00E5EA86 /* TalkActor.swift in Sources */,
+				1FB7B9912BF0CDF80093CE98 /* BannedActor.swift in Sources */,
+				2C62B01C24C1BDC9007E460A /* CCCertificate.m in Sources */,
+				2C4446F9265D5A0700DF1DBC /* NotificationCenterNotifications.m in Sources */,
+				1F35F8ED2AEEBC1600044BDA /* UIView+SLKAdditions.m in Sources */,
+				2C1ABD8725769E7D00AEDFB6 /* ShareItemController.m in Sources */,
+				1FDFC94F2BA50B9100670DF4 /* UIFontExtension.swift in Sources */,
+				1F35F8EA2AEEBC0E00044BDA /* SLKTextViewController.m in Sources */,
+				2C6955132B0CE1A20070F6E1 /* NCUtils.swift in Sources */,
+				1F8AAC342C518B8A004DA20A /* SignalingSettings.swift in Sources */,
+				2C62AFFF24C1BDAA007E460A /* NCUser.m in Sources */,
+				1FAB2EF22AD1EAA3001214EB /* RLMSupport.swift in Sources */,
+				1FB78E212B6ADBB700B0D69D /* NCAPIControllerExtensions.swift in Sources */,
+				2C62B00724C1BDBD007E460A /* NCAPIController.m in Sources */,
+				2C1ABDD0257E939600AEDFB6 /* NCContact.m in Sources */,
+				2C4446FE265D5DFA00DF1DBC /* ABContact.m in Sources */,
+				1F35F8EF2AEEBC1A00044BDA /* SLKDefaultReplyView.m in Sources */,
+				1F35F8E22AEEBAF900044BDA /* InputbarViewController.swift in Sources */,
+				1F35F8E12AEEB9DE00044BDA /* ShareConfirmationViewController.swift in Sources */,
+				1F90EFBE25FE39F800F3FA55 /* NCIntentController.m in Sources */,
+				1FF4DA842C025DC000C1B952 /* NCAPISessionManager.swift in Sources */,
+				1F1DF8452C64006E00E5EA86 /* SignalingParticipant.swift in Sources */,
+				2C3195C224C5E2100066F221 /* ShareTableViewCell.m in Sources */,
+				2CC1FF4A2818395F009F7288 /* NCDeckCardParameter.m in Sources */,
+				1FF136112BFB4F8C006A6101 /* NCRoom.swift in Sources */,
+				2C4446DA265814D100DF1DBC /* ServerCapabilities.m in Sources */,
+				1FF4DA932C02678000C1B952 /* NCImageSessionManager.swift in Sources */,
+				2C444705265D641300DF1DBC /* NCUserDefaults.m in Sources */,
+				1FF4DA8E2C0264B200C1B952 /* NCPushProxySessionManager.swift in Sources */,
+				2C4446F5265D583200DF1DBC /* NCKeyChainController.m in Sources */,
+				1FDDB0D92AF440DD00FBAFB7 /* BoundsChangedFlowLayout.swift in Sources */,
+				2C62AFFA24C1BDA5007E460A /* NCChatMessage.m in Sources */,
+				1FC940BA2A5F21FD00FFFADE /* SwiftMarkdownObjCBridge.swift in Sources */,
+				2C4446D52658147900DF1DBC /* TalkAccount.m in Sources */,
+				1F35F9062AEEE3C400044BDA /* NCMessageTextView.m in Sources */,
+				2C1EF36D25505DCE007C9768 /* NCNavigationController.m in Sources */,
+				1F35F8F02AEEBC1D00044BDA /* SLKDefaultTypingIndicatorView.m in Sources */,
+				1F35F8FC2AEEDBC600044BDA /* ChatViewControllerExtension.swift in Sources */,
+				1FEDE3D0257D43AB00853F79 /* NCMessageFileParameter.m in Sources */,
+				2C62AFB924C1A4E6007E460A /* ShareViewController.m in Sources */,
+				1F8AAC392C519577004DA20A /* TurnServer.swift in Sources */,
+				1F35F8F32AEEC29A00044BDA /* AvatarButton.swift in Sources */,
+				2C4446DF2658158000DF1DBC /* NCChatBlock.m in Sources */,
+				1FF4DAAC2C0A114900C1B952 /* OcsResponse.swift in Sources */,
+				1F1B504C2B90CF0C00B0F2F4 /* FederatedCapabilities.m in Sources */,
+				1FB78E282B6AE8C900B0D69D /* FederationInvitation.swift in Sources */,
+				2CC32E9A27F5DADB00BB8C39 /* NCChatReaction.m in Sources */,
+				2C62AFBB24C1B7B1007E460A /* NCDatabaseManager.m in Sources */,
+				2C5BFBF4288AA37F00E75118 /* NCPoll.m in Sources */,
+				1F35F9072AEEE3EC00044BDA /* NCUserStatus.m in Sources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		2CC0014B24A1F0E900A20167 /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				2C1ABDCF257E939600AEDFB6 /* NCContact.m in Sources */,
+				2CC001DC24A37AD400A20167 /* NCAppBranding.m in Sources */,
+				2C4446D42658147900DF1DBC /* TalkAccount.m in Sources */,
+				1FDCC3E329EC787400DEB39B /* AvatarManager.swift in Sources */,
+				2CC0015324A1F0E900A20167 /* NotificationService.m in Sources */,
+				1FF4DA852C025DC000C1B952 /* NCAPISessionManager.swift in Sources */,
+				1FEDE3CF257D43AB00853F79 /* NCMessageFileParameter.m in Sources */,
+				1FB78E222B6ADBB700B0D69D /* NCAPIControllerExtensions.swift in Sources */,
+				1F1C999D2909846400EACF02 /* BGTaskHelper.swift in Sources */,
+				1FAB2EF12AD1EAA3001214EB /* RLMSupport.swift in Sources */,
+				1FDFC94E2BA50B9100670DF4 /* UIFontExtension.swift in Sources */,
+				2C4446F4265D51A600DF1DBC /* NCPushNotificationsUtils.m in Sources */,
+				1FA38C9129A4B3C6008871B8 /* NCNotificationAction.swift in Sources */,
+				1F1DF83D2C5C17AF00E5EA86 /* TalkActor.swift in Sources */,
+				1F1B504D2B90CF0C00B0F2F4 /* FederatedCapabilities.m in Sources */,
+				2C4446DE2658158000DF1DBC /* NCChatBlock.m in Sources */,
+				2C5BFBF3288AA37F00E75118 /* NCPoll.m in Sources */,
+				2C4446D9265814D100DF1DBC /* ServerCapabilities.m in Sources */,
+				2CC001CE24A37ACA00A20167 /* NCRoom.m in Sources */,
+				1FF136162BFB74CF006A6101 /* NCChatMessage.swift in Sources */,
+				1FB7B9922BF0CDF90093CE98 /* BannedActor.swift in Sources */,
+				1F8AAC382C519577004DA20A /* TurnServer.swift in Sources */,
+				2CC001C124A37AC500A20167 /* NCNotification.m in Sources */,
+				2C4446FD265D5DFA00DF1DBC /* ABContact.m in Sources */,
+				1F1DF8442C64006E00E5EA86 /* SignalingParticipant.swift in Sources */,
+				1F8AAC3D2C519689004DA20A /* StunServer.swift in Sources */,
+				2C4446F8265D5A0700DF1DBC /* NotificationCenterNotifications.m in Sources */,
+				2C6955142B0CE1A20070F6E1 /* NCUtils.swift in Sources */,
+				1FF4DA942C02678000C1B952 /* NCImageSessionManager.swift in Sources */,
+				1F0B0A752BA265310073FF8D /* MentionSuggestion.swift in Sources */,
+				1FC940B92A5F21FC00FFFADE /* SwiftMarkdownObjCBridge.swift in Sources */,
+				1FF4DA882C0262BA00C1B952 /* NCBaseSessionManager.swift in Sources */,
+				1F8AAC332C518B8A004DA20A /* SignalingSettings.swift in Sources */,
+				2CB6ACDB2641483800D3D641 /* NCMessageLocationParameter.m in Sources */,
+				2C4446FB265D5C5700DF1DBC /* NCRoomParticipants.m in Sources */,
+				2CC1FF492818395E009F7288 /* NCDeckCardParameter.m in Sources */,
+				2CC001C224A37AC500A20167 /* NCPushNotification.m in Sources */,
+				1FF4DA972C0327FF00C1B952 /* NCWebImageDownloaderOperation.swift in Sources */,
+				1FF4DAAB2C0A114900C1B952 /* OcsResponse.swift in Sources */,
+				1F90EFBD25FE39F800F3FA55 /* NCIntentController.m in Sources */,
+				2CC001DB24A37AD000A20167 /* CCCertificate.m in Sources */,
+				1F4DD3EC2571C688007DC98E /* EmojiUtils.swift in Sources */,
+				2C4446ED265D25BA00DF1DBC /* NCKeyChainController.m in Sources */,
+				1F1B50482B90CF0800B0F2F4 /* TalkCapabilities.m in Sources */,
+				2CC0016724A25BE100A20167 /* NCChatMessage.m in Sources */,
+				1FF4DA8F2C0264B200C1B952 /* NCPushProxySessionManager.swift in Sources */,
+				2CC0016324A25B7400A20167 /* NCDatabaseManager.m in Sources */,
+				1FF136102BFB4F8C006A6101 /* NCRoom.swift in Sources */,
+				2CC0016924A25C3400A20167 /* NCMessageParameter.m in Sources */,
+				1FB78E292B6AE8CA00B0D69D /* FederationInvitation.swift in Sources */,
+				2C444704265D641300DF1DBC /* NCUserDefaults.m in Sources */,
+				2CC001B724A37A9A00A20167 /* NCUser.m in Sources */,
+				2CC0016124A25B5500A20167 /* NCAPIController.m in Sources */,
+				2CC32E9927F5DADA00BB8C39 /* NCChatReaction.m in Sources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXTargetDependency section */
+		1F6D8C352B2E3756004376B8 /* PBXTargetDependency */ = {
+			isa = PBXTargetDependency;
+			target = 2C05747C1EDD9E8E00D9E7F2 /* NextcloudTalk */;
+			targetProxy = 1F6D8C342B2E3756004376B8 /* PBXContainerItemProxy */;
+		};
+		1FD8AD912A3A162100787C16 /* PBXTargetDependency */ = {
+			isa = PBXTargetDependency;
+			target = 2C05747C1EDD9E8E00D9E7F2 /* NextcloudTalk */;
+			targetProxy = 1FD8AD902A3A162100787C16 /* PBXContainerItemProxy */;
+		};
+		5EE5ACC52CF48BCA004D7EDB /* PBXTargetDependency */ = {
+			isa = PBXTargetDependency;
+			target = 1FF2FD5A2AB99CCB000C9905 /* BroadcastUploadExtension */;
+			targetProxy = 5EE5ACC42CF48BCA004D7EDB /* PBXContainerItemProxy */;
+		};
+		5EE5ACC92CF48BDA004D7EDB /* PBXTargetDependency */ = {
+			isa = PBXTargetDependency;
+			target = 2CC0014E24A1F0E900A20167 /* NotificationServiceExtension */;
+			targetProxy = 5EE5ACC82CF48BDA004D7EDB /* PBXContainerItemProxy */;
+		};
+		5EE5ACCC2CF48BDF004D7EDB /* PBXTargetDependency */ = {
+			isa = PBXTargetDependency;
+			target = 2C62AFA224C08845007E460A /* ShareExtension */;
+			targetProxy = 5EE5ACCB2CF48BDF004D7EDB /* PBXContainerItemProxy */;
+		};
+/* End PBXTargetDependency section */
+
+/* Begin PBXVariantGroup section */
+		2C05748C1EDD9E8E00D9E7F2 /* Main.storyboard */ = {
+			isa = PBXVariantGroup;
+			children = (
+				2C05748D1EDD9E8E00D9E7F2 /* Base */,
+			);
+			name = Main.storyboard;
+			sourceTree = "<group>";
+		};
+		2C1D13A1253760EE00EC0533 /* LaunchScreen.xib */ = {
+			isa = PBXVariantGroup;
+			children = (
+				2C1D13A2253760EE00EC0533 /* Base */,
+			);
+			name = LaunchScreen.xib;
+			sourceTree = "<group>";
+		};
+		2C330374255E6EBC00BDB4E4 /* InfoPlist.strings */ = {
+			isa = PBXVariantGroup;
+			children = (
+				2C330373255E6EBC00BDB4E4 /* en */,
+				2CA80EDC256C1249006BA449 /* es */,
+				2CA80EDE256C1296006BA449 /* de */,
+				2CA80EE0256C12E7006BA449 /* it */,
+				2C604A2B25E4556E00F23615 /* tr */,
+				2C604A2D25E455AC00F23615 /* sl */,
+				2C604A2F25E455C500F23615 /* hr */,
+				2C604A3125E455D900F23615 /* pl */,
+				2C604A3325E455ED00F23615 /* pt-BR */,
+				2C604A3925E4568400F23615 /* nl */,
+				2C604A3B25E4569300F23615 /* fr */,
+				2C604A3D25E4569F00F23615 /* el */,
+				2C604A4125E45A9400F23615 /* zh-Hans */,
+				2C604A4325E45BAE00F23615 /* gl */,
+				2C928BD3268A06BB00729332 /* cs */,
+				2C928BD5268A0AAD00729332 /* hu */,
+				2C928BD7268A0B2800729332 /* ko */,
+				2C928BD9268A0BC000729332 /* eu */,
+				2C4CDCD426AF16650023F403 /* ar */,
+				1F21A0622C77863500ED8C0C /* nb-NO */,
+				1F21A0652C77865D00ED8C0C /* ga */,
+				1F21A06A2C77868000ED8C0C /* sv */,
+				1F21A06B2C77869600ED8C0C /* sr */,
+			);
+			name = InfoPlist.strings;
+			sourceTree = "<group>";
+		};
+		2C477C1828B79D980044DEB4 /* Localizable.stringsdict */ = {
+			isa = PBXVariantGroup;
+			children = (
+				2C477C1728B79D980044DEB4 /* en */,
+				2CEA990828B8B5780029216A /* de */,
+				2CB2B24D28BCB9D900A9D606 /* cs */,
+				2CB2B24E28BCB9E800A9D606 /* hu */,
+				2CB2B24F28BCBABC00A9D606 /* pl */,
+				2CB2B25128BF957100A9D606 /* tr */,
+				2C57CD8228C204C600B22E03 /* es */,
+				2C57CD8528CB3FAF00B22E03 /* eu */,
+				2C67905128D35BEB00762744 /* sl */,
+				1F21A0642C77863500ED8C0C /* nb-NO */,
+				1F21A0662C77865D00ED8C0C /* ga */,
+				1F21A0682C77868000ED8C0C /* sv */,
+				1F21A06D2C77869600ED8C0C /* sr */,
+			);
+			name = Localizable.stringsdict;
+			sourceTree = "<group>";
+		};
+		2C7F47AC20289B9600081CC7 /* Localizable.strings */ = {
+			isa = PBXVariantGroup;
+			children = (
+				2C7F47AB20289B9600081CC7 /* en */,
+				2CA80EDB256C1249006BA449 /* es */,
+				2CA80EDD256C1296006BA449 /* de */,
+				2CA80EDF256C12E7006BA449 /* it */,
+				2C604A2A25E4556E00F23615 /* tr */,
+				2C604A2C25E455AC00F23615 /* sl */,
+				2C604A2E25E455C500F23615 /* hr */,
+				2C604A3025E455D900F23615 /* pl */,
+				2C604A3225E455ED00F23615 /* pt-BR */,
+				2C604A3825E4568400F23615 /* nl */,
+				2C604A3A25E4569300F23615 /* fr */,
+				2C604A3C25E4569F00F23615 /* el */,
+				2C604A4025E45A9400F23615 /* zh-Hans */,
+				2C604A4225E45BAE00F23615 /* gl */,
+				2C928BD2268A06BB00729332 /* cs */,
+				2C928BD4268A0AAD00729332 /* hu */,
+				2C928BD6268A0B2800729332 /* ko */,
+				2C928BD8268A0BC000729332 /* eu */,
+				2C928BDA268A103600729332 /* ja */,
+				2C4CDCD326AF16650023F403 /* ar */,
+				1F21A0632C77863500ED8C0C /* nb-NO */,
+				1F21A0672C77865D00ED8C0C /* ga */,
+				1F21A0692C77868000ED8C0C /* sv */,
+				1F21A06C2C77869600ED8C0C /* sr */,
+			);
+			name = Localizable.strings;
+			sourceTree = "<group>";
+		};
+/* End PBXVariantGroup section */
+
+/* Begin XCBuildConfiguration section */
+		1F6D8C372B2E3756004376B8 /* Debug */ = {
+			isa = XCBuildConfiguration;
+			baseConfigurationReference = 342600BABD1AD1FCA48B5E59 /* Pods-NextcloudTalkTests.debug.xcconfig */;
+			buildSettings = {
+				ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+				BUNDLE_LOADER = "$(TEST_HOST)";
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+				CLANG_ENABLE_MODULES = YES;
+				CLANG_ENABLE_OBJC_WEAK = YES;
+				CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+				CODE_SIGN_STYLE = Automatic;
+				CURRENT_PROJECT_VERSION = 1;
+				DEVELOPMENT_TEAM = L8PXSNYX82;
+				ENABLE_USER_SCRIPT_SANDBOXING = NO;
+				GCC_C_LANGUAGE_STANDARD = gnu17;
+				GENERATE_INFOPLIST_FILE = YES;
+				HEADER_SEARCH_PATHS = (
+					"$(inherited)",
+					"\"${PODS_ROOT}/Headers/Public\"",
+					"\"${PODS_ROOT}/Headers/Public/AFNetworking\"",
+					"\"${PODS_ROOT}/Headers/Public/DateTools\"",
+					"\"${PODS_ROOT}/Headers/Public/GoogleToolboxForMac\"",
+					"\"${PODS_ROOT}/Headers/Public/GoogleWebRTC\"",
+					"\"${PODS_ROOT}/Headers/Public/Protobuf\"",
+					"\"${PODS_ROOT}/Headers/Public/nanopb\"",
+					"\"$(PROJECT_DIR)/ThirdParty\"/**",
+				);
+				IPHONEOS_DEPLOYMENT_TARGET = 15.0;
+				LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+				MARKETING_VERSION = 1.0;
+				MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
+				MTL_FAST_MATH = YES;
+				PRODUCT_BUNDLE_IDENTIFIER = com.nextcloud.Talk.NextcloudTalkTests;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
+				SWIFT_EMIT_LOC_STRINGS = NO;
+				SWIFT_OBJC_BRIDGING_HEADER = "NextcloudTalk/NextcloudTalk-Bridging-Header.h";
+				SWIFT_OBJC_INTERFACE_HEADER_NAME = "NextcloudTalk-Swift.h";
+				SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+				SWIFT_VERSION = 5.0;
+				TARGETED_DEVICE_FAMILY = "1,2";
+				TEST_HOST = "$(BUILT_PRODUCTS_DIR)/NextcloudTalk.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/NextcloudTalk";
+			};
+			name = Debug;
+		};
+		1F6D8C382B2E3756004376B8 /* Release */ = {
+			isa = XCBuildConfiguration;
+			baseConfigurationReference = B7874918820589BF8FD69BED /* Pods-NextcloudTalkTests.release.xcconfig */;
+			buildSettings = {
+				ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+				BUNDLE_LOADER = "$(TEST_HOST)";
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+				CLANG_ENABLE_MODULES = YES;
+				CLANG_ENABLE_OBJC_WEAK = YES;
+				CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+				CODE_SIGN_STYLE = Automatic;
+				CURRENT_PROJECT_VERSION = 1;
+				DEVELOPMENT_TEAM = L8PXSNYX82;
+				ENABLE_USER_SCRIPT_SANDBOXING = NO;
+				GCC_C_LANGUAGE_STANDARD = gnu17;
+				GENERATE_INFOPLIST_FILE = YES;
+				HEADER_SEARCH_PATHS = (
+					"$(inherited)",
+					"\"${PODS_ROOT}/Headers/Public\"",
+					"\"${PODS_ROOT}/Headers/Public/AFNetworking\"",
+					"\"${PODS_ROOT}/Headers/Public/DateTools\"",
+					"\"${PODS_ROOT}/Headers/Public/GoogleToolboxForMac\"",
+					"\"${PODS_ROOT}/Headers/Public/GoogleWebRTC\"",
+					"\"${PODS_ROOT}/Headers/Public/Protobuf\"",
+					"\"${PODS_ROOT}/Headers/Public/nanopb\"",
+					"\"$(PROJECT_DIR)/ThirdParty\"/**",
+				);
+				IPHONEOS_DEPLOYMENT_TARGET = 15.0;
+				LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+				MARKETING_VERSION = 1.0;
+				MTL_FAST_MATH = YES;
+				PRODUCT_BUNDLE_IDENTIFIER = com.nextcloud.Talk.NextcloudTalkTests;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SWIFT_EMIT_LOC_STRINGS = NO;
+				SWIFT_OBJC_BRIDGING_HEADER = "NextcloudTalk/NextcloudTalk-Bridging-Header.h";
+				SWIFT_OBJC_INTERFACE_HEADER_NAME = "NextcloudTalk-Swift.h";
+				SWIFT_VERSION = 5.0;
+				TARGETED_DEVICE_FAMILY = "1,2";
+				TEST_HOST = "$(BUILT_PRODUCTS_DIR)/NextcloudTalk.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/NextcloudTalk";
+			};
+			name = Release;
+		};
+		1FD8AD932A3A162100787C16 /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+				CLANG_ENABLE_OBJC_WEAK = YES;
+				CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+				CODE_SIGN_STYLE = Automatic;
+				CURRENT_PROJECT_VERSION = 1;
+				DEVELOPMENT_TEAM = L8PXSNYX82;
+				GCC_C_LANGUAGE_STANDARD = gnu11;
+				GENERATE_INFOPLIST_FILE = YES;
+				IPHONEOS_DEPLOYMENT_TARGET = 15.0;
+				MARKETING_VERSION = 1.0;
+				MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
+				MTL_FAST_MATH = YES;
+				PRODUCT_BUNDLE_IDENTIFIER = com.sharix.rooms.NextcloudTalkUITests;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
+				SUPPORTS_MACCATALYST = NO;
+				SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
+				SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
+				SWIFT_EMIT_LOC_STRINGS = NO;
+				SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+				SWIFT_VERSION = 5.0;
+				TARGETED_DEVICE_FAMILY = "1,2";
+				TEST_TARGET_NAME = NextcloudTalk;
+			};
+			name = Debug;
+		};
+		1FD8AD942A3A162100787C16 /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+				CLANG_ENABLE_OBJC_WEAK = YES;
+				CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+				CODE_SIGN_STYLE = Automatic;
+				CURRENT_PROJECT_VERSION = 1;
+				DEVELOPMENT_TEAM = L8PXSNYX82;
+				GCC_C_LANGUAGE_STANDARD = gnu11;
+				GENERATE_INFOPLIST_FILE = YES;
+				IPHONEOS_DEPLOYMENT_TARGET = 15.0;
+				MARKETING_VERSION = 1.0;
+				MTL_FAST_MATH = YES;
+				PRODUCT_BUNDLE_IDENTIFIER = com.sharix.rooms.NextcloudTalkUITests;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
+				SUPPORTS_MACCATALYST = NO;
+				SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
+				SWIFT_EMIT_LOC_STRINGS = NO;
+				SWIFT_OPTIMIZATION_LEVEL = "-O";
+				SWIFT_VERSION = 5.0;
+				TARGETED_DEVICE_FAMILY = "1,2";
+				TEST_TARGET_NAME = NextcloudTalk;
+			};
+			name = Release;
+		};
+		1FF2FD732AB99CCB000C9905 /* Debug */ = {
+			isa = XCBuildConfiguration;
+			baseConfigurationReference = 82CD0527E04B844CAD762ADE /* Pods-BroadcastUploadExtension.debug.xcconfig */;
+			buildSettings = {
+				BUILD_LIBRARY_FOR_DISTRIBUTION = NO;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+				CLANG_ENABLE_OBJC_WEAK = YES;
+				CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+				CODE_SIGN_ENTITLEMENTS = BroadcastUploadExtension/BroadcastUploadExtension.entitlements;
+				CODE_SIGN_IDENTITY = "iPhone Developer";
+				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+				CODE_SIGN_STYLE = Automatic;
+				CURRENT_PROJECT_VERSION = 1;
+				DEVELOPMENT_TEAM = L8PXSNYX82;
+				GCC_C_LANGUAGE_STANDARD = gnu11;
+				GCC_PREPROCESSOR_DEFINITIONS = (
+					"$(inherited)",
+					"COCOAPODS=1",
+					"APP_EXTENSION=1",
+				);
+				GENERATE_INFOPLIST_FILE = YES;
+				INFOPLIST_FILE = BroadcastUploadExtension/Info.plist;
+				INFOPLIST_KEY_CFBundleDisplayName = "Nextcloud Talk";
+				INFOPLIST_KEY_NSHumanReadableCopyright = "";
+				IPHONEOS_DEPLOYMENT_TARGET = 15.0;
+				LD_RUNPATH_SEARCH_PATHS = (
+					"$(inherited)",
+					"@executable_path/Frameworks",
+					"@executable_path/../../Frameworks",
+				);
+				MARKETING_VERSION = 1.0;
+				MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
+				MTL_FAST_MATH = YES;
+				ONLY_ACTIVE_ARCH = YES;
+				PRODUCT_BUNDLE_IDENTIFIER = com.sharix.sxrooms.BroadcastUploadExtension;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SKIP_INSTALL = YES;
+				SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
+				SUPPORTS_MACCATALYST = NO;
+				SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
+				SWIFT_ACTIVE_COMPILATION_CONDITIONS = APP_EXTENSION;
+				SWIFT_EMIT_LOC_STRINGS = YES;
+				SWIFT_OBJC_BRIDGING_HEADER = "NextcloudTalk/NextcloudTalk-Bridging-Header-Extensions.h";
+				SWIFT_OBJC_INTERFACE_HEADER_NAME = "NextcloudTalk-Swift.h";
+				SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+				SWIFT_VERSION = 5.0;
+				TARGETED_DEVICE_FAMILY = "1,2";
+			};
+			name = Debug;
+		};
+		1FF2FD742AB99CCB000C9905 /* Release */ = {
+			isa = XCBuildConfiguration;
+			baseConfigurationReference = A8F95DE6635ABC1E64CA8E4A /* Pods-BroadcastUploadExtension.release.xcconfig */;
+			buildSettings = {
+				BUILD_LIBRARY_FOR_DISTRIBUTION = NO;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+				CLANG_ENABLE_OBJC_WEAK = YES;
+				CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+				CODE_SIGN_ENTITLEMENTS = BroadcastUploadExtension/BroadcastUploadExtension.entitlements;
+				CODE_SIGN_IDENTITY = "iPhone Distribution: Nextcloud GmbH (NKUJUXUJ3B)";
+				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+				CODE_SIGN_STYLE = Automatic;
+				CURRENT_PROJECT_VERSION = 1;
+				DEVELOPMENT_TEAM = L8PXSNYX82;
+				GCC_C_LANGUAGE_STANDARD = gnu11;
+				GCC_PREPROCESSOR_DEFINITIONS = (
+					"$(inherited)",
+					"COCOAPODS=1",
+					"APP_EXTENSION=1",
+				);
+				GENERATE_INFOPLIST_FILE = YES;
+				INFOPLIST_FILE = BroadcastUploadExtension/Info.plist;
+				INFOPLIST_KEY_CFBundleDisplayName = "Nextcloud Talk";
+				INFOPLIST_KEY_NSHumanReadableCopyright = "";
+				IPHONEOS_DEPLOYMENT_TARGET = 15.0;
+				LD_RUNPATH_SEARCH_PATHS = (
+					"$(inherited)",
+					"@executable_path/Frameworks",
+					"@executable_path/../../Frameworks",
+				);
+				MARKETING_VERSION = 1.0;
+				MTL_FAST_MATH = YES;
+				ONLY_ACTIVE_ARCH = NO;
+				PRODUCT_BUNDLE_IDENTIFIER = com.sharix.sxrooms.BroadcastUploadExtension;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SKIP_INSTALL = YES;
+				SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
+				SUPPORTS_MACCATALYST = NO;
+				SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
+				SWIFT_ACTIVE_COMPILATION_CONDITIONS = APP_EXTENSION;
+				SWIFT_EMIT_LOC_STRINGS = YES;
+				SWIFT_OBJC_BRIDGING_HEADER = "NextcloudTalk/NextcloudTalk-Bridging-Header-Extensions.h";
+				SWIFT_OBJC_INTERFACE_HEADER_NAME = "NextcloudTalk-Swift.h";
+				SWIFT_OPTIMIZATION_LEVEL = "-O";
+				SWIFT_VERSION = 5.0;
+				TARGETED_DEVICE_FAMILY = "1,2";
+			};
+			name = Release;
+		};
+		2C0574951EDD9E8E00D9E7F2 /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
+				CLANG_ANALYZER_NONNULL = YES;
+				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+				CLANG_CXX_LIBRARY = "libc++";
+				CLANG_ENABLE_MODULES = YES;
+				CLANG_ENABLE_OBJC_ARC = YES;
+				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+				CLANG_WARN_BOOL_CONVERSION = YES;
+				CLANG_WARN_COMMA = YES;
+				CLANG_WARN_CONSTANT_CONVERSION = YES;
+				CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+				CLANG_WARN_EMPTY_BODY = YES;
+				CLANG_WARN_ENUM_CONVERSION = YES;
+				CLANG_WARN_INFINITE_RECURSION = YES;
+				CLANG_WARN_INT_CONVERSION = YES;
+				CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+				CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+				CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+				CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+				CLANG_WARN_STRICT_PROTOTYPES = YES;
+				CLANG_WARN_SUSPICIOUS_MOVE = YES;
+				CLANG_WARN_UNREACHABLE_CODE = YES;
+				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+				COPY_PHASE_STRIP = NO;
+				DEBUG_INFORMATION_FORMAT = dwarf;
+				DEFINES_MODULE = YES;
+				ENABLE_STRICT_OBJC_MSGSEND = YES;
+				ENABLE_TESTABILITY = YES;
+				GCC_C_LANGUAGE_STANDARD = gnu99;
+				GCC_DYNAMIC_NO_PIC = NO;
+				GCC_NO_COMMON_BLOCKS = YES;
+				GCC_OPTIMIZATION_LEVEL = 0;
+				GCC_PREPROCESSOR_DEFINITIONS = (
+					"DEBUG=1",
+					"$(inherited)",
+				);
+				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+				GCC_WARN_UNDECLARED_SELECTOR = YES;
+				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+				GCC_WARN_UNUSED_FUNCTION = YES;
+				GCC_WARN_UNUSED_VARIABLE = YES;
+				IPHONEOS_DEPLOYMENT_TARGET = 15.0;
+				MTL_ENABLE_DEBUG_INFO = YES;
+				ONLY_ACTIVE_ARCH = YES;
+				SDKROOT = iphoneos;
+				TARGETED_DEVICE_FAMILY = "1,2";
+			};
+			name = Debug;
+		};
+		2C0574961EDD9E8E00D9E7F2 /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
+				CLANG_ANALYZER_NONNULL = YES;
+				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+				CLANG_CXX_LIBRARY = "libc++";
+				CLANG_ENABLE_MODULES = YES;
+				CLANG_ENABLE_OBJC_ARC = YES;
+				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+				CLANG_WARN_BOOL_CONVERSION = YES;
+				CLANG_WARN_COMMA = YES;
+				CLANG_WARN_CONSTANT_CONVERSION = YES;
+				CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+				CLANG_WARN_EMPTY_BODY = YES;
+				CLANG_WARN_ENUM_CONVERSION = YES;
+				CLANG_WARN_INFINITE_RECURSION = YES;
+				CLANG_WARN_INT_CONVERSION = YES;
+				CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+				CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+				CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+				CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+				CLANG_WARN_STRICT_PROTOTYPES = YES;
+				CLANG_WARN_SUSPICIOUS_MOVE = YES;
+				CLANG_WARN_UNREACHABLE_CODE = YES;
+				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+				COPY_PHASE_STRIP = NO;
+				DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+				DEFINES_MODULE = YES;
+				ENABLE_NS_ASSERTIONS = NO;
+				ENABLE_STRICT_OBJC_MSGSEND = YES;
+				GCC_C_LANGUAGE_STANDARD = gnu99;
+				GCC_NO_COMMON_BLOCKS = YES;
+				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+				GCC_WARN_UNDECLARED_SELECTOR = YES;
+				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+				GCC_WARN_UNUSED_FUNCTION = YES;
+				GCC_WARN_UNUSED_VARIABLE = YES;
+				IPHONEOS_DEPLOYMENT_TARGET = 15.0;
+				MTL_ENABLE_DEBUG_INFO = NO;
+				SDKROOT = iphoneos;
+				SWIFT_COMPILATION_MODE = wholemodule;
+				TARGETED_DEVICE_FAMILY = "1,2";
+				VALIDATE_PRODUCT = YES;
+			};
+			name = Release;
+		};
+		2C0574981EDD9E8E00D9E7F2 /* Debug */ = {
+			isa = XCBuildConfiguration;
+			baseConfigurationReference = 4202C63030F0FFBB1C16D75E /* Pods-NextcloudTalk.debug.xcconfig */;
+			buildSettings = {
+				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				CLANG_ENABLE_MODULES = YES;
+				CODE_SIGN_ENTITLEMENTS = NextcloudTalk/NextcloudTalk.entitlements;
+				CODE_SIGN_IDENTITY = "iPhone Developer";
+				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+				CODE_SIGN_STYLE = Automatic;
+				CURRENT_PROJECT_VERSION = 1;
+				DEFINES_MODULE = YES;
+				DEVELOPMENT_TEAM = L8PXSNYX82;
+				ENABLE_BITCODE = NO;
+				FRAMEWORK_SEARCH_PATHS = "$(inherited)";
+				GCC_SYMBOLS_PRIVATE_EXTERN = NO;
+				HEADER_SEARCH_PATHS = (
+					"$(inherited)",
+					"\"${PODS_ROOT}/Headers/Public\"",
+					"\"${PODS_ROOT}/Headers/Public/AFNetworking\"",
+					"\"${PODS_ROOT}/Headers/Public/DateTools\"",
+					"\"${PODS_ROOT}/Headers/Public/GoogleToolboxForMac\"",
+					"\"${PODS_ROOT}/Headers/Public/GoogleWebRTC\"",
+					"\"${PODS_ROOT}/Headers/Public/Protobuf\"",
+					"\"${PODS_ROOT}/Headers/Public/nanopb\"",
+					"\"$(PROJECT_DIR)/ThirdParty\"/**",
+				);
+				"HEADER_SEARCH_PATHS[arch=*]" = (
+					"$(inherited)",
+					"\"${PODS_ROOT}/Headers/Public\"",
+					"\"${PODS_ROOT}/Headers/Public/AFNetworking\"",
+					"\"${PODS_ROOT}/Headers/Public/DateTools\"",
+					"\"${PODS_ROOT}/Headers/Public/GoogleToolboxForMac\"",
+					"\"${PODS_ROOT}/Headers/Public/GoogleWebRTC\"",
+					"\"${PODS_ROOT}/Headers/Public/Protobuf\"",
+					"\"${PODS_ROOT}/Headers/Public/nanopb\"",
+					"\"$(PROJECT_DIR)/ThirdParty\"/**",
+				);
+				INFOPLIST_FILE = NextcloudTalk/Info.plist;
+				INFOPLIST_KEY_CFBundleDisplayName = "SX Rooms";
+				IPHONEOS_DEPLOYMENT_TARGET = 15.0;
+				LD_RUNPATH_SEARCH_PATHS = (
+					"$(inherited)",
+					"@executable_path/Frameworks",
+				);
+				LIBRARY_SEARCH_PATHS = "$(inherited)";
+				MARKETING_VERSION = 20.0.5;
+				PRODUCT_BUNDLE_IDENTIFIER = com.sharix.sxrooms;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				PROVISIONING_PROFILE_SPECIFIER = "";
+				SWIFT_OBJC_BRIDGING_HEADER = "NextcloudTalk/NextcloudTalk-Bridging-Header.h";
+				SWIFT_OBJC_INTERFACE_HEADER_NAME = "NextcloudTalk-Swift.h";
+				SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+				SWIFT_VERSION = 5.0;
+			};
+			name = Debug;
+		};
+		2C0574991EDD9E8E00D9E7F2 /* Release */ = {
+			isa = XCBuildConfiguration;
+			baseConfigurationReference = 4D4C7BF2F97F47B0D9094618 /* Pods-NextcloudTalk.release.xcconfig */;
+			buildSettings = {
+				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				CLANG_ENABLE_MODULES = YES;
+				CODE_SIGN_ENTITLEMENTS = NextcloudTalk/NextcloudTalk.entitlements;
+				CODE_SIGN_IDENTITY = "iPhone Distribution: Nextcloud GmbH (NKUJUXUJ3B)";
+				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+				CODE_SIGN_STYLE = Automatic;
+				CURRENT_PROJECT_VERSION = 1;
+				DEFINES_MODULE = YES;
+				DEVELOPMENT_TEAM = L8PXSNYX82;
+				ENABLE_BITCODE = NO;
+				FRAMEWORK_SEARCH_PATHS = "$(inherited)";
+				GCC_SYMBOLS_PRIVATE_EXTERN = NO;
+				HEADER_SEARCH_PATHS = (
+					"$(inherited)",
+					"\"${PODS_ROOT}/Headers/Public\"",
+					"\"${PODS_ROOT}/Headers/Public/AFNetworking\"",
+					"\"${PODS_ROOT}/Headers/Public/DateTools\"",
+					"\"${PODS_ROOT}/Headers/Public/GoogleToolboxForMac\"",
+					"\"${PODS_ROOT}/Headers/Public/GoogleWebRTC\"",
+					"\"${PODS_ROOT}/Headers/Public/Protobuf\"",
+					"\"${PODS_ROOT}/Headers/Public/nanopb\"",
+					"\"$(PROJECT_DIR)/ThirdParty\"/**",
+				);
+				INFOPLIST_FILE = NextcloudTalk/Info.plist;
+				INFOPLIST_KEY_CFBundleDisplayName = "SX Rooms";
+				IPHONEOS_DEPLOYMENT_TARGET = 15.0;
+				LD_RUNPATH_SEARCH_PATHS = (
+					"$(inherited)",
+					"@executable_path/Frameworks",
+				);
+				LIBRARY_SEARCH_PATHS = "$(inherited)";
+				MARKETING_VERSION = 20.0.5;
+				PRODUCT_BUNDLE_IDENTIFIER = com.sharix.sxrooms;
+				PRODUCT_NAME = "SX Rooms";
+				PROVISIONING_PROFILE_SPECIFIER = "";
+				SWIFT_OBJC_BRIDGING_HEADER = "NextcloudTalk/NextcloudTalk-Bridging-Header.h";
+				SWIFT_OBJC_INTERFACE_HEADER_NAME = "NextcloudTalk-Swift.h";
+				SWIFT_VERSION = 5.0;
+			};
+			name = Release;
+		};
+		2C62AFAF24C08845007E460A /* Debug */ = {
+			isa = XCBuildConfiguration;
+			baseConfigurationReference = 584BF273DF09DE4D5EE0DA0F /* Pods-ShareExtension.debug.xcconfig */;
+			buildSettings = {
+				APPLICATION_EXTENSION_API_ONLY = YES;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+				CLANG_ENABLE_MODULES = YES;
+				CLANG_ENABLE_OBJC_WEAK = YES;
+				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+				CLANG_WARN_COMMA = YES;
+				CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+				CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+				CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+				CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+				CLANG_WARN_STRICT_PROTOTYPES = YES;
+				CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+				CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
+				CODE_SIGN_IDENTITY = "iPhone Developer";
+				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+				CODE_SIGN_STYLE = Automatic;
+				CURRENT_PROJECT_VERSION = 1;
+				DEFINES_MODULE = YES;
+				DEVELOPMENT_TEAM = L8PXSNYX82;
+				ENABLE_BITCODE = NO;
+				FRAMEWORK_SEARCH_PATHS = "$(inherited)";
+				GCC_C_LANGUAGE_STANDARD = gnu11;
+				GCC_PREPROCESSOR_DEFINITIONS = (
+					"$(inherited)",
+					"APP_EXTENSION=1",
+				);
+				HEADER_SEARCH_PATHS = (
+					"$(inherited)",
+					"\"${PODS_ROOT}/Headers/Public\"",
+					"\"${PODS_ROOT}/Headers/Public/AFNetworking\"",
+					"\"${PODS_ROOT}/Headers/Public/AFViewShaker\"",
+					"\"${PODS_ROOT}/Headers/Public/BKPasscodeView\"",
+					"\"${PODS_ROOT}/Headers/Public/DBImageColorPicker\"",
+					"\"${PODS_ROOT}/Headers/Public/DateTools\"",
+					"\"${PODS_ROOT}/Headers/Public/JDStatusBarNotification\"",
+					"\"${PODS_ROOT}/Headers/Public/MDFInternationalization\"",
+					"\"${PODS_ROOT}/Headers/Public/MaterialComponents\"",
+					"\"${PODS_ROOT}/Headers/Public/MotionAnimator\"",
+					"\"${PODS_ROOT}/Headers/Public/MotionInterchange\"",
+					"\"${PODS_ROOT}/Headers/Public/PulsingHalo\"",
+					"\"${PODS_ROOT}/Headers/Public/Realm\"",
+					"\"${PODS_ROOT}/Headers/Public/SocketRocket\"",
+					"\"${PODS_ROOT}/Headers/Public/Toast\"",
+					"\"${PODS_ROOT}/Headers/Public/UICKeyChainStore\"",
+					"\"$(PROJECT_DIR)/ThirdParty\"/**",
+				);
+				INFOPLIST_FILE = ShareExtension/Info.plist;
+				IPHONEOS_DEPLOYMENT_TARGET = 15.0;
+				LD_RUNPATH_SEARCH_PATHS = (
+					"$(inherited)",
+					"@executable_path/Frameworks",
+					"@executable_path/../../Frameworks",
+				);
+				LIBRARY_SEARCH_PATHS = (
+					"$(inherited)",
+					"\"${PODS_CONFIGURATION_BUILD_DIR}/AFNetworking\"",
+					"\"${PODS_CONFIGURATION_BUILD_DIR}/AFViewShaker\"",
+					"\"${PODS_CONFIGURATION_BUILD_DIR}/BKPasscodeView\"",
+					"\"${PODS_CONFIGURATION_BUILD_DIR}/DBImageColorPicker\"",
+					"\"${PODS_CONFIGURATION_BUILD_DIR}/DateTools\"",
+					"\"${PODS_CONFIGURATION_BUILD_DIR}/JDStatusBarNotification\"",
+					"\"${PODS_CONFIGURATION_BUILD_DIR}/MDFInternationalization\"",
+					"\"${PODS_CONFIGURATION_BUILD_DIR}/MaterialComponents\"",
+					"\"${PODS_CONFIGURATION_BUILD_DIR}/MotionAnimator\"",
+					"\"${PODS_CONFIGURATION_BUILD_DIR}/MotionInterchange\"",
+					"\"${PODS_CONFIGURATION_BUILD_DIR}/PulsingHalo\"",
+					"\"${PODS_CONFIGURATION_BUILD_DIR}/Realm\"",
+					"\"${PODS_CONFIGURATION_BUILD_DIR}/SocketRocket\"",
+					"\"${PODS_CONFIGURATION_BUILD_DIR}/Toast\"",
+					"\"${PODS_CONFIGURATION_BUILD_DIR}/UICKeyChainStore\"",
+					"\"${PODS_ROOT}/Realm/core\"",
+				);
+				MARKETING_VERSION = 20.0.5;
+				MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
+				MTL_FAST_MATH = YES;
+				ONLY_ACTIVE_ARCH = YES;
+				PRODUCT_BUNDLE_IDENTIFIER = com.sharix.sxrooms.ShareExtension;
+				PRODUCT_NAME = ShareExtension;
+				SKIP_INSTALL = YES;
+				SWIFT_ACTIVE_COMPILATION_CONDITIONS = APP_EXTENSION;
+				SWIFT_INSTALL_OBJC_HEADER = YES;
+				SWIFT_OBJC_BRIDGING_HEADER = "NextcloudTalk/NextcloudTalk-Bridging-Header-Extensions.h";
+				SWIFT_OBJC_INTERFACE_HEADER_NAME = "NextcloudTalk-Swift.h";
+				SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+				SWIFT_VERSION = 5.0;
+				TARGETED_DEVICE_FAMILY = "1,2";
+			};
+			name = Debug;
+		};
+		2C62AFB024C08845007E460A /* Release */ = {
+			isa = XCBuildConfiguration;
+			baseConfigurationReference = 95D756208A81284B975853EC /* Pods-ShareExtension.release.xcconfig */;
+			buildSettings = {
+				APPLICATION_EXTENSION_API_ONLY = YES;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+				CLANG_ENABLE_MODULES = YES;
+				CLANG_ENABLE_OBJC_WEAK = YES;
+				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+				CLANG_WARN_COMMA = YES;
+				CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+				CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+				CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+				CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+				CLANG_WARN_STRICT_PROTOTYPES = YES;
+				CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+				CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
+				CODE_SIGN_IDENTITY = "iPhone Distribution: Nextcloud GmbH (NKUJUXUJ3B)";
+				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+				CODE_SIGN_STYLE = Automatic;
+				CURRENT_PROJECT_VERSION = 1;
+				DEFINES_MODULE = YES;
+				DEVELOPMENT_TEAM = L8PXSNYX82;
+				ENABLE_BITCODE = NO;
+				FRAMEWORK_SEARCH_PATHS = "$(inherited)";
+				GCC_C_LANGUAGE_STANDARD = gnu11;
+				GCC_PREPROCESSOR_DEFINITIONS = (
+					"$(inherited)",
+					"APP_EXTENSION=1",
+				);
+				HEADER_SEARCH_PATHS = (
+					"$(inherited)",
+					"\"${PODS_ROOT}/Headers/Public\"",
+					"\"${PODS_ROOT}/Headers/Public/AFNetworking\"",
+					"\"${PODS_ROOT}/Headers/Public/AFViewShaker\"",
+					"\"${PODS_ROOT}/Headers/Public/BKPasscodeView\"",
+					"\"${PODS_ROOT}/Headers/Public/DBImageColorPicker\"",
+					"\"${PODS_ROOT}/Headers/Public/DateTools\"",
+					"\"${PODS_ROOT}/Headers/Public/JDStatusBarNotification\"",
+					"\"${PODS_ROOT}/Headers/Public/MDFInternationalization\"",
+					"\"${PODS_ROOT}/Headers/Public/MaterialComponents\"",
+					"\"${PODS_ROOT}/Headers/Public/MotionAnimator\"",
+					"\"${PODS_ROOT}/Headers/Public/MotionInterchange\"",
+					"\"${PODS_ROOT}/Headers/Public/PulsingHalo\"",
+					"\"${PODS_ROOT}/Headers/Public/Realm\"",
+					"\"${PODS_ROOT}/Headers/Public/SocketRocket\"",
+					"\"${PODS_ROOT}/Headers/Public/Toast\"",
+					"\"${PODS_ROOT}/Headers/Public/UICKeyChainStore\"",
+					"\"$(PROJECT_DIR)/ThirdParty\"/**",
+				);
+				INFOPLIST_FILE = ShareExtension/Info.plist;
+				IPHONEOS_DEPLOYMENT_TARGET = 15.0;
+				LD_RUNPATH_SEARCH_PATHS = (
+					"$(inherited)",
+					"@executable_path/Frameworks",
+					"@executable_path/../../Frameworks",
+				);
+				LIBRARY_SEARCH_PATHS = (
+					"$(inherited)",
+					"\"${PODS_CONFIGURATION_BUILD_DIR}/AFNetworking\"",
+					"\"${PODS_CONFIGURATION_BUILD_DIR}/AFViewShaker\"",
+					"\"${PODS_CONFIGURATION_BUILD_DIR}/BKPasscodeView\"",
+					"\"${PODS_CONFIGURATION_BUILD_DIR}/DBImageColorPicker\"",
+					"\"${PODS_CONFIGURATION_BUILD_DIR}/DateTools\"",
+					"\"${PODS_CONFIGURATION_BUILD_DIR}/JDStatusBarNotification\"",
+					"\"${PODS_CONFIGURATION_BUILD_DIR}/MDFInternationalization\"",
+					"\"${PODS_CONFIGURATION_BUILD_DIR}/MaterialComponents\"",
+					"\"${PODS_CONFIGURATION_BUILD_DIR}/MotionAnimator\"",
+					"\"${PODS_CONFIGURATION_BUILD_DIR}/MotionInterchange\"",
+					"\"${PODS_CONFIGURATION_BUILD_DIR}/PulsingHalo\"",
+					"\"${PODS_CONFIGURATION_BUILD_DIR}/Realm\"",
+					"\"${PODS_CONFIGURATION_BUILD_DIR}/SocketRocket\"",
+					"\"${PODS_CONFIGURATION_BUILD_DIR}/Toast\"",
+					"\"${PODS_CONFIGURATION_BUILD_DIR}/UICKeyChainStore\"",
+					"\"${PODS_ROOT}/Realm/core\"",
+				);
+				MARKETING_VERSION = 20.0.5;
+				MTL_FAST_MATH = YES;
+				PRODUCT_BUNDLE_IDENTIFIER = com.sharix.sxrooms.ShareExtension;
+				PRODUCT_NAME = ShareExtension;
+				SKIP_INSTALL = YES;
+				SWIFT_ACTIVE_COMPILATION_CONDITIONS = APP_EXTENSION;
+				SWIFT_INSTALL_OBJC_HEADER = YES;
+				SWIFT_OBJC_BRIDGING_HEADER = "NextcloudTalk/NextcloudTalk-Bridging-Header-Extensions.h";
+				SWIFT_OBJC_INTERFACE_HEADER_NAME = "NextcloudTalk-Swift.h";
+				SWIFT_VERSION = 5.0;
+				TARGETED_DEVICE_FAMILY = "1,2";
+			};
+			name = Release;
+		};
+		2CC0015824A1F0E900A20167 /* Debug */ = {
+			isa = XCBuildConfiguration;
+			baseConfigurationReference = D6DF51D976DC0F681FF83F7B /* Pods-NotificationServiceExtension.debug.xcconfig */;
+			buildSettings = {
+				ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO;
+				APPLICATION_EXTENSION_API_ONLY = YES;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+				CLANG_ENABLE_MODULES = YES;
+				CLANG_ENABLE_OBJC_WEAK = YES;
+				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+				CLANG_WARN_COMMA = YES;
+				CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+				CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+				CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+				CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+				CLANG_WARN_STRICT_PROTOTYPES = YES;
+				CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+				CODE_SIGN_ENTITLEMENTS = NotificationServiceExtension/NotificationServiceExtension.entitlements;
+				CODE_SIGN_IDENTITY = "iPhone Developer";
+				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+				CODE_SIGN_STYLE = Automatic;
+				CURRENT_PROJECT_VERSION = 1;
+				DEVELOPMENT_TEAM = L8PXSNYX82;
+				ENABLE_BITCODE = NO;
+				FRAMEWORK_SEARCH_PATHS = "$(inherited)";
+				GCC_C_LANGUAGE_STANDARD = gnu11;
+				GCC_PREPROCESSOR_DEFINITIONS = (
+					"$(inherited)",
+					"APP_EXTENSION=1",
+				);
+				HEADER_SEARCH_PATHS = (
+					"$(inherited)",
+					"\"${PODS_ROOT}/Headers/Public\"",
+					"\"${PODS_ROOT}/Headers/Public/AFNetworking\"",
+					"\"${PODS_ROOT}/Headers/Public/AFViewShaker\"",
+					"\"${PODS_ROOT}/Headers/Public/BKPasscodeView\"",
+					"\"${PODS_ROOT}/Headers/Public/DBImageColorPicker\"",
+					"\"${PODS_ROOT}/Headers/Public/DateTools\"",
+					"\"${PODS_ROOT}/Headers/Public/JDStatusBarNotification\"",
+					"\"${PODS_ROOT}/Headers/Public/MDFInternationalization\"",
+					"\"${PODS_ROOT}/Headers/Public/MaterialComponents\"",
+					"\"${PODS_ROOT}/Headers/Public/MotionAnimator\"",
+					"\"${PODS_ROOT}/Headers/Public/MotionInterchange\"",
+					"\"${PODS_ROOT}/Headers/Public/PulsingHalo\"",
+					"\"${PODS_ROOT}/Headers/Public/Realm\"",
+					"\"${PODS_ROOT}/Headers/Public/SocketRocket\"",
+					"\"${PODS_ROOT}/Headers/Public/Toast\"",
+					"\"${PODS_ROOT}/Headers/Public/UICKeyChainStore\"",
+					"\"$(PROJECT_DIR)/ThirdParty\"/**",
+				);
+				INFOPLIST_FILE = NotificationServiceExtension/Info.plist;
+				IPHONEOS_DEPLOYMENT_TARGET = 15.0;
+				LD_RUNPATH_SEARCH_PATHS = (
+					"$(inherited)",
+					"@executable_path/Frameworks",
+					"@executable_path/../../Frameworks",
+				);
+				LIBRARY_SEARCH_PATHS = "$(inherited)";
+				MARKETING_VERSION = 20.0.5;
+				MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
+				MTL_FAST_MATH = YES;
+				PRODUCT_BUNDLE_IDENTIFIER = com.sharix.sxrooms.NotificationServiceExtension;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SKIP_INSTALL = YES;
+				SWIFT_ACTIVE_COMPILATION_CONDITIONS = APP_EXTENSION;
+				SWIFT_OBJC_BRIDGING_HEADER = "NextcloudTalk/NextcloudTalk-Bridging-Header-Extensions.h";
+				SWIFT_OBJC_INTERFACE_HEADER_NAME = "NextcloudTalk-Swift.h";
+				SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+				SWIFT_VERSION = 5.0;
+				TARGETED_DEVICE_FAMILY = "1,2";
+			};
+			name = Debug;
+		};
+		2CC0015924A1F0E900A20167 /* Release */ = {
+			isa = XCBuildConfiguration;
+			baseConfigurationReference = D86091EC1125C3057B9A299B /* Pods-NotificationServiceExtension.release.xcconfig */;
+			buildSettings = {
+				ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO;
+				APPLICATION_EXTENSION_API_ONLY = YES;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+				CLANG_ENABLE_MODULES = YES;
+				CLANG_ENABLE_OBJC_WEAK = YES;
+				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+				CLANG_WARN_COMMA = YES;
+				CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+				CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+				CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+				CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+				CLANG_WARN_STRICT_PROTOTYPES = YES;
+				CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+				CODE_SIGN_ENTITLEMENTS = NotificationServiceExtension/NotificationServiceExtension.entitlements;
+				CODE_SIGN_IDENTITY = "iPhone Distribution: Nextcloud GmbH (NKUJUXUJ3B)";
+				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+				CODE_SIGN_STYLE = Automatic;
+				CURRENT_PROJECT_VERSION = 1;
+				DEVELOPMENT_TEAM = L8PXSNYX82;
+				ENABLE_BITCODE = NO;
+				FRAMEWORK_SEARCH_PATHS = "$(inherited)";
+				GCC_C_LANGUAGE_STANDARD = gnu11;
+				GCC_PREPROCESSOR_DEFINITIONS = (
+					"$(inherited)",
+					"APP_EXTENSION=1",
+				);
+				HEADER_SEARCH_PATHS = (
+					"$(inherited)",
+					"\"${PODS_ROOT}/Headers/Public\"",
+					"\"${PODS_ROOT}/Headers/Public/AFNetworking\"",
+					"\"${PODS_ROOT}/Headers/Public/AFViewShaker\"",
+					"\"${PODS_ROOT}/Headers/Public/BKPasscodeView\"",
+					"\"${PODS_ROOT}/Headers/Public/DBImageColorPicker\"",
+					"\"${PODS_ROOT}/Headers/Public/DateTools\"",
+					"\"${PODS_ROOT}/Headers/Public/JDStatusBarNotification\"",
+					"\"${PODS_ROOT}/Headers/Public/MDFInternationalization\"",
+					"\"${PODS_ROOT}/Headers/Public/MaterialComponents\"",
+					"\"${PODS_ROOT}/Headers/Public/MotionAnimator\"",
+					"\"${PODS_ROOT}/Headers/Public/MotionInterchange\"",
+					"\"${PODS_ROOT}/Headers/Public/PulsingHalo\"",
+					"\"${PODS_ROOT}/Headers/Public/Realm\"",
+					"\"${PODS_ROOT}/Headers/Public/SocketRocket\"",
+					"\"${PODS_ROOT}/Headers/Public/Toast\"",
+					"\"${PODS_ROOT}/Headers/Public/UICKeyChainStore\"",
+					"\"$(PROJECT_DIR)/ThirdParty\"/**",
+				);
+				INFOPLIST_FILE = NotificationServiceExtension/Info.plist;
+				IPHONEOS_DEPLOYMENT_TARGET = 15.0;
+				LD_RUNPATH_SEARCH_PATHS = (
+					"$(inherited)",
+					"@executable_path/Frameworks",
+					"@executable_path/../../Frameworks",
+				);
+				LIBRARY_SEARCH_PATHS = "$(inherited)";
+				MARKETING_VERSION = 20.0.5;
+				MTL_FAST_MATH = YES;
+				PRODUCT_BUNDLE_IDENTIFIER = com.sharix.sxrooms.NotificationServiceExtension;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SKIP_INSTALL = YES;
+				SWIFT_ACTIVE_COMPILATION_CONDITIONS = APP_EXTENSION;
+				SWIFT_OBJC_BRIDGING_HEADER = "NextcloudTalk/NextcloudTalk-Bridging-Header-Extensions.h";
+				SWIFT_OBJC_INTERFACE_HEADER_NAME = "NextcloudTalk-Swift.h";
+				SWIFT_VERSION = 5.0;
+				TARGETED_DEVICE_FAMILY = "1,2";
+			};
+			name = Release;
+		};
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+		1F6D8C362B2E3756004376B8 /* Build configuration list for PBXNativeTarget "NextcloudTalkTests" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				1F6D8C372B2E3756004376B8 /* Debug */,
+				1F6D8C382B2E3756004376B8 /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+		1FD8AD922A3A162100787C16 /* Build configuration list for PBXNativeTarget "NextcloudTalkUITests" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				1FD8AD932A3A162100787C16 /* Debug */,
+				1FD8AD942A3A162100787C16 /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+		1FF2FD782AB99CCC000C9905 /* Build configuration list for PBXNativeTarget "BroadcastUploadExtension" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				1FF2FD732AB99CCB000C9905 /* Debug */,
+				1FF2FD742AB99CCB000C9905 /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+		2C0574781EDD9E8E00D9E7F2 /* Build configuration list for PBXProject "NextcloudTalk" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				2C0574951EDD9E8E00D9E7F2 /* Debug */,
+				2C0574961EDD9E8E00D9E7F2 /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+		2C0574971EDD9E8E00D9E7F2 /* Build configuration list for PBXNativeTarget "NextcloudTalk" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				2C0574981EDD9E8E00D9E7F2 /* Debug */,
+				2C0574991EDD9E8E00D9E7F2 /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+		2C62AFB124C08845007E460A /* Build configuration list for PBXNativeTarget "ShareExtension" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				2C62AFAF24C08845007E460A /* Debug */,
+				2C62AFB024C08845007E460A /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+		2CC0015A24A1F0E900A20167 /* Build configuration list for PBXNativeTarget "NotificationServiceExtension" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				2CC0015824A1F0E900A20167 /* Debug */,
+				2CC0015924A1F0E900A20167 /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+/* End XCConfigurationList section */
+
+/* Begin XCRemoteSwiftPackageReference section */
+		1F0ECBF32A68274400921E90 /* XCRemoteSwiftPackageReference "CDMarkdownKit" */ = {
+			isa = XCRemoteSwiftPackageReference;
+			repositoryURL = "https://github.com/nextcloud-deps/CDMarkdownKit.git";
+			requirement = {
+				kind = revision;
+				revision = 818927d8bcbcc70e2395232f9b733cfc40e8f0bd;
+			};
+		};
+		1F45A1142A01D6EC005FE87D /* XCRemoteSwiftPackageReference "SDWebImage" */ = {
+			isa = XCRemoteSwiftPackageReference;
+			repositoryURL = "https://github.com/SDWebImage/SDWebImage.git";
+			requirement = {
+				kind = exactVersion;
+				version = 5.15.7;
+			};
+		};
+		1F45A11F2A01D8BA005FE87D /* XCRemoteSwiftPackageReference "SDWebImageSVGKitPlugin" */ = {
+			isa = XCRemoteSwiftPackageReference;
+			repositoryURL = "https://github.com/SDWebImage/SDWebImageSVGKitPlugin.git";
+			requirement = {
+				kind = exactVersion;
+				version = 1.4.0;
+			};
+		};
+		1F468E7428DCC6C60099597B /* XCRemoteSwiftPackageReference "Dynamic" */ = {
+			isa = XCRemoteSwiftPackageReference;
+			repositoryURL = "https://github.com/mhdhejazi/Dynamic.git";
+			requirement = {
+				kind = upToNextMajorVersion;
+				minimumVersion = 1.0.0;
+			};
+		};
+		1F628CB82842BAAF0083A425 /* XCRemoteSwiftPackageReference "QRCodeReader" */ = {
+			isa = XCRemoteSwiftPackageReference;
+			repositoryURL = "https://github.com/yannickl/QRCodeReader.swift";
+			requirement = {
+				kind = upToNextMajorVersion;
+				minimumVersion = 10.1.1;
+			};
+		};
+		1F66B72D29FABD01003FB168 /* XCRemoteSwiftPackageReference "SwiftyAttributes" */ = {
+			isa = XCRemoteSwiftPackageReference;
+			repositoryURL = "https://github.com/eddiekaiger/SwiftyAttributes.git";
+			requirement = {
+				kind = revision;
+				revision = 1ae513a1617309455a115c3fc2d558f744b43788;
+			};
+		};
+		1F759C2A2B63CB93000534AB /* XCRemoteSwiftPackageReference "realm-swift-binary" */ = {
+			isa = XCRemoteSwiftPackageReference;
+			repositoryURL = "https://github.com/nextcloud-deps/realm-swift-binary";
+			requirement = {
+				kind = revision;
+				revision = cab57855cff4612dc8c290191b41f5036befb2ec;
+			};
+		};
+		1F7AE07629142CA1009F72AD /* XCRemoteSwiftPackageReference "NextcloudKit" */ = {
+			isa = XCRemoteSwiftPackageReference;
+			repositoryURL = "https://github.com/nextcloud/NextcloudKit";
+			requirement = {
+				kind = exactVersion;
+				version = 2.6.0;
+			};
+		};
+		1FAB2E7B2AC99326001214EB /* XCRemoteSwiftPackageReference "TOCropViewController" */ = {
+			isa = XCRemoteSwiftPackageReference;
+			repositoryURL = "https://github.com/TimOliver/TOCropViewController";
+			requirement = {
+				kind = exactVersion;
+				version = 2.6.1;
+			};
+		};
+		1FAB2E862ACD44CF001214EB /* XCRemoteSwiftPackageReference "talk-clients-webrtc" */ = {
+			isa = XCRemoteSwiftPackageReference;
+			repositoryURL = "https://github.com/nextcloud-releases/talk-clients-webrtc";
+			requirement = {
+				kind = revision;
+				revision = 5f15c82f4b02072c1595980580eade9325e1819f;
+			};
+		};
+		1FCE3D512C9B5918009C68A9 /* XCRemoteSwiftPackageReference "SwiftyGif" */ = {
+			isa = XCRemoteSwiftPackageReference;
+			repositoryURL = "https://github.com/alexiscreuzot/SwiftyGif";
+			requirement = {
+				kind = upToNextMajorVersion;
+				minimumVersion = 5.4.5;
+			};
+		};
+		2CCCD21B2835088F00F076CE /* XCRemoteSwiftPackageReference "OpenSSL" */ = {
+			isa = XCRemoteSwiftPackageReference;
+			repositoryURL = "https://github.com/krzyzanowskim/OpenSSL";
+			requirement = {
+				kind = exactVersion;
+				version = 3.1.6000;
+			};
+		};
+		80CDF8C22A8E098900CB57AE /* XCRemoteSwiftPackageReference "swiftui-introspect" */ = {
+			isa = XCRemoteSwiftPackageReference;
+			repositoryURL = "https://github.com/siteline/swiftui-introspect";
+			requirement = {
+				kind = upToNextMajorVersion;
+				minimumVersion = 0.9.2;
+			};
+		};
+/* End XCRemoteSwiftPackageReference section */
+
+/* Begin XCSwiftPackageProductDependency section */
+		1F0ECBF42A68274400921E90 /* CDMarkdownKit */ = {
+			isa = XCSwiftPackageProductDependency;
+			package = 1F0ECBF32A68274400921E90 /* XCRemoteSwiftPackageReference "CDMarkdownKit" */;
+			productName = CDMarkdownKit;
+		};
+		1F0ECBF62A68277000921E90 /* CDMarkdownKit */ = {
+			isa = XCSwiftPackageProductDependency;
+			package = 1F0ECBF32A68274400921E90 /* XCRemoteSwiftPackageReference "CDMarkdownKit" */;
+			productName = CDMarkdownKit;
+		};
+		1F0ECBF82A68277C00921E90 /* CDMarkdownKit */ = {
+			isa = XCSwiftPackageProductDependency;
+			package = 1F0ECBF32A68274400921E90 /* XCRemoteSwiftPackageReference "CDMarkdownKit" */;
+			productName = CDMarkdownKit;
+		};
+		1F35F8F42AEEDA9800044BDA /* SwiftyAttributes */ = {
+			isa = XCSwiftPackageProductDependency;
+			package = 1F66B72D29FABD01003FB168 /* XCRemoteSwiftPackageReference "SwiftyAttributes" */;
+			productName = SwiftyAttributes;
+		};
+		1F45A1152A01D6EC005FE87D /* SDWebImage */ = {
+			isa = XCSwiftPackageProductDependency;
+			package = 1F45A1142A01D6EC005FE87D /* XCRemoteSwiftPackageReference "SDWebImage" */;
+			productName = SDWebImage;
+		};
+		1F45A1192A01D70E005FE87D /* SDWebImage */ = {
+			isa = XCSwiftPackageProductDependency;
+			package = 1F45A1142A01D6EC005FE87D /* XCRemoteSwiftPackageReference "SDWebImage" */;
+			productName = SDWebImage;
+		};
+		1F45A11D2A01D719005FE87D /* SDWebImage */ = {
+			isa = XCSwiftPackageProductDependency;
+			package = 1F45A1142A01D6EC005FE87D /* XCRemoteSwiftPackageReference "SDWebImage" */;
+			productName = SDWebImage;
+		};
+		1F45A1202A01D8BA005FE87D /* SDWebImageSVGKitPlugin */ = {
+			isa = XCSwiftPackageProductDependency;
+			package = 1F45A11F2A01D8BA005FE87D /* XCRemoteSwiftPackageReference "SDWebImageSVGKitPlugin" */;
+			productName = SDWebImageSVGKitPlugin;
+		};
+		1F45A1222A01D8F1005FE87D /* SDWebImageSVGKitPlugin */ = {
+			isa = XCSwiftPackageProductDependency;
+			package = 1F45A11F2A01D8BA005FE87D /* XCRemoteSwiftPackageReference "SDWebImageSVGKitPlugin" */;
+			productName = SDWebImageSVGKitPlugin;
+		};
+		1F45A1242A01D8F7005FE87D /* SDWebImageSVGKitPlugin */ = {
+			isa = XCSwiftPackageProductDependency;
+			package = 1F45A11F2A01D8BA005FE87D /* XCRemoteSwiftPackageReference "SDWebImageSVGKitPlugin" */;
+			productName = SDWebImageSVGKitPlugin;
+		};
+		1F468E7528DCC6C60099597B /* Dynamic */ = {
+			isa = XCSwiftPackageProductDependency;
+			package = 1F468E7428DCC6C60099597B /* XCRemoteSwiftPackageReference "Dynamic" */;
+			productName = Dynamic;
+		};
+		1F628CB92842BAAF0083A425 /* QRCodeReader */ = {
+			isa = XCSwiftPackageProductDependency;
+			package = 1F628CB82842BAAF0083A425 /* XCRemoteSwiftPackageReference "QRCodeReader" */;
+			productName = QRCodeReader;
+		};
+		1F66B72E29FABD01003FB168 /* SwiftyAttributes */ = {
+			isa = XCSwiftPackageProductDependency;
+			package = 1F66B72D29FABD01003FB168 /* XCRemoteSwiftPackageReference "SwiftyAttributes" */;
+			productName = SwiftyAttributes;
+		};
+		1F759C082B63B9A7000534AB /* SDWebImage */ = {
+			isa = XCSwiftPackageProductDependency;
+			package = 1F45A1142A01D6EC005FE87D /* XCRemoteSwiftPackageReference "SDWebImage" */;
+			productName = SDWebImage;
+		};
+		1F759C0A2B63B9A7000534AB /* SDWebImageSVGKitPlugin */ = {
+			isa = XCSwiftPackageProductDependency;
+			package = 1F45A11F2A01D8BA005FE87D /* XCRemoteSwiftPackageReference "SDWebImageSVGKitPlugin" */;
+			productName = SDWebImageSVGKitPlugin;
+		};
+		1F759C0D2B63B9BA000534AB /* WebRTC */ = {
+			isa = XCSwiftPackageProductDependency;
+			package = 1FAB2E862ACD44CF001214EB /* XCRemoteSwiftPackageReference "talk-clients-webrtc" */;
+			productName = WebRTC;
+		};
+		1F759C0F2B63B9D9000534AB /* OpenSSL */ = {
+			isa = XCSwiftPackageProductDependency;
+			package = 2CCCD21B2835088F00F076CE /* XCRemoteSwiftPackageReference "OpenSSL" */;
+			productName = OpenSSL;
+		};
+		1F759C132B63B9D9000534AB /* QRCodeReader */ = {
+			isa = XCSwiftPackageProductDependency;
+			package = 1F628CB82842BAAF0083A425 /* XCRemoteSwiftPackageReference "QRCodeReader" */;
+			productName = QRCodeReader;
+		};
+		1F759C152B63B9D9000534AB /* NextcloudKit */ = {
+			isa = XCSwiftPackageProductDependency;
+			package = 1F7AE07629142CA1009F72AD /* XCRemoteSwiftPackageReference "NextcloudKit" */;
+			productName = NextcloudKit;
+		};
+		1F759C172B63B9D9000534AB /* SwiftyAttributes */ = {
+			isa = XCSwiftPackageProductDependency;
+			package = 1F66B72D29FABD01003FB168 /* XCRemoteSwiftPackageReference "SwiftyAttributes" */;
+			productName = SwiftyAttributes;
+		};
+		1F759C192B63B9D9000534AB /* CDMarkdownKit */ = {
+			isa = XCSwiftPackageProductDependency;
+			package = 1F0ECBF32A68274400921E90 /* XCRemoteSwiftPackageReference "CDMarkdownKit" */;
+			productName = CDMarkdownKit;
+		};
+		1F759C1B2B63B9D9000534AB /* TOCropViewController */ = {
+			isa = XCSwiftPackageProductDependency;
+			package = 1FAB2E7B2AC99326001214EB /* XCRemoteSwiftPackageReference "TOCropViewController" */;
+			productName = TOCropViewController;
+		};
+		1F759C1D2B63B9D9000534AB /* SwiftUIIntrospect */ = {
+			isa = XCSwiftPackageProductDependency;
+			package = 80CDF8C22A8E098900CB57AE /* XCRemoteSwiftPackageReference "swiftui-introspect" */;
+			productName = SwiftUIIntrospect;
+		};
+		1F759C2B2B63CB93000534AB /* Realm */ = {
+			isa = XCSwiftPackageProductDependency;
+			package = 1F759C2A2B63CB93000534AB /* XCRemoteSwiftPackageReference "realm-swift-binary" */;
+			productName = Realm;
+		};
+		1F759C2D2B63CB9A000534AB /* Realm */ = {
+			isa = XCSwiftPackageProductDependency;
+			package = 1F759C2A2B63CB93000534AB /* XCRemoteSwiftPackageReference "realm-swift-binary" */;
+			productName = Realm;
+		};
+		1F759C2F2B63CBA0000534AB /* Realm */ = {
+			isa = XCSwiftPackageProductDependency;
+			package = 1F759C2A2B63CB93000534AB /* XCRemoteSwiftPackageReference "realm-swift-binary" */;
+			productName = Realm;
+		};
+		1F759C312B63CBA5000534AB /* Realm */ = {
+			isa = XCSwiftPackageProductDependency;
+			package = 1F759C2A2B63CB93000534AB /* XCRemoteSwiftPackageReference "realm-swift-binary" */;
+			productName = Realm;
+		};
+		1F759C332B63CBAA000534AB /* Realm */ = {
+			isa = XCSwiftPackageProductDependency;
+			package = 1F759C2A2B63CB93000534AB /* XCRemoteSwiftPackageReference "realm-swift-binary" */;
+			productName = Realm;
+		};
+		1F77A5EE2AB9A41E007B6037 /* SDWebImage */ = {
+			isa = XCSwiftPackageProductDependency;
+			package = 1F45A1142A01D6EC005FE87D /* XCRemoteSwiftPackageReference "SDWebImage" */;
+			productName = SDWebImage;
+		};
+		1F77A5F02AB9A423007B6037 /* SDWebImageSVGKitPlugin */ = {
+			isa = XCSwiftPackageProductDependency;
+			package = 1F45A11F2A01D8BA005FE87D /* XCRemoteSwiftPackageReference "SDWebImageSVGKitPlugin" */;
+			productName = SDWebImageSVGKitPlugin;
+		};
+		1F77A5FF2AB9A50D007B6037 /* NextcloudKit */ = {
+			isa = XCSwiftPackageProductDependency;
+			package = 1F7AE07629142CA1009F72AD /* XCRemoteSwiftPackageReference "NextcloudKit" */;
+			productName = NextcloudKit;
+		};
+		1F77A60B2AB9A5BE007B6037 /* CDMarkdownKit */ = {
+			isa = XCSwiftPackageProductDependency;
+			package = 1F0ECBF32A68274400921E90 /* XCRemoteSwiftPackageReference "CDMarkdownKit" */;
+			productName = CDMarkdownKit;
+		};
+		1F7AE07729142CA1009F72AD /* NextcloudKit */ = {
+			isa = XCSwiftPackageProductDependency;
+			package = 1F7AE07629142CA1009F72AD /* XCRemoteSwiftPackageReference "NextcloudKit" */;
+			productName = NextcloudKit;
+		};
+		1F7AE07929142E62009F72AD /* NextcloudKit */ = {
+			isa = XCSwiftPackageProductDependency;
+			package = 1F7AE07629142CA1009F72AD /* XCRemoteSwiftPackageReference "NextcloudKit" */;
+			productName = NextcloudKit;
+		};
+		1F7AE07B29142E6A009F72AD /* NextcloudKit */ = {
+			isa = XCSwiftPackageProductDependency;
+			package = 1F7AE07629142CA1009F72AD /* XCRemoteSwiftPackageReference "NextcloudKit" */;
+			productName = NextcloudKit;
+		};
+		1FAB2E7C2AC99326001214EB /* TOCropViewController */ = {
+			isa = XCSwiftPackageProductDependency;
+			package = 1FAB2E7B2AC99326001214EB /* XCRemoteSwiftPackageReference "TOCropViewController" */;
+			productName = TOCropViewController;
+		};
+		1FAB2E7E2AC99367001214EB /* TOCropViewController */ = {
+			isa = XCSwiftPackageProductDependency;
+			package = 1FAB2E7B2AC99326001214EB /* XCRemoteSwiftPackageReference "TOCropViewController" */;
+			productName = TOCropViewController;
+		};
+		1FAB2E872ACD44D0001214EB /* WebRTC */ = {
+			isa = XCSwiftPackageProductDependency;
+			package = 1FAB2E862ACD44CF001214EB /* XCRemoteSwiftPackageReference "talk-clients-webrtc" */;
+			productName = WebRTC;
+		};
+		1FCE3D522C9B5918009C68A9 /* SwiftyGif */ = {
+			isa = XCSwiftPackageProductDependency;
+			package = 1FCE3D512C9B5918009C68A9 /* XCRemoteSwiftPackageReference "SwiftyGif" */;
+			productName = SwiftyGif;
+		};
+		1FF136192BFBC841006A6101 /* SwiftyAttributes */ = {
+			isa = XCSwiftPackageProductDependency;
+			package = 1F66B72D29FABD01003FB168 /* XCRemoteSwiftPackageReference "SwiftyAttributes" */;
+			productName = SwiftyAttributes;
+		};
+		1FF1361B2BFBC86A006A6101 /* SwiftyAttributes */ = {
+			isa = XCSwiftPackageProductDependency;
+			package = 1F66B72D29FABD01003FB168 /* XCRemoteSwiftPackageReference "SwiftyAttributes" */;
+			productName = SwiftyAttributes;
+		};
+		2CCCD21C2835088F00F076CE /* OpenSSL */ = {
+			isa = XCSwiftPackageProductDependency;
+			package = 2CCCD21B2835088F00F076CE /* XCRemoteSwiftPackageReference "OpenSSL" */;
+			productName = OpenSSL;
+		};
+		80CDF8C32A8E098900CB57AE /* SwiftUIIntrospect */ = {
+			isa = XCSwiftPackageProductDependency;
+			package = 80CDF8C22A8E098900CB57AE /* XCRemoteSwiftPackageReference "swiftui-introspect" */;
+			productName = SwiftUIIntrospect;
+		};
+/* End XCSwiftPackageProductDependency section */
+	};
+	rootObject = 2C0574751EDD9E8E00D9E7F2 /* Project object */;
+}

+ 10 - 0
NextcloudTalk.xcworkspace/contents.xcworkspacedata

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Workspace
+   version = "1.0">
+   <FileRef
+      location = "group:/Users/mex3/Documents/talk-ios-20.0.5/NextcloudTalk.xcodeproj">
+   </FileRef>
+   <FileRef
+      location = "group:Pods/Pods.xcodeproj">
+   </FileRef>
+</Workspace>

+ 20 - 0
NextcloudTalk/ABContact.h

@@ -0,0 +1,20 @@
+/**
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#import <Foundation/Foundation.h>
+#import <Realm/Realm.h>
+
+@interface ABContact : RLMObject
+
+@property (nonatomic, copy) NSString *identifier;
+@property (nonatomic, copy) NSString *name;
+@property (nonatomic, strong) RLMArray<RLMString> *phoneNumbers;
+@property (nonatomic, assign) NSInteger lastUpdate;
+
++ (instancetype)contactWithIdentifier:(NSString *)identifier name:(NSString *)name phoneNumbers:(NSArray *)phoneNumbers lastUpdate:(NSInteger)lastUpdate;
++ (void)updateContact:(ABContact *)managedContact withContact:(ABContact *)contact;
+
+@end
+

+ 27 - 0
NextcloudTalk/ABContact.m

@@ -0,0 +1,27 @@
+/**
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#import "ABContact.h"
+
+@implementation ABContact
+
++ (instancetype)contactWithIdentifier:(NSString *)identifier name:(NSString *)name phoneNumbers:(NSArray *)phoneNumbers lastUpdate:(NSInteger)lastUpdate
+{
+    ABContact *contact = [[ABContact alloc] init];
+    contact.identifier = identifier;
+    contact.name = name;
+    contact.phoneNumbers = (RLMArray<RLMString> *)phoneNumbers;
+    contact.lastUpdate = lastUpdate;
+    return contact;
+}
+
++ (void)updateContact:(ABContact *)managedContact withContact:(ABContact *)contact
+{
+    managedContact.name = contact.name;
+    managedContact.phoneNumbers = contact.phoneNumbers;
+    managedContact.lastUpdate = contact.lastUpdate;
+}
+
+@end

+ 25 - 0
NextcloudTalk/AddParticipantsTableViewController.h

@@ -0,0 +1,25 @@
+/**
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#import <UIKit/UIKit.h>
+
+#import "NCRoom.h"
+#import "NCUser.h"
+
+@class AddParticipantsTableViewController;
+@protocol AddParticipantsTableViewControllerDelegate <NSObject>
+@optional
+- (void)addParticipantsTableViewController:(AddParticipantsTableViewController *)viewController wantsToAdd:(NSArray<NCUser *> *)participants;
+- (void)addParticipantsTableViewControllerDidFinish:(AddParticipantsTableViewController *)viewController;
+@end
+
+@interface AddParticipantsTableViewController : UITableViewController
+
+@property (nonatomic, weak) id<AddParticipantsTableViewControllerDelegate> delegate;
+
+- (instancetype)initForRoom:(NCRoom *)room;
+- (instancetype)initWithParticipants:(NSArray<NCUser *> *)room;
+
+@end

+ 476 - 0
NextcloudTalk/AddParticipantsTableViewController.m

@@ -0,0 +1,476 @@
+/**
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#import "AddParticipantsTableViewController.h"
+
+#import "NCAPIController.h"
+#import "NCAppBranding.h"
+#import "NCContact.h"
+#import "NCDatabaseManager.h"
+#import "NCUserInterfaceController.h"
+#import "PlaceholderView.h"
+#import "ResultMultiSelectionTableViewController.h"
+
+#import "NextcloudTalk-Swift.h"
+
+@interface AddParticipantsTableViewController () <UISearchBarDelegate, UISearchControllerDelegate, UISearchResultsUpdating>
+{
+    NSMutableDictionary *_participants;
+    NSArray *_indexes;
+    NCRoom *_room;
+    NSArray *_participantsInRoom;
+    UISearchController *_searchController;
+    ResultMultiSelectionTableViewController *_resultTableViewController;
+    NSMutableArray *_selectedParticipants;
+    PlaceholderView *_participantsBackgroundView;
+    NSTimer *_searchTimer;
+    NSURLSessionTask *_searchParticipantsTask;
+    UIActivityIndicatorView *_addingParticipantsIndicator;
+    BOOL _errorAddingParticipants;
+}
+@end
+
+@implementation AddParticipantsTableViewController
+
+- (instancetype)initForRoom:(NCRoom *)room
+{
+    self = [super init];
+    if (!self) {
+        return nil;
+    }
+    
+    if (room) {
+        _room = room;
+        _participantsInRoom = [room.participants valueForKey:@"self"];
+    }
+
+    _participants = [[NSMutableDictionary alloc] init];
+    _indexes = [[NSArray alloc] init];
+    _selectedParticipants = [[NSMutableArray alloc] init];
+
+    _addingParticipantsIndicator = [[UIActivityIndicatorView alloc] init];
+    _addingParticipantsIndicator.color = [NCAppBranding themeTextColor];
+
+    return self;
+}
+
+- (instancetype)initWithParticipants:(NSArray<NCUser *> *)participants
+{
+    self = [self initForRoom:nil];
+    if (!self) {
+        return nil;
+    }
+
+    _selectedParticipants = [[NSMutableArray alloc] initWithArray:participants];
+
+    return self;
+}
+
+- (void)viewDidLoad
+{
+    [super viewDidLoad];
+    
+    [self.tableView registerNib:[UINib nibWithNibName:kContactsTableCellNibName bundle:nil] forCellReuseIdentifier:kContactCellIdentifier];
+    self.tableView.separatorInset = UIEdgeInsetsMake(0, 72, 0, 0);
+    self.tableView.sectionIndexBackgroundColor = [UIColor clearColor];
+    
+    _resultTableViewController = [[ResultMultiSelectionTableViewController alloc] init];
+    _resultTableViewController.selectedParticipants = _selectedParticipants;
+    _resultTableViewController.room = _room;
+
+    _searchController = [[UISearchController alloc] initWithSearchResultsController:_resultTableViewController];
+    _searchController.searchResultsUpdater = self;
+    [_searchController.searchBar sizeToFit];
+
+    UIColor *themeColor = [NCAppBranding themeColor];
+    UINavigationBarAppearance *appearance = [[UINavigationBarAppearance alloc] init];
+    [appearance configureWithOpaqueBackground];
+    appearance.backgroundColor = themeColor;
+    appearance.titleTextAttributes = @{NSForegroundColorAttributeName:[NCAppBranding themeTextColor]};
+    self.navigationItem.standardAppearance = appearance;
+    self.navigationItem.compactAppearance = appearance;
+    self.navigationItem.scrollEdgeAppearance = appearance;
+
+    self.navigationItem.searchController = _searchController;
+    self.navigationItem.searchController.searchBar.searchTextField.backgroundColor = [NCUtils searchbarBGColorForColor:themeColor];
+
+    if (@available(iOS 16.0, *)) {
+        self.navigationItem.preferredSearchBarPlacement = UINavigationItemSearchBarPlacementStacked;
+    }
+    
+    _searchController.searchBar.tintColor = [NCAppBranding themeTextColor];
+    UITextField *searchTextField = [_searchController.searchBar valueForKey:@"searchField"];
+    UIButton *clearButton = [searchTextField valueForKey:@"_clearButton"];
+    searchTextField.tintColor = [NCAppBranding themeTextColor];
+    searchTextField.textColor = [NCAppBranding themeTextColor];
+    searchTextField.autocapitalizationType = UITextAutocapitalizationTypeNone;
+    dispatch_async(dispatch_get_main_queue(), ^{
+        // Search bar placeholder
+        searchTextField.attributedPlaceholder = [[NSAttributedString alloc] initWithString:NSLocalizedString(@"Search", nil)
+        attributes:@{NSForegroundColorAttributeName:[[NCAppBranding themeTextColor] colorWithAlphaComponent:0.5]}];
+        // Search bar search icon
+        UIImageView *searchImageView = (UIImageView *)searchTextField.leftView;
+        searchImageView.image = [searchImageView.image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
+        [searchImageView setTintColor:[[NCAppBranding themeTextColor] colorWithAlphaComponent:0.5]];
+        // Search bar search clear button
+        UIImage *clearButtonImage = [clearButton.imageView.image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
+        [clearButton setImage:clearButtonImage forState:UIControlStateNormal];
+        [clearButton setImage:clearButtonImage forState:UIControlStateHighlighted];
+        [clearButton setTintColor:[NCAppBranding themeTextColor]];
+    });
+
+    self.tableView.tableFooterView = [[UIView alloc] initWithFrame:CGRectZero];
+    // Contacts placeholder view
+    _participantsBackgroundView = [[PlaceholderView alloc] init];
+    [_participantsBackgroundView setImage:[UIImage imageNamed:@"contacts-placeholder"]];
+    [_participantsBackgroundView.placeholderTextView setText:NSLocalizedString(@"No participants found", nil)];
+    [_participantsBackgroundView.placeholderView setHidden:YES];
+    [_participantsBackgroundView.loadingView startAnimating];
+    self.tableView.backgroundView = _participantsBackgroundView;
+    
+    // We want ourselves to be the delegate for the result table so didSelectRowAtIndexPath is called for both tables.
+    _resultTableViewController.tableView.delegate = self;
+    _searchController.delegate = self;
+    _searchController.searchBar.delegate = self;
+    _searchController.hidesNavigationBarDuringPresentation = NO;
+
+    [self updateCounter];
+
+    self.definesPresentationContext = YES;
+    
+    UIBarButtonItem *cancelButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemCancel
+                                                                                  target:self action:@selector(cancelButtonPressed)];
+    self.navigationController.navigationBar.topItem.leftBarButtonItem = cancelButton;
+    self.navigationItem.title = NSLocalizedString(@"Add participants", nil);
+    [self.navigationController.navigationBar setTitleTextAttributes:
+     @{NSForegroundColorAttributeName:[NCAppBranding themeTextColor]}];
+    self.navigationController.navigationBar.tintColor = [NCAppBranding themeTextColor];
+    self.navigationController.navigationBar.translucent = NO;
+    self.navigationController.navigationBar.barTintColor = [NCAppBranding themeColor];
+    
+    // Fix uisearchcontroller animation
+    self.extendedLayoutIncludesOpaqueBars = YES;
+}
+
+- (void)dealloc
+{
+    [[NSNotificationCenter defaultCenter] removeObserver:self];
+}
+
+- (void)viewDidAppear:(BOOL)animated
+{
+    [super viewDidAppear:animated];
+    
+    self.navigationItem.hidesSearchBarWhenScrolling = NO;
+
+    [self getPossibleParticipants];
+}
+
+- (void)viewWillAppear:(BOOL)animated
+{
+    [super viewWillAppear:animated];
+
+    self.navigationItem.hidesSearchBarWhenScrolling = NO;
+}
+
+- (void)didReceiveMemoryWarning
+{
+    [super didReceiveMemoryWarning];
+}
+
+#pragma mark - View controller actions
+
+- (void)cancelButtonPressed
+{
+    [self close];
+}
+
+- (void)close
+{
+    if ([self.delegate respondsToSelector:@selector(addParticipantsTableViewControllerDidFinish:)]) {
+        [self.delegate addParticipantsTableViewControllerDidFinish:self];
+    }
+    [self.navigationController dismissViewControllerAnimated:YES completion:nil];
+}
+
+- (void)addButtonPressed
+{
+    if (_room && _selectedParticipants.count > 0) {
+        dispatch_group_t addParticipantsGroup = dispatch_group_create();
+
+        [self showAddingParticipantsView];
+        for (NCUser *participant in _selectedParticipants) {
+            [self addParticipantToRoom:participant withDispatchGroup:addParticipantsGroup];
+        }
+
+        dispatch_group_notify(addParticipantsGroup, dispatch_get_main_queue(), ^{
+            [self removeAddingParticipantsView];
+
+            if (!self->_errorAddingParticipants) {
+                [self close];
+            }
+
+            // Reset flag once adding participants process has finished
+            self->_errorAddingParticipants = NO;
+        });
+    } else if ([self.delegate respondsToSelector:@selector(addParticipantsTableViewController:wantsToAdd:)]) {
+        [self.delegate addParticipantsTableViewController:self wantsToAdd:_selectedParticipants];
+        [self close];
+    }
+}
+
+- (void)addParticipantToRoom:(NCUser *)participant withDispatchGroup:(dispatch_group_t)dispatchGroup
+{
+    dispatch_group_enter(dispatchGroup);
+
+    [[NCAPIController sharedInstance] addParticipant:participant.userId ofType:participant.source toRoom:_room.token forAccount:[[NCDatabaseManager sharedInstance] activeAccount] withCompletionBlock:^(NSError *error) {
+        if (error) {
+            UIAlertController * alert = [UIAlertController
+                                         alertControllerWithTitle:NSLocalizedString(@"Could not add participant", nil)
+                                         message:[NSString stringWithFormat:NSLocalizedString(@"An error occurred while adding %@ to the room", nil), participant.name]
+                                         preferredStyle:UIAlertControllerStyleAlert];
+            
+            UIAlertAction* okButton = [UIAlertAction
+                                       actionWithTitle:NSLocalizedString(@"OK", nil)
+                                       style:UIAlertActionStyleDefault
+                                       handler:nil];
+            
+            [alert addAction:okButton];
+
+            self->_errorAddingParticipants = YES;
+
+            [[NCUserInterfaceController sharedInstance] presentAlertViewController:alert];
+        }
+
+        dispatch_group_leave(dispatchGroup);
+    }];
+}
+
+- (void)updateCounter
+{
+    UIBarButtonItem *addButton = nil;
+    if (!_room) {
+        addButton = [[UIBarButtonItem alloc]
+                     initWithBarButtonSystemItem:UIBarButtonSystemItemDone
+                     target:self
+                     action:@selector(addButtonPressed)];
+    } else if (_selectedParticipants.count > 0) {
+        addButton = [[UIBarButtonItem alloc]
+                     initWithTitle:[NSString stringWithFormat:NSLocalizedString(@"Add (%lu)", nil), (unsigned long)_selectedParticipants.count]
+                     style:UIBarButtonItemStylePlain
+                     target:self
+                     action:@selector(addButtonPressed)];
+    }
+
+    self.navigationController.navigationBar.topItem.rightBarButtonItem = addButton;
+}
+
+- (void)showAddingParticipantsView
+{
+    [_addingParticipantsIndicator startAnimating];
+    UIBarButtonItem *addingParticipantButton = [[UIBarButtonItem alloc] initWithCustomView:_addingParticipantsIndicator];
+    self.navigationItem.rightBarButtonItems = @[addingParticipantButton];
+    self.tableView.allowsSelection = NO;
+    _resultTableViewController.tableView.allowsSelection = NO;
+}
+
+- (void)removeAddingParticipantsView
+{
+    [_addingParticipantsIndicator stopAnimating];
+    [self updateCounter];
+    self.tableView.allowsSelection = YES;
+    _resultTableViewController.tableView.allowsSelection = YES;
+}
+
+#pragma mark - Participants actions
+
+- (NSMutableArray *)filterContacts:(NSMutableArray *)contacts
+{
+    NSMutableArray *participants = [[NSMutableArray alloc] init];
+    for (NCUser *user in contacts) {
+        if (![_participantsInRoom containsObject:user.userId]) {
+            [participants addObject:user];
+        } else if (![user.source isEqualToString:kParticipantTypeUser]) {
+            [participants addObject:user];
+        }
+    }
+    return participants;
+}
+
+- (void)getPossibleParticipants
+{
+    TalkAccount *activeAccount = [[NCDatabaseManager sharedInstance] activeAccount];
+    [[NCAPIController sharedInstance] getContactsForAccount:activeAccount forRoom:_room.token groupRoom:YES withSearchParam:nil andCompletionBlock:^(NSArray *indexes, NSMutableDictionary *contacts, NSMutableArray *contactList, NSError *error) {
+        if (!error) {
+            NSMutableArray *storedContacts = [NCContact contactsForAccountId:activeAccount.accountId contains:nil];
+            NSMutableArray *combinedContactList = [NCUser combineUsersArray:storedContacts withUsersArray:contactList];
+            NSMutableArray *filteredParticipants = [self filterContacts:combinedContactList];
+            NSMutableDictionary *participants = [NCUser indexedUsersFromUsersArray:filteredParticipants];
+            self->_participants = participants;
+            self->_indexes = [[participants allKeys] sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)];
+            [self->_participantsBackgroundView.loadingView stopAnimating];
+            [self->_participantsBackgroundView.loadingView setHidden:YES];
+            [self->_participantsBackgroundView.placeholderView setHidden:(participants.count > 0)];
+            [self.tableView reloadData];
+        } else {
+            NSLog(@"Error while trying to get participants: %@", error);
+        }
+    }];
+}
+
+- (void)searchForParticipantsWithString:(NSString *)searchString
+{
+    [_searchParticipantsTask cancel];
+    TalkAccount *activeAccount = [[NCDatabaseManager sharedInstance] activeAccount];
+    _searchParticipantsTask = [[NCAPIController sharedInstance] getContactsForAccount:[[NCDatabaseManager sharedInstance] activeAccount] forRoom:_room.token groupRoom:YES withSearchParam:searchString andCompletionBlock:^(NSArray *indexes, NSMutableDictionary *contacts, NSMutableArray *contactList, NSError *error) {
+        if (!error) {
+            NSMutableArray *storedContacts = [NCContact contactsForAccountId:activeAccount.accountId contains:searchString];
+            NSMutableArray *combinedContactList = [NCUser combineUsersArray:storedContacts withUsersArray:contactList];
+            NSMutableArray *filteredParticipants = [self filterContacts:combinedContactList];
+            NSMutableDictionary *participants = [NCUser indexedUsersFromUsersArray:filteredParticipants];
+            NSArray *sortedIndexes = [[participants allKeys] sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)];
+            [self->_resultTableViewController setSearchResultContacts:participants withIndexes:sortedIndexes];
+        } else {
+            if (error.code != -999) {
+                NSLog(@"Error while searching for participants: %@", error);
+            }
+        }
+    }];
+}
+
+- (BOOL)isParticipantAlreadySelected:(NCUser *)participant
+{
+    for (NCUser *user in _selectedParticipants) {
+        if ([user.userId isEqualToString:participant.userId] &&
+            [user.source isEqualToString:participant.source]) {
+            return YES;
+        }
+    }
+    return NO;
+}
+
+- (void)removeSelectedParticipant:(NCUser *)participant
+{
+    NCUser *userToDelete = nil;
+    for (NCUser *user in _selectedParticipants) {
+        if ([user.userId isEqualToString:participant.userId] &&
+            [user.source isEqualToString:participant.source]) {
+            userToDelete = user;
+        }
+    }
+    
+    if (userToDelete) {
+        [_selectedParticipants removeObject:userToDelete];
+    }
+}
+
+#pragma mark - Search controller
+
+- (void)updateSearchResultsForSearchController:(UISearchController *)searchController
+{
+    [_searchTimer invalidate];
+    _searchTimer = nil;
+    [_resultTableViewController showSearchingUI];
+    dispatch_async(dispatch_get_main_queue(), ^{
+        self->_searchTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(searchForParticipants) userInfo:nil repeats:NO];
+    });
+}
+
+- (void)searchForParticipants
+{
+    NSString *searchString = _searchController.searchBar.text;
+    if (![searchString isEqualToString:@""]) {
+        [self searchForParticipantsWithString:searchString];
+    }
+}
+
+- (void)didDismissSearchController:(UISearchController *)searchController
+{
+    [self.tableView reloadData];
+}
+
+#pragma mark - Table view data source
+
+- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
+    return _indexes.count;
+}
+
+- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
+    NSString *index = [_indexes objectAtIndex:section];
+    NSArray *participants = [_participants objectForKey:index];
+    return participants.count;
+}
+
+- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
+{
+    return kContactsTableCellHeight;
+}
+
+- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
+{
+    return [_indexes objectAtIndex:section];
+}
+
+- (NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView
+{
+    return _indexes;
+}
+
+- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
+{
+    NSString *index = [_indexes objectAtIndex:indexPath.section];
+    NSArray *participants = [_participants objectForKey:index];
+    NCUser *participant = [participants objectAtIndex:indexPath.row];
+    ContactsTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kContactCellIdentifier forIndexPath:indexPath];
+    if (!cell) {
+        cell = [[ContactsTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:kContactCellIdentifier];
+    }
+    
+    cell.labelTitle.text = participant.name;
+    [cell.contactImage setActorAvatarForId:participant.userId withType:participant.source withDisplayName:participant.name withRoomToken:_room.token];
+
+    UIImage *selectionImage = [UIImage systemImageNamed:@"circle"];
+    UIColor *selectionImageColor = [UIColor tertiaryLabelColor];
+    if ([self isParticipantAlreadySelected:participant]) {
+        selectionImage = [UIImage systemImageNamed:@"checkmark.circle.fill"];
+        selectionImageColor = [NCAppBranding elementColor];
+    }
+    UIImageView *selectionImageView = [[UIImageView alloc] initWithImage:selectionImage];
+    selectionImageView.tintColor = selectionImageColor;
+    cell.accessoryView = selectionImageView;
+
+    return cell;
+}
+
+- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
+{
+    NSString *index = nil;
+    NSArray *participants = nil;
+    
+    if (_searchController.active && _resultTableViewController.contacts.count > 0) {
+        index = [_resultTableViewController.indexes objectAtIndex:indexPath.section];
+        participants = [_resultTableViewController.contacts objectForKey:index];
+    } else {
+        index = [_indexes objectAtIndex:indexPath.section];
+        participants = [_participants objectForKey:index];
+    }
+    
+    NCUser *participant = [participants objectAtIndex:indexPath.row];
+    if (![self isParticipantAlreadySelected:participant]) {
+        [_selectedParticipants addObject:participant];
+    } else {
+        [self removeSelectedParticipant:participant];
+    }
+    
+    _resultTableViewController.selectedParticipants = _selectedParticipants;
+    
+    [tableView beginUpdates];
+    [tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
+    [tableView endUpdates];
+    
+    [self updateCounter];
+}
+
+@end

+ 31 - 0
NextcloudTalk/AddParticipantsTableViewController.xib

@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="14109" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
+    <device id="retina4_7" orientation="portrait">
+        <adaptation id="fullscreen"/>
+    </device>
+    <dependencies>
+        <deployment identifier="iOS"/>
+        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14088"/>
+        <capability name="Safe area layout guides" minToolsVersion="9.0"/>
+        <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
+    </dependencies>
+    <objects>
+        <placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="AddParticipantsTableViewController">
+            <connections>
+                <outlet property="view" destination="i5M-Pr-FkT" id="sfx-zR-JGt"/>
+            </connections>
+        </placeholder>
+        <placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
+        <tableView opaque="NO" clipsSubviews="YES" clearsContextBeforeDrawing="NO" contentMode="scaleToFill" bouncesZoom="NO" style="plain" separatorStyle="default" rowHeight="80" sectionHeaderHeight="22" sectionFooterHeight="22" id="i5M-Pr-FkT">
+            <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
+            <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+            <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
+            <viewLayoutGuide key="safeArea" id="NQg-xp-Ubj"/>
+            <inset key="separatorInset" minX="72" minY="0.0" maxX="0.0" maxY="0.0"/>
+            <connections>
+                <outlet property="dataSource" destination="-1" id="Tng-2m-Rnh"/>
+                <outlet property="delegate" destination="-1" id="9aC-8N-iBw"/>
+            </connections>
+        </tableView>
+    </objects>
+</document>

+ 56 - 0
NextcloudTalk/AllocationTracker.swift

@@ -0,0 +1,56 @@
+//
+// SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+
+import UIKit
+
+@objcMembers class AllocationTracker: NSObject {
+
+    public static let shared = AllocationTracker()
+
+    private var allocationDict: [String: Int] = [:]
+    private lazy var isTestEnvironment = {
+        let arguments = ProcessInfo.processInfo.arguments
+
+        return arguments.contains(where: { $0 == "-TestEnvironment" })
+    }()
+
+    public func addAllocation(_ name: String) {
+        if !isTestEnvironment {
+            return
+        }
+
+        allocationDict[name, default: 0] += 1
+    }
+
+    public func removeAllocation(_ name: String) {
+        if !isTestEnvironment {
+            return
+        }
+
+        if let currentAllocations = allocationDict[name] {
+            if currentAllocations == 1 {
+                allocationDict.removeValue(forKey: name)
+            } else {
+                allocationDict[name] = currentAllocations - 1
+            }
+        } else {
+            print("WARNING: Removing non-existing allocation")
+        }
+    }
+
+    override var description: String {
+        if !isTestEnvironment {
+            return "Not running in testing environment."
+        }
+
+        if let jsonData = try? JSONSerialization.data(withJSONObject: allocationDict, options: .sortedKeys),
+           let jsonString = String(data: jsonData, encoding: .utf8) {
+
+            return jsonString
+        }
+
+        return "Unknown"
+    }
+}

+ 22 - 0
NextcloudTalk/AppDelegate.h

@@ -0,0 +1,22 @@
+/**
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#import <UIKit/UIKit.h>
+#import <PushKit/PushKit.h>
+
+@interface AppDelegate : UIResponder <UIApplicationDelegate, PKPushRegistryDelegate>
+{
+    PKPushRegistry *pushRegistry;
+    NSString *normalPushToken;
+    NSString *pushKitToken;
+}
+@property (strong, nonatomic) UIWindow *window;
+@property (assign, nonatomic) BOOL shouldLockInterfaceOrientation;
+@property (assign, nonatomic) UIInterfaceOrientation lockedInterfaceOrientation;
+
+- (void)keepExternalSignalingConnectionAliveTemporarily;
+
+@end
+

+ 575 - 0
NextcloudTalk/AppDelegate.m

@@ -0,0 +1,575 @@
+/**
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#import "AppDelegate.h"
+
+#import "AFNetworkReachabilityManager.h"
+#import "AFNetworkActivityIndicatorManager.h"
+
+#import <Intents/Intents.h>
+#import <UserNotifications/UserNotifications.h>
+
+#import <BackgroundTasks/BGTaskScheduler.h>
+#import <BackgroundTasks/BGTaskRequest.h>
+#import <BackgroundTasks/BGTask.h>
+
+#import "NCAudioController.h"
+#import "NCAppBranding.h"
+#import "NCDatabaseManager.h"
+#import "NCKeyChainController.h"
+#import "NCNavigationController.h"
+#import "NCNotificationController.h"
+#import "NCPushNotification.h"
+#import "NCPushNotificationsUtils.h"
+#import "NCRoomsManager.h"
+#import "NCSettingsController.h"
+#import "NCUserInterfaceController.h"
+
+#import "NextcloudTalk-Swift.h"
+
+@interface AppDelegate ()
+
+@property (nonatomic, strong) NSTimer *keepAliveTimer;
+@property (nonatomic, strong) BGTaskHelper *keepAliveBGTask;
+@property (nonatomic, strong) UILabel *debugLabel;
+@property (nonatomic, strong) NSTimer *debugLabelTimer;
+@property (nonatomic, strong) NSTimer *fileDescriptorTimer;
+
+@end
+
+@implementation AppDelegate
+
+
+- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
+{
+#if DEBUG
+    [AFNetworkActivityIndicatorManager sharedManager].enabled = YES;
+#endif
+    [[AFNetworkReachabilityManager sharedManager] startMonitoring];
+    
+    [[NCNotificationController sharedInstance] requestAuthorization];
+    
+    [application registerForRemoteNotifications];
+    
+    pushRegistry = [[PKPushRegistry alloc] initWithQueue:dispatch_get_main_queue()];
+    pushRegistry.delegate = self;
+    pushRegistry.desiredPushTypes = [NSSet setWithObject:PKPushTypeVoIP];
+
+    [[WebRTCCommon shared] dispatch:^{
+        NSLog(@"Configure Audio Session");
+        [NCAudioController sharedInstance];
+    }];
+    
+    NSLog(@"Configure App Settings");
+    [NCSettingsController sharedInstance];
+
+    // Perform logfile cleanup only once in app lifecycle
+    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
+        [NCUtils removeOldLogfiles];
+    });
+
+    UIDevice *currentDevice = [UIDevice currentDevice];
+    [NCUtils log:[NSString stringWithFormat:@"Starting %@, version %@, %@ %@, model %@", NSBundle.mainBundle.bundleIdentifier, [NCAppBranding getAppVersionString], currentDevice.systemName, currentDevice.systemVersion, currentDevice.model]];
+
+    //Init rooms manager to start receiving NSNotificationCenter notifications
+    [NCRoomsManager sharedInstance];
+    
+    [self registerBackgroundFetchTask];
+
+    [NCUserInterfaceController sharedInstance].mainViewController = (NCSplitViewController *) self.window.rootViewController;
+    [NCUserInterfaceController sharedInstance].roomsTableViewController = [NCUserInterfaceController sharedInstance].mainViewController.viewControllers.firstObject.childViewControllers.firstObject;
+    [NCUserInterfaceController sharedInstance].mainViewController.displayModeButtonVisibility = UISplitViewControllerDisplayModeButtonVisibilityNever;
+
+    NSArray *arguments = [[NSProcessInfo processInfo] arguments];
+
+    if ([arguments containsObject:@"-TestEnvironment"]) {
+        UIView *mainView = [NCUserInterfaceController sharedInstance].mainViewController.view;
+
+        self.debugLabel = [[UILabel alloc] initWithFrame:CGRectMake(20, 30, 200, 20)];
+        self.debugLabel.font = [UIFont systemFontOfSize:[UIFont smallSystemFontSize]];
+        self.debugLabel.translatesAutoresizingMaskIntoConstraints = NO;
+
+        [mainView addSubview:self.debugLabel];
+        [NSLayoutConstraint activateConstraints:@[
+            [self.debugLabel.topAnchor constraintEqualToAnchor:mainView.safeAreaLayoutGuide.topAnchor constant:-15],
+            [self.debugLabel.leadingAnchor constraintEqualToAnchor:mainView.safeAreaLayoutGuide.leadingAnchor constant:5],
+            [self.debugLabel.trailingAnchor constraintEqualToAnchor:mainView.safeAreaLayoutGuide.trailingAnchor]
+        ]];
+
+        __weak typeof(self) weakSelf = self;
+        self.debugLabelTimer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
+            [weakSelf.debugLabel setText:[AllocationTracker shared].description];
+        }];
+    }
+
+    // Comment out the following code to log the number of open socket file descriptors
+    /*
+     self.fileDescriptorTimer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
+        [[WebRTCCommon shared] printNumberOfOpenSocketDescriptors];
+    }];
+     */
+
+    // When we include VLCKit we need to manually call this because otherwise, device rotation might not work
+    [[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications];
+    
+    return YES;
+}
+
+- (BOOL)application:(UIApplication *)application continueUserActivity:(nonnull NSUserActivity *)userActivity restorationHandler:(nonnull void (^)(NSArray<id<UIUserActivityRestoring>> * _Nullable))restorationHandler
+{
+    BOOL audioCallIntent = [userActivity.interaction.intent isKindOfClass:[INStartAudioCallIntent class]];
+    BOOL videoCallIntent = [userActivity.interaction.intent isKindOfClass:[INStartVideoCallIntent class]];
+    if (audioCallIntent || videoCallIntent) {
+        INPerson *person = [[(INStartAudioCallIntent*)userActivity.interaction.intent contacts] firstObject];
+        NSString *roomToken = person.personHandle.value;
+        if (roomToken) {
+            [[NCUserInterfaceController sharedInstance] presentCallKitCallInRoom:roomToken withVideoEnabled:videoCallIntent];
+        }
+    }
+    return YES;
+}
+
+- (void)applicationWillResignActive:(UIApplication *)application
+{
+    // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
+    // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
+}
+
+
+- (void)applicationDidEnterBackground:(UIApplication *)application
+{
+    // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
+    // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
+
+    [self keepExternalSignalingConnectionAliveTemporarily];
+    [self scheduleAppRefresh];
+}
+
+
+- (void)applicationWillEnterForeground:(UIApplication *)application
+{
+    // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
+}
+
+
+- (void)applicationDidBecomeActive:(UIApplication *)application
+{
+    // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
+
+    [self checkForDisconnectedExternalSignalingConnection];
+
+    [[NCNotificationController sharedInstance] removeAllNotificationsForAccountId:[[NCDatabaseManager sharedInstance] activeAccount].accountId];
+}
+
+- (void)applicationProtectedDataDidBecomeAvailable:(UIApplication *)application
+{
+    if ([[CallKitManager sharedInstance].calls count] > 0) {
+        [NCUtils log:@"Protected data did become available"];
+    }
+}
+
+- (void)applicationProtectedDataWillBecomeUnavailable:(UIApplication *)application
+{
+    if ([[CallKitManager sharedInstance].calls count] > 0) {
+        [NCUtils log:@"Protected data did become unavailable"];
+    }
+}
+
+- (void)applicationWillTerminate:(UIApplication *)application
+{
+    // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
+    [[UIDevice currentDevice] endGeneratingDeviceOrientationNotifications];
+
+    // Invalidate a potentially existing label timer
+    [self.debugLabelTimer invalidate];
+
+    [self.fileDescriptorTimer invalidate];
+}
+
+- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options
+{
+    NSURLComponents *urlComponents = [NSURLComponents componentsWithURL:url resolvingAgainstBaseURL:NO];
+    NSString *scheme = urlComponents.scheme;
+    if ([scheme isEqualToString:@"nextcloudtalk"]) {
+        NSString *action = urlComponents.host;
+        if ([action isEqualToString:@"open-conversation"]) {
+            [[NCUserInterfaceController sharedInstance] presentChatForURL:urlComponents];
+            return YES;
+        } else if ([action isEqualToString:@"login"] && multiAccountEnabled) {
+            NSArray *queryItems = urlComponents.queryItems;
+            NSString *server = [NCUtils valueForKey:@"server" fromQueryItems:queryItems];
+            NSString *user = [NCUtils valueForKey:@"user" fromQueryItems:queryItems];
+            
+            if (server) {
+                [[NCUserInterfaceController sharedInstance] presentLoginViewControllerForServerURL:server withUser:user];
+            }
+            return YES;
+        }
+    }
+    
+    return NO;
+}
+
+- (UIInterfaceOrientationMask)application:(UIApplication *)application supportedInterfaceOrientationsForWindow:(UIWindow *)window
+{
+    if (_shouldLockInterfaceOrientation) {
+        if (_lockedInterfaceOrientation == UIInterfaceOrientationPortrait) {
+            return UIInterfaceOrientationMaskPortrait;
+        } else if (_lockedInterfaceOrientation == UIInterfaceOrientationLandscapeLeft) {
+            return UIInterfaceOrientationMaskLandscapeLeft;
+        } else if (_lockedInterfaceOrientation == UIInterfaceOrientationLandscapeRight) {
+            return UIInterfaceOrientationMaskLandscapeRight;
+        }
+    }
+    return UIInterfaceOrientationMaskAllButUpsideDown;
+}
+
+- (void)setShouldLockInterfaceOrientation:(BOOL)shouldLockInterfaceOrientation
+{
+    _shouldLockInterfaceOrientation = shouldLockInterfaceOrientation;
+    _lockedInterfaceOrientation = [[UIApplication sharedApplication] statusBarOrientation];
+}
+
+#pragma mark - Push Notifications Registration
+
+- (void)checkForPushNotificationSubscription
+{
+    if (!normalPushToken || !pushKitToken) {
+        return;
+    }
+
+    // Store new Normal Push & PushKit tokens in Keychain
+    UICKeyChainStore *keychain = [UICKeyChainStore keyChainStoreWithService:bundleIdentifier accessGroup:groupIdentifier];
+    [keychain setString:normalPushToken forKey:kNCNormalPushTokenKey];
+    [keychain setString:pushKitToken forKey:kNCPushKitTokenKey];
+
+    BOOL isAppInBackground = [[UIApplication sharedApplication] applicationState] == UIApplicationStateBackground;
+    // Subscribe only if both tokens have been generated and app is not running in the background (do not try to subscribe
+    // when the app is running in background e.g. when the app is launched due to a VoIP push notification)
+    if (!isAppInBackground) {
+        // Try to subscribe for push notifications in all accounts
+        for (TalkAccount *account in [[NCDatabaseManager sharedInstance] allAccounts]) {
+            [[NCSettingsController sharedInstance] subscribeForPushNotificationsForAccountId:account.accountId withCompletionBlock:nil];
+        }
+    }
+}
+
+#pragma mark - Normal Push Notifications Delegate Methods
+
+- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken
+{
+    if([deviceToken length] == 0) {
+        NSLog(@"Failed to create Normal Push token.");
+        return;
+    }
+    
+    normalPushToken = [self stringWithDeviceToken:deviceToken];
+    [self checkForPushNotificationSubscription];
+    [self registerInteractivePushNotification];
+}
+
+- (void)registerInteractivePushNotification
+{
+    // Reply directly to a chat notification action/category
+    UNTextInputNotificationAction *replyAction = [UNTextInputNotificationAction actionWithIdentifier:NCNotificationActionReplyToChat
+                                                                                          title:NSLocalizedString(@"Reply", nil)
+                                                                                        options:UNNotificationActionOptionAuthenticationRequired];
+    
+    UNNotificationCategory *chatCategory = [UNNotificationCategory categoryWithIdentifier:@"CATEGORY_CHAT"
+                                                                              actions:@[replyAction]
+                                                                    intentIdentifiers:@[]
+                                                                              options:UNNotificationCategoryOptionNone];
+
+    // Recording actions/category
+    UNNotificationAction *recordingShareAction = [UNNotificationAction actionWithIdentifier:NCNotificationActionShareRecording
+                                                                                      title:NSLocalizedString(@"Share to chat", nil)
+                                                                                    options:UNNotificationActionOptionAuthenticationRequired];
+
+    UNNotificationAction *recordingDismissAction = [UNNotificationAction actionWithIdentifier:NCNotificationActionDismissRecordingNotification
+                                                                                      title:NSLocalizedString(@"Dismiss notification", nil)
+                                                                                    options:UNNotificationActionOptionAuthenticationRequired | UNNotificationActionOptionDestructive];
+
+    UNNotificationCategory *recordingCategory = [UNNotificationCategory categoryWithIdentifier:@"CATEGORY_RECORDING"
+                                                                                       actions:@[recordingShareAction, recordingDismissAction]
+                                                                             intentIdentifiers:@[]
+                                                                                       options:UNNotificationCategoryOptionNone];
+
+    // Federation invitation
+    UNNotificationAction *federationAccept = [UNNotificationAction actionWithIdentifier:NCNotificationActionFederationInvitationAccept
+                                                                                  title:NSLocalizedString(@"Accept", nil)
+                                                                                options:UNNotificationActionOptionAuthenticationRequired];
+
+    UNNotificationAction *federationReject = [UNNotificationAction actionWithIdentifier:NCNotificationActionFederationInvitationReject
+                                                                                  title:NSLocalizedString(@"Reject", nil)
+                                                                                options:UNNotificationActionOptionAuthenticationRequired | UNNotificationActionOptionDestructive];
+
+    UNNotificationCategory *federationCategory = [UNNotificationCategory categoryWithIdentifier:@"CATEGORY_FEDERATION"
+                                                                                       actions:@[federationAccept, federationReject]
+                                                                             intentIdentifiers:@[]
+                                                                                       options:UNNotificationCategoryOptionNone];
+
+    NSSet *categories = [NSSet setWithObjects:chatCategory, recordingCategory, federationCategory, nil];
+    [[UNUserNotificationCenter currentNotificationCenter] setNotificationCategories:categories];
+}
+
+- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
+{
+    // Called when a background notification is delivered.
+    NSString *message = [userInfo objectForKey:@"subject"];
+    for (TalkAccount *account in [[NCDatabaseManager sharedInstance] allAccounts]) {
+        NSData *pushNotificationPrivateKey = [[NCKeyChainController sharedInstance] pushNotificationPrivateKeyForAccountId:account.accountId];
+        if (message && pushNotificationPrivateKey) {
+            NSString *decryptedMessage = [NCPushNotificationsUtils decryptPushNotification:message withDevicePrivateKey:pushNotificationPrivateKey];
+            if (decryptedMessage) {
+                NCPushNotification *pushNotification = [NCPushNotification pushNotificationFromDecryptedString:decryptedMessage withAccountId:account.accountId];
+                [[NCNotificationController sharedInstance] processBackgroundPushNotification:pushNotification];
+            }
+        }
+    }
+    completionHandler(UIBackgroundFetchResultNewData);
+}
+
+
+#pragma mark - PushKit Delegate Methods
+
+- (void)pushRegistry:(PKPushRegistry *)registry didUpdatePushCredentials:(PKPushCredentials *)credentials forType:(NSString *)type
+{
+    if([credentials.token length] == 0) {
+        NSLog(@"Failed to create PushKit token.");
+        return;
+    }
+    
+    pushKitToken = [self stringWithDeviceToken:credentials.token];
+    [self checkForPushNotificationSubscription];
+}
+
+- (void)pushRegistry:(PKPushRegistry *)registry didReceiveIncomingPushWithPayload:(PKPushPayload *)payload forType:(PKPushType)type withCompletionHandler:(void (^)(void))completion
+{
+    [NCUtils log:@"Received PushKit notification"];
+
+    NSString *message = [payload.dictionaryPayload objectForKey:@"subject"];
+    for (TalkAccount *account in [[NCDatabaseManager sharedInstance] allAccounts]) {
+        NSData *pushNotificationPrivateKey = [[NCKeyChainController sharedInstance] pushNotificationPrivateKeyForAccountId:account.accountId];
+
+        if (!message || !pushNotificationPrivateKey) {
+            continue;
+        }
+
+        NSString *decryptedMessage = [NCPushNotificationsUtils decryptPushNotification:message withDevicePrivateKey:pushNotificationPrivateKey];
+
+        if (!decryptedMessage) {
+            continue;
+        }
+
+        NCPushNotification *pushNotification = [NCPushNotification pushNotificationFromDecryptedString:decryptedMessage withAccountId:account.accountId];
+
+        if ( pushNotification && pushNotification.type == NCPushNotificationTypeCall) {
+            [[NCNotificationController sharedInstance] showIncomingCallForPushNotification:pushNotification];
+            completion();
+            return;
+        }
+    }
+
+    [[NCNotificationController sharedInstance] showIncomingCallForOldAccount];
+    [[NCSettingsController sharedInstance] setDidReceiveCallsFromOldAccount:YES];
+    completion();
+}
+
+- (NSString *)stringWithDeviceToken:(NSData *)deviceToken
+{
+    const char *data = [deviceToken bytes];
+    NSMutableString *token = [NSMutableString string];
+    
+    for (NSUInteger i = 0; i < [deviceToken length]; i++) {
+        [token appendFormat:@"%02.2hhX", data[i]];
+    }
+    
+    return [token copy];
+}
+
+#pragma mark - BackgroundFetch / AppRefresh
+
+- (void)registerBackgroundFetchTask {
+    NSString *refreshTaskIdentifier = [NSString stringWithFormat:@"%@.refresh", NSBundle.mainBundle.bundleIdentifier];
+
+    // see: https://developer.apple.com/documentation/backgroundtasks/bgtaskscheduler?language=objc
+    [[BGTaskScheduler sharedScheduler] registerForTaskWithIdentifier:refreshTaskIdentifier
+                                                          usingQueue:nil
+                                                       launchHandler:^(__kindof BGTask * _Nonnull task) {
+        [self handleAppRefresh:task];
+    }];
+}
+
+- (void)scheduleAppRefresh
+{
+    NSString *refreshTaskIdentifier = [NSString stringWithFormat:@"%@.refresh", NSBundle.mainBundle.bundleIdentifier];
+    
+    BGAppRefreshTaskRequest *request = [[BGAppRefreshTaskRequest alloc] initWithIdentifier:refreshTaskIdentifier];
+    request.earliestBeginDate = [NSDate dateWithTimeIntervalSinceNow:UIApplicationBackgroundFetchIntervalMinimum];
+    
+    NSError *error = nil;
+    [[BGTaskScheduler sharedScheduler] submitTaskRequest:request error:&error];
+
+    if (error) {
+        NSLog(@"Failed to submit apprefresh request: %@", error);
+    }
+}
+
+- (void)handleAppRefresh:(BGTask *)task
+{
+    [NCUtils log:@"Performing background fetch -> handleAppRefresh"];
+    
+    // With BGTasks (iOS >= 13) we need to schedule another refresh when running in background
+    [self scheduleAppRefresh];
+
+    [self performBackgroundFetchWithCompletionHandler:^(BOOL errorOccurred) {
+        [task setTaskCompletedWithSuccess:!errorOccurred];
+    }];
+}
+
+// This method is called when you simulate a background fetch from the debug menu in XCode
+// so we keep it around, although it's deprecated on iOS 13 onwards
+- (void)application:(UIApplication *)application performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
+{
+    [NCUtils log:@"Performing background fetch -> performFetchWithCompletionHandler"];
+
+    [self performBackgroundFetchWithCompletionHandler:^(BOOL errorOccurred) {
+         if (errorOccurred) {
+             completionHandler(UIBackgroundFetchResultFailed);
+         } else {
+             completionHandler(UIBackgroundFetchResultNewData);
+         }
+     }];
+}
+
+
+- (void)performBackgroundFetchWithCompletionHandler:(void (^)(BOOL errorOccurred))completionHandler
+{
+    dispatch_group_t backgroundRefreshGroup = dispatch_group_create();
+    __block BOOL errorOccurred = NO;
+    __block BOOL expired = NO;
+
+    BGTaskHelper *bgTask = [BGTaskHelper startBackgroundTaskWithName:@"NCBackgroundFetch" expirationHandler:^(BGTaskHelper *task) {
+        [NCUtils log:@"ExpirationHandler called"];
+
+        /*
+        expired = YES;
+        completionHandler(YES);
+        
+        [task stopBackgroundTask];
+         */
+    }];
+
+    [NCUtils log:@"Start performBackgroundFetchWithCompletionHandler"];
+
+    dispatch_group_enter(backgroundRefreshGroup);
+    [[NCRoomsManager sharedInstance] resendOfflineMessagesWithCompletionBlock:^{
+        [NCUtils log:@"CompletionHandler resendOfflineMessagesWithCompletionBlock"];
+
+        dispatch_group_leave(backgroundRefreshGroup);
+    }];
+
+    // Check if the shown notifications are still available on the server
+    dispatch_group_enter(backgroundRefreshGroup);
+    [[NCNotificationController sharedInstance] checkNotificationExistanceWithCompletionBlock:^(NSError *error) {
+        [NCUtils log:@"CompletionHandler checkNotificationExistance"];
+
+        if (error) {
+            errorOccurred = YES;
+        }
+
+        dispatch_group_leave(backgroundRefreshGroup);
+    }];
+
+    /* Disable checking for new messages for now, until we can prevent them from showing twice
+    dispatch_group_enter(backgroundRefreshGroup);
+    [[NCNotificationController sharedInstance] checkForNewNotificationsWithCompletionBlock:^(NSError *error) {
+        [NCUtils log:@"CompletionHandler checkForNewNotificationsWithCompletionBlock"];
+
+        if (error) {
+            errorOccurred = YES;
+        }
+
+        dispatch_group_leave(backgroundRefreshGroup);
+    }];
+     */
+
+    dispatch_group_enter(backgroundRefreshGroup);
+    [[NCRoomsManager sharedInstance] updateRoomsAndChatsUpdatingUserStatus:NO onlyLastModified:YES withCompletionBlock:^(NSError *error) {
+        [NCUtils log:@"CompletionHandler updateRoomsAndChatsUpdatingUserStatus"];
+
+        if (error) {
+            errorOccurred = YES;
+        }
+
+        dispatch_group_leave(backgroundRefreshGroup);
+    }];
+
+    NSDateComponents *dayComponent = [[NSDateComponents alloc] init];
+    dayComponent.day = -1;
+
+    NSDate *thresholdDate = [[NSCalendar currentCalendar] dateByAddingComponents:dayComponent toDate:[NSDate date] options:0];
+    NSInteger thresholdTimestamp = [thresholdDate timeIntervalSince1970];
+
+    // Push proxy should be subscrided atleast every 24h
+    // Check if we reached the threshold and start the subscription process
+    for (TalkAccount *account in [[NCDatabaseManager sharedInstance] allAccounts]) {
+        if (account.lastPushSubscription < thresholdTimestamp) {
+            dispatch_group_enter(backgroundRefreshGroup);
+
+            [[NCSettingsController sharedInstance] subscribeForPushNotificationsForAccountId:account.accountId withCompletionBlock:^(BOOL success) {
+                if (!success) {
+                    errorOccurred = YES;
+                }
+
+                dispatch_group_leave(backgroundRefreshGroup);
+            }];
+        }
+    }
+
+    dispatch_group_notify(backgroundRefreshGroup, dispatch_get_main_queue(), ^{
+         [NCUtils log:@"CompletionHandler performBackgroundFetchWithCompletionHandler dispatch_group_notify"];
+
+         if (!expired) {
+             completionHandler(errorOccurred);
+         }
+
+         [bgTask stopBackgroundTask];
+     });
+}
+
+- (void)keepExternalSignalingConnectionAliveTemporarily
+{
+    [_keepAliveTimer invalidate];
+
+    _keepAliveBGTask = [BGTaskHelper startBackgroundTaskWithName:@"NCWebSocketKeepAlive" expirationHandler:nil];
+    _keepAliveTimer = [NSTimer scheduledTimerWithTimeInterval:20 repeats:NO block:^(NSTimer * _Nonnull timer) {
+        // Stop the external signaling connections only if the app keeps in the background and not in a call
+        if ([[UIApplication sharedApplication] applicationState] == UIApplicationStateBackground &&
+            ![NCRoomsManager sharedInstance].callViewController) {
+            [[NCSettingsController sharedInstance] disconnectAllExternalSignalingControllers];
+        }
+
+        // Disconnect is dispatched to the main queue, so in theory it can happen that we stop the background task
+        // before the disconnect is run/completed. So we dispatch the stopBackgroundTask to main as well
+        // to be sure it's called after everything else is run.
+        dispatch_async(dispatch_get_main_queue(), ^{
+            [self->_keepAliveBGTask stopBackgroundTask];
+        });
+    }];
+
+    [[NSRunLoop mainRunLoop] addTimer:_keepAliveTimer forMode:NSRunLoopCommonModes];
+}
+
+- (void)checkForDisconnectedExternalSignalingConnection
+{
+    [_keepAliveTimer invalidate];
+    [_keepAliveBGTask stopBackgroundTask];
+
+    [[NCSettingsController sharedInstance] connectDisconnectedExternalSignalingControllers];
+}
+
+
+@end

+ 115 - 0
NextcloudTalk/AudioPlayerView.swift

@@ -0,0 +1,115 @@
+//
+// SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+
+import UIKit
+
+protocol AudioPlayerViewDelegate: AnyObject {
+
+    func audioPlayerPlayButtonPressed()
+    func audioPlayerPauseButtonPressed()
+    func audioPlayerProgressChanged(progress: CGFloat)
+}
+
+class AudioPlayerView: UIView {
+
+    @IBOutlet var contentView: UIView!
+    @IBOutlet weak var playPauseButton: UIButton!
+    @IBOutlet weak var slider: UISlider!
+    @IBOutlet weak var durationLabel: UILabel!
+
+    var isPlaying: Bool = false
+
+    public weak var delegate: AudioPlayerViewDelegate?
+
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        commonInit()
+    }
+
+    required init?(coder aDecoder: NSCoder) {
+        super.init(coder: aDecoder)
+        commonInit()
+    }
+
+    func commonInit() {
+        Bundle.main.loadNibNamed("AudioPlayerView", owner: self, options: nil)
+
+        contentView.frame = self.bounds
+        contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
+
+        // Play/Pause button
+        playPauseButton.addAction(for: .touchUpInside) { [unowned self] in
+            if isPlaying {
+                delegate?.audioPlayerPauseButtonPressed()
+            } else {
+                delegate?.audioPlayerPlayButtonPressed()
+            }
+        }
+
+        // Slider
+        let thumbImage = UIImage(systemName: "circle.fill")?
+                    .withConfiguration(UIImage.SymbolConfiguration(pointSize: 16))
+                    .withTintColor(.label, renderingMode: .alwaysOriginal)
+        slider.setThumbImage(thumbImage, for: .normal)
+        slider.semanticContentAttribute = .forceLeftToRight
+        slider.addAction(for: .valueChanged) { [unowned self] in
+            delegate?.audioPlayerProgressChanged(progress: CGFloat(slider.value))
+        }
+
+        // Duration label
+        hideDurationLabel()
+
+        backgroundColor = .secondarySystemBackground
+        layer.cornerRadius = 8.0
+        layer.masksToBounds = true
+
+        self.addSubview(contentView)
+    }
+
+    func setPlayerProgress(_ progress: CGFloat, isPlaying playing: Bool, maximumValue maxValue: CGFloat) {
+        setPlayPauseButton(playing: playing)
+        slider.isEnabled = true
+        slider.value = Float(progress)
+        slider.maximumValue = Float(maxValue)
+        setDurationLabel(progress: progress, duration: maxValue)
+        slider.setNeedsLayout()
+    }
+
+    func resetPlayer() {
+        setPlayPauseButton(playing: false)
+        slider.isEnabled = false
+        slider.value = 0
+        hideDurationLabel()
+        slider.setNeedsLayout()
+    }
+
+    func setPlayPauseButton(playing: Bool) {
+        isPlaying = playing
+
+        if isPlaying {
+            playPauseButton.setImage(UIImage(systemName: "pause.fill"), for: .normal)
+        } else {
+            playPauseButton.setImage(UIImage(systemName: "play.fill"), for: .normal)
+        }
+    }
+
+    func setDurationLabel(progress: CGFloat, duration: CGFloat) {
+        let dateComponentsFormatter = DateComponentsFormatter()
+        dateComponentsFormatter.allowedUnits = [.minute, .second]
+        dateComponentsFormatter.zeroFormattingBehavior = []
+
+        let progressTime = dateComponentsFormatter.string(from: TimeInterval(progress)) ?? "0:00"
+        let durationTime = dateComponentsFormatter.string(from: TimeInterval(duration)) ?? "0:00"
+
+        let playerTimeString = "\(progressTime)".withTextColor(.label).withFont(.systemFont(ofSize: 13, weight: .medium))
+        playerTimeString.append(" / \(durationTime)".withTextColor(.secondaryLabel).withFont(.systemFont(ofSize: 13)))
+
+        durationLabel.attributedText = playerTimeString
+    }
+
+    func hideDurationLabel() {
+        durationLabel.text = ""
+    }
+}

+ 67 - 0
NextcloudTalk/AudioPlayerView.xib

@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="23094" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
+    <device id="retina6_12" orientation="portrait" appearance="light"/>
+    <dependencies>
+        <deployment identifier="iOS"/>
+        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23084"/>
+        <capability name="Safe area layout guides" minToolsVersion="9.0"/>
+        <capability name="System colors in document resources" minToolsVersion="11.0"/>
+        <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
+    </dependencies>
+    <objects>
+        <placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="AudioPlayerView" customModule="NextcloudTalk" customModuleProvider="target">
+            <connections>
+                <outlet property="contentView" destination="iN0-l3-epB" id="WHP-2s-6Il"/>
+                <outlet property="durationLabel" destination="7vy-DL-v2I" id="gLr-z8-lYD"/>
+                <outlet property="playPauseButton" destination="weF-VJ-kQg" id="6O8-WK-cv9"/>
+                <outlet property="slider" destination="Cgz-Rx-FyJ" id="TME-tZ-DL1"/>
+            </connections>
+        </placeholder>
+        <placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
+        <view contentMode="scaleToFill" id="iN0-l3-epB">
+            <rect key="frame" x="0.0" y="0.0" width="441" height="52"/>
+            <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+            <subviews>
+                <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="weF-VJ-kQg">
+                    <rect key="frame" x="8" y="4" width="44" height="44"/>
+                    <constraints>
+                        <constraint firstAttribute="width" constant="44" id="TwB-9N-gEK"/>
+                        <constraint firstAttribute="height" constant="44" id="XIa-Qd-GZD"/>
+                    </constraints>
+                    <color key="tintColor" systemColor="labelColor"/>
+                    <state key="normal" title="Button"/>
+                    <buttonConfiguration key="configuration" style="plain" image="play.fill" catalog="system"/>
+                </button>
+                <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" horizontalCompressionResistancePriority="751" text="0:00 / 0:02" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="7vy-DL-v2I">
+                    <rect key="frame" x="366" y="0.0" width="67" height="52"/>
+                    <fontDescription key="fontDescription" type="system" pointSize="13"/>
+                    <nil key="textColor"/>
+                    <nil key="highlightedColor"/>
+                </label>
+                <slider opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" value="0.5" minValue="0.0" maxValue="1" translatesAutoresizingMaskIntoConstraints="NO" id="Cgz-Rx-FyJ">
+                    <rect key="frame" x="58" y="11" width="302" height="31"/>
+                </slider>
+            </subviews>
+            <viewLayoutGuide key="safeArea" id="vUN-kp-3ea"/>
+            <constraints>
+                <constraint firstItem="Cgz-Rx-FyJ" firstAttribute="centerY" secondItem="iN0-l3-epB" secondAttribute="centerY" id="0pD-pS-YhM"/>
+                <constraint firstAttribute="bottom" secondItem="7vy-DL-v2I" secondAttribute="bottom" id="1PB-ja-hxc"/>
+                <constraint firstItem="Cgz-Rx-FyJ" firstAttribute="leading" secondItem="weF-VJ-kQg" secondAttribute="trailing" constant="8" symbolic="YES" id="H3b-Y3-6vI"/>
+                <constraint firstItem="weF-VJ-kQg" firstAttribute="top" secondItem="iN0-l3-epB" secondAttribute="top" constant="4" id="MAo-xn-Ott"/>
+                <constraint firstAttribute="bottom" secondItem="weF-VJ-kQg" secondAttribute="bottom" constant="4" id="Ms8-VQ-WhR"/>
+                <constraint firstItem="weF-VJ-kQg" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" constant="8" id="QBV-dj-HsA"/>
+                <constraint firstItem="7vy-DL-v2I" firstAttribute="top" secondItem="iN0-l3-epB" secondAttribute="top" id="QFI-XE-9Fd"/>
+                <constraint firstAttribute="trailing" secondItem="7vy-DL-v2I" secondAttribute="trailing" constant="8" id="VnI-wM-6MZ"/>
+                <constraint firstItem="7vy-DL-v2I" firstAttribute="leading" secondItem="Cgz-Rx-FyJ" secondAttribute="trailing" constant="8" id="nHt-Db-jET"/>
+            </constraints>
+            <freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
+            <point key="canvasLocation" x="130.53435114503816" y="-233.09859154929578"/>
+        </view>
+    </objects>
+    <resources>
+        <image name="play.fill" catalog="system" width="120" height="128"/>
+        <systemColor name="labelColor">
+            <color white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+        </systemColor>
+    </resources>
+</document>

+ 26 - 0
NextcloudTalk/AuthenticationViewController.h

@@ -0,0 +1,26 @@
+/**
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#import <UIKit/UIKit.h>
+#import <WebKit/WebKit.h>
+
+@class AuthenticationViewController;
+@protocol AuthenticationViewControllerDelegate <NSObject>
+
+- (void)authenticationViewControllerDidFinish:(AuthenticationViewController *)viewController;
+
+@end
+
+@interface AuthenticationViewController : UIViewController
+
+@property (nonatomic, weak) id<AuthenticationViewControllerDelegate> delegate;
+
+@property(strong, nonatomic) WKWebView *webView;
+@property(strong, nonatomic) NSString *serverUrl;
+@property(strong, nonatomic) NSString *user;
+
+- (id)initWithServerUrl:(NSString *)serverUrl;
+
+@end

+ 200 - 0
NextcloudTalk/AuthenticationViewController.m

@@ -0,0 +1,200 @@
+/**
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#import "AuthenticationViewController.h"
+
+
+#import "NextcloudTalk-Swift.h"
+
+#import "CCCertificate.h"
+#import "NCAPIController.h"
+#import "NCAppBranding.h"
+#import "NCDatabaseManager.h"
+#import "NCSettingsController.h"
+
+NSString * const kNCAuthTokenFlowEndpoint               = @"/index.php/login/flow";
+
+@interface AuthenticationViewController () <WKNavigationDelegate>
+{
+    UIActivityIndicatorView *_activityIndicatorView;
+}
+
+@end
+
+@implementation AuthenticationViewController
+
+@synthesize delegate = _delegate;
+
+- (id)initWithServerUrl:(NSString *)serverUrl
+{
+    self = [super init];
+    if (self) {
+        self.serverUrl = serverUrl;
+    }
+    
+    return self;
+}
+
+- (void)viewDidLoad
+{
+    [super viewDidLoad];
+    WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];
+    configuration.websiteDataStore = [WKWebsiteDataStore nonPersistentDataStore];
+
+    NSString *urlString = [NSString stringWithFormat:@"%@%@", _serverUrl, kNCAuthTokenFlowEndpoint];
+
+    if (_user) {
+        urlString = [NSString stringWithFormat:@"%@?user=%@", urlString, _user];
+    }
+
+    NSURL *url = [NSURL URLWithString:urlString];
+
+
+    NSHTTPCookieStorage *storage = [NSHTTPCookieStorage sharedHTTPCookieStorage];
+    for (NSHTTPCookie *cookie in [storage cookies])
+    {
+        [storage deleteCookie:cookie];
+    }
+
+    NSSet *websiteDataTypes = [WKWebsiteDataStore allWebsiteDataTypes];
+    NSDate *dateFrom = [NSDate dateWithTimeIntervalSince1970:0];
+    [[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:websiteDataTypes modifiedSince:dateFrom completionHandler:^{
+        NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url];
+        [request setValue:@"true" forHTTPHeaderField:@"OCS-APIRequest"];
+
+        self->_webView = [[DebounceWebView alloc] initWithFrame:self.view.frame configuration:configuration];
+        NSString *appDisplayName = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleDisplayName"];
+        NSString *deviceName = [[UIDevice currentDevice] name];
+        NSString *userAgent = [NSString stringWithFormat:@"%@ (%@)", deviceName, appDisplayName];
+        self->_webView.customUserAgent = [[NSString alloc] initWithCString:[userAgent UTF8String] encoding:NSASCIIStringEncoding];
+        self->_webView.navigationDelegate = self;
+        self->_webView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
+
+        [self->_webView loadRequest:request];
+        [self.view addSubview:self->_webView];
+
+        self->_activityIndicatorView = [[UIActivityIndicatorView alloc] init];
+        self->_activityIndicatorView.center = self.view.center;
+        self->_activityIndicatorView.color = [NCAppBranding brandColor];
+        [self->_activityIndicatorView startAnimating];
+        [self.view addSubview:self->_activityIndicatorView];
+    }];
+}
+
+- (void)viewWillAppear:(BOOL)animated
+{
+    [super viewWillAppear:animated];
+
+    UIColor *themeColor = [NCAppBranding themeColor];
+    [self.view setBackgroundColor:themeColor];
+
+    [self.navigationController.navigationBar setTitleTextAttributes:
+     @{NSForegroundColorAttributeName:[NCAppBranding themeTextColor]}];
+    self.navigationController.navigationBar.tintColor = [NCAppBranding themeTextColor];
+    self.navigationController.navigationBar.translucent = YES;
+    self.navigationController.navigationBar.barTintColor = [NCAppBranding themeColor];
+
+    UIBarButtonItem *cancelButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemCancel
+                                                                                  target:self action:@selector(cancelButtonPressed)];
+    cancelButton.accessibilityHint = NSLocalizedString(@"Double tap to dismiss authentication dialog", nil);
+    self.navigationController.navigationBar.topItem.leftBarButtonItem = cancelButton;
+
+    UINavigationBarAppearance *appearance = [[UINavigationBarAppearance alloc] init];
+    [appearance configureWithOpaqueBackground];
+    appearance.backgroundColor = themeColor;
+    appearance.titleTextAttributes = @{NSForegroundColorAttributeName:[NCAppBranding themeTextColor]};
+    self.navigationItem.standardAppearance = appearance;
+    self.navigationItem.compactAppearance = appearance;
+    self.navigationItem.scrollEdgeAppearance = appearance;
+}
+
+- (void)cancelButtonPressed
+{
+    [self dismissViewControllerAnimated:YES completion:nil];
+}
+
+- (void)didReceiveMemoryWarning
+{
+    [super didReceiveMemoryWarning];
+    // Dispose of any resources that can be recreated.
+}
+
+- (UIStatusBarStyle)preferredStatusBarStyle
+{
+    return [NCAppBranding statusBarStyleForBrandColor];
+}
+
+- (BOOL)shouldAutorotate
+{
+    return YES;
+}
+
+- (UIInterfaceOrientationMask)supportedInterfaceOrientations
+{
+    return UIInterfaceOrientationMaskPortrait;
+}
+
+#pragma mark - WKWebView Navigation Delegate
+
+- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler
+{
+    NSURL *url = navigationAction.request.URL;
+    NSArray *components = [url.absoluteString componentsSeparatedByString:@"&"];
+    NSString *ncScheme = @"nc";
+    
+    if ([url.scheme isEqualToString:ncScheme]) {
+        NSString *user = nil;
+        NSString *token = nil;
+        NSString *userPrefix = @"user:";
+        NSString *passPrefix = @"password:";
+        
+        for (NSString *component in components)
+        {
+            if ([component hasPrefix:userPrefix])
+                user = [[[component substringFromIndex:[userPrefix length]] stringByReplacingOccurrencesOfString:@"+" withString:@" "] stringByRemovingPercentEncoding];
+            if ([component hasPrefix:passPrefix])
+                token = [[[component substringFromIndex:[passPrefix length]] stringByReplacingOccurrencesOfString:@"+" withString:@" "] stringByRemovingPercentEncoding];
+        }
+        
+        [[NCSettingsController sharedInstance] addNewAccountForUser:user withToken:token inServer:_serverUrl];
+        
+        [self.delegate authenticationViewControllerDidFinish:self];
+        
+        decisionHandler(WKNavigationActionPolicyCancel);
+        return;
+    }
+
+    if (navigationAction.targetFrame == nil) {
+        [NCUtils openLinkInBrowserWithLink:url.absoluteString];
+        decisionHandler(WKNavigationActionPolicyCancel);
+        return;
+    }
+
+    decisionHandler(WKNavigationActionPolicyAllow);
+}
+
+- (void)webView:(WKWebView *)webView didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
+{
+    if ([[CCCertificate sharedManager] checkTrustedChallenge:challenge]) {
+        completionHandler(NSURLSessionAuthChallengeUseCredential, [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]);
+    } else {
+        completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
+    }
+}
+
+- (void)webView:(WKWebView *)webView didCommitNavigation:(WKNavigation *)navigation
+{
+    // Disable user interaction to prevent any unwanted zooming while the navigation is ongoing
+    [self.webView setUserInteractionEnabled:NO];
+    [_activityIndicatorView stopAnimating];
+    [_activityIndicatorView removeFromSuperview];
+}
+
+- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {
+    [self.webView setUserInteractionEnabled:YES];
+}
+
+
+@end

+ 21 - 0
NextcloudTalk/AutoCompletionTableViewCell.h

@@ -0,0 +1,21 @@
+/**
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#import <UIKit/UIKit.h>
+
+static CGFloat kAutoCompletionCellHeight        = 50.0;
+static NSString *AutoCompletionCellIdentifier   = @"AutoCompletionCellIdentifier";
+
+@class AvatarButton;
+
+@interface AutoCompletionTableViewCell : UITableViewCell
+
+@property (nonatomic, strong) UILabel *titleLabel;
+@property (nonatomic, strong) AvatarButton *avatarButton;
+@property (nonatomic, strong) UIImageView *userStatusImageView;
+
+- (void)setUserStatus:(NSString *)userStatus;
+
+@end

+ 122 - 0
NextcloudTalk/AutoCompletionTableViewCell.m

@@ -0,0 +1,122 @@
+/**
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#import "AutoCompletionTableViewCell.h"
+#import "ChatTableViewCell.h"
+
+#import "SLKUIConstants.h"
+
+#import "NextcloudTalk-Swift.h"
+
+#import "NCAPIController.h"
+#import "NCAppBranding.h"
+#import "NCDatabaseManager.h"
+
+@implementation AutoCompletionTableViewCell
+
+- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
+{
+    self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
+    if (self) {
+        self.backgroundColor = [NCAppBranding backgroundColor];
+        [self configureSubviews];
+    }
+    return self;
+}
+
+- (void)configureSubviews
+{
+    _avatarButton = [[AvatarButton alloc] initWithFrame:CGRectMake(0, 0, kChatCellAvatarHeight, kChatCellAvatarHeight)];
+    _avatarButton.translatesAutoresizingMaskIntoConstraints = NO;
+    _avatarButton.backgroundColor = [NCAppBranding placeholderColor];
+    _avatarButton.layer.cornerRadius = kChatCellAvatarHeight/2.0;
+    _avatarButton.layer.masksToBounds = YES;
+    _avatarButton.showsMenuAsPrimaryAction = YES;
+    _avatarButton.imageView.contentMode = UIViewContentModeScaleToFill;
+
+    [self.contentView addSubview:_avatarButton];
+
+    _userStatusImageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 12, 12)];
+    _userStatusImageView.translatesAutoresizingMaskIntoConstraints = NO;
+    _userStatusImageView.userInteractionEnabled = NO;
+    [self.contentView addSubview:_userStatusImageView];
+    
+    [self.contentView addSubview:self.titleLabel];
+
+    NSDictionary *views = @{
+        @"avatarButton": self.avatarButton,
+        @"userStatusImageView": self.userStatusImageView,
+        @"titleLabel": self.titleLabel
+    };
+
+    NSDictionary *metrics = @{
+        @"avatarSize": @(kChatCellAvatarHeight),
+        @"right": @10
+    };
+
+    [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-right-[avatarButton(avatarSize)]-right-[titleLabel]-right-|" options:0 metrics:metrics views:views]];
+    [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[titleLabel]|" options:0 metrics:metrics views:views]];
+    [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-32-[userStatusImageView(12)]-(>=0)-|" options:0 metrics:metrics views:views]];
+    [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-32-[userStatusImageView(12)]-(>=0)-|" options:0 metrics:metrics views:views]];
+    self.backgroundColor = [UIColor secondarySystemBackgroundColor];
+    self.titleLabel.textColor = [UIColor labelColor];
+
+    [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-right-[avatarButton(avatarSize)]-(>=0)-|" options:0 metrics:metrics views:views]];
+}
+
+- (void)prepareForReuse
+{
+    [super prepareForReuse];
+
+    self.titleLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleBody];
+    self.titleLabel.text = @"";
+
+    [self.avatarButton cancelCurrentRequest];
+    [self.avatarButton setImage:nil forState:UIControlStateNormal];
+    
+    self.userStatusImageView.image = nil;
+    self.userStatusImageView.backgroundColor = [UIColor clearColor];
+}
+
+#pragma mark - Getters
+
+- (UILabel *)titleLabel
+{
+    if (!_titleLabel) {
+        _titleLabel = [UILabel new];
+        _titleLabel.translatesAutoresizingMaskIntoConstraints = NO;
+        _titleLabel.backgroundColor = [UIColor clearColor];
+        _titleLabel.userInteractionEnabled = NO;
+        _titleLabel.numberOfLines = 1;
+        _titleLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleBody];
+        _titleLabel.textColor = [UIColor secondaryLabelColor];
+    }
+    return _titleLabel;
+}
+
+- (void)setUserStatus:(NSString *)userStatus
+{
+    UIImage *statusImage = nil;
+    if ([userStatus isEqualToString:@"online"]) {
+        statusImage = [UIImage imageNamed:@"user-status-online-10"];
+    } else if ([userStatus isEqualToString:@"away"]) {
+        statusImage = [UIImage imageNamed:@"user-status-away-10"];
+    } else if ([userStatus isEqualToString:@"dnd"]) {
+        statusImage = [UIImage imageNamed:@"user-status-dnd-10"];
+    }
+    
+    if (statusImage) {
+        [_userStatusImageView setImage:statusImage];
+        _userStatusImageView.contentMode = UIViewContentModeCenter;
+        _userStatusImageView.layer.cornerRadius = 6;
+        _userStatusImageView.clipsToBounds = YES;
+
+        // When a background color is set directly to the cell it seems that there is no background configuration.
+        // In this class, even when no background color is set, the background configuration is nil.
+        _userStatusImageView.backgroundColor = (self.backgroundColor) ? self.backgroundColor : [[self backgroundConfiguration] backgroundColor];
+    }
+}
+
+@end

+ 22 - 0
NextcloudTalk/AvatarBackgroundImageView.h

@@ -0,0 +1,22 @@
+/**
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#import <UIKit/UIKit.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface GradientView : UIView
+
+@property (nonatomic, strong, readonly) CAGradientLayer *layer;
+
+@end
+
+@interface AvatarBackgroundImageView : UIImageView
+
+@property (nonatomic, strong)  GradientView *gradientView;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 75 - 0
NextcloudTalk/AvatarBackgroundImageView.m

@@ -0,0 +1,75 @@
+/**
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#import "AvatarBackgroundImageView.h"
+
+@implementation GradientView
+
+@dynamic layer;
+
++ (Class)layerClass {
+    return [CAGradientLayer class];
+}
+
+@end
+
+@implementation AvatarBackgroundImageView
+
+- (instancetype)init
+{
+    self = [super init];
+    if (self) {
+        [self initGradientLayer];
+    }
+    
+    return self;
+}
+
+- (instancetype)initWithFrame:(CGRect)frame
+{
+    self = [super initWithFrame:frame];
+    if (self) {
+        [self initGradientLayer];
+    }
+    
+    return self;
+}
+
+- (instancetype)initWithImage:(UIImage *)image
+{
+    self = [super initWithImage:image];
+    if (self) {
+        [self initGradientLayer];
+    }
+    
+    return self;
+}
+
+- (instancetype)initWithCoder:(NSCoder *)aDecoder
+{
+    self = [super initWithCoder:aDecoder];
+    if (self) {
+        [self initGradientLayer];
+    }
+    
+    return self;
+}
+
+- (void)initGradientLayer
+{
+    _gradientView = [[GradientView alloc] initWithFrame:self.bounds];
+    _gradientView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
+    _gradientView.layer.colors = @[(id)[[UIColor colorWithWhite:0 alpha:0.6] CGColor], (id)[[UIColor colorWithWhite:0 alpha:0.6] CGColor]];
+    _gradientView.layer.locations = @[@0.0, @1.0];
+    [self addSubview:_gradientView];
+}
+
+- (void)layoutSubviews
+{
+    [super layoutSubviews];
+//    _gradientLayer.frame = self.bounds;
+}
+
+@end

+ 85 - 0
NextcloudTalk/AvatarButton.swift

@@ -0,0 +1,85 @@
+//
+// SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+
+import UIKit
+import SDWebImage
+
+@objcMembers class AvatarButton: UIButton {
+
+    private var currentRequest: SDWebImageCombinedOperation?
+
+    public func cancelCurrentRequest() {
+        self.currentRequest?.cancel()
+    }
+
+    // MARK: - Init
+
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        self.commonInit()
+    }
+
+    required init?(coder: NSCoder) {
+        super.init(coder: coder)
+        self.commonInit()
+    }
+
+    private func commonInit() {
+        self.layer.masksToBounds = true
+        self.imageView?.contentMode = .scaleToFill
+        self.imageView?.frame = self.frame
+        self.contentVerticalAlignment = .fill
+        self.contentHorizontalAlignment = .fill
+        self.backgroundColor = .systemGray3
+    }
+
+    override func layoutSubviews() {
+        super.layoutSubviews()
+        self.layer.cornerRadius = self.frame.width / 2.0
+    }
+
+    // MARK: - Conversation avatars
+
+    public func setAvatar(for room: NCRoom) {
+        self.cancelCurrentRequest()
+
+        self.currentRequest = AvatarManager.shared.getAvatar(for: room, with: self.traitCollection.userInterfaceStyle) { image in
+            guard let image = image else {
+                return
+            }
+
+            self.setImage(image, for: .normal)
+            self.backgroundColor = .clear
+        }
+    }
+
+    public func setGroupAvatar() {
+        if let image = AvatarManager.shared.getGroupAvatar(with: self.traitCollection.userInterfaceStyle) {
+            self.setImage(image, for: .normal)
+        }
+    }
+
+    // MARK: - User avatars
+
+    public func setActorAvatar(forMessage message: NCChatMessage) {
+        self.setActorAvatar(forId: message.actorId, withType: message.actorType, withDisplayName: message.actorDisplayName, withRoomToken: message.token)
+    }
+
+    public func setActorAvatar(forId actorId: String?, withType actorType: String?, withDisplayName actorDisplayName: String?, withRoomToken roomToken: String?) {
+        self.setActorAvatar(forId: actorId, withType: actorType, withDisplayName: actorDisplayName, withRoomToken: roomToken, using: nil)
+    }
+
+    public func setActorAvatar(forId actorId: String?, withType actorType: String?, withDisplayName actorDisplayName: String?, withRoomToken roomToken: String?, using account: TalkAccount?) {
+        self.cancelCurrentRequest()
+
+        self.currentRequest = AvatarManager.shared.getActorAvatar(forId: actorId, withType: actorType, withDisplayName: actorDisplayName, withRoomToken: roomToken, withStyle: self.traitCollection.userInterfaceStyle, usingAccount: account) { image in
+            guard let image = image else {
+                return
+            }
+
+            self.setImage(image, for: .normal)
+        }
+    }
+}

+ 77 - 0
NextcloudTalk/AvatarEditView.swift

@@ -0,0 +1,77 @@
+//
+// SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+
+import Foundation
+
+@objc protocol AvatarEditViewDelegate {
+    @objc func avatarEditViewPresentCamera(_ controller: AvatarEditView?)
+    @objc func avatarEditViewPresentPhotoLibrary(_ controller: AvatarEditView?)
+    @objc optional func avatarEditViewPresentEmojiAvatarPicker(_ controller: AvatarEditView?)
+    @objc func avatarEditViewRemoveAvatar(_ controller: AvatarEditView?)
+}
+
+@objcMembers class AvatarEditView: UIView, UIImagePickerControllerDelegate, UINavigationControllerDelegate, TOCropViewControllerDelegate {
+
+    public weak var delegate: AvatarEditViewDelegate?
+
+    @IBOutlet var contentView: UIView!
+    @IBOutlet weak var avatarImageView: AvatarImageView!
+    @IBOutlet weak var nameLabel: UILabel!
+    @IBOutlet weak var editView: UIView!
+    @IBOutlet weak var scopeButton: UIButton!
+    @IBOutlet weak var cameraButton: UIButton!
+    @IBOutlet weak var photoLibraryButton: UIButton!
+    @IBOutlet weak var emojiButton: UIButton!
+    @IBOutlet weak var trashButton: UIButton!
+
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        commonInit()
+    }
+
+    required init?(coder aDecoder: NSCoder) {
+        super.init(coder: aDecoder)
+        commonInit()
+    }
+
+    func commonInit() {
+        Bundle.main.loadNibNamed("AvatarEditView", owner: self, options: nil)
+
+        contentView.frame = self.bounds
+        contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
+
+        self.avatarImageView.layer.masksToBounds = true
+
+        self.addSubview(contentView)
+    }
+
+    override func layoutSubviews() {
+        self.avatarImageView.layer.cornerRadius = self.avatarImageView.frame.size.height / 2
+    }
+
+    func changeButtonState(to state: Bool) {
+        self.scopeButton.isEnabled = state
+        self.cameraButton.isEnabled = state
+        self.photoLibraryButton.isEnabled = state
+        self.emojiButton.isEnabled = state
+        self.trashButton.isEnabled = state
+    }
+
+    @IBAction func cameraButtonTouchUpInside(_ sender: Any) {
+        self.delegate?.avatarEditViewPresentCamera(self)
+    }
+
+    @IBAction func photoLibraryTouchUpInside(_ sender: Any) {
+        self.delegate?.avatarEditViewPresentPhotoLibrary(self)
+    }
+
+    @IBAction func trashTouchUpInside(_ sender: Any) {
+        self.delegate?.avatarEditViewRemoveAvatar(self)
+    }
+
+    @IBAction func emojiTouchUpInside(_ sender: Any) {
+        self.delegate?.avatarEditViewPresentEmojiAvatarPicker?(self)
+    }
+}

+ 153 - 0
NextcloudTalk/AvatarEditView.xib

@@ -0,0 +1,153 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="21701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
+    <device id="retina6_1" orientation="portrait" appearance="light"/>
+    <dependencies>
+        <deployment identifier="iOS"/>
+        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21678"/>
+        <capability name="Safe area layout guides" minToolsVersion="9.0"/>
+        <capability name="System colors in document resources" minToolsVersion="11.0"/>
+        <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
+    </dependencies>
+    <objects>
+        <placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="AvatarEditView" customModule="NextcloudTalk" customModuleProvider="target">
+            <connections>
+                <outlet property="avatarImageView" destination="P5c-gH-ijQ" id="jla-8k-Ttl"/>
+                <outlet property="cameraButton" destination="DOh-QQ-Zuc" id="541-2C-APt"/>
+                <outlet property="contentView" destination="iN0-l3-epB" id="9z6-a7-d0W"/>
+                <outlet property="editView" destination="f8R-m5-KRj" id="A73-fq-npa"/>
+                <outlet property="emojiButton" destination="hsf-S7-TSS" id="dnb-q9-r7e"/>
+                <outlet property="nameLabel" destination="cHc-KT-D8F" id="9TU-eh-vjt"/>
+                <outlet property="photoLibraryButton" destination="Uio-mq-k5m" id="3L3-8f-paz"/>
+                <outlet property="scopeButton" destination="qSd-WE-0Jc" id="9gD-KD-zgi"/>
+                <outlet property="trashButton" destination="qDP-Sw-Ndu" id="8kG-IB-Fqi"/>
+            </connections>
+        </placeholder>
+        <placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
+        <view contentMode="scaleToFill" id="iN0-l3-epB">
+            <rect key="frame" x="0.0" y="0.0" width="459" height="267"/>
+            <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+            <subviews>
+                <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="center" translatesAutoresizingMaskIntoConstraints="NO" id="iFx-jE-7z7">
+                    <rect key="frame" x="0.0" y="0.0" width="459" height="267"/>
+                    <subviews>
+                        <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="toF-wm-Jvh">
+                            <rect key="frame" x="0.0" y="0.0" width="459" height="110"/>
+                            <subviews>
+                                <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="1000" translatesAutoresizingMaskIntoConstraints="NO" id="P5c-gH-ijQ" customClass="AvatarImageView" customModule="NextcloudTalk" customModuleProvider="target">
+                                    <rect key="frame" x="189.5" y="20" width="80" height="80"/>
+                                    <constraints>
+                                        <constraint firstAttribute="width" constant="80" id="60X-el-yu8"/>
+                                        <constraint firstAttribute="height" constant="80" id="M7l-w4-bd2"/>
+                                    </constraints>
+                                </imageView>
+                                <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="qSd-WE-0Jc">
+                                    <rect key="frame" x="269.5" y="20" width="30" height="30"/>
+                                </button>
+                            </subviews>
+                            <constraints>
+                                <constraint firstItem="P5c-gH-ijQ" firstAttribute="centerX" secondItem="toF-wm-Jvh" secondAttribute="centerX" id="Epd-Lt-BDN"/>
+                                <constraint firstItem="qSd-WE-0Jc" firstAttribute="leading" secondItem="P5c-gH-ijQ" secondAttribute="trailing" id="ZRv-Cn-hW5"/>
+                                <constraint firstAttribute="bottom" secondItem="P5c-gH-ijQ" secondAttribute="bottom" constant="10" id="fb6-Wk-zq7"/>
+                                <constraint firstItem="P5c-gH-ijQ" firstAttribute="top" secondItem="toF-wm-Jvh" secondAttribute="top" constant="20" id="iyo-n2-Cok"/>
+                                <constraint firstItem="qSd-WE-0Jc" firstAttribute="top" secondItem="toF-wm-Jvh" secondAttribute="top" constant="20" id="mO4-y1-aN0"/>
+                            </constraints>
+                        </view>
+                        <view opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="f8R-m5-KRj" userLabel="EditView">
+                            <rect key="frame" x="0.0" y="110" width="459" height="104.5"/>
+                            <subviews>
+                                <stackView opaque="NO" contentMode="scaleToFill" distribution="fillEqually" alignment="top" spacing="16" translatesAutoresizingMaskIntoConstraints="NO" id="lFq-aG-VCf">
+                                    <rect key="frame" x="117.5" y="0.0" width="224" height="94.5"/>
+                                    <subviews>
+                                        <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="top" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="DOh-QQ-Zuc" userLabel="Camera Button">
+                                            <rect key="frame" x="0.0" y="0.0" width="44" height="44"/>
+                                            <constraints>
+                                                <constraint firstAttribute="width" constant="44" id="0a5-qp-s0h"/>
+                                                <constraint firstAttribute="height" constant="44" id="IRa-Vy-2OR"/>
+                                            </constraints>
+                                            <inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/>
+                                            <state key="normal" image="camera" catalog="system"/>
+                                            <connections>
+                                                <action selector="cameraButtonTouchUpInside:" destination="-1" eventType="touchUpInside" id="D5Q-GZ-ufL"/>
+                                            </connections>
+                                        </button>
+                                        <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="top" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Uio-mq-k5m" userLabel="Photo Library Button">
+                                            <rect key="frame" x="60" y="0.0" width="44" height="44"/>
+                                            <constraints>
+                                                <constraint firstAttribute="height" constant="44" id="5io-pC-8Bl"/>
+                                                <constraint firstAttribute="width" constant="44" id="krh-i6-eDy"/>
+                                            </constraints>
+                                            <inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/>
+                                            <state key="normal" image="photo.on.rectangle.angled" catalog="system"/>
+                                            <connections>
+                                                <action selector="photoLibraryTouchUpInside:" destination="-1" eventType="touchUpInside" id="HDu-vL-hSU"/>
+                                            </connections>
+                                        </button>
+                                        <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="top" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="hsf-S7-TSS" userLabel="Emoji Button">
+                                            <rect key="frame" x="120" y="0.0" width="44" height="44"/>
+                                            <constraints>
+                                                <constraint firstAttribute="height" constant="44" id="Yv2-J3-02f"/>
+                                                <constraint firstAttribute="width" constant="44" id="eHB-6A-KtS"/>
+                                            </constraints>
+                                            <inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/>
+                                            <state key="normal" image="face.smiling" catalog="system"/>
+                                            <connections>
+                                                <action selector="emojiTouchUpInside:" destination="-1" eventType="touchUpInside" id="62u-tY-5Ve"/>
+                                            </connections>
+                                        </button>
+                                        <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="top" buttonType="system" lineBreakMode="middleTruncation" role="destructive" translatesAutoresizingMaskIntoConstraints="NO" id="qDP-Sw-Ndu" userLabel="Trash Button">
+                                            <rect key="frame" x="180" y="0.0" width="44" height="44"/>
+                                            <constraints>
+                                                <constraint firstAttribute="height" constant="44" id="NJ7-jc-j3t"/>
+                                                <constraint firstAttribute="width" constant="44" id="but-G4-cvK"/>
+                                            </constraints>
+                                            <color key="tintColor" systemColor="systemRedColor"/>
+                                            <inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/>
+                                            <state key="normal" image="trash" catalog="system"/>
+                                            <connections>
+                                                <action selector="trashTouchUpInside:" destination="-1" eventType="touchUpInside" id="a7B-2x-XoO"/>
+                                            </connections>
+                                        </button>
+                                    </subviews>
+                                </stackView>
+                            </subviews>
+                            <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+                            <constraints>
+                                <constraint firstAttribute="bottom" secondItem="lFq-aG-VCf" secondAttribute="bottom" constant="10" id="80w-pE-AI3"/>
+                                <constraint firstItem="lFq-aG-VCf" firstAttribute="top" secondItem="f8R-m5-KRj" secondAttribute="top" id="pzz-XK-rKE"/>
+                                <constraint firstItem="lFq-aG-VCf" firstAttribute="centerX" secondItem="f8R-m5-KRj" secondAttribute="centerX" id="vKW-ZO-Hmc"/>
+                            </constraints>
+                        </view>
+                        <label opaque="NO" userInteractionEnabled="NO" contentMode="left" text="Label" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.59999999999999998" translatesAutoresizingMaskIntoConstraints="NO" id="cHc-KT-D8F" userLabel="NameLabel">
+                            <rect key="frame" x="199" y="214.5" width="61.5" height="52.5"/>
+                            <fontDescription key="fontDescription" type="system" pointSize="26"/>
+                            <nil key="textColor"/>
+                            <nil key="highlightedColor"/>
+                        </label>
+                    </subviews>
+                    <constraints>
+                        <constraint firstAttribute="trailing" secondItem="f8R-m5-KRj" secondAttribute="trailing" id="dlH-x0-EOq"/>
+                        <constraint firstItem="f8R-m5-KRj" firstAttribute="leading" secondItem="iFx-jE-7z7" secondAttribute="leading" id="qAU-tY-EtW"/>
+                    </constraints>
+                </stackView>
+            </subviews>
+            <viewLayoutGuide key="safeArea" id="vUN-kp-3ea"/>
+            <constraints>
+                <constraint firstItem="vUN-kp-3ea" firstAttribute="trailing" secondItem="iFx-jE-7z7" secondAttribute="trailing" id="5OK-9r-qSb"/>
+                <constraint firstAttribute="bottom" secondItem="iFx-jE-7z7" secondAttribute="bottom" id="83Z-1F-oe3"/>
+                <constraint firstItem="iFx-jE-7z7" firstAttribute="leading" secondItem="vUN-kp-3ea" secondAttribute="leading" id="Ign-KS-12J"/>
+                <constraint firstItem="iFx-jE-7z7" firstAttribute="top" secondItem="iN0-l3-epB" secondAttribute="top" id="ZWG-zG-m66"/>
+            </constraints>
+            <freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
+            <point key="canvasLocation" x="-73.188405797101453" y="-120.20089285714285"/>
+        </view>
+    </objects>
+    <resources>
+        <image name="camera" catalog="system" width="128" height="93"/>
+        <image name="face.smiling" catalog="system" width="128" height="123"/>
+        <image name="photo.on.rectangle.angled" catalog="system" width="128" height="98"/>
+        <image name="trash" catalog="system" width="117" height="128"/>
+        <systemColor name="systemRedColor">
+            <color red="1" green="0.23137254901960785" blue="0.18823529411764706" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
+        </systemColor>
+    </resources>
+</document>

+ 83 - 0
NextcloudTalk/AvatarImageView.swift

@@ -0,0 +1,83 @@
+//
+// SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+
+import UIKit
+import SDWebImage
+
+@objcMembers class AvatarImageView: UIImageView {
+
+    private var currentRequest: SDWebImageCombinedOperation?
+
+    public func cancelCurrentRequest() {
+        self.currentRequest?.cancel()
+    }
+
+    // MARK: - Init
+
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        self.commonInit()
+    }
+
+    required init?(coder: NSCoder) {
+        super.init(coder: coder)
+        self.commonInit()
+    }
+
+    private func commonInit() {
+        self.contentMode = .scaleToFill
+    }
+
+    // MARK: - Conversation avatars
+
+    public func setAvatar(for room: NCRoom) {
+        self.cancelCurrentRequest()
+
+        self.currentRequest = AvatarManager.shared.getAvatar(for: room, with: self.traitCollection.userInterfaceStyle) { image in
+            guard let image = image else {
+                return
+            }
+
+            self.image = image
+            self.contentMode = .scaleToFill
+            self.backgroundColor = .clear
+        }
+    }
+
+    public func setGroupAvatar() {
+        if let image = AvatarManager.shared.getGroupAvatar(with: self.traitCollection.userInterfaceStyle) {
+            self.image = image
+        }
+    }
+
+    public func setMailAvatar() {
+        if let image = AvatarManager.shared.getMailAvatar(with: self.traitCollection.userInterfaceStyle) {
+            self.image = image
+        }
+    }
+
+    // MARK: - User avatars
+
+    public func setActorAvatar(forMessage message: NCChatMessage) {
+        self.setActorAvatar(forId: message.actorId, withType: message.actorType, withDisplayName: message.actorDisplayName, withRoomToken: message.token)
+    }
+
+    public func setActorAvatar(forId actorId: String?, withType actorType: String?, withDisplayName actorDisplayName: String?, withRoomToken roomToken: String?) {
+        self.setActorAvatar(forId: actorId, withType: actorType, withDisplayName: actorDisplayName, withRoomToken: roomToken, using: nil)
+    }
+
+    public func setActorAvatar(forId actorId: String?, withType actorType: String?, withDisplayName actorDisplayName: String?, withRoomToken roomToken: String?, using account: TalkAccount?) {
+        self.cancelCurrentRequest()
+
+        self.currentRequest = AvatarManager.shared.getActorAvatar(forId: actorId, withType: actorType, withDisplayName: actorDisplayName, withRoomToken: roomToken, withStyle: self.traitCollection.userInterfaceStyle, usingAccount: account) { image in
+            guard let image = image else {
+                return
+            }
+
+            self.image = image
+            self.contentMode = .scaleToFill
+        }
+    }
+}

+ 177 - 0
NextcloudTalk/AvatarManager.swift

@@ -0,0 +1,177 @@
+//
+// SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+
+import UIKit
+import SDWebImage
+
+@objcMembers class AvatarManager: NSObject {
+
+    public static let shared = AvatarManager()
+
+    private let avatarDefaultSize = CGRect(x: 0, y: 0, width: 32, height: 32)
+
+    // MARK: - Conversation avatars
+
+    public func getAvatar(for room: NCRoom, with style: UIUserInterfaceStyle, completionBlock: @escaping (_ image: UIImage?) -> Void) -> SDWebImageCombinedOperation? {
+        if room.accountId != nil, NCDatabaseManager.sharedInstance().serverHasTalkCapability(kCapabilityConversationAvatars, forAccountId: room.accountId) {
+            // Server supports conversation avatars -> try to get the avatar using this API
+
+            return NCAPIController.sharedInstance().getAvatarFor(room, with: style) { image, _ in
+                completionBlock(image)
+            }
+        } else {
+            // Server does not support conversation avatars -> use the legacy way to obtain an avatar
+            return self.getFallbackAvatar(for: room, with: style, completionBlock: completionBlock)
+        }
+    }
+
+    public func getGroupAvatar(with style: UIUserInterfaceStyle) -> UIImage? {
+        let traitCollection = UITraitCollection(userInterfaceStyle: style)
+        return UIImage(named: "group-avatar", in: nil, compatibleWith: traitCollection)
+    }
+
+    public func getMailAvatar(with style: UIUserInterfaceStyle) -> UIImage? {
+        let traitCollection = UITraitCollection(userInterfaceStyle: style)
+        return UIImage(named: "mail-avatar", in: nil, compatibleWith: traitCollection)
+    }
+
+    private func getFallbackAvatar(for room: NCRoom,
+                                   with style: UIUserInterfaceStyle,
+                                   completionBlock: @escaping (_ image: UIImage?) -> Void) -> SDWebImageCombinedOperation? {
+
+        let traitCollection = UITraitCollection(userInterfaceStyle: style)
+
+        if room.objectType == NCRoomObjectTypeFile {
+            completionBlock(UIImage(named: "file-avatar", in: nil, compatibleWith: traitCollection))
+        } else if room.objectType == NCRoomObjectTypeSharePassword {
+            completionBlock(UIImage(named: "password-avatar", in: nil, compatibleWith: traitCollection))
+        } else {
+            switch room.type {
+            case .oneToOne:
+                let account = NCDatabaseManager.sharedInstance().talkAccount(forAccountId: room.accountId)
+                return self.getUserAvatar(forId: room.name, withStyle: style, usingAccount: account, completionBlock: completionBlock)
+            case .formerOneToOne:
+                completionBlock(UIImage(named: "user-avatar", in: nil, compatibleWith: traitCollection))
+            case .public:
+                completionBlock(UIImage(named: "public-avatar", in: nil, compatibleWith: traitCollection))
+            case .group:
+                completionBlock(UIImage(named: "group-avatar", in: nil, compatibleWith: traitCollection))
+            case .changelog:
+                completionBlock(UIImage(named: "changelog-avatar", in: nil, compatibleWith: traitCollection))
+            default:
+                completionBlock(nil)
+            }
+        }
+
+        return nil
+    }
+
+    // MARK: - Actor avatars
+
+    // swiftlint:disable:next function_parameter_count
+    public func getActorAvatar(forId actorId: String?, withType actorType: String?, withDisplayName actorDisplayName: String?, withRoomToken roomToken: String?, withStyle style: UIUserInterfaceStyle, usingAccount account: TalkAccount?, completionBlock: @escaping (_ image: UIImage?) -> Void) -> SDWebImageCombinedOperation? {
+        if let actorId {
+            if actorType == "bots" {
+                return getBotsAvatar(forId: actorId, withStyle: style, completionBlock: completionBlock)
+            } else if actorType == "guests" {
+                return getGuestsAvatar(withDisplayName: actorDisplayName ?? "", completionBlock: completionBlock)
+            } else if actorType == "users" {
+                return getUserAvatar(forId: actorId, withStyle: style, usingAccount: account, completionBlock: completionBlock)
+            } else if actorType == "federated_users" {
+                return getFederatedUserAvatar(forId: actorId, withRoomToken: roomToken, withStyle: style, usingAccount: account, completionBlock: completionBlock)
+            } else if actorType == "deleted_users" {
+                return getDeletedUserAvatar(completionBlock: completionBlock)
+            }
+        }
+
+        var image: UIImage?
+
+        if actorType == NCAttendeeTypeEmail {
+            image = self.getMailAvatar(with: style)
+        } else if actorType == NCAttendeeTypeGroup || actorType == NCAttendeeTypeCircle {
+            image = self.getGroupAvatar(with: style)
+        } else {
+            image = NCUtils.getImage(withString: "?", withBackgroundColor: .systemGray3, withBounds: self.avatarDefaultSize, isCircular: true)
+        }
+
+        completionBlock(image)
+        return nil
+    }
+
+    private func getBotsAvatar(forId actorId: String, withStyle style: UIUserInterfaceStyle, completionBlock: @escaping (_ image: UIImage?) -> Void) -> SDWebImageCombinedOperation? {
+        if actorId == "changelog" {
+            let traitCollection = UITraitCollection(userInterfaceStyle: style)
+            completionBlock(UIImage(named: "changelog-avatar", in: nil, compatibleWith: traitCollection))
+        } else {
+            let image = NCUtils.getImage(withString: ">", withBackgroundColor: .systemGray3, withBounds: self.avatarDefaultSize, isCircular: true)
+            completionBlock(image)
+        }
+
+        return nil
+    }
+
+    private func getGuestsAvatar(withDisplayName actorDisplayName: String, completionBlock: @escaping (_ image: UIImage?) -> Void) -> SDWebImageCombinedOperation? {
+        let name = actorDisplayName.isEmpty ? "?" : actorDisplayName
+        let image = NCUtils.getImage(withString: name, withBackgroundColor: .systemGray3, withBounds: self.avatarDefaultSize, isCircular: true)
+
+        completionBlock(image)
+
+        return nil
+    }
+
+    private func getDeletedUserAvatar(completionBlock: @escaping (_ image: UIImage?) -> Void) -> SDWebImageCombinedOperation? {
+        let image = NCUtils.getImage(withString: "X", withBackgroundColor: .systemGray3, withBounds: self.avatarDefaultSize, isCircular: true)
+
+        completionBlock(image)
+
+        return nil
+    }
+
+    private func getUserAvatar(forId actorId: String, withStyle style: UIUserInterfaceStyle, usingAccount account: TalkAccount?, completionBlock: @escaping (_ image: UIImage?) -> Void) -> SDWebImageCombinedOperation? {
+        let account = account ?? NCDatabaseManager.sharedInstance().activeAccount()
+
+        return NCAPIController.sharedInstance().getUserAvatar(forUser: actorId, using: account, with: style) { image, _ in
+            if image != nil {
+                completionBlock(image)
+            } else {
+                NSLog("Unable to get avatar for user %@", actorId)
+
+                let traitCollection = UITraitCollection(userInterfaceStyle: style)
+                completionBlock(UIImage(named: "user-avatar", in: nil, compatibleWith: traitCollection))
+            }
+        }
+    }
+
+    private func getFederatedUserAvatar(forId actorId: String, withRoomToken roomToken: String?, withStyle style: UIUserInterfaceStyle, usingAccount account: TalkAccount?, completionBlock: @escaping (_ image: UIImage?) -> Void) -> SDWebImageCombinedOperation? {
+        let account = account ?? NCDatabaseManager.sharedInstance().activeAccount()
+
+        return NCAPIController.sharedInstance().getFederatedUserAvatar(forUser: actorId, inRoom: roomToken, using: account, with: style) { image, _ in
+            if image != nil {
+                completionBlock(image)
+            } else {
+                NSLog("Unable to get federated avatar for user %@", actorId)
+
+                let traitCollection = UITraitCollection(userInterfaceStyle: style)
+                completionBlock(UIImage(named: "user-avatar", in: nil, compatibleWith: traitCollection))
+            }
+        }
+    }
+
+    // MARK: - Utils
+
+    public func createRenderedImage(image: UIImage) -> UIImage? {
+        return self.createRenderedImage(image: image, width: 120, height: 120)
+    }
+
+    private func createRenderedImage(image: UIImage, width: Int, height: Int) -> UIImage? {
+        UIGraphicsBeginImageContextWithOptions(.init(width: width, height: height), false, 0.0)
+        image.draw(in: .init(x: 0, y: 0, width: width, height: height))
+        let newImage = UIGraphicsGetImageFromCurrentImageContext()
+        UIGraphicsEndImageContext()
+
+        return newImage
+    }
+
+}

+ 40 - 0
NextcloudTalk/BGTaskHelper.swift

@@ -0,0 +1,40 @@
+//
+// SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+
+import Foundation
+
+@objcMembers class BGTaskHelper: NSObject {
+
+#if !APP_EXTENSION
+    var backgroundTaskIdentifier: UIBackgroundTaskIdentifier = .invalid
+#endif
+
+    public class func startBackgroundTask(withName taskName: String? = nil, expirationHandler handler: ((BGTaskHelper) -> Void)? = nil) -> BGTaskHelper {
+        let taskHelper = BGTaskHelper()
+
+        let expirationhandler = {
+            if let handler = handler {
+                handler(taskHelper)
+            }
+        }
+
+#if !APP_EXTENSION
+        let backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(withName: taskName, expirationHandler: expirationhandler)
+        taskHelper.backgroundTaskIdentifier = backgroundTaskIdentifier
+#endif
+
+        return taskHelper
+    }
+
+    public func stopBackgroundTask() {
+#if !APP_EXTENSION
+        if backgroundTaskIdentifier != .invalid {
+            UIApplication.shared.endBackgroundTask(backgroundTaskIdentifier)
+            backgroundTaskIdentifier = .invalid
+        }
+#endif
+    }
+
+}

+ 33 - 0
NextcloudTalk/BannedActor.swift

@@ -0,0 +1,33 @@
+//
+// SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+
+import Foundation
+
+@objcMembers public class BannedActor: NSObject {
+
+    public var banId: Int = 0
+    public var moderatorActorType: String?
+    public var moderatorActorId: String?
+    public var moderatorDisplayName: String?
+    public var bannedType: String?
+    public var bannedId: String?
+    public var bannedDisplayName: String?
+    public var bannedTime: Int?
+    public var internalNote: String?
+
+    init(dictionary: [String: Any]) {
+        super.init()
+
+        self.banId = dictionary["id"] as? Int ?? 0
+        self.moderatorActorType = dictionary["moderatorActorType"] as? String
+        self.moderatorActorId = dictionary["moderatorActorId"] as? String
+        self.moderatorDisplayName = dictionary["moderatorDisplayName"] as? String
+        self.bannedType = dictionary["bannedActorType"] as? String
+        self.bannedId = dictionary["bannedActorId"] as? String
+        self.bannedDisplayName = dictionary["bannedDisplayName"] as? String
+        self.bannedTime = dictionary["bannedTime"] as? Int
+        self.internalNote = dictionary["internalNote"] as? String
+    }
+}

+ 89 - 0
NextcloudTalk/BannedActorCell.swift

@@ -0,0 +1,89 @@
+//
+// SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+
+protocol BannedActorCellDelegate: AnyObject {
+    func bannedActorCellUnbanActor(_ cell: BannedActorCell, bannedActor: BannedActor)
+}
+
+class BannedActorCell: UITableViewCell {
+
+    public weak var delegate: BannedActorCellDelegate?
+
+    @IBOutlet weak var titleLabel: UILabel!
+    @IBOutlet weak var detailsLabel: UILabel!
+    @IBOutlet weak var unbanButton: NCButton!
+
+    private var bannedActor: BannedActor?
+
+    override func awakeFromNib() {
+        super.awakeFromNib()
+
+        self.selectionStyle = .none
+    }
+
+    override func prepareForReuse() {
+        super.prepareForReuse()
+
+        self.titleLabel.text = ""
+        self.detailsLabel.text = ""
+        self.bannedActor = nil
+
+        self.setEnabledState()
+    }
+
+    public func setupFor(bannedActor: BannedActor) {
+        self.bannedActor = bannedActor
+
+        self.titleLabel.text = bannedActor.bannedDisplayName ?? "Unknown"
+
+        var bannedDate = ""
+
+        if let time = bannedActor.bannedTime {
+            bannedDate = NCUtils.readableDateTime(fromDate: Date(timeIntervalSince1970: TimeInterval(time)))
+        }
+
+        let bannedByLabel = NSLocalizedString("Banned by:", comment: "Date and time of ban creation")
+        let bannedDateLabel = NSLocalizedString("Date:", comment: "name of a moderator who banned a participant")
+        let bannedNoteLabel = NSLocalizedString("Note:", comment: "Internal note for moderators, usually a reason for this ban")
+
+        var details = NSMutableAttributedString()
+        let attributedNewLine = NSAttributedString(string: "\n")
+
+        details.append(bannedByLabel.withFont(.preferredFont(for: .caption1, weight: .bold)))
+        details.append(" \(bannedActor.bannedDisplayName ?? NSLocalizedString("Unknown", comment: ""))".withFont(.preferredFont(forTextStyle: .caption1)))
+        details.append(attributedNewLine)
+        details.append(bannedDateLabel.withFont(.preferredFont(for: .caption1, weight: .bold)))
+        details.append(" \(bannedDate)".withFont(.preferredFont(forTextStyle: .caption1)))
+
+        if let internalNote = bannedActor.internalNote, !internalNote.isEmpty {
+            details.append(attributedNewLine)
+            details.append(bannedNoteLabel.withFont(.preferredFont(for: .caption1, weight: .bold)))
+            details.append(" \(internalNote)".withFont(.preferredFont(forTextStyle: .caption1)))
+        }
+
+        self.detailsLabel.attributedText = details
+
+        self.unbanButton.setTitle(NSLocalizedString("Unban", comment: ""), for: .normal)
+        self.unbanButton.setButtonStyle(style: .primary)
+        self.unbanButton.setButtonAction(target: self, selector: #selector(unbanButtonPressed))
+    }
+
+    public func setDisabledState() {
+        self.contentView.isUserInteractionEnabled = false
+        self.contentView.alpha = 0.5
+    }
+
+    public func setEnabledState() {
+        self.contentView.isUserInteractionEnabled = true
+        self.contentView.alpha = 1
+    }
+
+    @objc
+    func unbanButtonPressed() {
+        if let bannedActor {
+            self.delegate?.bannedActorCellUnbanActor(self, bannedActor: bannedActor)
+        }
+    }
+}

+ 100 - 0
NextcloudTalk/BannedActorCell.xib

@@ -0,0 +1,100 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="32700.99.1234" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
+    <device id="retina6_1" orientation="portrait" appearance="dark"/>
+    <dependencies>
+        <deployment identifier="iOS"/>
+        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22685"/>
+        <capability name="Safe area layout guides" minToolsVersion="9.0"/>
+        <capability name="System colors in document resources" minToolsVersion="11.0"/>
+        <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
+    </dependencies>
+    <objects>
+        <placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
+        <placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
+        <tableViewCell contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" rowHeight="145" id="KGk-i7-Jjw" customClass="BannedActorCell" customModule="NextcloudTalk" customModuleProvider="target">
+            <rect key="frame" x="0.0" y="0.0" width="422" height="145"/>
+            <autoresizingMask key="autoresizingMask"/>
+            <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="KGk-i7-Jjw" id="H2p-sc-9uM">
+                <rect key="frame" x="0.0" y="0.0" width="422" height="145"/>
+                <autoresizingMask key="autoresizingMask"/>
+                <subviews>
+                    <view contentMode="scaleToFill" verticalCompressionResistancePriority="1000" translatesAutoresizingMaskIntoConstraints="NO" id="mNx-yG-fAe" userLabel="Label View">
+                        <rect key="frame" x="12" y="12" width="398" height="79"/>
+                        <subviews>
+                            <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="XBj-oY-kPW" userLabel="Title Label">
+                                <rect key="frame" x="0.0" y="0.0" width="398" height="20"/>
+                                <constraints>
+                                    <constraint firstAttribute="height" constant="20" id="YoD-zL-1qj"/>
+                                </constraints>
+                                <fontDescription key="fontDescription" style="UICTFontTextStyleHeadline"/>
+                                <nil key="textColor"/>
+                                <nil key="highlightedColor"/>
+                            </label>
+                            <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="1000" text="Details" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ghb-EI-AhT" userLabel="Details Label">
+                                <rect key="frame" x="0.0" y="20" width="398" height="59"/>
+                                <constraints>
+                                    <constraint firstAttribute="height" relation="greaterThanOrEqual" id="oP4-Gm-srE"/>
+                                </constraints>
+                                <fontDescription key="fontDescription" style="UICTFontTextStyleCaption1"/>
+                                <color key="textColor" systemColor="secondaryLabelColor"/>
+                                <nil key="highlightedColor"/>
+                            </label>
+                        </subviews>
+                        <constraints>
+                            <constraint firstAttribute="trailing" secondItem="ghb-EI-AhT" secondAttribute="trailing" id="1SQ-c8-RyU"/>
+                            <constraint firstItem="XBj-oY-kPW" firstAttribute="leading" secondItem="mNx-yG-fAe" secondAttribute="leading" id="2Xu-Rj-GrX"/>
+                            <constraint firstAttribute="bottom" secondItem="ghb-EI-AhT" secondAttribute="bottom" id="6Ac-cy-zKg"/>
+                            <constraint firstItem="ghb-EI-AhT" firstAttribute="leading" secondItem="mNx-yG-fAe" secondAttribute="leading" id="DWw-7B-Hae"/>
+                            <constraint firstAttribute="trailing" secondItem="XBj-oY-kPW" secondAttribute="trailing" id="d4Q-Ra-0d6"/>
+                            <constraint firstItem="XBj-oY-kPW" firstAttribute="top" secondItem="mNx-yG-fAe" secondAttribute="top" id="e9M-Gj-Xs8"/>
+                            <constraint firstItem="ghb-EI-AhT" firstAttribute="top" secondItem="XBj-oY-kPW" secondAttribute="bottom" id="n9O-tQ-0fY"/>
+                            <constraint firstAttribute="height" relation="greaterThanOrEqual" id="qo9-SF-MyP"/>
+                        </constraints>
+                    </view>
+                    <stackView opaque="NO" contentMode="scaleToFill" distribution="equalSpacing" spacing="10" translatesAutoresizingMaskIntoConstraints="NO" id="OmP-GW-Hgh">
+                        <rect key="frame" x="341" y="103" width="69" height="34"/>
+                        <subviews>
+                            <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="z0q-UE-cMb" userLabel="Unban button" customClass="NCButton" customModule="NextcloudTalk" customModuleProvider="target">
+                                <rect key="frame" x="0.0" y="0.0" width="69" height="34"/>
+                                <inset key="contentEdgeInsets" minX="12" minY="8" maxX="12" maxY="8"/>
+                                <inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/>
+                                <state key="normal" title="Unban"/>
+                            </button>
+                        </subviews>
+                        <constraints>
+                            <constraint firstAttribute="height" constant="34" id="1Ph-uc-hCu"/>
+                        </constraints>
+                    </stackView>
+                </subviews>
+                <viewLayoutGuide key="safeArea" id="ML2-GP-tWC"/>
+                <constraints>
+                    <constraint firstItem="OmP-GW-Hgh" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="ML2-GP-tWC" secondAttribute="leading" constant="12" id="1ul-2K-2y0"/>
+                    <constraint firstItem="mNx-yG-fAe" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="top" constant="12" id="RZZ-by-PX9"/>
+                    <constraint firstItem="OmP-GW-Hgh" firstAttribute="top" secondItem="mNx-yG-fAe" secondAttribute="bottom" constant="12" id="WlF-Ng-EWa"/>
+                    <constraint firstItem="mNx-yG-fAe" firstAttribute="leading" secondItem="H2p-sc-9uM" secondAttribute="leading" constant="12" id="eP9-Qz-l2e"/>
+                    <constraint firstItem="ML2-GP-tWC" firstAttribute="trailing" secondItem="OmP-GW-Hgh" secondAttribute="trailing" constant="12" id="omg-Cr-POK"/>
+                    <constraint firstAttribute="trailing" secondItem="mNx-yG-fAe" secondAttribute="trailing" constant="12" id="wTA-9x-zNY"/>
+                    <constraint firstAttribute="bottom" secondItem="OmP-GW-Hgh" secondAttribute="bottom" constant="8" id="y6Q-Ax-M0E"/>
+                </constraints>
+            </tableViewCellContentView>
+            <viewLayoutGuide key="safeArea" id="aW0-zy-SZf"/>
+            <constraints>
+                <constraint firstItem="H2p-sc-9uM" firstAttribute="bottom" secondItem="aW0-zy-SZf" secondAttribute="bottom" id="DE8-g9-bsC"/>
+                <constraint firstItem="H2p-sc-9uM" firstAttribute="trailing" secondItem="aW0-zy-SZf" secondAttribute="trailing" id="F2e-bf-8Q4"/>
+                <constraint firstItem="H2p-sc-9uM" firstAttribute="top" secondItem="aW0-zy-SZf" secondAttribute="top" id="bo7-w2-Dl1"/>
+                <constraint firstItem="H2p-sc-9uM" firstAttribute="leading" secondItem="aW0-zy-SZf" secondAttribute="leading" id="sdi-ee-8qh"/>
+            </constraints>
+            <connections>
+                <outlet property="detailsLabel" destination="ghb-EI-AhT" id="ean-qp-TVZ"/>
+                <outlet property="titleLabel" destination="XBj-oY-kPW" id="t8J-mo-10v"/>
+                <outlet property="unbanButton" destination="z0q-UE-cMb" id="mqC-nt-cAr"/>
+            </connections>
+            <point key="canvasLocation" x="146.37681159420291" y="51.227678571428569"/>
+        </tableViewCell>
+    </objects>
+    <resources>
+        <systemColor name="secondaryLabelColor">
+            <color red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
+        </systemColor>
+    </resources>
+</document>

+ 118 - 0
NextcloudTalk/BannedActorTableViewController.swift

@@ -0,0 +1,118 @@
+//
+// SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+
+@objcMembers class BannedActorTableViewController: UITableViewController, BannedActorCellDelegate {
+
+    private let bannedActorCellIdentifier = "BannedActorCell"
+    private var bannedActors: [BannedActor] = []
+
+    var room: NCRoom
+    var backgroundView: PlaceholderView = PlaceholderView(for: .grouped)
+    var modifyingViewIndicator = UIActivityIndicatorView()
+
+    init(room: NCRoom) {
+        self.room = room
+        super.init(style: .insetGrouped)
+    }
+
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+
+    override func viewDidLoad() {
+        super.viewDidLoad()
+        self.navigationController?.navigationBar.isTranslucent = false
+        self.navigationItem.title = NSLocalizedString("Banned users and guests", comment: "")
+        self.navigationController?.navigationBar.titleTextAttributes = [NSAttributedString.Key.foregroundColor: NCAppBranding.themeTextColor()]
+        self.navigationController?.navigationBar.tintColor = NCAppBranding.themeTextColor()
+        self.navigationController?.navigationBar.barTintColor = NCAppBranding.themeColor()
+        self.tabBarController?.tabBar.tintColor = NCAppBranding.themeColor()
+        let themeColor: UIColor = NCAppBranding.themeColor()
+        let appearance = UINavigationBarAppearance()
+        appearance.configureWithOpaqueBackground()
+        appearance.backgroundColor = themeColor
+        appearance.titleTextAttributes = [.foregroundColor: NCAppBranding.themeTextColor()]
+        self.navigationItem.standardAppearance = appearance
+        self.navigationItem.compactAppearance = appearance
+        self.navigationItem.scrollEdgeAppearance = appearance
+
+        self.tableView.register(UINib(nibName: bannedActorCellIdentifier, bundle: nil), forCellReuseIdentifier: bannedActorCellIdentifier)
+        self.tableView.backgroundView = backgroundView
+
+        self.backgroundView.placeholderView.isHidden = true
+        self.backgroundView.placeholderTextView.text = NSLocalizedString("No banned users or guests", comment: "")
+        self.backgroundView.setImage(UIImage(systemName: "person.badge.minus"))
+        self.backgroundView.loadingView.startAnimating()
+
+        self.modifyingViewIndicator.color = NCAppBranding.themeTextColor()
+    }
+
+    override func viewWillAppear(_ animated: Bool) {
+        self.getData()
+    }
+
+    func getData() {
+        let activeAccount = NCDatabaseManager.sharedInstance().activeAccount()
+
+        NCAPIController.sharedInstance().listBans(for: activeAccount.accountId, in: room.token) { [weak self] bannedActors in
+            guard let self else { return }
+
+            self.bannedActors = bannedActors ?? []
+
+            self.backgroundView.loadingView.stopAnimating()
+            self.backgroundView.loadingView.isHidden = true
+            self.backgroundView.placeholderView.isHidden = !self.bannedActors.isEmpty
+
+            self.tableView.reloadData()
+            self.hideActivityIndicator()
+        }
+    }
+
+    func showActivityIndicator() {
+        self.modifyingViewIndicator.startAnimating()
+        self.navigationItem.leftBarButtonItem = UIBarButtonItem(customView: modifyingViewIndicator)
+    }
+
+    func hideActivityIndicator() {
+        self.modifyingViewIndicator.stopAnimating()
+        self.navigationItem.leftBarButtonItem = nil
+    }
+
+    // MARK: - Table view data source
+
+    override func numberOfSections(in tableView: UITableView) -> Int {
+        return 1
+    }
+
+    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+        return bannedActors.count
+    }
+
+    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
+        guard let cell = self.tableView.dequeueReusableCell(withIdentifier: bannedActorCellIdentifier, for: indexPath) as? BannedActorCell
+        else { return UITableViewCell() }
+
+        let bannedActor = self.bannedActors[indexPath.row]
+        cell.setupFor(bannedActor: bannedActor)
+        cell.delegate = self
+
+        return cell
+    }
+
+    func bannedActorCellUnbanActor(_ cell: BannedActorCell, bannedActor: BannedActor) {
+        self.showActivityIndicator()
+        cell.setDisabledState()
+
+        let activeAccount = NCDatabaseManager.sharedInstance().activeAccount()
+
+        NCAPIController.sharedInstance().unbanActor(for: activeAccount.accountId, in: self.room.token, with: bannedActor.banId) { [weak self] success in
+            if !success {
+                NotificationPresenter.shared().present(text: NSLocalizedString("Failed to unban selected entry", comment: ""), dismissAfterDelay: 5.0, includedStyle: .error)
+            }
+
+            self?.getData()
+        }
+    }
+}

+ 18 - 0
NextcloudTalk/BarButtonItemWithActivity.h

@@ -0,0 +1,18 @@
+/**
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#import <UIKit/UIKit.h>
+
+@interface BarButtonItemWithActivity : UIBarButtonItem
+
+@property (nonatomic, strong) UIButton * _Nonnull innerButton;
+@property (nonatomic, strong) UIActivityIndicatorView * _Nonnull activityIndicator;
+
+- (nonnull instancetype)initWithWidth:(CGFloat)buttonWidth withImage:(UIImage * _Nullable)buttonImage;
+- (void)showActivityIndicator;
+- (void)hideActivityIndicator;
+
+@end
+

+ 57 - 0
NextcloudTalk/BarButtonItemWithActivity.m

@@ -0,0 +1,57 @@
+/**
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#import "BarButtonItemWithActivity.h"
+
+#import "NCAppBranding.h"
+
+@interface BarButtonItemWithActivity ()
+
+@end
+
+@implementation BarButtonItemWithActivity
+
+- (instancetype)init {
+    return [super init];
+}
+
+- (instancetype)initWithWidth:(CGFloat)buttonWidth withImage:(UIImage *)buttonImage {
+    self = [self init];
+    
+    if (self) {
+        UIColor *themeTextColor = [NCAppBranding themeTextColor];
+        
+        // Use UIButton as CustomView in UIBarButtonItem to have a fixed-size item
+        self.innerButton = [[UIButton alloc] init];
+
+        [self.innerButton setImage:buttonImage forState:UIControlStateNormal];
+        self.innerButton.frame = CGRectMake(0, 0, buttonWidth, buttonWidth);
+        self.innerButton.tintColor = themeTextColor;
+        
+        // Make sure the size of UIBarButtonItem stays the same when displaying the ActivityIndicator
+        self.activityIndicator = [[UIActivityIndicatorView alloc] init];
+        self.activityIndicator.color = themeTextColor;
+        self.activityIndicator.frame = CGRectMake(0, 0, buttonWidth, buttonWidth);
+        
+        [self setCustomView:self.innerButton];
+    }
+    
+    return self;
+}
+
+- (void)showActivityIndicator
+{
+    [self.activityIndicator startAnimating];
+    [self setCustomView:self.activityIndicator];
+}
+
+- (void)hideActivityIndicator
+{
+    [self setCustomView:self.innerButton];
+    [self.activityIndicator stopAnimating];
+    
+}
+
+@end

+ 38 - 0
NextcloudTalk/Base.lproj/LaunchScreen.xib

@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="22505" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" colorMatched="YES">
+    <device id="retina4_7" orientation="portrait" appearance="light"/>
+    <dependencies>
+        <deployment identifier="iOS"/>
+        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22504"/>
+        <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
+    </dependencies>
+    <objects>
+        <placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
+        <placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
+        <view contentMode="scaleToFill" id="iN0-l3-epB">
+            <rect key="frame" x="0.0" y="0.0" width="480" height="480"/>
+            <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+            <subviews>
+                <imageView userInteractionEnabled="NO" contentMode="center" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="launchscreen" translatesAutoresizingMaskIntoConstraints="NO" id="Rrv-MP-hCW">
+                    <rect key="frame" x="8" y="8" width="464" height="464"/>
+                    <color key="tintColor" red="0.22352941176470587" green="0.70980392156862748" blue="0.29019607843137252" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
+                    <constraints>
+                        <constraint firstAttribute="width" constant="464" id="9WT-Cv-gy8"/>
+                        <constraint firstAttribute="height" constant="464" id="TGB-Bg-IvO"/>
+                    </constraints>
+                </imageView>
+            </subviews>
+            <color key="backgroundColor" red="0.22352941179999999" green="0.70980392160000005" blue="0.2901960784" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
+            <constraints>
+                <constraint firstAttribute="centerX" secondItem="Rrv-MP-hCW" secondAttribute="centerX" id="0bC-RK-gM4"/>
+                <constraint firstAttribute="centerY" secondItem="Rrv-MP-hCW" secondAttribute="centerY" id="Uob-Sg-luZ"/>
+            </constraints>
+            <nil key="simulatedStatusBarMetrics"/>
+            <freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
+            <point key="canvasLocation" x="876.79999999999995" y="409.29535232383813"/>
+        </view>
+    </objects>
+    <resources>
+        <image name="launchscreen" width="200" height="200"/>
+    </resources>
+</document>

+ 209 - 0
NextcloudTalk/Base.lproj/Main.storyboard

@@ -0,0 +1,209 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="22505" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="ecK-na-6ig">
+    <device id="ipad12_9rounded" orientation="landscape" layout="fullscreen" appearance="light"/>
+    <dependencies>
+        <deployment identifier="iOS"/>
+        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22504"/>
+        <capability name="System colors in document resources" minToolsVersion="11.0"/>
+        <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
+    </dependencies>
+    <scenes>
+        <!--RoomsTableViewController-->
+        <scene sceneID="Vbx-je-b0o">
+            <objects>
+                <tableViewController title="RoomsTableViewController" extendedLayoutIncludesOpaqueBars="YES" id="d5a-it-gRL" customClass="RoomsTableViewController" sceneMemberID="viewController">
+                    <tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" allowsSelectionDuringEditing="YES" rowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" id="pE7-Go-f5G">
+                        <rect key="frame" x="0.0" y="0.0" width="420" height="1024"/>
+                        <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+                        <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+                        <prototypes>
+                            <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" id="lNr-59-sMg">
+                                <rect key="frame" x="0.0" y="50" width="420" height="52"/>
+                                <autoresizingMask key="autoresizingMask"/>
+                                <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="lNr-59-sMg" id="WiD-wD-jK1">
+                                    <rect key="frame" x="100" y="0.0" width="320" height="52"/>
+                                    <autoresizingMask key="autoresizingMask"/>
+                                </tableViewCellContentView>
+                            </tableViewCell>
+                        </prototypes>
+                        <connections>
+                            <outlet property="dataSource" destination="d5a-it-gRL" id="Cfp-CP-VeH"/>
+                            <outlet property="delegate" destination="d5a-it-gRL" id="bDT-EF-LeB"/>
+                        </connections>
+                    </tableView>
+                    <navigationItem key="navigationItem" id="Xw3-a9-moF"/>
+                </tableViewController>
+                <placeholder placeholderIdentifier="IBFirstResponder" id="Dw9-Dz-er8" userLabel="First Responder" sceneMemberID="firstResponder"/>
+            </objects>
+            <point key="canvasLocation" x="2190" y="675"/>
+        </scene>
+        <!--Split View Placeholder View Controller-->
+        <scene sceneID="ss6-np-6pN">
+            <objects>
+                <viewController storyboardIdentifier="placeholderChatViewController" id="Mvl-Nd-ZXL" customClass="NCSplitViewPlaceholderViewController" customModule="NextcloudTalk" customModuleProvider="target" sceneMemberID="viewController">
+                    <layoutGuides>
+                        <viewControllerLayoutGuide type="top" id="gR4-1Y-UnZ"/>
+                        <viewControllerLayoutGuide type="bottom" id="PYN-Yc-Nt1"/>
+                    </layoutGuides>
+                    <view key="view" contentMode="scaleToFill" id="ufw-Yg-V9l">
+                        <rect key="frame" x="0.0" y="0.0" width="1045.5" height="1024"/>
+                        <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
+                        <subviews>
+                            <stackView opaque="NO" contentMode="scaleToFill" horizontalCompressionResistancePriority="250" verticalCompressionResistancePriority="250" axis="vertical" distribution="fillProportionally" alignment="center" spacing="10" translatesAutoresizingMaskIntoConstraints="NO" id="4je-R6-T4G">
+                                <rect key="frame" x="40" y="460.5" width="965.5" height="103.5"/>
+                                <subviews>
+                                    <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="center" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="app-logo-callkit" translatesAutoresizingMaskIntoConstraints="NO" id="p23-aq-R5e">
+                                        <rect key="frame" x="463" y="0.0" width="40" height="40"/>
+                                    </imageView>
+                                    <label opaque="NO" userInteractionEnabled="NO" contentMode="center" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Join a conversation or start a new one" textAlignment="center" lineBreakMode="wordWrap" numberOfLines="3" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="pLM-th-fh4">
+                                        <rect key="frame" x="306" y="50" width="354" height="24"/>
+                                        <fontDescription key="fontDescription" type="boldSystem" pointSize="20"/>
+                                        <nil key="textColor"/>
+                                        <nil key="highlightedColor"/>
+                                    </label>
+                                    <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Say hi to your friends and colleagues!" textAlignment="natural" lineBreakMode="wordWrap" numberOfLines="3" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="rFf-QX-ZN4">
+                                        <rect key="frame" x="347.5" y="84" width="271" height="19.5"/>
+                                        <fontDescription key="fontDescription" type="system" pointSize="16"/>
+                                        <color key="textColor" systemColor="secondaryLabelColor"/>
+                                        <nil key="highlightedColor"/>
+                                    </label>
+                                </subviews>
+                            </stackView>
+                        </subviews>
+                        <color key="backgroundColor" systemColor="systemBackgroundColor"/>
+                        <constraints>
+                            <constraint firstItem="4je-R6-T4G" firstAttribute="leading" secondItem="ufw-Yg-V9l" secondAttribute="leadingMargin" constant="20" id="IG1-bN-NoI"/>
+                            <constraint firstItem="4je-R6-T4G" firstAttribute="centerY" secondItem="ufw-Yg-V9l" secondAttribute="centerY" id="Jvy-Ac-6TQ"/>
+                            <constraint firstItem="4je-R6-T4G" firstAttribute="centerX" secondItem="ufw-Yg-V9l" secondAttribute="centerX" id="KTw-qM-i9Y"/>
+                        </constraints>
+                    </view>
+                    <navigationItem key="navigationItem" id="dMP-bU-p4J"/>
+                    <connections>
+                        <outlet property="logoImage" destination="p23-aq-R5e" id="cTb-t2-hcu"/>
+                        <outlet property="subtitleLabel" destination="rFf-QX-ZN4" id="57g-Fb-Klw"/>
+                        <outlet property="titleLabel" destination="pLM-th-fh4" id="NPE-0i-u1e"/>
+                    </connections>
+                </viewController>
+                <placeholder placeholderIdentifier="IBFirstResponder" id="4uM-kt-VTe" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
+            </objects>
+            <point key="canvasLocation" x="2190" y="1444"/>
+        </scene>
+        <!--Navigation Controller-->
+        <scene sceneID="2B9-D5-xjb">
+            <objects>
+                <navigationController id="cEs-8p-AhA" customClass="NCNavigationController" sceneMemberID="viewController">
+                    <navigationBar key="navigationBar" contentMode="scaleToFill" id="u19-wP-Dh8">
+                        <rect key="frame" x="0.0" y="24" width="1045.5" height="50"/>
+                        <autoresizingMask key="autoresizingMask"/>
+                    </navigationBar>
+                    <toolbar key="toolbar" opaque="NO" clearsContextBeforeDrawing="NO" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="VtL-b4-wDm">
+                        <rect key="frame" x="-320.5" y="0.0" width="0.0" height="0.0"/>
+                        <autoresizingMask key="autoresizingMask"/>
+                    </toolbar>
+                    <connections>
+                        <segue destination="Mvl-Nd-ZXL" kind="relationship" relationship="rootViewController" id="gdr-kc-bkV"/>
+                    </connections>
+                </navigationController>
+                <placeholder placeholderIdentifier="IBFirstResponder" id="EAM-70-lOQ" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
+            </objects>
+            <point key="canvasLocation" x="1194" y="1444"/>
+        </scene>
+        <!--Navigation Controller-->
+        <scene sceneID="rjH-tQ-hYv">
+            <objects>
+                <navigationController id="Zgl-h4-MON" customClass="NCNavigationController" sceneMemberID="viewController">
+                    <navigationBar key="navigationBar" contentMode="scaleToFill" id="xyF-sn-ttH">
+                        <rect key="frame" x="0.0" y="24" width="420" height="50"/>
+                        <autoresizingMask key="autoresizingMask"/>
+                    </navigationBar>
+                    <toolbar key="toolbar" opaque="NO" clearsContextBeforeDrawing="NO" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="OCT-Zu-7bm">
+                        <rect key="frame" x="100" y="0.0" width="0.0" height="0.0"/>
+                        <autoresizingMask key="autoresizingMask"/>
+                    </toolbar>
+                    <connections>
+                        <segue destination="d5a-it-gRL" kind="relationship" relationship="rootViewController" id="sPp-Rq-25b"/>
+                    </connections>
+                </navigationController>
+                <placeholder placeholderIdentifier="IBFirstResponder" id="iHv-Fg-yLc" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
+            </objects>
+            <point key="canvasLocation" x="1194" y="675"/>
+        </scene>
+        <!--Split View Controller-->
+        <scene sceneID="vSL-eq-3yh">
+            <objects>
+                <splitViewController allowDoubleColumnStyle="YES" maximumPrimaryColumnWidth="600" minimumPrimaryColumnWidth="300" preferredDisplayMode="beside" behavior="tile" id="ecK-na-6ig" customClass="NCSplitViewController" customModule="NextcloudTalk" customModuleProvider="target" sceneMemberID="viewController">
+                    <navigationItem key="navigationItem" id="clo-lG-PAu"/>
+                    <connections>
+                        <segue destination="Zgl-h4-MON" kind="relationship" relationship="masterViewController" id="fXD-cN-fs0"/>
+                        <segue destination="cEs-8p-AhA" kind="relationship" relationship="detailViewController" id="Dcb-zn-fBP"/>
+                    </connections>
+                </splitViewController>
+                <placeholder placeholderIdentifier="IBFirstResponder" id="aT0-S7-WFz" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
+            </objects>
+            <point key="canvasLocation" x="277" y="980"/>
+        </scene>
+        <!--Settings Table View Controller-->
+        <scene sceneID="ZbW-mC-4pI">
+            <objects>
+                <tableViewController id="IZS-G3-LBe" customClass="SettingsTableViewController" customModule="NextcloudTalk" customModuleProvider="target" sceneMemberID="viewController">
+                    <tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="insetGrouped" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="18" sectionFooterHeight="18" id="b5X-rI-l6i">
+                        <rect key="frame" x="0.0" y="0.0" width="1366" height="950"/>
+                        <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+                        <prototypes>
+                            <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" id="y90-mb-BxJ">
+                                <rect key="frame" x="20" y="55.5" width="1326" height="51.5"/>
+                                <autoresizingMask key="autoresizingMask"/>
+                                <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="y90-mb-BxJ" id="XO0-Yu-aVx">
+                                    <rect key="frame" x="0.0" y="0.0" width="1326" height="51.5"/>
+                                    <autoresizingMask key="autoresizingMask"/>
+                                </tableViewCellContentView>
+                            </tableViewCell>
+                        </prototypes>
+                        <connections>
+                            <outlet property="dataSource" destination="IZS-G3-LBe" id="cuA-h8-0E0"/>
+                            <outlet property="delegate" destination="IZS-G3-LBe" id="vZ7-0q-aW2"/>
+                        </connections>
+                    </tableView>
+                    <navigationItem key="navigationItem" id="Pp3-yk-a14">
+                        <barButtonItem key="leftBarButtonItem" style="plain" systemItem="cancel" id="Gz2-po-4Ua">
+                            <connections>
+                                <action selector="cancelButtonPressed:" destination="IZS-G3-LBe" id="zzB-Uz-Wpo"/>
+                            </connections>
+                        </barButtonItem>
+                    </navigationItem>
+                    <connections>
+                        <outlet property="cancelButton" destination="Gz2-po-4Ua" id="96j-HY-dJv"/>
+                    </connections>
+                </tableViewController>
+                <placeholder placeholderIdentifier="IBFirstResponder" id="7ze-MS-nZa" userLabel="First Responder" sceneMemberID="firstResponder"/>
+            </objects>
+            <point key="canvasLocation" x="1799" y="-868"/>
+        </scene>
+        <!--Navigation Controller-->
+        <scene sceneID="WPn-o2-Zlg">
+            <objects>
+                <navigationController storyboardIdentifier="settingsNC" id="h6t-CT-B8r" customClass="NCNavigationController" sceneMemberID="viewController">
+                    <simulatedNavigationBarMetrics key="simulatedTopBarMetrics" translucent="NO" prompted="NO"/>
+                    <navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" translucent="NO" id="Mqg-tq-GRj">
+                        <rect key="frame" x="0.0" y="24" width="1366" height="50"/>
+                        <autoresizingMask key="autoresizingMask"/>
+                    </navigationBar>
+                    <connections>
+                        <segue destination="IZS-G3-LBe" kind="relationship" relationship="rootViewController" id="muZ-2w-5f8"/>
+                    </connections>
+                </navigationController>
+                <placeholder placeholderIdentifier="IBFirstResponder" id="Lh2-Pq-2Uq" userLabel="First Responder" sceneMemberID="firstResponder"/>
+            </objects>
+            <point key="canvasLocation" x="847" y="-868"/>
+        </scene>
+    </scenes>
+    <resources>
+        <image name="app-logo-callkit" width="40" height="40"/>
+        <systemColor name="secondaryLabelColor">
+            <color red="0.23529411764705882" green="0.23529411764705882" blue="0.2627450980392157" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
+        </systemColor>
+        <systemColor name="systemBackgroundColor">
+            <color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+        </systemColor>
+    </resources>
+</document>

+ 55 - 0
NextcloudTalk/BaseChatTableViewCell+Audio.swift

@@ -0,0 +1,55 @@
+//
+// SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+
+extension BaseChatTableViewCell {
+
+    func setupForAudioCell(with message: NCChatMessage) {
+        if self.audioPlayerView == nil {
+            // Audio player view
+            let audioPlayerView = AudioPlayerView(frame: CGRect(x: 0, y: 0, width: 0, height: voiceMessageCellPlayerHeight))
+            self.audioPlayerView = audioPlayerView
+            self.audioPlayerView?.delegate = self
+
+            audioPlayerView.translatesAutoresizingMaskIntoConstraints = false
+
+            self.messageBodyView.addSubview(audioPlayerView)
+
+            NSLayoutConstraint.activate([
+                audioPlayerView.leftAnchor.constraint(equalTo: self.messageBodyView.leftAnchor),
+                audioPlayerView.rightAnchor.constraint(equalTo: self.messageBodyView.rightAnchor),
+                audioPlayerView.topAnchor.constraint(equalTo: self.messageBodyView.topAnchor)
+            ])
+        }
+    }
+
+    func prepareForReuseAudioCell() {
+        self.audioPlayerView?.resetPlayer()
+        self.clearFileStatusView()
+    }
+
+    func audioPlayerPlayButtonPressed() {
+        guard let audioFileParameter = message?.file() else {
+            return
+        }
+
+        self.delegate?.cellWants(toPlayAudioFile: audioFileParameter)
+    }
+
+    func audioPlayerPauseButtonPressed() {
+        guard let audioFileParameter = message?.file() else {
+            return
+        }
+
+        self.delegate?.cellWants(toPauseAudioFile: audioFileParameter)
+    }
+
+    func audioPlayerProgressChanged(progress: CGFloat) {
+        guard let audioFileParameter = message?.file() else {
+            return
+        }
+
+        self.delegate?.cellWants(toChangeProgress: progress, fromAudioFile: audioFileParameter)
+    }
+}

+ 305 - 0
NextcloudTalk/BaseChatTableViewCell+File.swift

@@ -0,0 +1,305 @@
+//
+// SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+
+extension BaseChatTableViewCell {
+
+    func setupForFileCell(with message: NCChatMessage, with account: TalkAccount) {
+        if self.filePreviewImageView == nil {
+            // Preview image view
+            let filePreviewImageView = FilePreviewImageView(frame: .init(x: 0, y: 0, width: fileMessageCellFileMaxPreviewHeight, height: fileMessageCellFileMaxPreviewWidth))
+            self.filePreviewImageView = filePreviewImageView
+
+            filePreviewImageView.translatesAutoresizingMaskIntoConstraints = false
+            filePreviewImageView.layer.cornerRadius = chatMessageCellPreviewCornerRadius
+            filePreviewImageView.layer.masksToBounds = true
+            filePreviewImageView.contentMode = .scaleAspectFit
+
+            self.messageBodyView.addSubview(filePreviewImageView)
+
+            let previewTap = UITapGestureRecognizer(target: self, action: #selector(filePreviewTapped))
+            filePreviewImageView.addGestureRecognizer(previewTap)
+            filePreviewImageView.isUserInteractionEnabled = true
+
+            // PlayIcon for video files with preview
+            let filePreviewPlayIconImageView = UIImageView(frame: .init(x: 0, y: 0, width: fileMessageCellFileMaxPreviewHeight, height: fileMessageCellFileMaxPreviewWidth))
+            self.filePreviewPlayIconImageView = filePreviewPlayIconImageView
+
+            filePreviewPlayIconImageView.isHidden = true
+            filePreviewPlayIconImageView.tintColor = .init(white: 1.0, alpha: 0.8)
+            filePreviewPlayIconImageView.image = .init(systemName: "play.fill", withConfiguration: UIImage.SymbolConfiguration(weight: .black))
+
+            filePreviewImageView.addSubview(filePreviewPlayIconImageView)
+            filePreviewImageView.bringSubviewToFront(filePreviewPlayIconImageView)
+
+            // Activity indicator while loading previews
+            let filePreviewActivityIndicator = MDCActivityIndicator(frame: .init(x: 0, y: 0, width: fileMessageCellMinimumHeight, height: fileMessageCellMinimumHeight))
+            self.filePreviewActivityIndicator = filePreviewActivityIndicator
+
+            filePreviewActivityIndicator.translatesAutoresizingMaskIntoConstraints = false
+            filePreviewActivityIndicator.radius = fileMessageCellMinimumHeight / 2
+            filePreviewActivityIndicator.cycleColors = [.systemGray2]
+            filePreviewActivityIndicator.indicatorMode = .indeterminate
+
+            filePreviewImageView.addSubview(filePreviewActivityIndicator)
+
+            NSLayoutConstraint.activate([
+                filePreviewActivityIndicator.centerYAnchor.constraint(equalTo: filePreviewImageView.centerYAnchor),
+                filePreviewActivityIndicator.centerXAnchor.constraint(equalTo: filePreviewImageView.centerXAnchor)
+            ])
+
+            // Add everything to messageBodyView
+            let heightConstraint = filePreviewImageView.heightAnchor.constraint(equalToConstant: fileMessageCellFileMaxPreviewHeight)
+            let widthConstraint = filePreviewImageView.widthAnchor.constraint(equalToConstant: fileMessageCellFileMaxPreviewWidth)
+
+            self.filePreviewImageViewHeightConstraint = heightConstraint
+            self.filePreviewImageViewWidthConstraint = widthConstraint
+
+            let messageTextView = MessageBodyTextView()
+            self.messageTextView = messageTextView
+
+            messageTextView.translatesAutoresizingMaskIntoConstraints = false
+
+            self.messageBodyView.addSubview(messageTextView)
+
+            NSLayoutConstraint.activate([
+                filePreviewImageView.leftAnchor.constraint(equalTo: self.messageBodyView.leftAnchor),
+                filePreviewImageView.topAnchor.constraint(equalTo: self.messageBodyView.topAnchor),
+                heightConstraint,
+                widthConstraint,
+                messageTextView.leftAnchor.constraint(equalTo: self.messageBodyView.leftAnchor),
+                messageTextView.rightAnchor.constraint(equalTo: self.messageBodyView.rightAnchor),
+                messageTextView.topAnchor.constraint(equalTo: filePreviewImageView.bottomAnchor, constant: 10),
+                messageTextView.bottomAnchor.constraint(equalTo: self.messageBodyView.bottomAnchor)
+            ])
+        }
+
+        guard let filePreviewImageView = self.filePreviewImageView,
+              let messageTextView = self.messageTextView
+        else { return }
+
+        messageTextView.attributedText = message.parsedMarkdownForChat()
+
+        if message.message == "{file}" {
+            messageTextView.dataDetectorTypes = []
+        } else {
+            messageTextView.dataDetectorTypes = .all
+        }
+
+        self.requestPreview(for: message, with: account)
+
+        if !message.sendingFailed {
+            if message.isTemporary {
+                self.addActivityIndicator(with: 0)
+            } else if let fileStatus = message.file().fileStatus {
+                if fileStatus.isDownloading, fileStatus.downloadProgress < 1 {
+                    self.addActivityIndicator(with: Float(fileStatus.downloadProgress))
+                }
+            }
+        }
+
+        if let contactImage = message.file().contactPhotoImage() {
+            filePreviewImageView.image = contactImage
+        }
+    }
+
+    func prepareForReuseFileCell() {
+        self.filePreviewImageView?.cancelImageDownloadTask()
+        self.filePreviewImageView?.layer.borderWidth = 0
+        self.filePreviewImageView?.image = nil
+        self.filePreviewPlayIconImageView?.isHidden = true
+
+        self.clearFileStatusView()
+    }
+
+    // MARK: - Preview
+
+    func requestPreview(for message: NCChatMessage, with account: TalkAccount) {
+        // Don't request a preview if we know that there's none
+        guard let file = message.file(), file.previewAvailable else {
+            self.showFallbackIcon(for: message)
+
+            return
+        }
+
+        // In case we can determine the height before requesting the preview, adjust the imageView constraints accordingly
+        if file.previewImageHeight > 0 {
+            self.filePreviewImageViewHeightConstraint?.constant = CGFloat(file.previewImageHeight)
+        } else {
+            let estimatedPreviewHeight = BaseChatTableViewCell.getEstimatedPreviewSize(for: message)
+
+            if estimatedPreviewHeight > 0 {
+                self.filePreviewImageViewHeightConstraint?.constant = estimatedPreviewHeight
+            }
+        }
+
+        self.filePreviewActivityIndicator?.isHidden = false
+        self.filePreviewActivityIndicator?.startAnimating()
+
+        if message.isAnimatableGif {
+            self.requestGifPreview(for: message, with: account)
+        } else {
+            self.requestDefaultPreview(for: message, with: account)
+        }
+    }
+
+    func requestGifPreview(for message: NCChatMessage, with account: TalkAccount) {
+        guard let fileId = message.file()?.parameterId else { return }
+
+        let fileControllerWrapper = NCChatFileControllerWrapper()
+        self.fileControllerWrapper = fileControllerWrapper
+
+        fileControllerWrapper.downloadFile(withFileId: fileId) { fileLocalPath in
+            // Check if we are still on the same cell
+            guard let cellMessage = self.message, let imageView = self.filePreviewImageView, cellMessage.file().parameterId == fileId
+            else {
+                // Different cell, don't do anything
+                return
+            }
+
+            guard let fileLocalPath, let data = try? Data(contentsOf: URL(fileURLWithPath: fileLocalPath)),
+                  let gifImage = try? UIImage(gifData: data), let baseImage = UIImage(data: data) else {
+
+                // No gif, try to request a normal preview
+                self.requestDefaultPreview(for: message, with: account)
+                return
+            }
+
+            imageView.setGifImage(gifImage)
+            self.adjustImageView(toImageSize: baseImage, ofMessage: message)
+        }
+    }
+
+    func requestDefaultPreview(for message: NCChatMessage, with account: TalkAccount) {
+        guard let file = message.file() else { return }
+
+        let requestedHeight = Int(3 * fileMessageCellFileMaxPreviewHeight)
+        guard let previewRequest = NCAPIController.sharedInstance().createPreviewRequest(forFile: file.parameterId, withMaxHeight: requestedHeight, using: account) else { return }
+
+        self.filePreviewImageView?.setImageWith(previewRequest, placeholderImage: nil, success: {  [weak self] _, _, image in
+            guard let self, let imageView = self.filePreviewImageView else { return }
+
+            imageView.image = image
+            self.adjustImageView(toImageSize: image, ofMessage: message)
+        }, failure: { _, _, _ in
+            self.showFallbackIcon(for: message)
+        })
+    }
+
+    func adjustImageView(toImageSize image: UIImage, ofMessage message: NCChatMessage) {
+        guard let imageView = self.filePreviewImageView, let file = message.file() else { return }
+
+        let isVideoFile = NCUtils.isVideo(fileType: file.mimetype)
+        let isMediaFile = isVideoFile || NCUtils.isImage(fileType: file.mimetype)
+
+        self.filePreviewActivityIndicator?.isHidden = true
+        self.filePreviewActivityIndicator?.stopAnimating()
+
+        let imageSize = CGSize(width: image.size.width * image.scale, height: image.size.height * image.scale)
+        let previewSize = BaseChatTableViewCell.getPreviewSize(from: imageSize, isMediaFile)
+
+        if !previewSize.width.isFinite || !previewSize.height.isFinite {
+            self.showFallbackIcon(for: message)
+            return
+        }
+
+        imageView.layer.borderColor = UIColor.secondarySystemFill.cgColor
+        imageView.layer.borderWidth = 1
+
+        self.filePreviewImageViewHeightConstraint?.constant = previewSize.height
+        self.filePreviewImageViewWidthConstraint?.constant = previewSize.width
+
+        if isVideoFile {
+            // only show the play icon if there is an image preview (not on top of the default video placeholder)
+            self.filePreviewPlayIconImageView?.isHidden = false
+            // if the video preview is very narrow, make the play icon fit inside
+            self.filePreviewPlayIconImageView?.frame = CGRect(x: 0, y: 0, width: min(min(previewSize.height, previewSize.width), fileMessageCellVideoPlayIconSize), height: min(min(previewSize.height, previewSize.width), fileMessageCellVideoPlayIconSize))
+            self.filePreviewPlayIconImageView?.center = CGPoint(x: previewSize.width / 2.0, y: previewSize.height / 2.0)
+        }
+
+        self.delegate?.cellHasDownloadedImagePreview(withHeight: ceil(previewSize.height), for: message)
+    }
+
+    func showFallbackIcon(for message: NCChatMessage) {
+        let imageName = NCUtils.previewImage(forMimeType: message.file().mimetype)
+
+        if let image = UIImage(named: imageName) {
+            let size = CGSize(width: fileMessageCellFileMaxPreviewWidth, height: fileMessageCellFileMaxPreviewHeight)
+
+            if let renderedImage = NCUtils.renderAspectImage(image: image, ofSize: size, centerImage: false) {
+                self.filePreviewImageView?.image = renderedImage
+
+                self.filePreviewImageViewHeightConstraint?.constant = renderedImage.size.height
+                self.filePreviewImageViewWidthConstraint?.constant = renderedImage.size.width
+            }
+        }
+
+        self.filePreviewActivityIndicator?.isHidden = true
+        self.filePreviewActivityIndicator?.stopAnimating()
+    }
+
+    @objc
+    func filePreviewTapped() {
+        guard let message = self.message,
+              let fileParameter = message.file(),
+              fileParameter.path != nil, fileParameter.link != nil
+        else { return }
+
+        self.delegate?.cellWants(toDownloadFile: fileParameter, for: message)
+    }
+
+    // MARK: - Preview height calculation
+
+    static func getPreviewSize(from imageSize: CGSize, _ isMediaFile: Bool) -> CGSize {
+        var width = imageSize.width
+        var height = imageSize.height
+
+        let previewMaxHeight = isMediaFile ? fileMessageCellMediaFilePreviewHeight : fileMessageCellFileMaxPreviewHeight
+        let previewMaxWidth = isMediaFile ? fileMessageCellMediaFileMaxPreviewWidth : fileMessageCellFileMaxPreviewWidth
+
+        if height < fileMessageCellMinimumHeight {
+            let ratio = fileMessageCellMinimumHeight / height
+            width *= ratio
+
+            if width > previewMaxWidth {
+                width = previewMaxWidth
+            }
+
+            height = fileMessageCellMinimumHeight
+        } else {
+            if height > previewMaxHeight {
+                let ratio = previewMaxHeight / height
+                width *= ratio
+                height = previewMaxHeight
+            }
+
+            if width > previewMaxWidth {
+                let ratio = previewMaxWidth / width
+                width = previewMaxWidth
+                height *= ratio
+            }
+        }
+
+        return CGSize(width: width, height: height)
+    }
+
+    static func getEstimatedPreviewSize(for message: NCChatMessage?) -> CGFloat {
+        guard let message, let fileParameter = message.file() else { return 0 }
+
+        // We don't have any information about the image to display
+        if fileParameter.width == 0 && fileParameter.height == 0 {
+            return 0
+        }
+
+        // We can only estimate the height for images and videos
+        if !NCUtils.isVideo(fileType: fileParameter.mimetype), !NCUtils.isImage(fileType: fileParameter.mimetype) {
+            return 0
+        }
+
+        let imageSize = CGSize(width: CGFloat(fileParameter.width), height: CGFloat(fileParameter.height))
+        let previewSize = self.getPreviewSize(from: imageSize, true)
+
+        return ceil(previewSize.height)
+    }
+}

+ 117 - 0
NextcloudTalk/BaseChatTableViewCell+Location.swift

@@ -0,0 +1,117 @@
+//
+// SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+
+extension BaseChatTableViewCell {
+
+    func setupForLocationCell(with message: NCChatMessage) {
+        if self.locationPreviewImageView == nil {
+            // Location preview image view
+            let locationPreviewImageView = UIImageView(frame: .init(x: 0, y: 0, width: locationMessageCellPreviewWidth, height: locationMessageCellPreviewHeight))
+            self.locationPreviewImageView = locationPreviewImageView
+
+            locationPreviewImageView.translatesAutoresizingMaskIntoConstraints = false
+            locationPreviewImageView.layer.cornerRadius = chatMessageCellPreviewCornerRadius
+            locationPreviewImageView.layer.masksToBounds = true
+            locationPreviewImageView.contentMode = .scaleAspectFit
+
+            self.messageBodyView.addSubview(locationPreviewImageView)
+
+            let previewTap = UITapGestureRecognizer(target: self, action: #selector(locationPreviewTapped))
+            locationPreviewImageView.addGestureRecognizer(previewTap)
+            locationPreviewImageView.isUserInteractionEnabled = true
+
+            // Add everything to messageBodyView
+            let heightConstraint = locationPreviewImageView.heightAnchor.constraint(equalToConstant: locationMessageCellPreviewHeight)
+            let widthConstraint = locationPreviewImageView.widthAnchor.constraint(equalToConstant: locationMessageCellPreviewWidth)
+
+            self.locationPreviewImageViewHeightConstraint = heightConstraint
+            self.locationPreviewImageViewWidthConstraint = widthConstraint
+
+            let messageTextView = MessageBodyTextView()
+            self.messageTextView = messageTextView
+
+            messageTextView.translatesAutoresizingMaskIntoConstraints = false
+
+            self.messageBodyView.addSubview(messageTextView)
+
+            NSLayoutConstraint.activate([
+                locationPreviewImageView.leftAnchor.constraint(equalTo: self.messageBodyView.leftAnchor),
+                locationPreviewImageView.topAnchor.constraint(equalTo: self.messageBodyView.topAnchor),
+                heightConstraint,
+                widthConstraint,
+                messageTextView.leftAnchor.constraint(equalTo: self.messageBodyView.leftAnchor),
+                messageTextView.rightAnchor.constraint(equalTo: self.messageBodyView.rightAnchor),
+                messageTextView.topAnchor.constraint(equalTo: locationPreviewImageView.bottomAnchor, constant: 10),
+                messageTextView.bottomAnchor.constraint(equalTo: self.messageBodyView.bottomAnchor)
+            ])
+        }
+
+        guard let locationPreviewImageView = self.locationPreviewImageView,
+              let messageTextView = self.messageTextView,
+              let geoLocationRichObject = message.geoLocation()
+        else { return }
+
+        let geoLocation = GeoLocationRichObject(from: geoLocationRichObject)
+
+        guard let latitude = Double(geoLocation.latitude),
+              let longitude = Double(geoLocation.longitude)
+        else { return }
+
+        let mapView = MKMapView(frame: .init(x: 0, y: 0, width: locationMessageCellPreviewWidth, height: locationMessageCellPreviewHeight))
+
+        let mapRegion = MKCoordinateRegion(center: .init(latitude: latitude, longitude: longitude),
+                                           span: .init(latitudeDelta: 0.005, longitudeDelta: 0.005))
+
+        let options: MKMapSnapshotter.Options = .init()
+        options.region = mapRegion
+        options.size = mapView.frame.size
+        options.scale = UIScreen.main.scale
+
+        let locationMapSnapshooter = MKMapSnapshotter(options: options)
+        self.locationMapSnapshooter = locationMapSnapshooter
+
+        locationMapSnapshooter.start { snapshot, _ in
+            guard let snapshot else { return }
+
+            let pin = MKPinAnnotationView(annotation: nil, reuseIdentifier: nil)
+            let image = snapshot.image
+
+            UIGraphicsBeginImageContextWithOptions(image.size, true, image.scale)
+            image.draw(at: CGPoint.zero)
+
+            let rect = CGRect(origin: CGPoint.zero, size: image.size)
+            let annotation = MKPointAnnotation()
+            annotation.coordinate = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
+            var point = snapshot.point(for: annotation.coordinate)
+            if rect.contains(point) {
+                point.x += pin.centerOffset.x - (pin.bounds.size.width / 2)
+                point.y += pin.centerOffset.y - (pin.bounds.size.height / 2)
+                pin.pinTintColor = NCAppBranding.elementColor()
+                pin.image?.draw(at: point)
+            }
+
+            let compositeImage = UIGraphicsGetImageFromCurrentImageContext()
+
+            self.locationPreviewImageView?.image = compositeImage
+            UIGraphicsEndImageContext()
+        }
+
+        messageTextView.attributedText = message.parsedMarkdownForChat()
+    }
+
+    func prepareForReuseLocationCell() {
+        self.locationMapSnapshooter?.cancel()
+        self.locationPreviewImageView?.image = nil
+    }
+
+    @objc func locationPreviewTapped() {
+        guard let geoLocationRichObject = self.message?.geoLocation()
+        else { return }
+
+        let geoLocation = GeoLocationRichObject(from: geoLocationRichObject)
+
+        self.delegate?.cellWants(toOpenLocation: geoLocation)
+    }
+}

+ 33 - 0
NextcloudTalk/BaseChatTableViewCell+Message.swift

@@ -0,0 +1,33 @@
+//
+// SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+
+extension BaseChatTableViewCell {
+
+    func setupForMessageCell(with message: NCChatMessage) {
+        if self.messageTextView == nil {
+            let messageTextView = MessageBodyTextView()
+            self.messageTextView = messageTextView
+
+            messageTextView.translatesAutoresizingMaskIntoConstraints = false
+
+            self.messageBodyView.addSubview(messageTextView)
+
+            NSLayoutConstraint.activate([
+                messageTextView.leftAnchor.constraint(equalTo: self.messageBodyView.leftAnchor),
+                messageTextView.rightAnchor.constraint(equalTo: self.messageBodyView.rightAnchor),
+                messageTextView.topAnchor.constraint(equalTo: self.messageBodyView.topAnchor),
+                messageTextView.bottomAnchor.constraint(equalTo: self.messageBodyView.bottomAnchor)
+            ])
+        }
+
+        guard let messageTextView = self.messageTextView else { return }
+
+        messageTextView.attributedText = message.parsedMarkdownForChat()
+    }
+
+    func prepareForReuseMessageCell() {
+        self.messageTextView?.text = ""
+    }
+}

+ 51 - 0
NextcloudTalk/BaseChatTableViewCell+Poll.swift

@@ -0,0 +1,51 @@
+//
+// SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+
+extension BaseChatTableViewCell {
+
+    func setupForPollCell(with message: NCChatMessage) {
+        if self.pollMessageView == nil {
+            // Poll message view
+            let pollMessageView = PollMessageView(frame: .zero)
+            self.pollMessageView = pollMessageView
+
+            pollMessageView.translatesAutoresizingMaskIntoConstraints = false
+
+            pollMessageView.layer.cornerRadius = 8.0
+            pollMessageView.layer.masksToBounds = true
+            pollMessageView.layer.borderWidth = 1.0
+            pollMessageView.layer.borderColor = NCAppBranding.placeholderColor().cgColor
+
+            self.messageBodyView.addSubview(pollMessageView)
+
+            let tapGesture = UITapGestureRecognizer(target: self, action: #selector(pollViewTapped))
+            pollMessageView.addGestureRecognizer(tapGesture)
+            pollMessageView.isUserInteractionEnabled = true
+
+            NSLayoutConstraint.activate([
+                pollMessageView.leftAnchor.constraint(equalTo: self.messageBodyView.leftAnchor),
+                pollMessageView.rightAnchor.constraint(equalTo: self.messageBodyView.rightAnchor),
+                pollMessageView.topAnchor.constraint(equalTo: self.messageBodyView.topAnchor),
+                pollMessageView.bottomAnchor.constraint(equalTo: self.messageBodyView.bottomAnchor)
+            ])
+        }
+
+        guard let pollMessageView = self.pollMessageView else { return }
+
+        pollMessageView.pollTitleTextView.text = message.parsedMessage().string
+    }
+
+    func prepareForReusePollCell() {
+        self.pollMessageView?.pollTitleTextView.text = ""
+    }
+
+    @objc func pollViewTapped() {
+        guard let poll = message?.poll else {
+            return
+        }
+
+        self.delegate?.cellWants(toOpenPoll: poll)
+    }
+}

+ 607 - 0
NextcloudTalk/BaseChatTableViewCell.swift

@@ -0,0 +1,607 @@
+//
+// SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+
+import MapKit
+import SwiftyGif
+
+protocol BaseChatTableViewCellDelegate: AnyObject {
+
+    func cellWantsToScroll(to message: NCChatMessage)
+    func cellWantsToReply(to message: NCChatMessage)
+    func cellDidSelectedReaction(_ reaction: NCChatReaction!, for message: NCChatMessage)
+
+    func cellWants(toDownloadFile fileParameter: NCMessageFileParameter, for message: NCChatMessage)
+    func cellHasDownloadedImagePreview(withHeight height: CGFloat, for message: NCChatMessage)
+
+    func cellWants(toOpenLocation geoLocationRichObject: GeoLocationRichObject)
+
+    func cellWants(toPlayAudioFile fileParameter: NCMessageFileParameter)
+    func cellWants(toPauseAudioFile fileParameter: NCMessageFileParameter)
+    func cellWants(toChangeProgress progress: CGFloat, fromAudioFile fileParameter: NCMessageFileParameter)
+
+    func cellWants(toOpenPoll poll: NCMessageParameter)
+}
+
+// Common elements
+public let chatMessageCellPreviewCornerRadius = 4.0
+
+// Message cell
+public let chatMessageCellIdentifier = "chatMessageCellIdentifier"
+public let chatGroupedMessageCellIdentifier = "chatGroupedMessageCellIdentifier"
+public let chatReplyMessageCellIdentifier = "chatReplyMessageCellIdentifier"
+public let chatMessageCellMinimumHeight = 45.0
+public let chatGroupedMessageCellMinimumHeight = 25.0
+
+// File cell
+public let fileMessageCellIdentifier = "fileMessageCellIdentifier"
+public let fileGroupedMessageCellIdentifier = "fileGroupedMessageCellIdentifier"
+public let fileMessageCellMinimumHeight = 50.0
+public let fileMessageCellFileMaxPreviewHeight = 120.0
+public let fileMessageCellFileMaxPreviewWidth = 230.0
+public let fileMessageCellMediaFilePreviewHeight = 230.0
+public let fileMessageCellMediaFileMaxPreviewWidth = 230.0
+public let fileMessageCellVideoPlayIconSize = 48.0
+
+// Location cell
+public let locationMessageCellIdentifier = "locationMessageCellIdentifier"
+public let locationGroupedMessageCellIdentifier = "locationGroupedMessageCellIdentifier"
+public let locationMessageCellMinimumHeight = 50.0
+public let locationMessageCellPreviewHeight = 120.0
+public let locationMessageCellPreviewWidth = 240.0
+
+// Voice message cell
+public let voiceMessageCellIdentifier = "voiceMessageCellIdentifier"
+public let voiceGroupedMessageCellIdentifier = "voiceGroupedMessageCellIdentifier"
+public let voiceMessageCellPlayerHeight = 52.0
+
+// Poll cell
+public let pollMessageCellIdentifier = "pollMessageCellIdentifier"
+public let pollGroupedMessageCellIdentifier = "pollGroupedMessageCellIdentifier"
+
+class BaseChatTableViewCell: UITableViewCell, AudioPlayerViewDelegate, ReactionsViewDelegate {
+
+    public weak var delegate: BaseChatTableViewCellDelegate?
+
+    @IBOutlet weak var avatarButton: AvatarButton!
+    @IBOutlet weak var titleLabel: UILabel!
+    @IBOutlet weak var dateLabel: UILabel!
+    @IBOutlet weak var statusView: UIStackView!
+    @IBOutlet weak var messageBodyView: UIView!
+
+    @IBOutlet weak var headerPart: UIView!
+    @IBOutlet weak var quotePart: UIView!
+    @IBOutlet weak var reactionPart: UIView!
+    @IBOutlet weak var referencePart: UIView!
+
+    public var message: NCChatMessage?
+    public var messageId: Int = 0
+
+    internal var quotedMessageView: QuotedMessageView?
+    internal var reactionView: ReactionsView?
+    internal var referenceView: ReferenceView?
+
+    internal var replyGestureRecognizer: DRCellSlideGestureRecognizer?
+
+    // Message cell
+    internal var messageTextView: MessageBodyTextView?
+
+    // File cell
+    internal var filePreviewImageView: UIImageView?
+    internal var filePreviewImageViewHeightConstraint: NSLayoutConstraint?
+    internal var filePreviewImageViewWidthConstraint: NSLayoutConstraint?
+    internal var fileActivityIndicator: MDCActivityIndicator?
+    internal var filePreviewActivityIndicator: MDCActivityIndicator?
+    internal var filePreviewPlayIconImageView: UIImageView?
+    internal var fileControllerWrapper: NCChatFileControllerWrapper?
+
+    // Location cell
+    internal var locationPreviewImageView: UIImageView?
+    internal var locationMapSnapshooter: MKMapSnapshotter?
+    internal var locationPreviewImageViewHeightConstraint: NSLayoutConstraint?
+    internal var locationPreviewImageViewWidthConstraint: NSLayoutConstraint?
+
+    // Audio cell
+    internal var audioPlayerView: AudioPlayerView?
+
+    // Poll cell
+    internal var pollMessageView: PollMessageView?
+
+    override func awakeFromNib() {
+        super.awakeFromNib()
+
+        self.commonInit()
+    }
+
+    func commonInit() {
+        self.headerPart.isHidden = false
+        self.quotePart.isHidden = true
+        self.referencePart.isHidden = true
+        self.reactionPart.isHidden = true
+    }
+
+    override func prepareForReuse() {
+        super.prepareForReuse()
+
+        self.message = nil
+        self.avatarButton.cancelCurrentRequest()
+        self.avatarButton.setImage(nil, for: .normal)
+
+        self.quotedMessageView?.avatarView.cancelCurrentRequest()
+        self.quotedMessageView?.avatarView.image = nil
+
+        self.titleLabel.text = ""
+        self.dateLabel.text = ""
+
+        self.headerPart.isHidden = false
+        self.quotePart.isHidden = true
+        self.referencePart.isHidden = true
+        self.reactionPart.isHidden = true
+
+        self.statusView.isHidden = false
+        self.statusView.subviews.forEach { $0.removeFromSuperview() }
+
+        self.referenceView?.prepareForReuse()
+
+        self.prepareForReuseMessageCell()
+        self.prepareForReuseFileCell()
+        self.prepareForReuseLocationCell()
+        self.prepareForReuseAudioCell()
+        self.prepareForReusePollCell()
+
+        if let replyGestureRecognizer {
+            self.removeGestureRecognizer(replyGestureRecognizer)
+            self.replyGestureRecognizer = nil
+        }
+    }
+
+    // swiftlint:disable:next cyclomatic_complexity
+    public func setup(for message: NCChatMessage, withLastCommonReadMessage lastCommonRead: Int) {
+        self.message = message
+        self.messageId = message.messageId
+
+        self.avatarButton.setActorAvatar(forMessage: message)
+        self.avatarButton.menu = self.getDeferredUserMenu()
+        self.avatarButton.showsMenuAsPrimaryAction = true
+
+        let date = Date(timeIntervalSince1970: TimeInterval(message.timestamp))
+        self.dateLabel.text = NCUtils.getTime(fromDate: date)
+
+        let messageActor = message.actor
+        let titleLabel = messageActor.attributedDisplayName
+
+        if let lastEditActorDisplayName = message.lastEditActorDisplayName, message.lastEditTimestamp > 0 {
+            var editedString = ""
+
+            if message.lastEditActorId == message.actorId, message.lastEditActorType == "users" {
+                editedString = NSLocalizedString("edited", comment: "A message was edited")
+                editedString = " (\(editedString))"
+            } else {
+                editedString = NSLocalizedString("edited by", comment: "A message was edited by ...")
+                editedString = " (\(editedString) \(lastEditActorDisplayName))"
+            }
+
+            let editedAttributedString = editedString.withTextColor(.tertiaryLabel)
+
+            titleLabel.append(editedAttributedString)
+        }
+
+        self.titleLabel.attributedText = titleLabel
+
+        let activeAccount = NCDatabaseManager.sharedInstance().activeAccount()
+        var shouldShowDeliveryStatus = false
+        var shouldShowReadStatus = false
+
+        if let room = NCDatabaseManager.sharedInstance().room(withToken: message.token, forAccountId: activeAccount.accountId) {
+            shouldShowDeliveryStatus = NCDatabaseManager.sharedInstance().roomHasTalkCapability(kCapabilityChatReadStatus, for: room)
+
+            if let roomCapabilities = NCDatabaseManager.sharedInstance().roomTalkCapabilities(for: room) {
+                shouldShowReadStatus = !(roomCapabilities.readStatusPrivacy)
+            }
+        }
+
+        // This check is just a workaround to fix the issue with the deleted parents returned by the API.
+        if let parent = message.parent {
+            self.showQuotePart()
+
+            let quoteString = parent.parsedMarkdownForChat()?.string ?? ""
+            self.quotedMessageView?.messageLabel.text = quoteString
+            self.quotedMessageView?.actorLabel.attributedText = parent.actor.attributedDisplayName
+            self.quotedMessageView?.highlighted = parent.isMessage(from: activeAccount.userId)
+            self.quotedMessageView?.avatarView.setActorAvatar(forMessage: parent)
+        }
+
+        if message.isGroupMessage, message.parent == nil {
+            self.headerPart.isHidden = true
+        }
+
+        // When `setDeliveryState` is not called, we still need to make sure the placeholder view is removed
+        self.statusView.subviews.forEach { $0.removeFromSuperview() }
+
+        if message.isDeleting {
+            self.setDeliveryState(to: .deleting)
+        } else if message.sendingFailed {
+            self.setDeliveryState(to: .failed)
+        } else if message.isTemporary {
+            self.setDeliveryState(to: .sending)
+        } else if message.isMessage(from: activeAccount.userId), shouldShowDeliveryStatus {
+            if lastCommonRead >= message.messageId, shouldShowReadStatus {
+                self.setDeliveryState(to: .read)
+            } else {
+                self.setDeliveryState(to: .sent)
+            }
+        } else if message.isSilent {
+            self.setDeliveryState(to: .silent)
+        }
+
+        let reactionsArray = message.reactionsArray()
+
+        if !reactionsArray.isEmpty {
+            self.showReactionsPart()
+            self.reactionView?.updateReactions(reactions: reactionsArray)
+        }
+
+        if message.containsURL() {
+            self.showReferencePart()
+
+            message.getReferenceData { message, referenceDataRaw, url in
+                guard let cellMessage = self.message,
+                      let referenceMessage = message,
+                      cellMessage.isSameMessage(referenceMessage)
+                else { return }
+
+                if referenceDataRaw == nil, let deckCard = cellMessage.deckCard() {
+                    // In case we were unable to retrieve reference data (for example if the user has no permissions)
+                    // but the message is a shared deck card, we use the shared information to show the deck view
+                    self.referenceView?.update(for: deckCard)
+                } else if let referenceData = referenceDataRaw as? [String: [String: AnyObject]], let url {
+                    self.referenceView?.update(for: referenceData, and: url)
+                }
+            }
+        }
+
+        if message.isReplyable, !message.isDeleting {
+            self.addSlideToReplyGestureRecognizer(for: message)
+        }
+
+        if message.isVoiceMessage {
+            // Audio message
+            self.setupForAudioCell(with: message)
+        } else if message.poll != nil {
+            // Poll message
+            self.setupForPollCell(with: message)
+        } else if message.file() != nil {
+            // File message
+            self.setupForFileCell(with: message, with: activeAccount)
+        } else if message.geoLocation() != nil {
+            // Location message
+            self.setupForLocationCell(with: message)
+        } else {
+            // Normal text message
+            self.setupForMessageCell(with: message)
+        }
+
+        if message.isDeletedMessage {
+            self.statusView.isHidden = true
+            self.messageTextView?.textColor = .tertiaryLabel
+        }
+
+        NotificationCenter.default.addObserver(self, selector: #selector(didChangeIsDownloading(notification:)), name: NSNotification.Name.NCChatFileControllerDidChangeIsDownloading, object: nil)
+        NotificationCenter.default.addObserver(self, selector: #selector(didChangeDownloadProgress(notification:)), name: NSNotification.Name.NCChatFileControllerDidChangeDownloadProgress, object: nil)
+    }
+
+    func addSlideToReplyGestureRecognizer(for message: NCChatMessage) {
+        if let action = DRCellSlideAction(forFraction: 0.2) {
+            action.behavior = .pullBehavior
+            action.activeColor = .label
+            action.inactiveColor = .placeholderText
+            action.activeBackgroundColor = self.backgroundColor
+            action.inactiveBackgroundColor = self.backgroundColor
+            action.icon = UIImage(systemName: "arrowshape.turn.up.left")
+
+            action.willTriggerBlock = { [unowned self] _, _ -> Void in
+                self.delegate?.cellWantsToReply(to: message)
+            }
+
+            action.didChangeStateBlock = { _, active -> Void in
+                if active {
+                    // Actuate `Peek` feedback (weak boom)
+                    AudioServicesPlaySystemSound(1519)
+                }
+            }
+
+            let replyGestureRecognizer = DRCellSlideGestureRecognizer()
+            self.replyGestureRecognizer = replyGestureRecognizer
+
+            replyGestureRecognizer.leftActionStartPosition = 80
+            replyGestureRecognizer.addActions(action)
+
+            self.addGestureRecognizer(replyGestureRecognizer)
+        }
+    }
+
+    func setDeliveryState(to deliveryState: ChatMessageDeliveryState) {
+        self.statusView.subviews.forEach { $0.removeFromSuperview() }
+
+        if deliveryState == .sending || deliveryState == .deleting {
+            let activityIndicator = MDCActivityIndicator(frame: .init(x: 0, y: 0, width: 20, height: 20))
+
+            activityIndicator.radius = 7.0
+            activityIndicator.cycleColors = [.systemGray2]
+            activityIndicator.startAnimating()
+            activityIndicator.heightAnchor.constraint(equalToConstant: 20).isActive = true
+
+            self.statusView.addArrangedSubview(activityIndicator)
+
+        } else if deliveryState == .failed {
+            let errorView = UIImageView(frame: .init(x: 0, y: 0, width: 20, height: 20))
+            let errorImage = UIImage(systemName: "exclamationmark.circle")?.withTintColor(.red).withRenderingMode(.alwaysOriginal)
+
+            errorView.image = errorImage
+            errorView.contentMode = .scaleAspectFit
+            errorView.heightAnchor.constraint(equalToConstant: 20).isActive = true
+
+            self.statusView.addArrangedSubview(errorView)
+
+        } else if deliveryState == .silent {
+            let silentView = UIImageView(frame: .init(x: 0, y: 0, width: 20, height: 20))
+            var silentImage = UIImage(systemName: "bell.slash")?.withTintColor(.systemGray2).withRenderingMode(.alwaysOriginal)
+            silentImage = silentImage?.withConfiguration(UIImage.SymbolConfiguration(textStyle: .subheadline))
+
+            silentView.image = silentImage
+            silentView.contentMode = .center
+            silentView.heightAnchor.constraint(equalToConstant: 20).isActive = true
+
+            self.statusView.addArrangedSubview(silentView)
+
+        } else if deliveryState == .sent || deliveryState == .read {
+            var checkImageName = "check"
+
+            if deliveryState == .read {
+                checkImageName = "check-all"
+            }
+
+            let checkImage = UIImage(named: checkImageName)?.withRenderingMode(.alwaysTemplate)
+            let checkView = UIImageView(frame: .init(x: 0, y: 0, width: 20, height: 20))
+
+            checkView.image = checkImage
+            checkView.contentMode = .scaleAspectFit
+            checkView.tintColor = .systemGray2
+            checkView.accessibilityIdentifier = "MessageSent"
+            checkView.heightAnchor.constraint(equalToConstant: 20).isActive = true
+
+            self.statusView.addArrangedSubview(checkView)
+        }
+    }
+
+    // MARK: - QuotePart
+
+    func showQuotePart() {
+        self.quotePart.isHidden = false
+
+        if self.quotedMessageView == nil {
+            let quotedMessageView = QuotedMessageView()
+            self.quotedMessageView = quotedMessageView
+
+            quotedMessageView.translatesAutoresizingMaskIntoConstraints = false
+
+            self.quotePart.addSubview(quotedMessageView)
+
+            NSLayoutConstraint.activate([
+                quotedMessageView.leftAnchor.constraint(equalTo: self.messageBodyView.leftAnchor),
+                quotedMessageView.rightAnchor.constraint(equalTo: self.quotePart.rightAnchor, constant: -10),
+                quotedMessageView.topAnchor.constraint(equalTo: self.quotePart.topAnchor),
+                quotedMessageView.bottomAnchor.constraint(equalTo: self.quotePart.bottomAnchor)
+            ])
+
+            let quoteTap = UITapGestureRecognizer(target: self, action: #selector(quoteTapped(_:)))
+            quotedMessageView.addGestureRecognizer(quoteTap)
+        }
+    }
+
+    @objc func quoteTapped(_ sender: UITapGestureRecognizer?) {
+        if let parent = self.message?.parent {
+            self.delegate?.cellWantsToScroll(to: parent)
+        }
+    }
+
+    // MARK: - ReferencePart
+
+    func showReferencePart() {
+        self.referencePart.isHidden = false
+
+        if self.referenceView == nil {
+            let referenceView = ReferenceView()
+            self.referenceView = referenceView
+
+            referenceView.translatesAutoresizingMaskIntoConstraints = false
+
+            self.referencePart.addSubview(referenceView)
+
+            NSLayoutConstraint.activate([
+                referenceView.leftAnchor.constraint(equalTo: self.messageBodyView.leftAnchor),
+                referenceView.rightAnchor.constraint(equalTo: self.referencePart.rightAnchor, constant: -10),
+                referenceView.topAnchor.constraint(equalTo: self.referencePart.topAnchor),
+                referenceView.bottomAnchor.constraint(equalTo: self.referencePart.bottomAnchor, constant: -5)
+            ])
+        }
+    }
+
+    // MARK: - ReactionsPart
+
+    func showReactionsPart() {
+        self.reactionPart.isHidden = false
+
+        if self.reactionView == nil {
+            let flowLayout = UICollectionViewFlowLayout()
+            flowLayout.scrollDirection = .horizontal
+
+            let reactionView = ReactionsView(frame: .init(x: 0, y: 0, width: 50, height: 40), collectionViewLayout: flowLayout)
+            reactionView.reactionsDelegate = self
+            self.reactionView = reactionView
+
+            reactionView.translatesAutoresizingMaskIntoConstraints = false
+
+            self.reactionPart.addSubview(reactionView)
+
+            NSLayoutConstraint.activate([
+                reactionView.leftAnchor.constraint(equalTo: self.messageBodyView.leftAnchor),
+                reactionView.rightAnchor.constraint(equalTo: self.reactionPart.rightAnchor, constant: -10),
+                reactionView.topAnchor.constraint(equalTo: self.reactionPart.topAnchor),
+                reactionView.bottomAnchor.constraint(equalTo: self.reactionPart.bottomAnchor, constant: -10)
+            ])
+        }
+    }
+
+    // MARK: - ReactionsView Delegate
+
+    func didSelectReaction(reaction: NCChatReaction) {
+        if let message = self.message {
+            self.delegate?.cellDidSelectedReaction(reaction, for: message)
+        }
+    }
+
+    // MARK: - Avatar User Menu
+
+    func getDeferredUserMenu() -> UIMenu? {
+        guard let message = self.message else { return nil }
+
+        let activeAccount = NCDatabaseManager.sharedInstance().activeAccount()
+
+        if message.actorType != "users" || message.actorId == activeAccount.userId {
+            return nil
+        }
+
+        // Use an uncached provider so local time is not cached
+        let deferredMenuElement = UIDeferredMenuElement.uncached { completion in
+            self.getMenuUserAction(for: message) { items in
+                completion(items)
+            }
+        }
+
+        return UIMenu(title: message.actorDisplayName, children: [deferredMenuElement])
+    }
+
+    func getMenuUserAction(for message: NCChatMessage, completionBlock: @escaping ([UIMenuElement]) -> Void) {
+        let activeAccount = NCDatabaseManager.sharedInstance().activeAccount()
+
+        NCAPIController.sharedInstance().getUserActions(forUser: message.actorId, using: activeAccount) { userActionsRaw, error in
+            guard error == nil,
+                  let userActionsDict = userActionsRaw as? [String: AnyObject],
+                  let userActions = userActionsDict["actions"] as? [[String: String]],
+                  let userId = userActionsDict["userId"] as? String
+            else {
+                let errorAction = UIAction(title: NSLocalizedString("No actions available", comment: "")) { _ in }
+                errorAction.attributes = .disabled
+                completionBlock([errorAction])
+
+                return
+            }
+
+            var menuItems: [UIMenuElement] = []
+
+            for userAction in userActions {
+                guard let appId = userAction["appId"],
+                      let title = userAction["title"],
+                      let link = userAction["hyperlink"],
+                      let linkEncoded = link.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
+                else { continue }
+
+                if appId == "spreed" {
+                    let talkAction = UIAction(title: title, image: UIImage(named: "talk-20")?.withRenderingMode(.alwaysTemplate)) { _ in
+                        NotificationCenter.default.post(name: NSNotification.Name.NCChatViewControllerTalkToUserNotification, object: self, userInfo: ["actorId": userId])
+                    }
+
+                    menuItems.append(talkAction)
+                    continue
+                }
+
+                let otherAction = UIAction(title: title) { _ in
+                    if let actionUrl = URL(string: linkEncoded) {
+                        UIApplication.shared.open(actionUrl)
+                    }
+                }
+
+                if appId == "profile" {
+                    otherAction.image = UIImage(systemName: "person")
+                } else if appId == "email" {
+                    otherAction.image = UIImage(systemName: "envelope")
+                } else if appId == "timezone" {
+                    otherAction.image = UIImage(systemName: "clock")
+                } else if appId == "social" {
+                    otherAction.image = UIImage(systemName: "heart")
+                }
+
+                menuItems.append(otherAction)
+            }
+
+            completionBlock(menuItems)
+        }
+    }
+
+    // MARK: - File status / activity indicator
+
+    func clearFileStatusView() {
+            self.fileActivityIndicator?.stopAnimating()
+            self.fileActivityIndicator?.removeFromSuperview()
+            self.fileActivityIndicator = nil
+    }
+
+    func addActivityIndicator(with progress: Float) {
+        self.clearFileStatusView()
+
+        let fileActivityIndicator = MDCActivityIndicator(frame: .init(x: 0, y: 0, width: 20, height: 20))
+        self.fileActivityIndicator = fileActivityIndicator
+
+        fileActivityIndicator.radius = 7
+        fileActivityIndicator.cycleColors = [.systemGray2]
+
+        if progress > 0 {
+            fileActivityIndicator.indicatorMode = .determinate
+            fileActivityIndicator.setProgress(progress, animated: false)
+        }
+
+        fileActivityIndicator.startAnimating()
+        fileActivityIndicator.heightAnchor.constraint(equalToConstant: 20).isActive = true
+        self.statusView.addArrangedSubview(fileActivityIndicator)
+    }
+
+    // MARK: - File notifications
+
+    @objc func didChangeIsDownloading(notification: Notification) {
+        DispatchQueue.main.async {
+            // Make sure this notification is really for this cell
+            guard let fileParameter = self.message?.file(),
+                  let receivedStatus = NCChatFileStatus.getStatus(from: notification, for: fileParameter)
+            else { return }
+
+            if receivedStatus.isDownloading, self.fileActivityIndicator == nil {
+                // Immediately show an indeterminate indicator as long as we don't have a progress value
+                self.addActivityIndicator(with: 0)
+            } else if !receivedStatus.isDownloading, self.fileActivityIndicator != nil {
+                self.clearFileStatusView()
+            }
+        }
+    }
+
+    @objc func didChangeDownloadProgress(notification: Notification) {
+        DispatchQueue.main.async {
+            // Make sure this notification is really for this cell
+            guard let fileParameter = self.message?.file(),
+                  let receivedStatus = NCChatFileStatus.getStatus(from: notification, for: fileParameter)
+            else { return }
+
+            if self.fileActivityIndicator != nil {
+                // Switch to determinate-mode and show progress
+                if receivedStatus.canReportProgress {
+                    self.fileActivityIndicator?.indicatorMode = .determinate
+                    self.fileActivityIndicator?.setProgress(Float(receivedStatus.downloadProgress), animated: true)
+                }
+            } else {
+                // Make sure we have an activity indicator added to this cell
+                self.addActivityIndicator(with: Float(receivedStatus.downloadProgress))
+            }
+        }
+    }
+}

+ 148 - 0
NextcloudTalk/BaseChatTableViewCell.xib

@@ -0,0 +1,148 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="32700.99.1234" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
+    <device id="retina6_1" orientation="portrait" appearance="dark"/>
+    <dependencies>
+        <deployment identifier="iOS"/>
+        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22684"/>
+        <capability name="Safe area layout guides" minToolsVersion="9.0"/>
+        <capability name="System colors in document resources" minToolsVersion="11.0"/>
+        <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
+    </dependencies>
+    <objects>
+        <placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
+        <placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
+        <tableViewCell opaque="NO" contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" rowHeight="502" id="KGk-i7-Jjw" userLabel="BaseChatTableViewCell" customClass="BaseChatTableViewCell" customModule="NextcloudTalk" customModuleProvider="target">
+            <rect key="frame" x="0.0" y="0.0" width="422" height="502"/>
+            <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
+            <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="KGk-i7-Jjw" id="H2p-sc-9uM">
+                <rect key="frame" x="0.0" y="0.0" width="422" height="502"/>
+                <autoresizingMask key="autoresizingMask"/>
+                <subviews>
+                    <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="jvd-Fb-cKa">
+                        <rect key="frame" x="0.0" y="0.0" width="422" height="502"/>
+                        <subviews>
+                            <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="3wA-Cg-eLo" userLabel="HeaderPart">
+                                <rect key="frame" x="0.0" y="0.0" width="422" height="40"/>
+                                <subviews>
+                                    <button opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="1000" verticalHuggingPriority="1000" horizontalCompressionResistancePriority="250" verticalCompressionResistancePriority="250" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="PCA-MF-seG" userLabel="AvatarButton" customClass="AvatarButton" customModule="NextcloudTalk" customModuleProvider="target">
+                                        <rect key="frame" x="10" y="10" width="30" height="30"/>
+                                        <constraints>
+                                            <constraint firstAttribute="width" constant="30" id="Qvt-oB-S1j"/>
+                                            <constraint firstAttribute="height" constant="30" id="ZSv-ql-Q0k"/>
+                                        </constraints>
+                                        <inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/>
+                                    </button>
+                                    <label opaque="NO" userInteractionEnabled="NO" contentMode="left" verticalHuggingPriority="251" horizontalCompressionResistancePriority="749" text="A very long author name that does not fit into this cell" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="lOK-xG-raw" userLabel="TitleLabel">
+                                        <rect key="frame" x="50" y="10" width="253" height="30"/>
+                                        <constraints>
+                                            <constraint firstAttribute="width" relation="greaterThanOrEqual" id="tdk-Yc-qpI"/>
+                                        </constraints>
+                                        <fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
+                                        <color key="textColor" systemColor="secondaryLabelColor"/>
+                                        <nil key="highlightedColor"/>
+                                    </label>
+                                    <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="An artificial date" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="obv-lg-PWE" userLabel="DateLabel">
+                                        <rect key="frame" x="313" y="10" width="99" height="30"/>
+                                        <fontDescription key="fontDescription" style="UICTFontTextStyleFootnote"/>
+                                        <color key="textColor" systemColor="secondaryLabelColor"/>
+                                        <nil key="highlightedColor"/>
+                                    </label>
+                                </subviews>
+                                <constraints>
+                                    <constraint firstAttribute="bottom" secondItem="obv-lg-PWE" secondAttribute="bottom" id="6Eu-Ua-CF6"/>
+                                    <constraint firstAttribute="bottom" secondItem="PCA-MF-seG" secondAttribute="bottom" id="7xg-IT-HlF"/>
+                                    <constraint firstItem="obv-lg-PWE" firstAttribute="leading" secondItem="lOK-xG-raw" secondAttribute="trailing" constant="10" id="8uf-1V-IkV"/>
+                                    <constraint firstAttribute="bottom" secondItem="lOK-xG-raw" secondAttribute="bottom" id="Yra-EG-Jgj"/>
+                                    <constraint firstAttribute="trailing" secondItem="obv-lg-PWE" secondAttribute="trailing" constant="10" id="bQo-e7-AIU"/>
+                                    <constraint firstItem="lOK-xG-raw" firstAttribute="top" secondItem="3wA-Cg-eLo" secondAttribute="top" constant="10" id="edY-hn-waS"/>
+                                    <constraint firstItem="obv-lg-PWE" firstAttribute="top" secondItem="3wA-Cg-eLo" secondAttribute="top" constant="10" id="fnX-0E-rsz"/>
+                                    <constraint firstItem="lOK-xG-raw" firstAttribute="leading" secondItem="PCA-MF-seG" secondAttribute="trailing" constant="10" id="i39-1u-6SF"/>
+                                    <constraint firstItem="PCA-MF-seG" firstAttribute="top" secondItem="3wA-Cg-eLo" secondAttribute="top" constant="10" id="lo5-Ho-Lhk"/>
+                                    <constraint firstItem="PCA-MF-seG" firstAttribute="leading" secondItem="3wA-Cg-eLo" secondAttribute="leading" constant="10" id="y0B-TK-xfX"/>
+                                </constraints>
+                            </view>
+                            <view opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Ei6-XM-uBR" userLabel="QuotePart">
+                                <rect key="frame" x="0.0" y="40" width="422" height="60"/>
+                                <constraints>
+                                    <constraint firstAttribute="height" constant="60" id="MNH-XL-wpW"/>
+                                </constraints>
+                            </view>
+                            <view opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="X2q-XX-H1j" userLabel="ContentPart">
+                                <rect key="frame" x="0.0" y="100" width="422" height="257"/>
+                                <subviews>
+                                    <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" distribution="fillProportionally" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="TkE-V6-ePd" userLabel="StatusView">
+                                        <rect key="frame" x="15" y="5" width="20" height="20"/>
+                                        <subviews>
+                                            <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="F90-3N-Ecf" userLabel="PlaceholderForInterfaceBuilder">
+                                                <rect key="frame" x="0.0" y="0.0" width="20" height="20"/>
+                                                <constraints>
+                                                    <constraint firstAttribute="height" constant="20" id="swq-f4-Rot"/>
+                                                </constraints>
+                                            </view>
+                                        </subviews>
+                                        <constraints>
+                                            <constraint firstAttribute="width" constant="20" id="kDD-1M-lbC"/>
+                                        </constraints>
+                                    </stackView>
+                                    <view opaque="NO" contentMode="scaleToFill" horizontalCompressionResistancePriority="1000" verticalCompressionResistancePriority="1000" translatesAutoresizingMaskIntoConstraints="NO" id="THT-eK-wa0" userLabel="MessageBodyView">
+                                        <rect key="frame" x="50" y="5" width="362" height="247"/>
+                                        <constraints>
+                                            <constraint firstAttribute="height" relation="greaterThanOrEqual" id="wmP-Zf-X0i"/>
+                                        </constraints>
+                                    </view>
+                                </subviews>
+                                <constraints>
+                                    <constraint firstItem="TkE-V6-ePd" firstAttribute="leading" secondItem="X2q-XX-H1j" secondAttribute="leading" constant="15" id="0Vd-ed-Org"/>
+                                    <constraint firstAttribute="bottom" secondItem="THT-eK-wa0" secondAttribute="bottom" constant="5" id="BEl-bJ-7y5"/>
+                                    <constraint firstItem="TkE-V6-ePd" firstAttribute="top" secondItem="X2q-XX-H1j" secondAttribute="top" constant="5" id="Btk-Nm-0In"/>
+                                    <constraint firstItem="THT-eK-wa0" firstAttribute="top" secondItem="X2q-XX-H1j" secondAttribute="top" constant="5" id="ZJ3-BO-VvU"/>
+                                    <constraint firstAttribute="height" relation="greaterThanOrEqual" id="pn0-mG-K2F"/>
+                                    <constraint firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="TkE-V6-ePd" secondAttribute="bottom" constant="5" id="vC0-Ra-mVF"/>
+                                    <constraint firstItem="THT-eK-wa0" firstAttribute="leading" secondItem="TkE-V6-ePd" secondAttribute="trailing" constant="15" id="yE9-F6-n6g"/>
+                                    <constraint firstAttribute="trailing" secondItem="THT-eK-wa0" secondAttribute="trailing" constant="10" id="zfI-Sw-bU2"/>
+                                </constraints>
+                            </view>
+                            <view opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="rpi-JE-I3o" userLabel="ReferencePart">
+                                <rect key="frame" x="0.0" y="357" width="422" height="105"/>
+                                <constraints>
+                                    <constraint firstAttribute="height" constant="105" id="9Ar-Lj-ucO"/>
+                                </constraints>
+                            </view>
+                            <view opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="xJ1-vA-lap" userLabel="ReactionsPart">
+                                <rect key="frame" x="0.0" y="462" width="422" height="40"/>
+                                <constraints>
+                                    <constraint firstAttribute="height" constant="40" id="WP6-65-H6V"/>
+                                </constraints>
+                            </view>
+                        </subviews>
+                    </stackView>
+                </subviews>
+                <viewLayoutGuide key="safeArea" id="ML2-GP-tWC"/>
+                <constraints>
+                    <constraint firstItem="jvd-Fb-cKa" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="top" id="7VB-KH-RpM"/>
+                    <constraint firstItem="jvd-Fb-cKa" firstAttribute="leading" secondItem="H2p-sc-9uM" secondAttribute="leading" id="CFK-mC-eqO"/>
+                    <constraint firstAttribute="trailing" secondItem="jvd-Fb-cKa" secondAttribute="trailing" id="eW6-Il-IPV"/>
+                    <constraint firstAttribute="bottom" secondItem="jvd-Fb-cKa" secondAttribute="bottom" id="kra-Sz-kLn"/>
+                </constraints>
+            </tableViewCellContentView>
+            <viewLayoutGuide key="safeArea" id="aW0-zy-SZf"/>
+            <connections>
+                <outlet property="avatarButton" destination="PCA-MF-seG" id="iaG-3y-X5d"/>
+                <outlet property="dateLabel" destination="obv-lg-PWE" id="Fl9-sa-2eI"/>
+                <outlet property="headerPart" destination="3wA-Cg-eLo" id="elk-yT-DZf"/>
+                <outlet property="messageBodyView" destination="THT-eK-wa0" id="EyQ-45-MFn"/>
+                <outlet property="quotePart" destination="Ei6-XM-uBR" id="EAa-7W-KZx"/>
+                <outlet property="reactionPart" destination="xJ1-vA-lap" id="2BH-GI-dXQ"/>
+                <outlet property="referencePart" destination="rpi-JE-I3o" id="8wJ-p7-cEh"/>
+                <outlet property="statusView" destination="TkE-V6-ePd" id="NcS-Q2-IjU"/>
+                <outlet property="titleLabel" destination="lOK-xG-raw" id="efR-bE-d0j"/>
+            </connections>
+            <point key="canvasLocation" x="121.73913043478262" y="79.017857142857139"/>
+        </tableViewCell>
+    </objects>
+    <resources>
+        <systemColor name="secondaryLabelColor">
+            <color red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
+        </systemColor>
+    </resources>
+</document>

+ 3483 - 0
NextcloudTalk/BaseChatViewController.swift

@@ -0,0 +1,3483 @@
+//
+// SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+
+import Foundation
+import NextcloudKit
+import PhotosUI
+import UIKit
+import Realm
+import ContactsUI
+import QuickLook
+
+@objcMembers public class BaseChatViewController: InputbarViewController,
+                                                  UITextFieldDelegate,
+                                                  UIImagePickerControllerDelegate,
+                                                  PHPickerViewControllerDelegate,
+                                                  UINavigationControllerDelegate,
+                                                  PollCreationViewControllerDelegate,
+                                                  ShareLocationViewControllerDelegate,
+                                                  CNContactPickerDelegate,
+                                                  UIDocumentPickerDelegate,
+                                                  VLCKitVideoViewControllerDelegate,
+                                                  ShareViewControllerDelegate,
+                                                  QLPreviewControllerDelegate,
+                                                  QLPreviewControllerDataSource,
+                                                  NCChatFileControllerDelegate,
+                                                  ShareConfirmationViewControllerDelegate,
+                                                  AVAudioRecorderDelegate,
+                                                  AVAudioPlayerDelegate,
+                                                  SystemMessageTableViewCellDelegate,
+                                                  BaseChatTableViewCellDelegate,
+                                                  UITableViewDataSourcePrefetching {
+
+    // MARK: - Internal var
+    internal var messages: [Date: [NCChatMessage]] = [:]
+    internal var dateSections: [Date] = []
+
+    internal var isVisible = false
+    internal var isTyping = false
+    internal var firstUnreadMessage: NCChatMessage?
+    internal var dismissNotificationsOnViewWillDisappear = true
+
+    internal var replyMessageView: ReplyMessageView?
+    internal var voiceMessagesPlayer: AVAudioPlayer?
+    internal var interactingMessage: NCChatMessage?
+    internal var lastMessageBeforeInteraction: IndexPath?
+    internal var contextMenuActionBlock: (() -> Void)?
+    internal var editingMessage: NCChatMessage?
+
+    internal lazy var emojiTextField: EmojiTextField = {
+        let emojiTextField = EmojiTextField()
+        emojiTextField.delegate = self
+
+        self.view.addSubview(emojiTextField)
+
+        return emojiTextField
+    }()
+
+    internal lazy var datePickerTextField: DatePickerTextField = {
+        let datePicker = DatePickerTextField()
+        datePicker.delegate = self
+
+        self.view.addSubview(datePicker)
+
+        return datePicker
+    }()
+
+    internal lazy var chatBackgroundView: PlaceholderView = {
+        let chatBackgroundView = PlaceholderView()
+        chatBackgroundView.placeholderView.isHidden = true
+        chatBackgroundView.loadingView.startAnimating()
+        chatBackgroundView.placeholderTextView.text = NSLocalizedString("No messages yet, start the conversation!", comment: "")
+        chatBackgroundView.setImage(UIImage(named: "chat-placeholder"))
+
+        return chatBackgroundView
+    }()
+
+    // MARK: - Private var
+    private var sendButtonTagMessage = 99
+    private var sendButtonTagVoice = 98
+
+    private var actionTypeTranscribeVoiceMessage = "transcribe-voice-message"
+
+    private var imagePicker: UIImagePickerController?
+
+    private var stopTypingTimer: Timer?
+    private var typingTimer: Timer?
+    private var voiceMessageLongPressGesture: UILongPressGestureRecognizer?
+    private var recorder: AVAudioRecorder?
+    private var voiceMessageRecordingView: VoiceMessageRecordingView?
+    private var longPressStartingPoint: CGPoint?
+    private var cancelHintLabelInitialPositionX: CGFloat?
+    private var recordCancelled: Bool = false
+
+    private var animationDispatchGroup = DispatchGroup()
+    private var animationDispatchQueue = DispatchQueue(label: "\(groupIdentifier).animationQueue")
+
+    private var loadingHistoryView: UIActivityIndicatorView?
+
+    private var isPreviewControllerShown: Bool = false
+    private var previewControllerFilePath: String?
+
+    private var playerProgressTimer: Timer?
+    private var playerAudioFileStatus: NCChatFileStatus?
+
+    private var photoPicker: PHPickerViewController?
+
+    private var contextMenuAccessoryView: UIView?
+    private var contextMenuMessageView: UIView?
+
+    private lazy var inputbarBorderView: UIView = {
+        let inputbarBorderView = UIView()
+        inputbarBorderView.autoresizingMask = [.flexibleWidth, .flexibleBottomMargin]
+        inputbarBorderView.frame = .init(x: 0, y: 0, width: self.textInputbar.frame.size.width, height: 1)
+        inputbarBorderView.isHidden = true
+        inputbarBorderView.backgroundColor = .systemGray6
+
+        self.textInputbar.addSubview(inputbarBorderView)
+
+        return inputbarBorderView
+    }()
+
+    private lazy var unreadMessageButton: UIButton = {
+        let unreadMessageButton = UIButton(frame: .init(x: 0, y: 0, width: 126, height: 24))
+
+        unreadMessageButton.backgroundColor = NCAppBranding.themeColor()
+        unreadMessageButton.setTitleColor(NCAppBranding.themeTextColor(), for: .normal)
+        unreadMessageButton.titleLabel?.font = UIFont.systemFont(ofSize: 12)
+        unreadMessageButton.layer.cornerRadius = 12
+        unreadMessageButton.clipsToBounds = true
+        unreadMessageButton.isHidden = true
+        unreadMessageButton.translatesAutoresizingMaskIntoConstraints = false
+        unreadMessageButton.contentEdgeInsets = .init(top: 0, left: 10, bottom: 0, right: 10)
+        unreadMessageButton.titleLabel?.minimumScaleFactor = 0.9
+        unreadMessageButton.titleLabel?.numberOfLines = 1
+        unreadMessageButton.titleLabel?.adjustsFontSizeToFitWidth = true
+        unreadMessageButton.setTitle(NSLocalizedString("↓ New messages", comment: ""), for: .normal)
+
+        unreadMessageButton.addAction { [weak self] in
+            guard let self,
+                  let firstUnreadMessage = self.firstUnreadMessage,
+                  let indexPath = self.indexPath(for: firstUnreadMessage)
+            else { return }
+
+            self.tableView?.scrollToRow(at: indexPath, at: .none, animated: true)
+        }
+
+        self.view.addSubview(unreadMessageButton)
+
+        return unreadMessageButton
+    }()
+
+    private lazy var scrollToBottomButton: UIButton = {
+        let button = UIButton(frame: .init(x: 0, y: 0, width: 44, height: 44), primaryAction: UIAction { [weak self] _ in
+            self?.tableView?.slk_scrollToBottom(animated: true)
+        })
+
+        button.backgroundColor = .secondarySystemBackground
+        button.tintColor = .systemBlue
+        button.layer.cornerRadius = button.frame.size.height / 2
+        button.clipsToBounds = true
+        button.alpha = 0
+        button.translatesAutoresizingMaskIntoConstraints = false
+        button.setImage(UIImage(systemName: "chevron.down"), for: .normal)
+
+        self.view.addSubview(button)
+
+        return button
+    }()
+
+    // MARK: - Init/Deinit
+
+    public init?(for room: NCRoom) {
+        super.init(for: room, tableViewStyle: .plain)
+
+        self.hidesBottomBarWhenPushed = true
+        self.tableView?.estimatedRowHeight = 0
+        self.tableView?.estimatedSectionHeaderHeight = 0
+        self.tableView?.prefetchDataSource = self
+
+        FilePreviewImageView.setSharedImageDownloader(NCAPIController.sharedInstance().imageDownloader)
+        NotificationCenter.default.addObserver(self, selector: #selector(willShowKeyboard(notification:)), name: UIWindow.keyboardWillShowNotification, object: nil)
+        NotificationCenter.default.addObserver(self, selector: #selector(willHideKeyboard(notification:)), name: UIWindow.keyboardWillHideNotification, object: nil)
+
+        AllocationTracker.shared.addAllocation("ChatViewController")
+    }
+
+    // Not using an optional here, because it is not available from ObjC
+    // Pass "0" as highlightMessageId to not highlight a message
+    public convenience init?(for room: NCRoom, withMessage messages: [NCChatMessage], withHighlightId highlightMessageId: Int) {
+        self.init(for: room)
+
+        // When we pass in a fixed number of messages, we hide the inputbar by default
+        self.textInputbar.isHidden = true
+
+        // Scroll to bottom manually after hiding the textInputbar, otherwise the
+        // scrollToBottom button might be briefly visible even if not needed
+        self.tableView?.slk_scrollToBottom(animated: false)
+
+        self.appendMessages(messages: messages)
+
+        self.tableView?.performBatchUpdates({
+            self.tableView?.reloadData()
+        }, completion: { _ in
+            self.highlightMessageWithContentOffset(messageId: highlightMessageId)
+        })
+    }
+
+    required init?(coder decoder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+
+    deinit {
+        NotificationCenter.default.removeObserver(self)
+        AllocationTracker.shared.removeAllocation("ChatViewController")
+        NSLog("Dealloc BaseChatViewController")
+    }
+
+    // MARK: - View lifecycle
+
+    public override func viewDidLoad() {
+        super.viewDidLoad()
+
+        self.shouldScrollToBottomAfterKeyboardShows = false
+        self.isInverted = false
+
+        self.showSendMessageButton()
+        self.leftButton.setImage(UIImage(systemName: "paperclip"), for: .normal)
+        self.leftButton.accessibilityLabel = NSLocalizedString("Share a file from your Nextcloud", comment: "")
+        self.leftButton.accessibilityHint = NSLocalizedString("Double tap to open file browser", comment: "")
+
+        // Set delegate to retrieve typing events
+        self.tableView?.separatorStyle = .none
+
+        self.tableView?.register(UINib(nibName: "BaseChatTableViewCell", bundle: nil), forCellReuseIdentifier: chatMessageCellIdentifier)
+        self.tableView?.register(UINib(nibName: "BaseChatTableViewCell", bundle: nil), forCellReuseIdentifier: chatGroupedMessageCellIdentifier)
+        self.tableView?.register(UINib(nibName: "BaseChatTableViewCell", bundle: nil), forCellReuseIdentifier: chatReplyMessageCellIdentifier)
+
+        self.tableView?.register(UINib(nibName: "BaseChatTableViewCell", bundle: nil), forCellReuseIdentifier: fileMessageCellIdentifier)
+        self.tableView?.register(UINib(nibName: "BaseChatTableViewCell", bundle: nil), forCellReuseIdentifier: fileGroupedMessageCellIdentifier)
+
+        self.tableView?.register(UINib(nibName: "BaseChatTableViewCell", bundle: nil), forCellReuseIdentifier: locationMessageCellIdentifier)
+        self.tableView?.register(UINib(nibName: "BaseChatTableViewCell", bundle: nil), forCellReuseIdentifier: locationGroupedMessageCellIdentifier)
+
+        self.tableView?.register(UINib(nibName: "BaseChatTableViewCell", bundle: nil), forCellReuseIdentifier: voiceMessageCellIdentifier)
+        self.tableView?.register(UINib(nibName: "BaseChatTableViewCell", bundle: nil), forCellReuseIdentifier: voiceGroupedMessageCellIdentifier)
+
+        self.tableView?.register(UINib(nibName: "BaseChatTableViewCell", bundle: nil), forCellReuseIdentifier: pollMessageCellIdentifier)
+        self.tableView?.register(UINib(nibName: "BaseChatTableViewCell", bundle: nil), forCellReuseIdentifier: pollGroupedMessageCellIdentifier)
+
+        self.tableView?.register(SystemMessageTableViewCell.self, forCellReuseIdentifier: SystemMessageCellIdentifier)
+        self.tableView?.register(SystemMessageTableViewCell.self, forCellReuseIdentifier: InvisibleSystemMessageCellIdentifier)
+        self.tableView?.register(MessageSeparatorTableViewCell.self, forCellReuseIdentifier: MessageSeparatorCellIdentifier)
+
+        let newMessagesButtonText = NSLocalizedString("↓ New messages", comment: "")
+
+        // Need to move down to NSLayout
+        let attributes = [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 12)]
+        let textSize = NSString(string: newMessagesButtonText).boundingRect(with: .init(width: 300, height: 24), options: .usesLineFragmentOrigin, attributes: attributes, context: nil)
+        let buttonWidth = textSize.size.width + 20
+
+        let views = [
+            "unreadMessageButton": self.unreadMessageButton,
+            "textInputbar": self.textInputbar,
+            "scrollToBottomButton": self.scrollToBottomButton,
+            "autoCompletionView": self.autoCompletionView
+        ]
+
+        let metrics = [
+            "buttonWidth": buttonWidth
+        ]
+
+        self.view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:[unreadMessageButton(24)]-5-[autoCompletionView]", metrics: metrics, views: views))
+        self.view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|-(>=0)-[unreadMessageButton(buttonWidth)]-(>=0)-|", metrics: metrics, views: views))
+
+        if let view = self.view {
+            self.view.addConstraint(NSLayoutConstraint(item: view, attribute: .centerX, relatedBy: .equal, toItem: self.unreadMessageButton, attribute: .centerX, multiplier: 1, constant: 0))
+        }
+
+        self.view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:[scrollToBottomButton(44)]-10-[autoCompletionView]", metrics: metrics, views: views))
+        self.view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|-(>=0)-[scrollToBottomButton(44)]-(>=0)-|", metrics: metrics, views: views))
+
+        self.scrollToBottomButton.trailingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.trailingAnchor, constant: -10).isActive = true
+
+        self.addMenuToLeftButton()
+
+        self.replyMessageView?.addObserver(self, forKeyPath: "visible", options: .new, context: nil)
+    }
+
+    // swiftlint:disable:next block_based_kvo
+    public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) {
+        super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
+
+        guard let object = object as? ReplyMessageView,
+              object == self.replyMessageView else { return }
+
+        // When the visible state of the replyMessageView changes, we need to update the toolbar to show the correct border
+        // Only do this if we are not already at the bottom, otherwise we briefly show the scroll button directly after sending a message
+        self.updateToolbar(animated: true)
+    }
+
+    public func updateToolbar(animated: Bool) {
+        guard let tableView else { return }
+
+        let animations = {
+            let minimumOffset = (tableView.contentSize.height - tableView.frame.size.height) - 10
+
+            if tableView.contentOffset.y < minimumOffset {
+                // Scrolled -> show top border
+
+                // When a reply view is visible, we show the border of that view
+                if let replyMessageView = self.replyMessageView {
+                    replyMessageView.topBorder.isHidden = !replyMessageView.isVisible
+                    self.inputbarBorderView.isHidden = replyMessageView.isVisible
+                } else {
+                    self.inputbarBorderView.isHidden = false
+                }
+            } else {
+                // At the bottom -> no top border
+                self.inputbarBorderView.isHidden = true
+
+                if let replyMessageView = self.replyMessageView {
+                    replyMessageView.topBorder.isHidden = true
+                }
+            }
+        }
+
+        let animationsScrollButton = {
+            let minimumOffset = (tableView.contentSize.height - tableView.frame.size.height) - 10
+
+            if tableView.contentOffset.y < minimumOffset {
+                // Scrolled -> show button
+                self.scrollToBottomButton.alpha = 1
+            } else {
+                // At the bottom -> hide button
+                self.scrollToBottomButton.alpha = 0
+            }
+        }
+
+        if animated {
+            self.animationDispatchQueue.async {
+                self.animationDispatchGroup.enter()
+                self.animationDispatchGroup.enter()
+
+                DispatchQueue.main.async {
+                    UIView.transition(with: self.textInputbar,
+                                      duration: 0.3,
+                                      options: .transitionCrossDissolve,
+                                      animations: animations) { _ in
+                        self.animationDispatchGroup.leave()
+                    }
+                }
+
+                DispatchQueue.main.async {
+                    UIView.animate(withDuration: 0.3,
+                                   animations: animationsScrollButton) { _ in
+                        self.animationDispatchGroup.leave()
+                    }
+                }
+
+                _ = self.animationDispatchGroup.wait(timeout: .distantFuture)
+            }
+        } else {
+            DispatchQueue.main.async {
+                animations()
+                animationsScrollButton()
+            }
+        }
+    }
+
+    public override func viewWillAppear(_ animated: Bool) {
+        super.viewWillAppear(animated)
+
+        self.isVisible = true
+    }
+
+    public override func viewWillDisappear(_ animated: Bool) {
+        super.viewWillDisappear(animated)
+
+        self.isVisible = false
+
+        if !self.textInputbar.isHidden {
+            self.savePendingMessage()
+        }
+
+        if dismissNotificationsOnViewWillDisappear {
+            NotificationPresenter.shared().dismiss(animated: false)
+        }
+    }
+
+    public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
+        super.traitCollectionDidChange(previousTraitCollection)
+
+        if self.traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) {
+            self.updateToolbar(animated: true)
+        }
+    }
+
+    // MARK: - Keyboard notifications
+
+    func willShowKeyboard(notification: Notification) {
+        guard let currentResponder = UIResponder.slk_currentFirst() else { return }
+
+        // Skip if it's not the emoji/date text field
+        if !currentResponder.isKind(of: EmojiTextField.self) && !currentResponder.isKind(of: DatePickerTextField.self) {
+            return
+        }
+
+        guard let keyboardFrame: NSValue = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue else {
+            return
+        }
+
+        let keyboardRect = keyboardFrame.cgRectValue
+        self.updateView(toShowOrHideEmojiKeyboard: keyboardRect.size.height)
+
+        guard let interactingMessage,
+              let indexPath = self.indexPath(for: interactingMessage) else {
+            return
+        }
+
+        DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) {
+            if let tableView = self.tableView {
+                let cellRect = tableView.rectForRow(at: indexPath)
+
+                if !tableView.bounds.contains(cellRect) {
+                    self.tableView?.scrollToRow(at: indexPath, at: .bottom, animated: true)
+                }
+            }
+        }
+    }
+
+    func willHideKeyboard(notification: Notification) {
+        guard let currentResponder = UIResponder.slk_currentFirst() else { return }
+
+        // Skip if it's not the emoji/date text field
+        if !currentResponder.isKind(of: EmojiTextField.self) && !currentResponder.isKind(of: DatePickerTextField.self) {
+            return
+        }
+
+        self.updateView(toShowOrHideEmojiKeyboard: 0.0)
+
+        guard let lastMessageBeforeInteraction, let tableView else { return }
+
+        if NCUtils.isValid(indexPath: lastMessageBeforeInteraction, forTableView: tableView) {
+            DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) {
+                tableView.scrollToRow(at: lastMessageBeforeInteraction, at: .bottom, animated: true)
+            }
+        }
+    }
+
+    // MARK: - Utils
+
+    internal func getHeaderString(fromDate date: Date) -> String {
+        let formatter = DateFormatter()
+        formatter.dateStyle = .medium
+        formatter.doesRelativeDateFormatting = true
+
+        return formatter.string(from: date)
+    }
+
+    internal func presentWithNavigation(_ viewControllerToPresent: UIViewController, animated flag: Bool) {
+        self.present(NCNavigationController(rootViewController: viewControllerToPresent), animated: flag)
+    }
+
+    // MARK: - Temporary messages
+
+    internal func createTemporaryMessage(message: String, replyTo parentMessage: NCChatMessage?, messageParameters: String, silently: Bool) -> NCChatMessage {
+        let temporaryMessage = NCChatMessage()
+        let activeAccount = NCDatabaseManager.sharedInstance().activeAccount()
+
+        temporaryMessage.accountId = activeAccount.accountId
+        temporaryMessage.actorDisplayName = activeAccount.userDisplayName
+        temporaryMessage.actorId = activeAccount.userId
+        temporaryMessage.actorType = "users"
+        temporaryMessage.timestamp = Int(Date().timeIntervalSince1970)
+        temporaryMessage.token = room.token
+        temporaryMessage.message = self.replaceMentionsDisplayNamesWithMentionsKeysInMessage(message: message, parameters: messageParameters)
+
+        let referenceId = "temp-\(Date().timeIntervalSince1970 * 1000)"
+        temporaryMessage.referenceId = NCUtils.sha1(fromString: referenceId)
+        temporaryMessage.internalId = referenceId
+        temporaryMessage.isTemporary = true
+        temporaryMessage.parentId = parentMessage?.internalId
+        temporaryMessage.messageParametersJSONString = messageParameters
+        temporaryMessage.isSilent = silently
+        temporaryMessage.isMarkdownMessage = NCDatabaseManager.sharedInstance().roomHasTalkCapability(kCapabilityMarkdownMessages, for: self.room)
+
+        let realm = RLMRealm.default()
+
+        try? realm.transaction {
+            realm.add(temporaryMessage)
+        }
+
+        let unmanagedTemporaryMessage = NCChatMessage(value: temporaryMessage)
+        return unmanagedTemporaryMessage
+    }
+
+    internal func replaceMessageMentionsKeysWithMentionsDisplayNames(message: String, parameters: String) -> String {
+        var resultMessage = message.trimmingCharacters(in: .whitespacesAndNewlines)
+
+        guard let messageParametersDict = NCMessageParameter.messageParametersDict(fromJSONString: parameters) else { return resultMessage }
+
+        for (parameterKey, parameter) in messageParametersDict {
+            let parameterKeyString = "{\(parameterKey)}"
+            resultMessage = resultMessage.replacingOccurrences(of: parameterKeyString, with: parameter.mentionDisplayName)
+        }
+
+        return resultMessage
+    }
+
+    internal func appendTemporaryMessage(temporaryMessage: NCChatMessage) {
+        DispatchQueue.main.async {
+            let lastSectionBeforeUpdate = self.dateSections.count - 1
+
+            self.appendMessages(messages: [temporaryMessage])
+
+            if let lastDateSection = self.dateSections.last, let messagesForLastDate = self.messages[lastDateSection] {
+                let lastMessageIndexPath = IndexPath(row: messagesForLastDate.count - 1, section: self.dateSections.count - 1)
+
+                self.tableView?.beginUpdates()
+
+                let newLastSection = self.dateSections.count - 1
+                if lastSectionBeforeUpdate != newLastSection {
+                    self.tableView?.insertSections(.init(integer: newLastSection), with: .none)
+                } else {
+                    self.tableView?.insertRows(at: [lastMessageIndexPath], with: .none)
+                }
+
+                self.tableView?.endUpdates()
+                self.tableView?.scrollToRow(at: lastMessageIndexPath, at: .none, animated: true)
+            }
+        }
+    }
+
+    internal func removePermanentlyTemporaryMessage(temporaryMessage: NCChatMessage) {
+        let realm = RLMRealm.default()
+
+        try? realm.transaction {
+            if let managedTemporaryMessage = NCChatMessage.objects(where: "referenceId = %@ AND isTemporary = true", temporaryMessage.referenceId).firstObject() {
+                realm.delete(managedTemporaryMessage)
+            }
+        }
+
+        self.removeTemporaryMessages(temporaryMessages: [temporaryMessage])
+    }
+
+    internal func removeTemporaryMessages(temporaryMessages: [NCChatMessage]) {
+        DispatchQueue.main.async {
+            for temporaryMessage in temporaryMessages {
+                if let indexPath = self.indexPath(for: temporaryMessage) {
+                    self.removeMessage(at: indexPath)
+                }
+            }
+        }
+    }
+
+    // MARK: - Message updates
+
+    internal func modifyMessageWith(referenceId: String, block: (NCChatMessage) -> Void) {
+        guard let (indexPath, message) = self.indexPathAndMessage(forReferenceId: referenceId)
+        else { return }
+
+        block(message)
+
+        self.tableView?.beginUpdates()
+        self.tableView?.reloadRows(at: [indexPath], with: .none)
+        self.tableView?.endUpdates()
+    }
+
+    internal func updateMessage(withMessageId messageId: Int, updatedMessage: NCChatMessage) {
+        DispatchQueue.main.async {
+            guard let (indexPath, message) = self.indexPathAndMessage(forMessageId: messageId) else { return }
+            var reloadIndexPaths = [indexPath]
+
+            let isAtBottom = self.shouldScrollOnNewMessages()
+            let keyDate = self.dateSections[indexPath.section]
+            updatedMessage.isGroupMessage = message.isGroupMessage && message.actorType != "bots" && updatedMessage.lastEditTimestamp == 0
+            self.messages[keyDate]?[indexPath.row] = updatedMessage
+
+            // Check if there are any messages that reference our message as a parent -> these need to be reloaded as well
+            if let visibleIndexPaths = self.tableView?.indexPathsForVisibleRows {
+                let referencingIndexPaths = visibleIndexPaths.filter({
+                    guard let message = self.message(for: $0),
+                          let parentMessage = message.parent
+                    else { return false }
+
+                    return parentMessage.messageId == messageId
+                })
+
+                reloadIndexPaths.append(contentsOf: referencingIndexPaths)
+            }
+
+            self.tableView?.beginUpdates()
+            self.tableView?.reloadRows(at: reloadIndexPaths, with: .none)
+            self.tableView?.endUpdates()
+
+            if isAtBottom {
+                // Make sure we're really at the bottom after updating a message
+                DispatchQueue.main.async {
+                    self.tableView?.slk_scrollToBottom(animated: false)
+                    self.updateToolbar(animated: false)
+                }
+            }
+        }
+    }
+
+    // MARK: - User interface
+
+    func showVoiceMessageRecordButton() {
+        self.rightButton.setTitle("", for: .normal)
+        self.rightButton.setImage(UIImage(systemName: "mic"), for: .normal)
+        self.rightButton.tag = sendButtonTagVoice
+        self.rightButton.accessibilityLabel = NSLocalizedString("Record voice message", comment: "")
+        self.rightButton.accessibilityHint = NSLocalizedString("Tap and hold to record a voice message", comment: "")
+
+        self.addGestureRecognizerToRightButton()
+    }
+
+    func showSendMessageButton() {
+        self.rightButton.setTitle("", for: .normal)
+        self.rightButton.setImage(UIImage(systemName: "paperplane"), for: .normal)
+        self.rightButton.tag = sendButtonTagMessage
+        self.rightButton.accessibilityLabel = NSLocalizedString("Send message", comment: "")
+        self.rightButton.accessibilityHint = NSLocalizedString("Double tap to send message", comment: "")
+
+        self.addMenuToRightButton()
+    }
+
+    // MARK: - Action methods
+
+    func sendChatMessage(message: String, withParentMessage parentMessage: NCChatMessage?, messageParameters: String, silently: Bool) {
+        // Overridden in sub class
+    }
+
+    func sendCurrentMessage(silently: Bool) {
+        var replyToMessage: NCChatMessage?
+
+        if let replyMessageView, replyMessageView.isVisible {
+            replyToMessage = replyMessageView.message
+        }
+
+        let messageParameters = NCMessageParameter.messageParametersJSONString(from: self.mentionsDict) ?? ""
+        self.sendChatMessage(message: self.textView.text, withParentMessage: replyToMessage, messageParameters: messageParameters, silently: silently)
+
+        self.mentionsDict.removeAll()
+        self.replyMessageView?.dismiss()
+        super.didPressRightButton(self)
+        self.clearPendingMessage()
+        self.stopTyping(force: true)
+    }
+
+    public override func didPressRightButton(_ sender: Any?) {
+        guard let button = sender as? UIButton else { return }
+
+        switch button.tag {
+        case sendButtonTagMessage:
+            self.sendCurrentMessage(silently: false)
+            super.didPressRightButton(sender)
+        case sendButtonTagVoice:
+            self.showVoiceMessageRecordHint()
+        default:
+            break
+        }
+    }
+
+    func addGestureRecognizerToRightButton() {
+        // Remove a potential menu so it does not interfere with the long gesture recognizer
+        self.rightButton.menu = nil
+
+        // Add long press gesture recognizer for voice message recording button
+        self.voiceMessageLongPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPressInVoiceMessageRecordButton(gestureRecognizer:)))
+
+        if let voiceMessageLongPressGesture {
+            voiceMessageLongPressGesture.delegate = self
+            self.rightButton.addGestureRecognizer(voiceMessageLongPressGesture)
+        }
+    }
+
+    func addMenuToRightButton() {
+        // Remove a gesture recognizer to not interfere with our menu
+        if let voiceMessageLongPressGesture = self.voiceMessageLongPressGesture {
+            self.rightButton.removeGestureRecognizer(voiceMessageLongPressGesture)
+            self.voiceMessageLongPressGesture = nil
+        }
+
+        let silentSendAction = UIAction(title: NSLocalizedString("Send without notification", comment: ""), image: UIImage(systemName: "bell.slash")) { [unowned self] _ in
+            self.sendCurrentMessage(silently: true)
+        }
+
+        self.rightButton.menu = UIMenu(children: [silentSendAction])
+    }
+
+    func addMenuToLeftButton() {
+        // The keyboard will be hidden when an action is invoked. Depending on what
+        // attachment is shared, not resigning might lead to a currupted chat view
+        var items: [UIAction] = []
+
+        let cameraAction = UIAction(title: NSLocalizedString("Camera", comment: ""), image: UIImage(systemName: "camera")) { [unowned self] _ in
+            self.textView.resignFirstResponder()
+            self.checkAndPresentCamera()
+        }
+
+        let photoLibraryAction = UIAction(title: NSLocalizedString("Photo Library", comment: ""), image: UIImage(systemName: "photo")) { [unowned self] _ in
+            self.textView.resignFirstResponder()
+            self.presentPhotoLibrary()
+        }
+
+        let shareLocationAction = UIAction(title: NSLocalizedString("Location", comment: ""), image: UIImage(systemName: "location")) { [unowned self] _ in
+            self.textView.resignFirstResponder()
+            self.presentShareLocation()
+        }
+
+        let contactShareAction = UIAction(title: NSLocalizedString("Contacts", comment: ""), image: UIImage(systemName: "person")) { [unowned self] _ in
+            self.textView.resignFirstResponder()
+            self.presentShareContact()
+        }
+
+        let filesAction = UIAction(title: NSLocalizedString("Files", comment: ""), image: UIImage(systemName: "doc")) { [unowned self] _ in
+            self.textView.resignFirstResponder()
+            self.presentDocumentPicker()
+        }
+
+        let ncFilesAction = UIAction(title: filesAppName, image: UIImage(named: "logo-action")?.withRenderingMode(.alwaysTemplate)) { [unowned self] _ in
+            self.textView.resignFirstResponder()
+            self.presentNextcloudFilesBrowser()
+        }
+
+        let pollAction = UIAction(title: NSLocalizedString("Poll", comment: ""), image: UIImage(systemName: "chart.bar")) { [unowned self] _ in
+            self.textView.resignFirstResponder()
+            self.presentPollCreation()
+        }
+
+        // Add actions (inverted)
+        items.append(ncFilesAction)
+        items.append(filesAction)
+        items.append(contactShareAction)
+
+        if NCDatabaseManager.sharedInstance().roomHasTalkCapability(kCapabilityLocationSharing, for: self.room) {
+            items.append(shareLocationAction)
+        }
+
+        if NCDatabaseManager.sharedInstance().roomHasTalkCapability(kCapabilityTalkPolls, for: self.room),
+            self.room.type != .oneToOne, self.room.type != .noteToSelf {
+
+            items.append(pollAction)
+        }
+
+        items.append(photoLibraryAction)
+
+        if UIImagePickerController.isSourceTypeAvailable(.camera) {
+            items.append(cameraAction)
+        }
+
+        self.leftButton.menu = UIMenu(children: items)
+        self.leftButton.showsMenuAsPrimaryAction = true
+    }
+
+    func presentNextcloudFilesBrowser() {
+        let directoryVC = DirectoryTableViewController(path: "", inRoom: self.room.token)
+        self.presentWithNavigation(directoryVC, animated: true)
+    }
+
+    func checkAndPresentCamera() {
+        // https://stackoverflow.com/a/20464727/2512312
+        let mediaType = AVMediaType.video
+        let authStatus = AVCaptureDevice.authorizationStatus(for: mediaType)
+
+        if authStatus == AVAuthorizationStatus.authorized {
+            self.presentCamera()
+            return
+        } else if authStatus == AVAuthorizationStatus.notDetermined {
+            AVCaptureDevice.requestAccess(for: mediaType, completionHandler: { (granted: Bool) in
+                if granted {
+                    self.presentCamera()
+                }
+            })
+            return
+        }
+
+        let alert = UIAlertController(title: NSLocalizedString("Could not access camera", comment: ""),
+                                      message: NSLocalizedString("Camera access is not allowed. Check your settings.", comment: ""),
+                                      preferredStyle: .alert)
+
+        alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default))
+        NCUserInterfaceController.sharedInstance().presentAlertViewController(alert)
+    }
+
+    func presentCamera() {
+        DispatchQueue.main.async {
+            self.imagePicker = UIImagePickerController()
+
+            if let imagePicker = self.imagePicker,
+                let sourceType = UIImagePickerController.availableMediaTypes(for: imagePicker.sourceType) {
+                imagePicker.sourceType = .camera
+                imagePicker.cameraFlashMode = UIImagePickerController.CameraFlashMode(rawValue: NCUserDefaults.preferredCameraFlashMode()) ?? .off
+                imagePicker.mediaTypes = sourceType
+                imagePicker.delegate = self
+                self.present(imagePicker, animated: true)
+            }
+        }
+    }
+
+    func presentPhotoLibrary() {
+        DispatchQueue.main.async {
+            var pickerConfig = PHPickerConfiguration()
+            pickerConfig.selectionLimit = 5
+            pickerConfig.filter = PHPickerFilter.any(of: [.images, .videos])
+
+            self.photoPicker = PHPickerViewController(configuration: pickerConfig)
+
+            if let photoPicker = self.photoPicker {
+                photoPicker.delegate = self
+                self.present(photoPicker, animated: true)
+            }
+        }
+    }
+
+    func presentPollCreation() {
+        let pollCreationVC = PollCreationViewController(style: .insetGrouped)
+        pollCreationVC.pollCreationDelegate = self
+        self.presentWithNavigation(pollCreationVC, animated: true)
+    }
+
+    func presentShareLocation() {
+        let shareLocationVC = ShareLocationViewController()
+        shareLocationVC.delegate = self
+        self.presentWithNavigation(shareLocationVC, animated: true)
+    }
+
+    func presentShareContact() {
+        let contactPicker = CNContactPickerViewController()
+        contactPicker.delegate = self
+        self.present(contactPicker, animated: true)
+    }
+
+    func presentDocumentPicker() {
+        DispatchQueue.main.async {
+            let documentPicker = UIDocumentPickerViewController(forOpeningContentTypes: [.item], asCopy: true)
+            documentPicker.delegate = self
+            self.present(documentPicker, animated: true)
+        }
+    }
+
+    func showReplyView(for message: NCChatMessage) {
+        let isAtBottom = self.shouldScrollOnNewMessages()
+        let activeAccount = NCDatabaseManager.sharedInstance().activeAccount()
+
+        if let replyProxyView = self.replyProxyView as? ReplyMessageView {
+            self.replyMessageView = replyProxyView
+
+            replyProxyView.presentReply(with: message, withUserId: activeAccount.userId)
+            self.presentKeyboard(true)
+
+            // Make sure we're really at the bottom after showing the replyMessageView
+            if isAtBottom {
+                self.tableView?.slk_scrollToBottom(animated: false)
+                self.updateToolbar(animated: false)
+            }
+        }
+    }
+
+    func didPressReply(for message: NCChatMessage) {
+        // Make sure we get a smooth animation after dismissing the context menu
+        DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
+            self.showReplyView(for: message)
+        }
+    }
+
+    func didPressReplyPrivately(for message: NCChatMessage) {
+        var userInfo: [String: String] = [:]
+        userInfo["actorId"] = message.actorId
+        NotificationCenter.default.post(name: .NCChatViewControllerReplyPrivatelyNotification, object: self, userInfo: userInfo)
+    }
+
+    func didPressAddReaction(for message: NCChatMessage, at indexPath: IndexPath) {
+        // Hide the keyboard because we are going to present the emoji keyboard
+        DispatchQueue.main.async {
+            self.textView.resignFirstResponder()
+        }
+
+        DispatchQueue.main.async {
+            self.interactingMessage = message
+            self.lastMessageBeforeInteraction = self.tableView?.indexPathsForVisibleRows?.last
+
+            if NCUtils.isiOSAppOnMac() {
+                // Move the emojiTextField to the position of the cell
+                if let rowRect = self.tableView?.rectForRow(at: indexPath),
+                   var convertedRowRect = self.tableView?.convert(rowRect, to: self.view) {
+
+                    // Show the emoji picker at the textView location of the cell
+                    convertedRowRect.origin.y += convertedRowRect.size.height - 16
+                    convertedRowRect.origin.x += 54
+
+                    // We don't want to have a clickable textField floating around
+                    convertedRowRect.size.width = 0
+                    convertedRowRect.size.height = 0
+
+                    // Remove and add the emojiTextField to the view, so the Mac OS emoji picker is always at the right location
+                    self.emojiTextField.removeFromSuperview()
+                    self.emojiTextField.frame = convertedRowRect
+                    self.view.addSubview(self.emojiTextField)
+                }
+            }
+
+            self.emojiTextField.becomeFirstResponder()
+        }
+    }
+
+    func didPressForward(for message: NCChatMessage) {
+        var shareViewController: ShareViewController
+
+        if message.isObjectShare {
+            shareViewController = ShareViewController(toForwardObjectShare: message, fromChatViewController: self)
+        } else {
+            shareViewController = ShareViewController(toForwardMessage: message.parsedMessage().string, fromChatViewController: self)
+        }
+
+        shareViewController.delegate = self
+        self.presentWithNavigation(shareViewController, animated: true)
+    }
+
+    func didPressNoteToSelf(for message: NCChatMessage) {
+        let activeAccount = NCDatabaseManager.sharedInstance().activeAccount()
+
+        NCAPIController.sharedInstance().getNoteToSelfRoom(forAccount: activeAccount) { roomDict, error in
+            if error == nil, let room = NCRoom(dictionary: roomDict, andAccountId: activeAccount.accountId) {
+
+                if message.isObjectShare {
+                    NCAPIController.sharedInstance().shareRichObject(message.richObjectFromObjectShare, inRoom: room.token, for: activeAccount) { error in
+                        if error == nil {
+                            NotificationPresenter.shared().present(text: NSLocalizedString("Added note to self", comment: ""), dismissAfterDelay: 5.0, includedStyle: .success)
+                        } else {
+                            NotificationPresenter.shared().present(text: NSLocalizedString("An error occurred while adding note", comment: ""), dismissAfterDelay: 5.0, includedStyle: .error)
+                        }
+                    }
+                } else {
+                    NCAPIController.sharedInstance().sendChatMessage(message.parsedMessage().string, toRoom: room.token, displayName: nil, replyTo: -1, referenceId: nil, silently: false, for: activeAccount) { error in
+                        if error == nil {
+                            NotificationPresenter.shared().present(text: NSLocalizedString("Added note to self", comment: ""), dismissAfterDelay: 5.0, includedStyle: .success)
+                        } else {
+                            NotificationPresenter.shared().present(text: NSLocalizedString("An error occurred while adding note", comment: ""), dismissAfterDelay: 5.0, includedStyle: .error)
+                        }
+                    }
+                }
+            } else {
+                NotificationPresenter.shared().present(text: NSLocalizedString("An error occurred while adding note", comment: ""), dismissAfterDelay: 5.0, includedStyle: .error)
+            }
+        }
+    }
+
+    func didPressResend(for message: NCChatMessage) {
+        // Make sure there's no unread message separator, as the indexpath could be invalid after removing a message
+        self.removeUnreadMessagesSeparator()
+
+        self.removePermanentlyTemporaryMessage(temporaryMessage: message)
+        let originalMessage = self.replaceMessageMentionsKeysWithMentionsDisplayNames(message: message.message, parameters: message.messageParametersJSONString ?? "")
+        self.sendChatMessage(message: originalMessage, withParentMessage: message.parent, messageParameters: message.messageParametersJSONString ?? "", silently: message.isSilent)
+    }
+
+    func didPressCopy(for message: NCChatMessage) {
+        let pasteboard = UIPasteboard.general
+        pasteboard.string = message.parsedMessage().string
+        NotificationPresenter.shared().present(text: NSLocalizedString("Message copied", comment: ""), dismissAfterDelay: 5.0, includedStyle: .dark)
+    }
+
+    func didPressCopyLink(for message: NCChatMessage) {
+        guard let link = room.linkURL else {
+            return
+        }
+
+        let url = "\(link)#message_\(message.messageId)"
+        let pasteboard = UIPasteboard.general
+        pasteboard.string = url
+
+        NotificationPresenter.shared().present(text: NSLocalizedString("Message link copied", comment: ""), dismissAfterDelay: 5.0, includedStyle: .dark)
+    }
+
+    func didPressTranslate(for message: NCChatMessage) {
+        let activeAccount = NCDatabaseManager.sharedInstance().activeAccount()
+        let translateMessageVC = MessageTranslationViewController(message: message.parsedMessage().string, availableTranslations: NCDatabaseManager.sharedInstance().availableTranslations(forAccountId: activeAccount.accountId))
+        self.presentWithNavigation(translateMessageVC, animated: true)
+    }
+
+    func didPressTranscribeVoiceMessage(for message: NCChatMessage) {
+        let downloader = NCChatFileController()
+        downloader.delegate = self
+        downloader.messageType = kMessageTypeVoiceMessage
+        downloader.actionType = actionTypeTranscribeVoiceMessage
+        downloader.downloadFile(fromMessage: message.file())
+    }
+
+    func didPressEdit(for message: NCChatMessage) {
+        self.savePendingMessage()
+
+        let warningString = NSLocalizedString("Adding a mention will only notify users that did not read the message yet", comment: "")
+        let warningView = UIView()
+        let warningLabel = UILabel()
+
+        warningView.addSubview(warningLabel)
+        warningLabel.translatesAutoresizingMaskIntoConstraints = false
+
+        NSLayoutConstraint.activate([
+            warningLabel.leftAnchor.constraint(equalTo: warningView.safeAreaLayoutGuide.leftAnchor, constant: 8),
+            warningLabel.rightAnchor.constraint(equalTo: warningView.safeAreaLayoutGuide.rightAnchor, constant: -8),
+            warningLabel.topAnchor.constraint(equalTo: warningView.safeAreaLayoutGuide.topAnchor, constant: 4),
+            warningLabel.bottomAnchor.constraint(equalTo: warningView.safeAreaLayoutGuide.bottomAnchor)
+        ])
+
+        let attributedWarningString = warningString.withFont(.systemFont(ofSize: 14)).withTextColor(.secondaryLabel)
+
+        warningLabel.attributedText = attributedWarningString
+        warningLabel.numberOfLines = 0
+
+        // Calculate the height needed to completely show the text
+        let maxWidth = self.autoCompletionView.frame.width - autoCompletionView.safeAreaInsets.left - autoCompletionView.safeAreaInsets.right - 16
+        let contraintRect = CGSize(width: maxWidth, height: .greatestFiniteMagnitude)
+        let size = attributedWarningString.boundingRect(with: contraintRect, options: .usesLineFragmentOrigin, context: nil)
+
+        // Update the frame for the new height and include the top padding
+        warningView.frame = .init(x: 0, y: 0, width: size.width, height: ceil(size.height) + 4)
+        self.autoCompletionView.tableHeaderView = warningView
+
+        DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
+            // Show the message to edit in the reply view
+            self.showReplyView(for: message)
+            self.replyMessageView!.hideCloseButton()
+
+            self.mentionsDict = [:]
+
+            // Try to reconstruct the mentionsDict
+            for (key, value) in message.messageParameters {
+                if let key = key as? String,
+                   key.hasPrefix("mention-"),
+                   let value = value as? [String: String] {
+
+                    guard let parameter = NCMessageParameter(dictionary: value),
+                          let paramaterDisplayName = parameter.name,
+                          let parameterId = parameter.parameterId
+                    else { continue }
+
+                    // For mentions the displayName is in the parameter "name", in our mentionsDict we use
+                    // "mentionsDisplayName" for the displayName with the prefix "@", so we need to construct
+                    // that manually here, so mentions are correctly removed while editing.
+                    // The same needs to happen for "mentionId" -> userId with a prefixed "@"
+                    parameter.mentionDisplayName = "@\(paramaterDisplayName)"
+                    parameter.mentionId = "@\(parameterId)"
+                    self.mentionsDict[key] = parameter
+                }
+            }
+
+            self.editingMessage = message
+
+            // For files without a caption we start with an empty text instead of "{file}"
+            if message.message == "{file}", message.file() != nil {
+                self.editText("")
+            } else {
+                self.editText(message.parsedMessage().string)
+            }
+        }
+    }
+
+    func didPressDelete(for message: NCChatMessage) {
+        if message.sendingFailed || message.isOfflineMessage {
+            self.removePermanentlyTemporaryMessage(temporaryMessage: message)
+            return
+        }
+
+        if let deletingMessage = message.copy() as? NCChatMessage {
+            deletingMessage.message = NSLocalizedString("Deleting message", comment: "")
+            deletingMessage.isDeleting = true
+            self.updateMessage(withMessageId: deletingMessage.messageId, updatedMessage: deletingMessage)
+        }
+
+        let activeAccount = NCDatabaseManager.sharedInstance().activeAccount()
+
+        NCAPIController.sharedInstance().deleteChatMessage(inRoom: self.room.token, withMessageId: message.messageId, for: activeAccount) { messageDict, error, statusCode in
+            if error == nil,
+               let messageDict,
+               let parent = messageDict["parent"] as? [AnyHashable: Any] {
+
+                if statusCode == 202 {
+                    self.view.makeToast(NSLocalizedString("Message deleted successfully, but Matterbridge is configured and the message might already be distributed to other services", comment: ""), duration: 5, position: CSToastPositionCenter)
+                } else if statusCode == 200 {
+                    NotificationPresenter.shared().present(text: NSLocalizedString("Message deleted successfully", comment: ""), dismissAfterDelay: 5.0, includedStyle: .success)
+                }
+
+                if let deleteMessage = NCChatMessage(dictionary: parent, andAccountId: activeAccount.accountId) {
+                    self.updateMessage(withMessageId: deleteMessage.messageId, updatedMessage: deleteMessage)
+                }
+            } else if error != nil {
+                switch statusCode {
+                case 400:
+                    NotificationPresenter.shared().present(text: NSLocalizedString("Message could not be deleted because it is too old", comment: ""), dismissAfterDelay: 5.0, includedStyle: .error)
+                case 405:
+                    NotificationPresenter.shared().present(text: NSLocalizedString("Only normal chat messages can be deleted", comment: ""), dismissAfterDelay: 5.0, includedStyle: .error)
+                default:
+                    NotificationPresenter.shared().present(text: NSLocalizedString("An error occurred while deleting the message", comment: ""), dismissAfterDelay: 5.0, includedStyle: .error)
+                }
+
+                self.updateMessage(withMessageId: message.messageId, updatedMessage: message)
+            }
+        }
+    }
+
+    func didPressOpenInNextcloud(for message: NCChatMessage) {
+        if let file = message.file(), let path = file.path, let link = file.link {
+            NCUtils.openFileInNextcloudAppOrBrowser(path: path, withFileLink: link)
+        }
+    }
+
+    // MARK: - Editing support
+
+    public override func didCancelTextEditing(_ sender: Any) {
+        super.didCancelTextEditing(sender)
+        self.autoCompletionView.tableHeaderView = nil
+        self.replyMessageView?.dismiss()
+        self.mentionsDict.removeAll()
+        self.editingMessage = nil
+        self.restorePendingMessage()
+    }
+
+    public override func didCommitTextEditing(_ sender: Any) {
+        super.didCommitTextEditing(sender)
+        self.autoCompletionView.tableHeaderView = nil
+        self.replyMessageView?.dismiss()
+        self.mentionsDict.removeAll()
+        self.editingMessage = nil
+        self.restorePendingMessage()
+    }
+
+    // MARK: - UITextField delegate
+
+    public func textFieldShouldReturn(_ textField: UITextField) -> Bool {
+        if textField == self.emojiTextField, self.interactingMessage != nil {
+            self.interactingMessage = nil
+            textField.resignFirstResponder()
+        }
+
+        return true
+    }
+
+    public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
+        if textField == self.emojiTextField, string.isSingleEmoji, let interactingMessage = self.interactingMessage {
+            self.addReaction(reaction: string, to: interactingMessage)
+            textField.resignFirstResponder()
+        }
+
+        return true
+    }
+
+    // MARK: - UITextViewDelegate
+
+    public override func textViewDidChange(_ textView: UITextView) {
+        self.startTyping()
+    }
+
+    // MARK: - TypingIndicator support
+
+    func sendStartedTypingMessage(to sessionId: String) {
+        // Workaround: TypingPrivacy should be checked locally, not from the remote server, use serverCapabilities for now
+        // TODO: Remove workaround for federated typing indicators.
+        guard let serverCapabilities = NCDatabaseManager.sharedInstance().serverCapabilities(forAccountId: self.room.accountId)
+        else { return }
+
+        if serverCapabilities.typingPrivacy {
+            return
+        }
+
+        if let signalingController = NCSettingsController.sharedInstance().externalSignalingController(forAccountId: self.room.accountId) {
+            let mySessionId = signalingController.sessionId()
+            let message = NCStartedTypingMessage(from: mySessionId, sendTo: sessionId, withPayload: [:], forRoomType: "")
+            signalingController.sendCall(message)
+        }
+    }
+
+    func sendStartedTypingMessageToAll() {
+        // Workaround: TypingPrivacy should be checked locally, not from the remote server, use serverCapabilities for now
+        // TODO: Remove workaround for federated typing indicators.
+        guard let serverCapabilities = NCDatabaseManager.sharedInstance().serverCapabilities(forAccountId: self.room.accountId),
+              !serverCapabilities.typingPrivacy,
+              let signalingController = NCSettingsController.sharedInstance().externalSignalingController(forAccountId: self.room.accountId)
+        else { return }
+
+        let participantMap = signalingController.getParticipantMap()
+        let mySessionId = signalingController.sessionId()
+
+        for (key, _) in participantMap {
+            if let sessionId = key as? String {
+                let message = NCStartedTypingMessage(from: mySessionId, sendTo: sessionId, withPayload: [:], forRoomType: "")
+                signalingController.sendCall(message)
+            }
+        }
+    }
+
+    func sendStoppedTypingMessageToAll() {
+        guard let serverCapabilities = NCDatabaseManager.sharedInstance().roomTalkCapabilities(for: self.room),
+              !serverCapabilities.typingPrivacy,
+              let signalingController = NCSettingsController.sharedInstance().externalSignalingController(forAccountId: self.room.accountId)
+        else { return }
+
+        let participantMap = signalingController.getParticipantMap()
+        let mySessionId = signalingController.sessionId()
+
+        for (key, _) in participantMap {
+            if let sessionId = key as? String {
+                let message = NCStoppedTypingMessage(from: mySessionId, sendTo: sessionId, withPayload: [:], forRoomType: "")
+                signalingController.sendCall(message)
+            }
+        }
+    }
+
+    func startTyping() {
+        if !self.isTyping {
+            self.isTyping = true
+
+            self.sendStartedTypingMessageToAll()
+            self.setTypingTimer()
+        }
+
+        self.setStopTypingTimer()
+    }
+
+    func stopTyping(force: Bool) {
+        if self.isTyping || force {
+            self.isTyping = false
+            self.sendStoppedTypingMessageToAll()
+            self.invalidateStopTypingTimer()
+            self.invalidateTypingTimer()
+        }
+    }
+
+    // TypingTimer is used to continously send "startedTyping" messages, while we are typing
+    func setTypingTimer() {
+        self.invalidateTypingTimer()
+        self.typingTimer = Timer.scheduledTimer(withTimeInterval: 10.0, repeats: false, block: { [weak self] _ in
+            guard let self else { return }
+
+            if self.isTyping {
+                // We're still typing, send signaling message again to all participants
+                self.sendStartedTypingMessageToAll()
+                self.setTypingTimer()
+            } else {
+                // We stopped typing, we don't send anything to the participants, we just remove our timer
+                self.invalidateTypingTimer()
+            }
+        })
+    }
+
+    func invalidateTypingTimer() {
+        self.typingTimer?.invalidate()
+        self.typingTimer = nil
+    }
+
+    // StopTypingTimer is used to detect when we stop typing (locally)
+    func setStopTypingTimer() {
+        self.invalidateStopTypingTimer()
+        self.stopTypingTimer = Timer.scheduledTimer(withTimeInterval: 5, repeats: false, block: { [weak self] _ in
+            guard let self else { return }
+
+            if self.isTyping {
+                self.isTyping = false
+                self.invalidateStopTypingTimer()
+            }
+        })
+    }
+
+    func invalidateStopTypingTimer() {
+        self.stopTypingTimer?.invalidate()
+        self.stopTypingTimer =  nil
+    }
+
+    func addTypingIndicator(withUserIdentifier userIdentifier: String, andDisplayName displayName: String) {
+        DispatchQueue.main.async {
+            if let view = self.textInputbar.typingView as? TypingIndicatorView {
+                view.addTyping(userIdentifier: userIdentifier, displayName: displayName)
+            }
+        }
+    }
+
+    func removeTypingIndicator(withUserIdentifier userIdentifier: String) {
+        DispatchQueue.main.async {
+            if let view = self.textInputbar.typingView as? TypingIndicatorView {
+                view.removeTyping(userIdentifier: userIdentifier)
+            }
+        }
+    }
+
+    // MARK: - ShareConfirmationViewController delegate & helper
+
+    public func shareConfirmationViewControllerDidFailed(_ viewController: ShareConfirmationViewController) {
+        self.dismiss(animated: true) {
+            if viewController.forwardingMessage {
+                NotificationPresenter.shared().present(text: NSLocalizedString("Failed to forward message", comment: ""), dismissAfterDelay: 5.0, includedStyle: .error)
+            }
+        }
+    }
+
+    public func shareConfirmationViewControllerDidFinish(_ viewController: ShareConfirmationViewController) {
+        self.dismiss(animated: true) {
+            if viewController.forwardingMessage {
+                var userInfo: [String: String] = [:]
+                userInfo["token"] = viewController.room.token
+                userInfo["accountId"] = viewController.account.accountId
+                NotificationCenter.default.post(name: .NCChatViewControllerForwardNotification, object: self, userInfo: userInfo)
+            }
+        }
+    }
+
+    internal func createShareConfirmationViewController() -> (shareConfirmationVC: ShareConfirmationViewController, navController: NCNavigationController) {
+        let activeAccount = NCDatabaseManager.sharedInstance().activeAccount()
+        let serverCapabilities = NCDatabaseManager.sharedInstance().serverCapabilities(forAccountId: activeAccount.accountId)
+        let shareConfirmationVC = ShareConfirmationViewController(room: self.room, account: activeAccount, serverCapabilities: serverCapabilities!)!
+        shareConfirmationVC.delegate = self
+        shareConfirmationVC.isModal = true
+        let navigationController = NCNavigationController(rootViewController: shareConfirmationVC)
+
+        return (shareConfirmationVC, navigationController)
+    }
+
+    // MARK: - ShareViewController Delegate
+
+    public func shareViewControllerDidCancel(_ viewController: ShareViewController) {
+        self.dismiss(animated: true)
+    }
+
+    // MARK: - PHPhotoPicker Delegate
+
+    public func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
+        if results.isEmpty {
+            picker.dismiss(animated: true)
+            return
+        }
+
+        let (shareConfirmationVC, navigationController) = self.createShareConfirmationViewController()
+
+        picker.dismiss(animated: true) {
+            self.present(navigationController, animated: true) {
+                for result in results {
+
+                    result.itemProvider.loadItem(forTypeIdentifier: "public.image", options: nil) { item, error in
+                        guard error == nil, let item = item as? URL else { return }
+
+                        var fileName: String
+
+                        if let suggestedFileName = result.itemProvider.suggestedName {
+                            fileName = "\(suggestedFileName).jpg"
+                        } else {
+                            fileName = "IMG_\(String(Date().timeIntervalSince1970 * 1000)).jpg"
+                        }
+
+                        shareConfirmationVC.shareItemController.addItem(withURLAndName: item, withName: fileName)
+                    }
+
+                    result.itemProvider.loadItem(forTypeIdentifier: "public.movie", options: nil) { item, error in
+                        guard error == nil, let item = item as? URL else { return }
+
+                        var fileName: String
+
+                        if let suggestedFileName = result.itemProvider.suggestedName {
+                            fileName = "\(suggestedFileName).mov"
+                        } else {
+                            fileName = "VID_\(String(Date().timeIntervalSince1970 * 1000)).mov"
+                        }
+
+                        shareConfirmationVC.shareItemController.addItem(withURLAndName: item, withName: fileName)
+                    }
+                }
+            }
+        }
+    }
+
+    // MARK: - UIImagePickerController delegate
+
+    public func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
+        self.saveImagePickerSettings(picker)
+
+        let (shareConfirmationVC, navigationController) = self.createShareConfirmationViewController()
+
+        guard let mediaType = info[.mediaType] as? String else { return }
+
+        if mediaType == "public.image" {
+            guard let image = info[.originalImage] as? UIImage else { return }
+
+            self.dismiss(animated: true) {
+                self.present(navigationController, animated: true) {
+                    shareConfirmationVC.shareItemController.addItem(with: image)
+                }
+            }
+        } else if mediaType == "public.movie" {
+            guard let imageUrl = info[.mediaURL] as? URL else { return }
+
+            self.dismiss(animated: true) {
+                self.present(navigationController, animated: true) {
+                    shareConfirmationVC.shareItemController.addItem(with: imageUrl)
+                }
+            }
+        }
+    }
+
+    public func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
+        self.saveImagePickerSettings(picker)
+        self.dismiss(animated: true)
+    }
+
+    public func saveImagePickerSettings(_ picker: UIImagePickerController) {
+        if picker.sourceType == .camera && picker.cameraCaptureMode == .photo {
+            NCUserDefaults.setPreferredCameraFlashMode(picker.cameraFlashMode.rawValue)
+        }
+    }
+
+    // MARK: - UIDocumentPickerViewController Delegate
+
+    public func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
+        let (shareConfirmationVC, navigationController) = self.createShareConfirmationViewController()
+
+        self.present(navigationController, animated: true) {
+            for url in urls {
+                shareConfirmationVC.shareItemController.addItem(with: url)
+            }
+        }
+    }
+
+    // MARK: - ShareLocationViewController Delegate
+
+    public func shareLocationViewController(_ viewController: ShareLocationViewController, didSelectLocationWithLatitude latitude: Double, longitude: Double, andName name: String) {
+        let richObject = GeoLocationRichObject(latitude: latitude, longitude: longitude, name: name)
+        let activeAccount = NCDatabaseManager.sharedInstance().activeAccount()
+
+        NCAPIController.sharedInstance().shareRichObject(richObject.richObjectDictionary(), inRoom: self.room.token, for: activeAccount) { error in
+            if let error {
+                print("Error sharing rich object: \(error)")
+            }
+        }
+
+        viewController.dismiss(animated: true)
+    }
+
+    // MARK: - CNContactPickerViewController Delegate
+
+    public func contactPicker(_ picker: CNContactPickerViewController, didSelect contact: CNContact) {
+        guard let vCardData = try? CNContactVCardSerialization.data(with: [contact]) else { return }
+
+        var vcString = String(data: vCardData, encoding: .utf8)
+
+        if let imageData = contact.imageData {
+            let base64Image = imageData.base64EncodedString()
+            let vcardImageString = "PHOTO;TYPE=JPEG;ENCODING=BASE64:\(base64Image)\n"
+            vcString = vcString?.replacingOccurrences(of: "END:VCARD", with: vcardImageString + "END:VCARD")
+        }
+
+        let paths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)
+        let folderPath = paths[0]
+        let filePath = (folderPath as NSString).appendingPathComponent("contact.vcf")
+
+        do {
+            try vcString?.write(toFile: filePath, atomically: true, encoding: .utf8)
+            let url = URL(fileURLWithPath: filePath)
+            let contactFileName = "\(contact.identifier).vcf"
+            let activeAccount = NCDatabaseManager.sharedInstance().activeAccount()
+
+            NCAPIController.sharedInstance().uniqueNameForFileUpload(withName: contactFileName, originalName: true, for: activeAccount) { fileServerURL, fileServerPath, _, _ in
+                if let fileServerURL, let fileServerPath {
+                    self.uploadFileAtPath(localPath: url.path, withFileServerURL: fileServerURL, andFileServerPath: fileServerPath, withMetaData: nil)
+                } else {
+                    print("Could not find unique name for contact file")
+                }
+            }
+        } catch {
+            print("Could not write contact file")
+        }
+    }
+
+    // MARK: - Voice messages recording
+
+    func showVoiceMessageRecordHint() {
+        let toastPosition = CGPoint(x: self.textInputbar.center.x, y: self.textInputbar.center.y - self.textInputbar.frame.size.height)
+        self.view.makeToast(NSLocalizedString("Tap and hold to record a voice message, release the button to send it.", comment: ""), duration: 3, position: toastPosition)
+    }
+
+    func showVoiceMessageRecordingView() {
+        self.voiceMessageRecordingView = VoiceMessageRecordingView()
+
+        guard let voiceMessageRecordingView = self.voiceMessageRecordingView else { return }
+
+        voiceMessageRecordingView.translatesAutoresizingMaskIntoConstraints = false
+
+        self.textInputbar.addSubview(voiceMessageRecordingView)
+        self.textInputbar.bringSubviewToFront(voiceMessageRecordingView)
+
+        let views = [
+            "voiceMessageRecordingView": voiceMessageRecordingView
+        ]
+
+        let metrics = [
+            "buttonWidth": self.rightButton.frame.size.width
+        ]
+
+        self.textInputbar.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|[voiceMessageRecordingView]|", metrics: metrics, views: views))
+        self.textInputbar.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|[voiceMessageRecordingView(>=0)]-(buttonWidth)-|", metrics: metrics, views: views))
+    }
+
+    func hideVoiceMessageRecordingView() {
+        self.voiceMessageRecordingView?.isHidden = true
+    }
+
+    func setupAudioRecorder() {
+        guard let userDocumentDirectory = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).last,
+              let outputFileURL = NSURL.fileURL(withPathComponents: [userDocumentDirectory, "voice-message-recording.m4a"])
+        else { return }
+
+        let session = AVAudioSession.sharedInstance()
+        try? session.setCategory(.playAndRecord)
+
+        let settings = [
+            AVFormatIDKey: Int(kAudioFormatMPEG4AAC),
+            AVSampleRateKey: 44100,
+            AVNumberOfChannelsKey: 2
+        ]
+
+        self.recorder = try? AVAudioRecorder(url: outputFileURL, settings: settings)
+        self.recorder?.delegate = self
+        self.recorder?.isMeteringEnabled = true
+        self.recorder?.prepareToRecord()
+    }
+
+    func checkPermissionAndRecordVoiceMessage() {
+        let mediaType = AVMediaType.audio
+        let authStatus = AVCaptureDevice.authorizationStatus(for: mediaType)
+
+        if authStatus == AVAuthorizationStatus.authorized {
+            self.startRecordingVoiceMessage()
+            return
+        } else if authStatus == AVAuthorizationStatus.notDetermined {
+            AVCaptureDevice.requestAccess(for: mediaType, completionHandler: { granted in
+                NSLog("Microphone permission granted: %@", granted ? "YES" : "NO")
+            })
+            return
+        }
+
+        let alert = UIAlertController(title: NSLocalizedString("Could not access microphone", comment: ""),
+                                      message: NSLocalizedString("Microphone access is not allowed. Check your settings.", comment: ""),
+                                      preferredStyle: .alert)
+
+        alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default))
+
+        NCUserInterfaceController.sharedInstance().presentAlertViewController(alert)
+    }
+
+    func startRecordingVoiceMessage() {
+        self.setupAudioRecorder()
+        self.showVoiceMessageRecordingView()
+        if let recorder = self.recorder, !recorder.isRecording {
+            let session = AVAudioSession.sharedInstance()
+            try? session.setActive(true)
+            recorder.record()
+        }
+    }
+
+    func stopRecordingVoiceMessage() {
+        self.hideVoiceMessageRecordingView()
+        if let recorder = self.recorder, recorder.isRecording {
+            recorder.stop()
+            let session = AVAudioSession.sharedInstance()
+            try? session.setActive(false)
+        }
+    }
+
+    func shareVoiceMessage() {
+        let dateFormatter = DateFormatter()
+        dateFormatter.dateFormat = "yyyy-MM-dd HH-mm-ss"
+        let dateString = dateFormatter.string(from: Date())
+
+        // Replace chars that are not allowed on the filesystem
+        let notAllowedCharSet = CharacterSet(charactersIn: "\\/:%")
+        var roomString = self.room.displayName.components(separatedBy: notAllowedCharSet).joined(separator: " ")
+
+        // Replace multiple spaces with 1
+        if let regex = try? NSRegularExpression(pattern: "  +") {
+            roomString = regex.stringByReplacingMatches(in: roomString, range: .init(location: 0, length: roomString.count), withTemplate: " ")
+        }
+
+        var audioFileName = "Talk recording from \(dateString) (\(roomString))"
+
+        // Trim the file name if too long
+        if audioFileName.count > 146 {
+            audioFileName = String(audioFileName.prefix(146))
+        }
+
+        audioFileName += ".mp3"
+
+        let activeAccount = NCDatabaseManager.sharedInstance().activeAccount()
+        NCAPIController.sharedInstance().uniqueNameForFileUpload(withName: audioFileName, originalName: true, for: activeAccount, withCompletionBlock: { fileServerURL, fileServerPath, _, _ in
+            if let fileServerURL, let fileServerPath, let recorder = self.recorder {
+                let talkMetaData: [String: String] = ["messageType": "voice-message"]
+                self.uploadFileAtPath(localPath: recorder.url.path, withFileServerURL: fileServerURL, andFileServerPath: fileServerPath, withMetaData: talkMetaData)
+            } else {
+                NSLog("Could not find unique name for voice message file.")
+            }
+        })
+    }
+
+    func uploadFileAtPath(localPath: String, withFileServerURL fileServerURL: String, andFileServerPath fileServerPath: String, withMetaData talkMetaData: [String: String]?) {
+        let activeAccount = NCDatabaseManager.sharedInstance().activeAccount()
+        NCAPIController.sharedInstance().setupNCCommunication(for: activeAccount)
+
+        NextcloudKit.shared.upload(serverUrlFileName: fileServerURL, fileNameLocalPath: localPath, taskHandler: { _ in
+            NSLog("Upload task")
+        }, progressHandler: { progress in
+            NSLog("Progress:%f", progress.fractionCompleted)
+        }, completionHandler: { _, _, _, _, _, _, _, error in
+            NSLog("Upload completed with error code: %ld", error.errorCode)
+
+            if error.errorCode == 0 {
+                NCAPIController.sharedInstance().shareFileOrFolder(for: activeAccount, atPath: fileServerPath, toRoom: self.room.token, talkMetaData: talkMetaData, withCompletionBlock: { error in
+                    if error != nil {
+                        NSLog("Failed to share voice message")
+                    }
+                })
+            } else if error.errorCode == 404 || error.errorCode == 409 {
+                NCAPIController.sharedInstance().checkOrCreateAttachmentFolder(for: activeAccount, withCompletionBlock: { created, _ in
+                    if created {
+                        self.uploadFileAtPath(localPath: localPath, withFileServerURL: fileServerURL, andFileServerPath: fileServerPath, withMetaData: talkMetaData)
+                    } else {
+                        NSLog("Failed to check or create attachment folder")
+                    }
+                })
+            } else {
+                NSLog("Failed upload voice message")
+            }
+        })
+    }
+
+    // MARK: - AVAudioRecorder Delegate
+
+    public func audioRecorderDidFinishRecording(_ recorder: AVAudioRecorder, successfully flag: Bool) {
+        if flag, recorder == self.recorder, !self.recordCancelled {
+            self.shareVoiceMessage()
+        }
+    }
+
+    // MARK: - Voice Messages Transcribe
+
+    func transcribeVoiceMessage(with fileStatus: NCChatFileStatus) {
+        guard let fileLocalPath = fileStatus.fileLocalPath else { return }
+
+        DispatchQueue.main.async {
+            let audioFileURL = URL(fileURLWithPath: fileLocalPath)
+            let viewController = VoiceMessageTranscribeViewController(audiofileUrl: audioFileURL)
+            let navController = NCNavigationController(rootViewController: viewController)
+            self.present(navController, animated: true)
+        }
+    }
+
+    // MARK: - Voice Message Player
+
+    func setupVoiceMessagePlayer(with fileStatus: NCChatFileStatus) {
+        guard let fileLocalPath = fileStatus.fileLocalPath,
+              let data = try? Data(contentsOf: URL(fileURLWithPath: fileLocalPath)),
+              let player = try? AVAudioPlayer(data: data)
+        else { return }
+
+        self.voiceMessagesPlayer = player
+        self.playerAudioFileStatus = fileStatus
+        player.delegate = self
+        self.playVoiceMessagePlayer()
+    }
+
+    func playVoiceMessagePlayer() {
+        self.setSpeakerAudioSession()
+        self.enableProximitySensor()
+
+        self.startVoiceMessagePlayerTimer()
+        self.voiceMessagesPlayer?.play()
+    }
+
+    func pauseVoiceMessagePlayer() {
+        self.disableProximitySensor()
+
+        self.stopVoiceMessagePlayerTimer()
+        self.voiceMessagesPlayer?.pause()
+        self.checkVisibleCellAudioPlayers()
+    }
+
+    func stopVoiceMessagePlayer() {
+        self.disableProximitySensor()
+
+        self.stopVoiceMessagePlayerTimer()
+        self.voiceMessagesPlayer?.stop()
+    }
+
+    func enableProximitySensor() {
+        NotificationCenter.default.addObserver(self, selector: #selector(sensorStateChange(notification:)), name: UIDevice.proximityStateDidChangeNotification, object: nil)
+        UIDevice.current.isProximityMonitoringEnabled = true
+    }
+
+    func disableProximitySensor() {
+        if UIDevice.current.proximityState == false {
+            // Only disable monitoring if proximity sensor state is not active.
+            // If not proximity sensor state is cached as active and next time we enable monitoring
+            // sensorStateChange won't be trigger until proximity sensor state changes to inactive.
+            NotificationCenter.default.removeObserver(self, name: UIDevice.proximityStateDidChangeNotification, object: nil)
+            UIDevice.current.isProximityMonitoringEnabled = false
+        }
+    }
+
+    func setSpeakerAudioSession() {
+        let session = AVAudioSession.sharedInstance()
+        try? session.setCategory(AVAudioSession.Category.playback)
+        try? session.setActive(true)
+    }
+
+    func setVoiceChatAudioSession() {
+        let session = AVAudioSession.sharedInstance()
+        try? session.setCategory(AVAudioSession.Category.playAndRecord, mode: AVAudioSession.Mode.voiceChat)
+        try? session.setActive(true)
+    }
+
+    func sensorStateChange(notification: Notification) {
+        if UIDevice.current.proximityState {
+            self.setVoiceChatAudioSession()
+        } else {
+            self.pauseVoiceMessagePlayer()
+            self.setSpeakerAudioSession()
+            self.disableProximitySensor()
+        }
+    }
+
+    func checkVisibleCellAudioPlayers() {
+        guard let tableView = self.tableView,
+              let indexPaths = tableView.indexPathsForVisibleRows,
+              let playerAudioFileStatus = self.playerAudioFileStatus,
+              let voiceMessagesPlayer = self.voiceMessagesPlayer
+        else { return }
+
+        for indexPath in indexPaths {
+            let sectionDate = self.dateSections[indexPath.section]
+
+            if let messages = self.messages[sectionDate] {
+                let message = messages[indexPath.row]
+
+                if message.isVoiceMessage {
+                    guard let cell = tableView.cellForRow(at: indexPath) as? BaseChatTableViewCell,
+                          let file = message.file()
+                    else { continue }
+
+                    if file.parameterId == playerAudioFileStatus.fileId, file.path == playerAudioFileStatus.filePath {
+                        cell.audioPlayerView?.setPlayerProgress(voiceMessagesPlayer.currentTime, isPlaying: voiceMessagesPlayer.isPlaying, maximumValue: voiceMessagesPlayer.duration)
+                        continue
+                    }
+
+                    cell.audioPlayerView?.resetPlayer()
+                }
+            }
+        }
+    }
+
+    func startVoiceMessagePlayerTimer() {
+        self.stopVoiceMessagePlayerTimer()
+        self.playerProgressTimer = Timer.scheduledTimer(timeInterval: 0.05, target: self, selector: #selector(checkVisibleCellAudioPlayers), userInfo: nil, repeats: true)
+    }
+
+    func stopVoiceMessagePlayerTimer() {
+        self.playerProgressTimer?.invalidate()
+        self.playerProgressTimer = nil
+    }
+
+    // MARK: - AVAudioPlayer Delegate
+
+    public func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
+        self.stopVoiceMessagePlayerTimer()
+        self.checkVisibleCellAudioPlayers()
+        self.disableProximitySensor()
+    }
+
+    // MARK: - Gesture recognizer
+
+    public override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
+        if gestureRecognizer == self.voiceMessageLongPressGesture {
+            return true
+        }
+
+        return super.gestureRecognizerShouldBegin(gestureRecognizer)
+    }
+
+    func handleLongPressInVoiceMessageRecordButton(gestureRecognizer: UILongPressGestureRecognizer) {
+        if self.rightButton.tag != sendButtonTagVoice {
+            return
+        }
+
+        let point = gestureRecognizer.location(in: self.view)
+
+        if gestureRecognizer.state == .began {
+            print("Start recording audio message")
+
+            // 'Pop' feedback (strong boom)
+            AudioServicesPlaySystemSound(1520)
+            self.checkPermissionAndRecordVoiceMessage()
+            self.shouldLockInterfaceOrientation(lock: true)
+            self.recordCancelled = false
+            self.longPressStartingPoint = point
+            self.cancelHintLabelInitialPositionX = voiceMessageRecordingView?.slideToCancelHintLabel?.frame.origin.x
+        } else if gestureRecognizer.state == .ended {
+            print("Stop recording audio message")
+            self.shouldLockInterfaceOrientation(lock: false)
+            if let recordingTime = self.recorder?.currentTime {
+                // Mark record as cancelled if audio message is no longer than one second
+                self.recordCancelled = recordingTime < 1
+            }
+            self.stopRecordingVoiceMessage()
+        } else if gestureRecognizer.state == .changed {
+            guard let longPressStartingPoint,
+                  let cancelHintLabelInitialPositionX,
+                  let voiceMessageRecordingView,
+                  let slideToCancelHintLabel = voiceMessageRecordingView.slideToCancelHintLabel
+            else { return }
+
+            let slideX = longPressStartingPoint.x - point.x
+
+            // Only slide view to the left
+            if slideX > 0 {
+                let maxSlideX = 100.0
+                var labelFrame = slideToCancelHintLabel.frame
+                labelFrame = .init(x: cancelHintLabelInitialPositionX - slideX, y: labelFrame.origin.y, width: labelFrame.size.width, height: labelFrame.size.height)
+
+                slideToCancelHintLabel.frame = labelFrame
+                slideToCancelHintLabel.alpha = (maxSlideX - slideX) / 100
+
+                // Cancel recording if slided more than maxSlideX
+                if slideX > maxSlideX, !self.recordCancelled {
+                    print("Cancel recording audio message")
+
+                    // 'Cancelled' feedback (three sequential weak booms)
+                    AudioServicesPlaySystemSound(1521)
+                    self.recordCancelled = true
+                    self.stopRecordingVoiceMessage()
+                }
+            }
+        } else if gestureRecognizer.state == .cancelled || gestureRecognizer.state == .failed {
+            print("Gesture cancelled or failed -> Cancel recording audio message")
+            self.shouldLockInterfaceOrientation(lock: false)
+            self.recordCancelled = false
+            self.stopRecordingVoiceMessage()
+        }
+    }
+
+    func shouldLockInterfaceOrientation(lock: Bool) {
+        if let appDelegate = UIApplication.shared.delegate as? AppDelegate {
+            appDelegate.shouldLockInterfaceOrientation = lock
+        }
+    }
+
+    // MARK: - UIScrollViewDelegate methods
+
+    public override func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
+        super.scrollViewDidEndDecelerating(scrollView)
+
+        guard scrollView == self.tableView
+        else { return }
+
+        if self.firstUnreadMessage != nil {
+            self.checkUnreadMessagesVisibility()
+        }
+
+        self.updateToolbar(animated: true)
+    }
+
+    public override func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
+        super.scrollViewDidEndDragging(scrollView, willDecelerate: decelerate)
+
+        guard scrollView == self.tableView
+        else { return }
+
+        if !decelerate, self.firstUnreadMessage != nil {
+            self.checkUnreadMessagesVisibility()
+        }
+
+        self.updateToolbar(animated: true)
+    }
+
+    public override func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
+        guard scrollView == self.tableView
+        else { return }
+
+        if self.firstUnreadMessage != nil {
+            self.checkUnreadMessagesVisibility()
+        }
+
+        self.updateToolbar(animated: true)
+    }
+
+    // MARK: - UITextViewDelegate methods
+
+    public override func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
+        // Do not allow to type while recording
+        if let voiceMessageLongPressGesture,
+           voiceMessageLongPressGesture.state != .possible {
+
+            return false
+        }
+
+        return super.textView(textView, shouldChangeTextIn: range, replacementText: text)
+    }
+
+    // MARK: - Chat functions
+
+    func prependMessages(historyMessages: [NCChatMessage], addingBlockSeparator shouldAddBlockSeparator: Bool) -> IndexPath? {
+        var historyDict: [Date: [NCChatMessage]] = [:]
+
+        self.internalAppendMessages(messages: historyMessages, inDictionary: &historyDict)
+
+        var chatSection: Date?
+        var historyMessagesForSection: [NCChatMessage]?
+
+        // Sort history sections
+        let historySections = historyDict.keys.sorted()
+
+        // Add every section in history that can't be merged with current chat messages
+        for historySection in historySections {
+            historyMessagesForSection = historyDict[historySection]
+            chatSection = self.getKeyForDate(date: historySection, inDictionary: self.messages)
+
+            if chatSection == nil {
+                self.messages[historySection] = historyMessagesForSection
+            }
+        }
+
+        self.sortDateSections()
+
+        if shouldAddBlockSeparator {
+            // Chat block separator
+            let blockSeparatorMessage = NCChatMessage()
+            blockSeparatorMessage.messageId = kChatBlockSeparatorIdentifier
+            historyMessagesForSection?.append(blockSeparatorMessage)
+        }
+
+        if let lastSection = historySections.last,
+           let lastHistoryMessages = historyDict[lastSection] {
+
+            let lastHistoryMessageIP = IndexPath(row: lastHistoryMessages.count - 1, section: historySections.count - 1)
+
+            // Merge last section of history messages with first section in current chat
+            if let chatSection,
+               let chatMessages = self.messages[chatSection] {
+
+                if var historyMessagesForSection,
+                   let lastHistoryMessage = historyMessagesForSection.last,
+                   let firstChatMessage = chatMessages.first {
+
+                    firstChatMessage.isGroupMessage = self.shouldGroupMessage(newMessage: firstChatMessage, withMessage: lastHistoryMessage)
+                    historyMessagesForSection.append(contentsOf: chatMessages)
+                    self.messages[chatSection] = historyMessagesForSection
+                }
+            }
+
+            return lastHistoryMessageIP
+        }
+
+        return nil
+    }
+
+    func insertMessages(messages: [NCChatMessage]) {
+        for newMessage in messages {
+            let newMessageDate = Date(timeIntervalSince1970: TimeInterval(newMessage.timestamp))
+
+            if let keyDate = self.getKeyForDate(date: newMessageDate, inDictionary: self.messages),
+               var messagesForDate = self.messages[keyDate] {
+
+                for messageIndex in messagesForDate.indices {
+                    let currentMessage = messagesForDate[messageIndex]
+
+                    if currentMessage.timestamp > newMessage.timestamp {
+                        // Message inserted in between other messages
+                        if messageIndex > 0 {
+                            let previousMessage = messagesForDate[messageIndex - 1]
+                            newMessage.isGroupMessage = self.shouldGroupMessage(newMessage: newMessage, withMessage: previousMessage)
+                        }
+
+                        currentMessage.isGroupMessage = self.shouldGroupMessage(newMessage: currentMessage, withMessage: newMessage)
+                        messagesForDate.insert(newMessage, at: messageIndex)
+                        break
+                    } else if messageIndex == (messagesForDate.count - 1) {
+                        // Message inserted at the end of a date section
+                        newMessage.isGroupMessage = self.shouldGroupMessage(newMessage: newMessage, withMessage: currentMessage)
+                        messagesForDate.append(newMessage)
+                        break
+                    }
+                }
+
+                self.messages[keyDate] = messagesForDate
+            } else {
+                // We don't have messages for that date in our dictionary right now, so add this message as the first one
+                self.messages[newMessageDate] = [newMessage]
+            }
+        }
+
+        self.sortDateSections()
+    }
+
+    func appendMessages(messages: [NCChatMessage]) {
+        // Because of the inout parameter, we can't call self.sortDateSections() inside the append function
+        // Therefore we wrap it in this append function
+        self.internalAppendMessages(messages: messages, inDictionary: &self.messages)
+        self.sortDateSections()
+    }
+
+    private func internalAppendMessages(messages: [NCChatMessage], inDictionary dictionary: inout [Date: [NCChatMessage]]) {
+        for newMessage in messages {
+            let newMessageDate = Date(timeIntervalSince1970: TimeInterval(newMessage.timestamp))
+            let keyDate = self.getKeyForDate(date: newMessageDate, inDictionary: dictionary)
+
+            if let keyDate, let messagesForDate = dictionary[keyDate] {
+                var messageUpdated = false
+
+                // Check if we can update the message instead of adding a new one
+                for messageIndex in messagesForDate.indices {
+                    let currentMessage = messagesForDate[messageIndex]
+
+                    if currentMessage.isSameMessage(newMessage) {
+                        // The newly received message either already exists or its temporary counterpart exists -> update
+                        // If the user type a command the newMessage.actorType will be "bots", then we should not group those messages
+                        // even if the original message was grouped.
+                        // Edited messages should not be grouped to make it clear, that the message was edited
+                        newMessage.isGroupMessage = currentMessage.isGroupMessage && newMessage.actorType != "bots" && newMessage.lastEditTimestamp == 0
+                        dictionary[keyDate]?[messageIndex] = newMessage
+                        messageUpdated = true
+                        break
+                    }
+                }
+
+                if !messageUpdated, let lastMessage = messagesForDate.last {
+                    newMessage.isGroupMessage = self.shouldGroupMessage(newMessage: newMessage, withMessage: lastMessage)
+                    dictionary[keyDate]?.append(newMessage)
+                }
+            } else {
+                // Section not found, create new section and add message
+                dictionary[newMessageDate] = [newMessage]
+            }
+        }
+    }
+
+    func removeMessage(at indexPath: IndexPath) {
+        guard indexPath.section < self.dateSections.count else { return }
+
+        let sectionKey = self.dateSections[indexPath.section]
+        if var messages = self.messages[sectionKey], indexPath.row < messages.count {
+            if messages.count == 1 {
+                // Remove section
+                self.messages.removeValue(forKey: sectionKey)
+                self.sortDateSections()
+                self.tableView?.beginUpdates()
+                self.tableView?.deleteSections([indexPath.section], with: .none)
+                self.tableView?.endUpdates()
+            } else {
+                // Remove message
+                let isLastMessage = indexPath.row == (messages.count - 1)
+                messages.remove(at: indexPath.row)
+                self.messages[sectionKey] = messages
+
+                self.tableView?.beginUpdates()
+                self.tableView?.deleteRows(at: [indexPath], with: .none)
+                self.tableView?.endUpdates()
+
+                if !isLastMessage {
+                    // Update the message next to removed message
+                    let nextMessage = messages[indexPath.row]
+                    nextMessage.isGroupMessage = false
+
+                    if indexPath.row > 0 {
+                        let previousMessage = messages[indexPath.row - 1]
+                        nextMessage.isGroupMessage = self.shouldGroupMessage(newMessage: nextMessage, withMessage: previousMessage)
+                    }
+
+                    self.tableView?.beginUpdates()
+                    self.tableView?.reloadRows(at: [indexPath], with: .none)
+                    self.tableView?.endUpdates()
+                }
+            }
+        }
+    }
+
+    func sortDateSections() {
+        self.dateSections = self.messages.keys.sorted()
+    }
+
+    // MARK: - Message grouping
+
+    func shouldGroupMessage(newMessage: NCChatMessage, withMessage lastMessage: NCChatMessage) -> Bool {
+        let sameActor = newMessage.actorId == lastMessage.actorId
+        let sameType = newMessage.isSystemMessage == lastMessage.isSystemMessage
+        let timeDiff = (newMessage.timestamp - lastMessage.timestamp) < kChatMessageGroupTimeDifference
+        let notEdited = newMessage.lastEditTimestamp == 0
+
+        // Try to collapse system messages if the new message is not already collapsing some messages
+        // Disable swiftlint -> not supported on Realm object
+        // swiftlint:disable:next empty_count
+        if newMessage.isSystemMessage, lastMessage.isSystemMessage, newMessage.collapsedMessages.count == 0 {
+            self.tryToGroupSystemMessage(newMessage: newMessage, withMessage: lastMessage)
+        }
+
+        return sameActor && sameType && timeDiff && notEdited
+    }
+
+    func tryToGroupSystemMessage(newMessage: NCChatMessage, withMessage lastMessage: NCChatMessage) {
+        if newMessage.systemMessage == lastMessage.systemMessage {
+            if newMessage.actorId == lastMessage.actorId {
+                // Same action and actor
+                if ["user_added", "user_removed", "moderator_promoted", "moderator_demoted"].contains(newMessage.systemMessage) {
+                    self.collapseSystemMessage(newMessage, withMessage: lastMessage, withAction: newMessage.systemMessage)
+                }
+            } else {
+                // Same action, different actor
+                if ["call_joined", "call_left"].contains(newMessage.systemMessage) {
+                    self.collapseSystemMessage(newMessage, withMessage: lastMessage, withAction: newMessage.systemMessage)
+                }
+            }
+        } else if newMessage.actorId == lastMessage.actorId {
+            if lastMessage.systemMessage == "call_left", newMessage.systemMessage == "call_joined" {
+                self.collapseSystemMessage(newMessage, withMessage: lastMessage, withAction: "call_reconnected")
+            }
+        }
+    }
+
+    // swiftlint:disable:next cyclomatic_complexity
+    func collapseSystemMessage(_ newMessage: NCChatMessage, withMessage lastMessage: NCChatMessage, withAction action: String) {
+        var collapseByMessage = lastMessage
+
+        if let lastCollapsedByMessage = lastMessage.collapsedBy {
+            collapseByMessage = lastCollapsedByMessage
+            collapseByMessage.collapsedBy = nil
+
+            self.tryToGroupSystemMessage(newMessage: newMessage, withMessage: collapseByMessage)
+            return
+        }
+
+        newMessage.collapsedBy = collapseByMessage
+        newMessage.isCollapsed = true
+
+        collapseByMessage.collapsedMessages.add(newMessage.messageId as NSNumber)
+        collapseByMessage.isCollapsed = true
+
+        let activeAccount = NCDatabaseManager.sharedInstance().activeAccount()
+
+        var isUser0Self = false
+        var isUser1Self = false
+
+        if let userDict = collapseByMessage.messageParameters["user"] as? [String: Any] {
+            isUser0Self = userDict["id"] as? String == activeAccount.userId && userDict["type"] as? String == "user"
+        }
+
+        if let userDict = newMessage.messageParameters["user"] as? [String: Any] {
+            isUser1Self = userDict["id"] as? String == activeAccount.userId && userDict["type"] as? String == "user"
+        }
+
+        let isActor0Self = collapseByMessage.actorId == activeAccount.userId && collapseByMessage.actorType == "users"
+        let isActor1Self = newMessage.actorId == activeAccount.userId && newMessage.actorType == "users"
+        let isActor0Admin = collapseByMessage.actorId == "cli" && collapseByMessage.actorType == "guests"
+
+        collapseByMessage.collapsedIncludesUserSelf = isUser0Self || isUser1Self
+        collapseByMessage.collapsedIncludesActorSelf = isActor0Self || isActor1Self
+
+        var collapsedMessageParameters: [String: Any] = [:]
+
+        if let actor0Dict = collapseByMessage.messageParameters["actor"],
+           let actor1Dict = newMessage.messageParameters["actor"] {
+
+            collapsedMessageParameters["actor0"] = isActor0Self ? actor1Dict : actor0Dict
+            collapsedMessageParameters["actor1"] = actor1Dict
+        }
+
+        if let user0Dict = collapseByMessage.messageParameters["user"],
+           let user1Dict = newMessage.messageParameters["user"] {
+
+            collapsedMessageParameters["user0"] = isUser0Self ? user1Dict : user0Dict
+            collapsedMessageParameters["user1"] = user1Dict
+        }
+
+        collapseByMessage.setCollapsedMessageParameters(collapsedMessageParameters)
+
+        if action == "user_added" {
+            if isActor0Self {
+                if collapseByMessage.collapsedMessages.count == 1 {
+                    collapseByMessage.collapsedMessage = NSLocalizedString("You added {user0} and {user1}", comment: "Please put {user0} and {user1} placeholders in the correct position on the translated text but do not translate them")
+                } else {
+                    collapseByMessage.collapsedMessage = String(format: NSLocalizedString("You added {user0} and %ld more participants", comment: "Please put {user0} and %ld placeholders in the correct position on the translated text but do not translate it"), collapseByMessage.collapsedMessages.count)
+                }
+            } else if isActor0Admin {
+                if collapseByMessage.collapsedMessages.count == 1 {
+                    if collapseByMessage.collapsedIncludesUserSelf {
+                        collapseByMessage.collapsedMessage = NSLocalizedString("An administrator added you and {user0}", comment: "Please put {user0} placeholder in the correct position on the translated text but do not translate it")
+                    } else {
+                        collapseByMessage.collapsedMessage = String(format: NSLocalizedString("An administrator added {user0} and {user1}", comment: "Please put {user0} and {user1} placeholders in the correct position on the translated text but do not translate them"))
+                    }
+                } else {
+                    if collapseByMessage.collapsedIncludesUserSelf {
+                        collapseByMessage.collapsedMessage = String(format: NSLocalizedString("An administrator added you and %ld more participants", comment: "Please put %ld placeholder in the correct position on the translated text but do not translate it"), collapseByMessage.collapsedMessages.count)
+                    } else {
+                        collapseByMessage.collapsedMessage = String(format: NSLocalizedString("An administrator added {user0} and %ld more participants", comment: "Please put {user0} and %ld placeholders in the correct position on the translated text but do not translate them"), collapseByMessage.collapsedMessages.count)
+                    }
+                }
+            } else {
+                if collapseByMessage.collapsedMessages.count == 1 {
+                    if collapseByMessage.collapsedIncludesUserSelf {
+                        collapseByMessage.collapsedMessage = NSLocalizedString("{actor0} added you and {user0}", comment: "Please put {actor0} and {user0} placeholders in the correct position on the translated text but do not translate them")
+                    } else {
+                        collapseByMessage.collapsedMessage = NSLocalizedString("{actor0} added {user0} and {user1}", comment: "Please put {actor0}, {user0} and {user1} placeholders in the correct position on the translated text but do not translate them")
+                    }
+                } else {
+                    if collapseByMessage.collapsedIncludesUserSelf {
+                        collapseByMessage.collapsedMessage = String(format: NSLocalizedString("{actor0} added you and %ld more participants", comment: "Please put {actor0} and %ld placeholders in the correct position on the translated text but do not translate them"), collapseByMessage.collapsedMessages.count)
+                    } else {
+                        collapseByMessage.collapsedMessage = String(format: NSLocalizedString("{actor0} added {user0} and %ld more participants", comment: "Please put {actor0}, {user0} and %ld placeholders in the correct position on the translated text but do not translate them"), collapseByMessage.collapsedMessages.count)
+                    }
+                }
+            }
+        } else if action == "user_removed" {
+            if isActor0Self {
+                if collapseByMessage.collapsedMessages.count == 1 {
+                    collapseByMessage.collapsedMessage = NSLocalizedString("You removed {user0} and {user1}", comment: "Please put {user0} and {user1} placeholders in the correct position on the translated text but do not translate them")
+                } else {
+                    collapseByMessage.collapsedMessage = String(format: NSLocalizedString("You removed {user0} and %ld more participants", comment: "Please put {user0} and %ld placeholders in the correct position on the translated text but do not translate it"), collapseByMessage.collapsedMessages.count)
+                }
+            } else if isActor0Admin {
+                if collapseByMessage.collapsedMessages.count == 1 {
+                    if collapseByMessage.collapsedIncludesUserSelf {
+                        collapseByMessage.collapsedMessage = NSLocalizedString("An administrator removed you and {user0}", comment: "Please put {user0} placeholder in the correct position on the translated text but do not translate it")
+                    } else {
+                        collapseByMessage.collapsedMessage = NSLocalizedString("An administrator removed {user0} and {user1}", comment: "Please put {user0} and {user1} placeholders in the correct position on the translated text but do not translate them")
+                    }
+                } else {
+                    if collapseByMessage.collapsedIncludesUserSelf {
+                        collapseByMessage.collapsedMessage = String(format: NSLocalizedString("An administrator removed you and %ld more participants", comment: "Please put %ld placeholder in the correct position on the translated text but do not translate it"), collapseByMessage.collapsedMessages.count)
+                    } else {
+                        collapseByMessage.collapsedMessage = String(format: NSLocalizedString("An administrator removed {user0} and %ld more participants", comment: "Please put {user0} and %ld placeholders in the correct position on the translated text but do not translate them"), collapseByMessage.collapsedMessages.count)
+                    }
+                }
+            } else {
+                if collapseByMessage.collapsedMessages.count == 1 {
+                    if collapseByMessage.collapsedIncludesUserSelf {
+                        collapseByMessage.collapsedMessage = NSLocalizedString("{actor0} removed you and {user0}", comment: "Please put {actor0} and {user0} placeholders in the correct position on the translated text but do not translate them")
+                    } else {
+                        collapseByMessage.collapsedMessage = NSLocalizedString("{actor0} removed {user0} and {user1}", comment: "Please put {actor0}, {user0} and {user1} placeholders in the correct position on the translated text but do not translate them")
+                    }
+                } else {
+                    if collapseByMessage.collapsedIncludesUserSelf {
+                        collapseByMessage.collapsedMessage = String(format: NSLocalizedString("{actor0} removed you and %ld more participants", comment: "Please put {actor0} and %ld placeholders in the correct position on the translated text but do not translate them"), collapseByMessage.collapsedMessages.count)
+                    } else {
+                        collapseByMessage.collapsedMessage = String(format: NSLocalizedString("{actor0} removed {user0} and %ld more participants", comment: "Please put {actor0}, {user0} and %ld placeholders in the correct position on the translated text but do not translate them"), collapseByMessage.collapsedMessages.count)
+                    }
+                }
+            }
+        } else if action == "moderator_promoted" {
+            if isActor0Self {
+                if collapseByMessage.collapsedMessages.count == 1 {
+                    collapseByMessage.collapsedMessage = NSLocalizedString("You promoted {user0} and {user1} to moderators", comment: "Please put {user0} and {user1} placeholders in the correct position on the translated text but do not translate them")
+                } else {
+                    collapseByMessage.collapsedMessage = String(format: NSLocalizedString("You promoted {user0} and %ld more participants to moderators", comment: "Please put {user0} and %ld placeholders in the correct position on the translated text but do not translate them"), collapseByMessage.collapsedMessages.count)
+                }
+            } else if isActor0Admin {
+                if collapseByMessage.collapsedMessages.count == 1 {
+                    if collapseByMessage.collapsedIncludesUserSelf {
+                        collapseByMessage.collapsedMessage = NSLocalizedString("An administrator promoted you and {user0} to moderators", comment: "Please put {user0} placeholder in the correct position on the translated text but do not translate it")
+                    } else {
+                        collapseByMessage.collapsedMessage = NSLocalizedString("An administrator promoted {user0} and {user1} to moderators", comment: "Please put {user0} and {user1} placeholders in the correct position on the translated text but do not translate them")
+                    }
+                } else {
+                    if collapseByMessage.collapsedIncludesUserSelf {
+                        collapseByMessage.collapsedMessage = String(format: NSLocalizedString("An administrator promoted you and %ld more participants to moderators", comment: "Please put %ld placeholder in the correct position on the translated text but do not translate it"), collapseByMessage.collapsedMessages.count)
+                    } else {
+                        collapseByMessage.collapsedMessage = String(format: NSLocalizedString("An administrator promoted {user0} and %ld more participants to moderators", comment: "Please put {user0} and %ld placeholders in the correct position on the translated text but do not translate them"), collapseByMessage.collapsedMessages.count)
+                    }
+                }
+            } else {
+                if collapseByMessage.collapsedMessages.count == 1 {
+                    if collapseByMessage.collapsedIncludesUserSelf {
+                        collapseByMessage.collapsedMessage = NSLocalizedString("{actor0} promoted you and {user0} to moderators", comment: "Please put {actor0} and {user0} placeholders in the correct position on the translated text but do not translate them")
+                    } else {
+                        collapseByMessage.collapsedMessage = NSLocalizedString("{actor0} promoted {user0} and {user1} to moderators", comment: "Please put {actor0}, {user0} and {user1} placeholders in the correct position on the translated text but do not translate them")
+                    }
+                } else {
+                    if collapseByMessage.collapsedIncludesUserSelf {
+                        collapseByMessage.collapsedMessage = String(format: NSLocalizedString("{actor0} promoted you and %ld more participants to moderators", comment: "Please put {actor0} and %ld placeholders in the correct position on the translated text but do not translate them"), collapseByMessage.collapsedMessages.count)
+                    } else {
+                        collapseByMessage.collapsedMessage = String(format: NSLocalizedString("{actor0} promoted {user0} and %ld more participants to moderators", comment: "Please put {actor0}, {user0} and %ld placeholders in the correct position on the translated text but do not translate them"), collapseByMessage.collapsedMessages.count)
+                    }
+                }
+            }
+
+        } else if action == "moderator_demoted" {
+            if isActor0Self {
+                if collapseByMessage.collapsedMessages.count == 1 {
+                    collapseByMessage.collapsedMessage = NSLocalizedString("You demoted {user0} and {user1} from moderators", comment: "Please put {user0} and {user1} placeholders in the correct position on the translated text but do not translate them")
+                } else {
+                    collapseByMessage.collapsedMessage = String(format: NSLocalizedString("You demoted {user0} and %ld more participants from moderators", comment: "Please put {user0} and %ld placeholders in the correct position on the translated text but do not translate them"), collapseByMessage.collapsedMessages.count)
+                }
+            } else if isActor0Admin {
+                if collapseByMessage.collapsedMessages.count == 1 {
+                    if collapseByMessage.collapsedIncludesUserSelf {
+                        collapseByMessage.collapsedMessage = NSLocalizedString("An administrator demoted you and {user0} from moderators", comment: "Please put {user0} placeholder in the correct position on the translated text but do not translate it")
+                    } else {
+                        collapseByMessage.collapsedMessage = NSLocalizedString("An administrator demoted {user0} and {user1} from moderators", comment: "Please put {user0} and {user1} placeholders in the correct position on the translated text but do not translate them")
+                    }
+                } else {
+                    if collapseByMessage.collapsedIncludesUserSelf {
+                        collapseByMessage.collapsedMessage = String(format: NSLocalizedString("An administrator demoted you and %ld more participants from moderators", comment: "Please put %ld placeholder in the correct position on the translated text but do not translate it"), collapseByMessage.collapsedMessages.count)
+                    } else {
+                        collapseByMessage.collapsedMessage = String(format: NSLocalizedString("An administrator demoted {user0} and %ld more participants from moderators", comment: "Please put {user0} and %ld placeholders in the correct position on the translated text but do not translate them"), collapseByMessage.collapsedMessages.count)
+                    }
+                }
+            } else {
+                if collapseByMessage.collapsedMessages.count == 1 {
+                    if collapseByMessage.collapsedIncludesUserSelf {
+                        collapseByMessage.collapsedMessage = NSLocalizedString("{actor0} demoted you and {user0} from moderators", comment: "Please put {actor0} and {user0} placeholders in the correct position on the translated text but do not translate them")
+                    } else {
+                        collapseByMessage.collapsedMessage = NSLocalizedString("{actor0} demoted {user0} and {user1} from moderators", comment: "Please put {actor0}, {user0} and {user1} placeholders in the correct position on the translated text but do not translate them")
+                    }
+                } else {
+                    if collapseByMessage.collapsedIncludesUserSelf {
+                        collapseByMessage.collapsedMessage = String(format: NSLocalizedString("{actor0} demoted you and %ld more participants from moderators", comment: "Please put {actor0} and %ld placeholders in the correct position on the translated text but do not translate them"), collapseByMessage.collapsedMessages.count)
+                    } else {
+                        collapseByMessage.collapsedMessage = String(format: NSLocalizedString("{actor0} demoted {user0} and %ld more participants from moderators", comment: "Please put {actor0}, {user0} and %ld placeholders in the correct position on the translated text but do not translate them"), collapseByMessage.collapsedMessages.count)
+                    }
+                }
+            }
+        } else if action == "call_joined" {
+            if collapseByMessage.collapsedIncludesActorSelf {
+                if collapseByMessage.collapsedMessages.count == 1 {
+                    collapseByMessage.collapsedMessage = NSLocalizedString("You and {actor0} joined the call", comment: "Please put {actor0} placeholder in the correct position on the translated text but do not translate it")
+                } else {
+                    collapseByMessage.collapsedMessage = String(format: NSLocalizedString("You and %ld more participants joined the call", comment: "Please put %ld placeholder in the correct position on the translated text but do not translate it"), collapseByMessage.collapsedMessages.count)
+                }
+            } else {
+                if collapseByMessage.collapsedMessages.count == 1 {
+                    collapseByMessage.collapsedMessage = NSLocalizedString("{actor0} and {actor1} joined the call", comment: "Please put {actor0} and {actor1} placeholders in the correct position on the translated text but do not translate them")
+                } else {
+                    collapseByMessage.collapsedMessage = String(format: NSLocalizedString("{actor0} and %ld more participants joined the call", comment: "Please put {actor0} and %ld placeholders in the correct position on the translated text but do not translate them"), collapseByMessage.collapsedMessages.count)
+                }
+            }
+        } else if action == "call_left" {
+            if collapseByMessage.collapsedIncludesActorSelf {
+                if collapseByMessage.collapsedMessages.count == 1 {
+                    collapseByMessage.collapsedMessage = NSLocalizedString("You and {actor0} left the call", comment: "Please put {actor0} placeholder in the correct position on the translated text but do not translate it")
+                } else {
+                    collapseByMessage.collapsedMessage = String(format: NSLocalizedString("You and %ld more participants left the call", comment: "Please put %ld placeholder in the correct position on the translated text but do not translate it"), collapseByMessage.collapsedMessages.count)
+                }
+            } else {
+                if collapseByMessage.collapsedMessages.count == 1 {
+                    collapseByMessage.collapsedMessage = NSLocalizedString("{actor0} and {actor1} left the call", comment: "Please put {actor0} and {actor1} placeholders in the correct position on the translated text but do not translate them")
+                } else {
+                    collapseByMessage.collapsedMessage = String(format: NSLocalizedString("{actor0} and %ld more participants left the call", comment: "Please put {actor0} and %ld placeholders in the correct position on the translated text but do not translate them"), collapseByMessage.collapsedMessages.count)
+                }
+            }
+        } else if action == "call_reconnected" {
+            if collapseByMessage.collapsedIncludesActorSelf {
+                collapseByMessage.collapsedMessage = NSLocalizedString("You reconnected to the call", comment: "")
+            } else {
+                collapseByMessage.collapsedMessage = NSLocalizedString("{actor0} reconnected to the call", comment: "Please put {actor0} placeholder in the correct position on the translated text but do not translate it")
+            }
+        }
+    }
+
+    // MARK: - Reactions
+
+    func addReaction(reaction: String, to message: NCChatMessage) {
+        if message.reactionsArray().contains(where: {$0.reaction == reaction && $0.userReacted }) {
+            // We can't add reaction twice
+            return
+        }
+
+        let activeAccount = NCDatabaseManager.sharedInstance().activeAccount()
+        self.setTemporaryReaction(reaction: reaction, withState: .adding, toMessage: message)
+
+        NCDatabaseManager.sharedInstance().increaseEmojiUsage(forEmoji: reaction, forAccount: activeAccount.accountId)
+
+        NCAPIController.sharedInstance().addReaction(reaction, toMessage: message.messageId, inRoom: self.room.token, for: activeAccount) { _, error, _ in
+            if error != nil {
+                NotificationPresenter.shared().present(text: NSLocalizedString("An error occurred while adding a reaction to a message", comment: ""), dismissAfterDelay: 5.0, includedStyle: .error)
+                self.removeTemporaryReaction(reaction: reaction, forMessageId: message.messageId)
+            }
+        }
+    }
+
+    func removeReaction(reaction: String, from message: NCChatMessage) {
+        let activeAccount = NCDatabaseManager.sharedInstance().activeAccount()
+        self.setTemporaryReaction(reaction: reaction, withState: .removing, toMessage: message)
+
+        NCAPIController.sharedInstance().removeReaction(reaction, fromMessage: message.messageId, inRoom: self.room.token, for: activeAccount) { _, error, _ in
+            if error != nil {
+                NotificationPresenter.shared().present(text: NSLocalizedString("An error occurred while removing a reaction from a message", comment: ""), dismissAfterDelay: 5.0, includedStyle: .error)
+                self.removeTemporaryReaction(reaction: reaction, forMessageId: message.messageId)
+            }
+        }
+    }
+
+    func addOrRemoveReaction(reaction: NCChatReaction, in message: NCChatMessage) {
+        if message.isReactionBeingModified(reaction.reaction) {
+            return
+        }
+
+        if reaction.userReacted {
+            self.removeReaction(reaction: reaction.reaction, from: message)
+        } else {
+            self.addReaction(reaction: reaction.reaction, to: message)
+        }
+    }
+
+    func removeTemporaryReaction(reaction: String, forMessageId messageId: Int) {
+        DispatchQueue.main.async {
+            guard let (indexPath, message) = self.indexPathAndMessage(forMessageId: messageId) else { return }
+
+            message.removeReactionTemporarily(reaction)
+
+            self.tableView?.beginUpdates()
+            self.tableView?.reloadRows(at: [indexPath], with: .none)
+            self.tableView?.endUpdates()
+        }
+    }
+
+    func setTemporaryReaction(reaction: String, withState state: NCChatReactionState, toMessage message: NCChatMessage) {
+        DispatchQueue.main.async {
+            let isAtBottom = self.shouldScrollOnNewMessages()
+
+            guard let (indexPath, message) = self.indexPathAndMessage(forMessageId: message.messageId) else { return }
+
+            if state == .adding {
+                message.addTemporaryReaction(reaction)
+            } else if state == .removing {
+                message.removeReactionTemporarily(reaction)
+            }
+
+            self.tableView?.performBatchUpdates({
+                self.tableView?.reloadRows(at: [indexPath], with: .none)
+            }, completion: { _ in
+                if !isAtBottom {
+                    return
+                }
+
+                if let (indexPath, _) = self.getLastNonUpdateMessage() {
+                    self.tableView?.scrollToRow(at: indexPath, at: .bottom, animated: true)
+                }
+            })
+
+        }
+    }
+
+    func showReactionsSummary(of message: NCChatMessage) {
+        // Actuate `Peek` feedback (weak boom)
+        AudioServicesPlaySystemSound(1519)
+
+        let reactionsVC = ReactionsSummaryView(style: .insetGrouped)
+        reactionsVC.room = self.room
+        self.presentWithNavigation(reactionsVC, animated: true)
+
+        let activeAccount = NCDatabaseManager.sharedInstance().activeAccount()
+        NCAPIController.sharedInstance().getReactions(nil, fromMessage: message.messageId, inRoom: self.room.token, for: activeAccount) { reactionsDict, error, _ in
+            if error == nil,
+               let reactions = reactionsDict as? [String: [[String: AnyObject]]] {
+
+                reactionsVC.updateReactions(reactions: reactions)
+            }
+        }
+    }
+
+    // MARK: - UITableViewDataSource methods
+
+    public override func numberOfSections(in tableView: UITableView) -> Int {
+        if tableView != self.tableView {
+            return super.numberOfSections(in: tableView)
+        }
+
+        // TODO: There should be a better place to do this
+        if tableView == self.tableView, !self.dateSections.isEmpty {
+            tableView.backgroundView = nil
+        } else {
+            tableView.backgroundView = self.chatBackgroundView
+        }
+
+        return self.dateSections.count
+    }
+
+    public override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+        if tableView != self.tableView {
+            return super.tableView(tableView, numberOfRowsInSection: section)
+        }
+
+        let dateKey = self.dateSections[section]
+        return self.messages[dateKey]?.count ?? 0
+    }
+
+    public override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
+        if tableView != self.tableView {
+            return super.tableView(tableView, titleForHeaderInSection: section)
+        }
+
+        let date = self.dateSections[section]
+        return self.getHeaderString(fromDate: date)
+    }
+
+    public override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
+        if tableView != self.tableView {
+            return super.tableView(tableView, heightForHeaderInSection: section)
+        }
+
+        let date = self.dateSections[section]
+
+        if let messages = self.messages[date], !messages.containsVisibleMessages() {
+            return 0
+        }
+
+        return kDateHeaderViewHeight
+    }
+
+    public override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
+        if tableView != self.tableView {
+            return super.tableView(tableView, viewForHeaderInSection: section)
+        }
+
+        let headerView = DateHeaderView()
+        headerView.dateLabel.text = self.tableView(tableView, titleForHeaderInSection: section)
+        headerView.dateLabel.layer.cornerRadius = 12
+        headerView.dateLabel.clipsToBounds = true
+
+        if let headerLabel = headerView.dateLabel as? DateLabelCustom {
+            headerLabel.tableView = tableView
+        }
+
+        return headerView
+    }
+
+    public func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
+        guard tableView == self.tableView else { return }
+
+        for indexPath in indexPaths {
+            guard let message = self.message(for: indexPath) else { continue }
+
+            DispatchQueue.global(qos: .userInitiated).async {
+                guard message.messageId != kUnreadMessagesSeparatorIdentifier, 
+                      message.messageId != kChatBlockSeparatorIdentifier
+                else { return }
+
+                if message.containsURL() {
+                    message.getReferenceData()
+                }
+            }
+        }
+    }
+
+    public override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
+        if tableView != self.autoCompletionView,
+           let message = self.message(for: indexPath) {
+            return self.getCell(for: message)
+        }
+
+        return super.tableView(tableView, cellForRowAt: indexPath)
+    }
+
+    // swiftlint:disable:next cyclomatic_complexity
+    func getCell(for message: NCChatMessage) -> UITableViewCell {
+        if message.messageId == kUnreadMessagesSeparatorIdentifier,
+           let cell = self.tableView?.dequeueReusableCell(withIdentifier: MessageSeparatorCellIdentifier) as? MessageSeparatorTableViewCell {
+
+            cell.messageId = message.messageId
+            cell.separatorLabel.text = NSLocalizedString("Unread messages", comment: "")
+            return cell
+        }
+
+        if message.messageId == kChatBlockSeparatorIdentifier,
+           let cell = self.tableView?.dequeueReusableCell(withIdentifier: MessageSeparatorCellIdentifier) as? MessageSeparatorTableViewCell {
+
+            cell.messageId = message.messageId
+            cell.separatorLabel.text = NSLocalizedString("Some messages not shown, will be downloaded when online", comment: "")
+            return cell
+        }
+
+        if message.isUpdateMessage,
+           let cell = self.tableView?.dequeueReusableCell(withIdentifier: InvisibleSystemMessageCellIdentifier) as? SystemMessageTableViewCell {
+
+            return cell
+        }
+
+        if message.isSystemMessage,
+           let cell = self.tableView?.dequeueReusableCell(withIdentifier: SystemMessageCellIdentifier) as? SystemMessageTableViewCell {
+
+            cell.delegate = self
+            cell.setup(for: message)
+            return cell
+        }
+
+        if message.isVoiceMessage {
+            let cellIdentifier = message.isGroupMessage ? voiceGroupedMessageCellIdentifier : voiceMessageCellIdentifier
+
+            if let cell = self.tableView?.dequeueReusableCell(withIdentifier: cellIdentifier) as? BaseChatTableViewCell {
+                cell.delegate = self
+                cell.setup(for: message, withLastCommonReadMessage: self.room.lastCommonReadMessage)
+
+                if let playerAudioFileStatus = self.playerAudioFileStatus,
+                   let voiceMessagesPlayer = self.voiceMessagesPlayer {
+
+                    if message.file().parameterId == playerAudioFileStatus.fileId, message.file().path == playerAudioFileStatus.filePath {
+                        cell.audioPlayerView?.setPlayerProgress(voiceMessagesPlayer.currentTime, isPlaying: voiceMessagesPlayer.isPlaying, maximumValue: voiceMessagesPlayer.duration)
+                    } else {
+                        cell.audioPlayerView?.resetPlayer()
+                    }
+                } else {
+                    cell.audioPlayerView?.resetPlayer()
+                }
+
+                return cell
+            }
+        }
+
+        if message.file() != nil {
+            let cellIdentifier = message.isGroupMessage ? fileGroupedMessageCellIdentifier : fileMessageCellIdentifier
+
+            if let cell = self.tableView?.dequeueReusableCell(withIdentifier: cellIdentifier) as? BaseChatTableViewCell {
+                cell.delegate = self
+                cell.setup(for: message, withLastCommonReadMessage: self.room.lastCommonReadMessage)
+
+                return cell
+            }
+        }
+
+        if message.geoLocation() != nil {
+            let cellIdentifier = message.isGroupMessage ? locationGroupedMessageCellIdentifier : locationMessageCellIdentifier
+
+            if let cell = self.tableView?.dequeueReusableCell(withIdentifier: cellIdentifier) as? BaseChatTableViewCell {
+                cell.delegate = self
+                cell.setup(for: message, withLastCommonReadMessage: self.room.lastCommonReadMessage)
+
+                return cell
+            }
+        }
+
+        if message.poll != nil {
+            let cellIdentifier = message.isGroupMessage ? pollGroupedMessageCellIdentifier : pollMessageCellIdentifier
+
+            if let cell = self.tableView?.dequeueReusableCell(withIdentifier: cellIdentifier) as? BaseChatTableViewCell {
+                cell.delegate = self
+                cell.setup(for: message, withLastCommonReadMessage: self.room.lastCommonReadMessage)
+
+                return cell
+            }
+        }
+
+        var cellIdentifier = chatMessageCellIdentifier
+
+        if message.isGroupMessage {
+            cellIdentifier = chatGroupedMessageCellIdentifier
+        } else if message.parent != nil {
+            cellIdentifier = chatReplyMessageCellIdentifier
+        }
+
+        if let cell = self.tableView?.dequeueReusableCell(withIdentifier: cellIdentifier) as? BaseChatTableViewCell {
+            cell.delegate = self
+            cell.setup(for: message, withLastCommonReadMessage: self.room.lastCommonReadMessage)
+
+            return cell
+        }
+
+        return UITableViewCell()
+    }
+
+    public override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
+        if tableView == self.autoCompletionView {
+            return super.tableView(tableView, heightForRowAt: indexPath)
+        }
+
+        if let message = self.message(for: indexPath) {
+            return self.getCellHeight(for: message)
+        }
+
+        return chatMessageCellMinimumHeight
+    }
+
+    func getCellHeight(for message: NCChatMessage) -> CGFloat {
+        guard let tableView = self.tableView else { return chatMessageCellMinimumHeight }
+
+        var width = tableView.frame.width - kChatCellAvatarHeight
+        width -= tableView.safeAreaInsets.left + tableView.safeAreaInsets.right
+
+        return self.getCellHeight(for: message, with: width)
+    }
+
+    lazy var textViewForSizing: UITextView = {
+        return MessageBodyTextView()
+    }()
+
+    // swiftlint:disable:next cyclomatic_complexity
+    func getCellHeight(for message: NCChatMessage, with originalWidth: CGFloat) -> CGFloat {
+        // Chat separators
+        if message.messageId == kUnreadMessagesSeparatorIdentifier ||
+            message.messageId == kChatBlockSeparatorIdentifier {
+            return kMessageSeparatorCellHeight
+        }
+
+        // Update messages (the ones that notify about an update in one message, they should not be displayed)
+        if message.message.isEmpty || message.isUpdateMessage || (message.isCollapsed && message.collapsedBy != nil) {
+            return 0.0
+        }
+
+        // Chat messages
+        let messageString = message.parsedMarkdownForChat() ?? NSMutableAttributedString()
+        var width = originalWidth
+        width -= message.isSystemMessage ? 80.0 : 30.0 // *right(10) + dateLabel(40) : 3*right(10)
+
+        self.textViewForSizing.attributedText = messageString
+
+        let bodyBounds = self.textViewForSizing.sizeThatFits(CGSize(width: width, height: CGFLOAT_MAX))
+        var height = ceil(bodyBounds.height)
+
+        if message.poll != nil {
+            height = PollMessageView().pollMessageBodyHeight(with: messageString.string, width: width)
+        }
+
+        if (message.isGroupMessage && message.parent == nil) || message.isSystemMessage {
+            height += 10 // 2*left(5)
+
+            if height < chatGroupedMessageCellMinimumHeight {
+                height = chatGroupedMessageCellMinimumHeight
+            }
+        } else {
+            height += kChatCellAvatarHeight
+            height += 20.0 // right(10) + 2*left(5)
+
+            if height < chatMessageCellMinimumHeight {
+                height = chatMessageCellMinimumHeight
+            }
+        }
+
+        if !message.reactionsArray().isEmpty {
+            height += 40 // reactionsView(40)
+        }
+
+        if message.containsURL() {
+            height += 105
+        }
+
+        if message.parent != nil {
+            height += 60 // quoteView(60)
+        }
+
+        // Voice message should be before message.file check since it contains a file
+        if message.isVoiceMessage {
+            height -= ceil(bodyBounds.height)
+            height += voiceMessageCellPlayerHeight
+
+        } else if let file = message.file() {
+            if file.previewImageHeight > 0 {
+                height += CGFloat(file.previewImageHeight)
+            } else if case let estimatedHeight = BaseChatTableViewCell.getEstimatedPreviewSize(for: message), estimatedHeight > 0 {
+                height += estimatedHeight
+                message.setPreviewImageHeight(estimatedHeight)
+            } else {
+                height += fileMessageCellFileMaxPreviewHeight
+            }
+
+            height += 10 // right(10)
+
+            // if the message is a media file, reduce the message height by the bodyTextView height to hide it since it usually just contains an autogenerated file name (e.g. IMG_1234.jpg)
+            if NCUtils.isImage(fileType: file.mimetype) || NCUtils.isVideo(fileType: file.mimetype) {
+                // Only hide the filename if there's a preview available and we didn't receive a file caption
+                if file.previewAvailable && message.message == "{file}" {
+                    height -= ceil(bodyBounds.height)
+                }
+            }
+        }
+
+        if message.geoLocation() != nil {
+            height += locationMessageCellPreviewHeight + 10 // right(10)
+        }
+
+        return height
+    }
+
+    public override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
+        if tableView == self.tableView {
+            self.emojiTextField.resignFirstResponder()
+            self.datePickerTextField.resignFirstResponder()
+
+            // Disable swiftlint -> not supported on a Realm object
+            // swiftlint:disable:next empty_count
+            if let message = self.message(for: indexPath), message.collapsedMessages.count > 0 {
+                self.cellWantsToCollapseMessages(with: message)
+            }
+
+            tableView.deselectRow(at: indexPath, animated: true)
+        } else {
+            super.tableView(tableView, didSelectRowAt: indexPath)
+        }
+    }
+
+    // MARK: - ContextMenu (Long press on message)
+
+    public override func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
+        if tableView == self.autoCompletionView {
+            return nil
+        }
+
+        let cell = tableView.cellForRow(at: indexPath)
+
+        // Show reactionSummary for legacy cells
+        if let cell = cell as? ChatTableViewCell {
+            let pointInCell = tableView.convert(point, to: cell)
+            let reactionView = cell.contentView.subviews.first(where: { $0 is ReactionsView && $0.frame.contains(pointInCell) })
+
+            if reactionView != nil {
+                self.showReactionsSummary(of: cell.message)
+                return nil
+            }
+        }
+
+        if let cell = cell as? BaseChatTableViewCell {
+            let pointInCell = tableView.convert(point, to: cell)
+            let pointInReactionPart = cell.convert(pointInCell, to: cell.reactionPart)
+            let reactionView = cell.reactionPart.subviews.first(where: { $0 is ReactionsView && $0.frame.contains(pointInReactionPart) })
+
+            if reactionView != nil, let message = cell.message {
+                self.showReactionsSummary(of: message)
+                return nil
+            }
+        }
+
+        guard let message = self.message(for: indexPath) else { return nil }
+
+        if message.isSystemMessage || message.isDeletedMessage || message.messageId == kUnreadMessagesSeparatorIdentifier {
+            return nil
+        }
+
+        var actions: [UIMenuElement] = []
+
+        // Copy option
+        actions.append(UIAction(title: NSLocalizedString("Copy", comment: ""), image: .init(systemName: "square.on.square")) { _ in
+            self.didPressCopy(for: message)
+        })
+
+        // Copy Link
+        actions.append(UIAction(title: NSLocalizedString("Copy message link", comment: ""), image: .init(systemName: "link")) { _ in
+            self.didPressCopyLink(for: message)
+        })
+
+        let menu = UIMenu(children: actions)
+
+        let configuration = UIContextMenuConfiguration(identifier: indexPath as NSIndexPath) {
+            return nil
+        } actionProvider: { _ in
+            return menu
+        }
+
+        return configuration
+    }
+
+    public override func tableView(_ tableView: UITableView, willDisplayContextMenu configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionAnimating?) {
+        animator?.addAnimations {
+            // Only set these, when the context menu is fully visible
+            self.contextMenuAccessoryView?.alpha = 1
+            self.contextMenuMessageView?.layer.cornerRadius = 10
+            self.contextMenuMessageView?.layer.mask = nil
+        }
+    }
+
+    public override func tableView(_ tableView: UITableView, willEndContextMenuInteraction configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionAnimating?) {
+        animator?.addCompletion {
+            // Wait until the context menu is completely hidden before we execute any method
+            if let contextMenuActionBlock = self.contextMenuActionBlock {
+                contextMenuActionBlock()
+                self.contextMenuActionBlock = nil
+            }
+        }
+    }
+
+    internal func getContextMenuAccessoryView(forMessage message: NCChatMessage, forIndexPath indexPath: IndexPath, withCellHeight cellHeight: CGFloat) -> UIView? {
+        // We don't provide a accessory view in the BaseChatViewController, but can add it in a subclass
+        return nil
+    }
+
+    public override func tableView(_ tableView: UITableView, previewForHighlightingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? {
+        guard let indexPath = configuration.identifier as? NSIndexPath,
+              let message = self.message(for: indexPath as IndexPath)
+        else { return nil }
+
+        let maxPreviewWidth = self.view.bounds.size.width - self.view.safeAreaInsets.left - self.view.safeAreaInsets.right
+        let maxPreviewHeight = self.view.bounds.size.height * 0.6
+
+        // TODO: Take padding into account
+        let maxTextWidth = maxPreviewWidth - kChatCellAvatarHeight
+
+        // We need to get the height of the original cell to center the preview correctly (as the preview is always non-grouped)
+        let heightOfOriginalCell = self.getCellHeight(for: message, with: maxTextWidth)
+
+        // Remember grouped-status -> Create a previewView which always is a non-grouped-message
+        let isGroupMessage = message.isGroupMessage
+        message.isGroupMessage = false
+
+        let previewTableViewCell = self.getCell(for: message)
+        var cellHeight = self.getCellHeight(for: message, with: maxTextWidth)
+
+        // Cut the height if bigger than max height
+        if cellHeight > maxPreviewHeight {
+            cellHeight = maxPreviewHeight
+        }
+
+        // Use the contentView of the UITableViewCell as a preview view
+        let previewMessageView = previewTableViewCell.contentView
+        previewMessageView.frame = CGRect(x: 0, y: 0, width: maxPreviewWidth, height: cellHeight)
+        previewMessageView.layer.masksToBounds = true
+
+        // Create a mask to not show the avatar part when showing a grouped messages while animating
+        // The mask will be reset in willDisplayContextMenuWithConfiguration so the avatar is visible when the context menu is shown
+        let maskLayer = CAShapeLayer()
+        let maskRect = CGRect(x: 0, y: previewMessageView.frame.size.height - heightOfOriginalCell, width: previewMessageView.frame.size.width, height: heightOfOriginalCell)
+        maskLayer.path = CGPath(rect: maskRect, transform: nil)
+
+        previewMessageView.layer.mask = maskLayer
+        previewMessageView.backgroundColor = .systemBackground
+        self.contextMenuMessageView = previewMessageView
+
+        // Restore grouped-status
+        message.isGroupMessage = isGroupMessage
+
+        var containerView: UIView
+        var cellCenter = CGPoint()
+
+        if let accessoryView = self.getContextMenuAccessoryView(forMessage: message, forIndexPath: indexPath as IndexPath, withCellHeight: cellHeight) {
+            self.contextMenuAccessoryView = accessoryView
+
+            // maxY = height + y
+            let totalAccessoryFrameHeight = accessoryView.frame.maxY - cellHeight
+
+            containerView = UIView(frame: .init(x: 0, y: 0, width: Int(maxPreviewWidth), height: Int(cellHeight + totalAccessoryFrameHeight)))
+            containerView.backgroundColor = .clear
+            containerView.addSubview(previewMessageView)
+            containerView.addSubview(accessoryView)
+
+            if let cell = tableView.cellForRow(at: indexPath as IndexPath) {
+                // On large iPhones (with regular landscape size, like iPhone X) we need to take the safe area into account when calculating the center
+                let cellCenterX = cell.center.x + self.view.safeAreaInsets.left / 2 - self.view.safeAreaInsets.right / 2
+                let cellCenterY = cell.center.y + (totalAccessoryFrameHeight) / 2 - (cellHeight - heightOfOriginalCell) / 2
+                cellCenter = CGPoint(x: cellCenterX, y: cellCenterY)
+            }
+        } else {
+            containerView = UIView(frame: .init(x: 0, y: 0, width: maxPreviewWidth, height: cellHeight))
+            containerView.backgroundColor = .clear
+            containerView.addSubview(previewMessageView)
+
+            if let cell = tableView.cellForRow(at: indexPath as IndexPath) {
+                // On large iPhones (with regular landscape size, like iPhone X) we need to take the safe area into account when calculating the center
+                let cellCenterX = cell.center.x + self.view.safeAreaInsets.left / 2 - self.view.safeAreaInsets.right / 2
+                let cellCenterY = cell.center.y - (cellHeight - heightOfOriginalCell) / 2
+                cellCenter = CGPoint(x: cellCenterX, y: cellCenterY)
+            }
+        }
+
+        // Create a preview target which allows us to have a transparent background
+        let previewTarget = UIPreviewTarget(container: tableView, center: cellCenter)
+        let previewParameter = UIPreviewParameters()
+
+        // Remove the background and the drop shadow from our custom preview view
+        previewParameter.backgroundColor = .clear
+        previewParameter.shadowPath = UIBezierPath()
+
+        return UITargetedPreview(view: containerView, parameters: previewParameter, target: previewTarget)
+    }
+
+    // MARK: - Chat functions
+
+    public func showLoadingHistoryView() {
+        self.loadingHistoryView = UIActivityIndicatorView(frame: .init(x: 0, y: 0, width: 30, height: 30))
+        self.loadingHistoryView?.color = .darkGray
+        self.loadingHistoryView?.startAnimating()
+        self.tableView?.tableHeaderView = self.loadingHistoryView
+    }
+
+    func hideLoadingHistoryView() {
+        self.loadingHistoryView = nil
+        self.tableView?.tableHeaderView = nil
+    }
+
+    func shouldScrollOnNewMessages() -> Bool {
+        guard self.isVisible, let tableView = self.tableView else { return false }
+
+        // Scroll if table view is at the bottom (or 80px up)
+        let minimumOffset = (tableView.contentSize.height - tableView.frame.size.height) - 80
+
+        if tableView.contentOffset.y >= minimumOffset {
+            return true
+        }
+
+        return false
+    }
+
+    public func cleanChat() {
+        self.messages = [:]
+        self.dateSections = []
+        self.hideNewMessagesView()
+        self.tableView?.reloadData()
+    }
+
+    public func savePendingMessage() {
+        if self.textInputbar.isEditing {
+            // We don't want to save a message that we are editing
+            return
+        }
+
+        self.room.pendingMessage = self.textView.text
+        NCRoomsManager.sharedInstance().updatePendingMessage(self.room.pendingMessage, for: self.room)
+    }
+
+    public func clearPendingMessage() {
+        self.room.pendingMessage = ""
+        NCRoomsManager.sharedInstance().updatePendingMessage("", for: self.room)
+    }
+
+    private func getKeyForDate(date: Date, inDictionary dict: [Date: [NCChatMessage]]) -> Date? {
+        let currentCalendar = NSCalendar.current
+        return dict.first(where: { currentCalendar.isDate(date, inSameDayAs: $0.key) })?.key
+    }
+
+    internal func message(for indexPath: IndexPath) -> NCChatMessage? {
+        let sectionDate = self.dateSections[indexPath.section]
+
+        if let message = self.messages[sectionDate]?[indexPath.row] {
+            return message
+        }
+
+        return nil
+    }
+
+    internal func indexPath(for message: NCChatMessage) -> IndexPath? {
+        let messageDate = Date(timeIntervalSince1970: TimeInterval(message.timestamp))
+
+        guard let keyDate = self.getKeyForDate(date: messageDate, inDictionary: self.messages),
+              let dateSection = dateSections.firstIndex(of: keyDate),
+              let messages = messages[keyDate]
+        else { return nil }
+
+        for i in messages.indices {
+            let chatMessage = messages[i]
+
+            if chatMessage.isSameMessage(message) {
+                return IndexPath(row: i, section: dateSection)
+            }
+        }
+
+        return nil
+    }
+
+    /// Iterate through all messages starting with the first message and returns the first message that fulfills the predicate
+    private func indexPathAndMessageFromStart(with predicate: (NCChatMessage) -> Bool) -> (indexPath: IndexPath, message: NCChatMessage)? {
+        for sectionIndex in dateSections.indices {
+            let section = dateSections[sectionIndex]
+
+            guard let messages = messages[section] else { continue }
+
+            for messageIndex in messages.indices {
+                let message = messages[messageIndex]
+
+                if predicate(message) {
+                    return (IndexPath(row: messageIndex, section: sectionIndex), message)
+                }
+            }
+        }
+
+        return nil
+    }
+
+    /// Iterate through all messages starting with the last message and returns the first message that fulfills the predicate
+    private func indexPathAndMessageFromEnd(with predicate: (NCChatMessage) -> Bool) -> (indexPath: IndexPath, message: NCChatMessage)? {
+        for sectionIndex in dateSections.indices.reversed() {
+            let section = dateSections[sectionIndex]
+
+            guard let messages = messages[section] else { continue }
+
+            for messageIndex in messages.indices.reversed() {
+                let message = messages[messageIndex]
+
+                if predicate(message) {
+                    return (IndexPath(row: messageIndex, section: sectionIndex), message)
+                }
+            }
+        }
+
+        return nil
+    }
+
+    internal func indexPathAndMessage(forMessageId messageId: Int) -> (indexPath: IndexPath, message: NCChatMessage)? {
+        return self.indexPathAndMessageFromEnd(with: { $0.messageId == messageId })
+    }
+
+    internal func indexPathAndMessage(forReferenceId referenceId: String) -> (indexPath: IndexPath, message: NCChatMessage)? {
+        return self.indexPathAndMessageFromEnd(with: { $0.referenceId == referenceId })
+    }
+
+    internal func indexPathForUnreadMessageSeparator() -> IndexPath? {
+        return self.indexPathAndMessageFromEnd(with: { $0.messageId == kUnreadMessagesSeparatorIdentifier })?.indexPath
+    }
+
+    internal func getLastNonUpdateMessage() -> (indexPath: IndexPath, message: NCChatMessage)? {
+        return self.indexPathAndMessageFromEnd(with: { !$0.isUpdateMessage })
+    }
+
+    internal func getLastRealMessage() -> (indexPath: IndexPath, message: NCChatMessage)? {
+        // Ignore temporary messages
+        return self.indexPathAndMessageFromEnd(with: { $0.messageId > 0 })
+    }
+
+    internal func getFirstRealMessage() -> (indexPath: IndexPath, message: NCChatMessage)? {
+        // Ignore temporary messages
+        return self.indexPathAndMessageFromStart(with: { $0.messageId > 0 })
+    }
+
+    internal func indexPathForLastMessage() -> IndexPath? {
+        return self.indexPathAndMessageFromEnd(with: { _ in return true })?.indexPath
+    }
+
+    internal func removeUnreadMessagesSeparator() {
+        if let indexPath = self.indexPathForUnreadMessageSeparator() {
+            let separatorDate = self.dateSections[indexPath.section]
+            self.messages[separatorDate]?.remove(at: indexPath.row)
+            self.tableView?.deleteRows(at: [indexPath], with: .fade)
+        }
+    }
+
+    internal func checkUnreadMessagesVisibility() {
+        DispatchQueue.main.async {
+            if let firstUnreadMessage = self.firstUnreadMessage,
+               let indexPath = self.indexPath(for: firstUnreadMessage) {
+
+                if self.tableView?.indexPathsForVisibleRows?.contains(indexPath) ?? false {
+                    self.hideNewMessagesView()
+                }
+            }
+        }
+    }
+
+    internal func highlightMessage(at indexPath: IndexPath, with scrollPosition: UITableView.ScrollPosition) {
+        self.tableView?.selectRow(at: indexPath, animated: true, scrollPosition: scrollPosition)
+
+        DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
+            self.tableView?.deselectRow(at: indexPath, animated: true)
+        }
+    }
+
+    internal func highlightMessageWithContentOffset(messageId: Int) {
+        guard messageId > 0,
+              let tableView = self.tableView,
+              let (indexPath, _) = self.indexPathAndMessage(forMessageId: messageId)
+        else { return }
+
+        self.highlightMessage(at: indexPath, with: .none)
+
+        let rect = tableView.rectForRow(at: indexPath)
+
+        // ContentOffset when the cell is at the top of the tableView
+        let contentOffsetTop = rect.origin.y - tableView.safeAreaInsets.top
+
+        // ContentOffset when the cell is at the middle of the tableView
+        let contentOffsetMiddle = contentOffsetTop - tableView.frame.height / 2 + rect.height / 2
+
+        // Fallback to the top offset in case the top of the cell would be scrolled outside of the view
+        let newContentOffset = min(contentOffsetTop, contentOffsetMiddle)
+
+        tableView.contentOffset.y = newContentOffset
+    }
+
+    public func reloadDataAndHighlightMessage(messageId: Int) {
+        self.tableView?.reloadData()
+        self.highlightMessageWithContentOffset(messageId: messageId)
+    }
+
+    func showNewMessagesView(until message: NCChatMessage) {
+        self.firstUnreadMessage = message
+        self.unreadMessageButton.isHidden = false
+        // Check if unread messages are already visible
+        self.checkUnreadMessagesVisibility()
+    }
+
+    func hideNewMessagesView() {
+        self.firstUnreadMessage = nil
+        self.unreadMessageButton.isHidden = true
+    }
+
+    // MARK: - FileMessageTableViewCellDelegate
+
+    public func cellWants(toDownloadFile fileParameter: NCMessageFileParameter, for message: NCChatMessage) {
+        if NCUtils.isImage(fileType: fileParameter.mimetype) {
+            let mediaViewController = NCMediaViewerViewController(initialMessage: message)
+            let navController = CustomPresentableNavigationController(rootViewController: mediaViewController)
+
+            self.present(navController, interactiveDismissalType: .standard)
+
+            return
+        }
+
+        if fileParameter.fileStatus != nil && fileParameter.fileStatus?.isDownloading ?? false {
+            print("File already downloading -> skipping new download")
+            return
+        }
+
+        let downloader = NCChatFileController()
+        downloader.delegate = self
+        downloader.downloadFile(fromMessage: fileParameter)
+    }
+
+    public func cellHasDownloadedImagePreview(withHeight height: CGFloat, for message: NCChatMessage) {
+        if message.file().previewImageHeight == Int(height) {
+            return
+        }
+
+        let isAtBottom = self.shouldScrollOnNewMessages()
+
+        message.setPreviewImageHeight(height)
+
+        CATransaction.begin()
+        CATransaction.setCompletionBlock {
+            DispatchQueue.main.async {
+                // make sure we're really at the bottom after updating a message since the file previews could grow in size if they contain a media file preview, thus giving the effect of not being at the bottom of the chat
+                if isAtBottom, !(self.tableView?.isDecelerating ?? false) {
+                    self.tableView?.slk_scrollToBottom(animated: true)
+                    self.updateToolbar(animated: true)
+                }
+            }
+        }
+
+        self.tableView?.beginUpdates()
+        self.tableView?.endUpdates()
+
+        CATransaction.commit()
+    }
+
+    // MARK: - VoiceMessageTableViewCellDelegate
+
+    public func cellWants(toPlayAudioFile fileParameter: NCMessageFileParameter) {
+        if fileParameter.fileStatus != nil && fileParameter.fileStatus?.isDownloading ?? false {
+            print("File already downloading -> skipping new download")
+            return
+        }
+
+        if let voiceMessagesPlayer = self.voiceMessagesPlayer,
+           let playerAudioFileStatus = self.playerAudioFileStatus,
+           !voiceMessagesPlayer.isPlaying,
+           fileParameter.parameterId == playerAudioFileStatus.fileId,
+           fileParameter.path == playerAudioFileStatus.filePath {
+
+            self.playVoiceMessagePlayer()
+            return
+        }
+
+        let downloader = NCChatFileController()
+        downloader.delegate = self
+        downloader.messageType = kMessageTypeVoiceMessage
+        downloader.downloadFile(fromMessage: fileParameter)
+    }
+
+    public func cellWants(toPauseAudioFile fileParameter: NCMessageFileParameter) {
+        if let voiceMessagesPlayer = self.voiceMessagesPlayer,
+           let playerAudioFileStatus = self.playerAudioFileStatus,
+           voiceMessagesPlayer.isPlaying,
+           fileParameter.parameterId == playerAudioFileStatus.fileId,
+           fileParameter.path == playerAudioFileStatus.filePath {
+
+            self.pauseVoiceMessagePlayer()
+        }
+    }
+
+    public func cellWants(toChangeProgress progress: CGFloat, fromAudioFile fileParameter: NCMessageFileParameter) {
+        if let playerAudioFileStatus = self.playerAudioFileStatus,
+           fileParameter.parameterId == playerAudioFileStatus.fileId,
+           fileParameter.path == playerAudioFileStatus.filePath {
+
+            self.pauseVoiceMessagePlayer()
+            self.voiceMessagesPlayer?.currentTime = progress
+            self.checkVisibleCellAudioPlayers()
+        }
+    }
+
+    // MARK: - LocationMessageTableViewCell
+
+    public func cellWants(toOpenLocation geoLocationRichObject: GeoLocationRichObject) {
+        self.presentWithNavigation(MapViewController(geoLocationRichObject: geoLocationRichObject), animated: true)
+    }
+
+    // MARK: - ObjectShareMessageTableViewCell
+
+    public func cellWants(toOpenPoll poll: NCMessageParameter) {
+        let pollVC = PollVotingView(style: .insetGrouped)
+        pollVC.room = self.room
+        self.presentWithNavigation(pollVC, animated: true)
+
+        if let pollId = Int(poll.parameterId) {
+            let activeAccount = NCDatabaseManager.sharedInstance().activeAccount()
+            NCAPIController.sharedInstance().getPollWithId(pollId, inRoom: self.room.token, for: activeAccount) { poll, error, _ in
+                if error == nil, let poll {
+                    pollVC.updatePoll(poll: poll)
+                }
+            }
+        }
+    }
+
+    // MARK: - PollCreationViewControllerDelegate
+
+    func pollCreationViewControllerWantsToCreatePoll(pollCreationViewController: PollCreationViewController, question: String, options: [String], resultMode: NCPollResultMode, maxVotes: Int) {
+        let activeAccount = NCDatabaseManager.sharedInstance().activeAccount()
+        NCAPIController.sharedInstance().createPoll(withQuestion: question, options: options, resultMode: resultMode, maxVotes: maxVotes, inRoom: self.room.token, for: activeAccount) { _, error, _ in
+            if error != nil {
+                pollCreationViewController.showCreationError()
+            } else {
+                pollCreationViewController.close()
+            }
+        }
+    }
+
+    // MARK: - SystemMessageTableViewCellDelegate
+
+    public func cellWantsToCollapseMessages(with message: NCChatMessage!) {
+        DispatchQueue.main.async {
+            guard let messageIds = message.collapsedMessages.value(forKey: "self") as? [NSNumber] else { return }
+
+            let collapse = !message.isCollapsed
+            var reloadIndexPath: [IndexPath] = []
+
+            if let indexPath = self.indexPath(for: message) {
+                reloadIndexPath.append(indexPath)
+                message.isCollapsed = collapse
+            }
+
+            for messageId in messageIds {
+                if let (indexPath, message) = self.indexPathAndMessage(forMessageId: messageId.intValue) {
+                    reloadIndexPath.append(indexPath)
+                    message.isCollapsed = collapse
+                }
+            }
+
+            self.tableView?.beginUpdates()
+            self.tableView?.reloadRows(at: reloadIndexPath, with: .automatic)
+            self.tableView?.endUpdates()
+        }
+    }
+
+    // MARK: - ChatMessageTableViewCellDelegate
+
+    public func cellWantsToScroll(to message: NCChatMessage) {
+        DispatchQueue.main.async {
+            if let indexPath = self.indexPath(for: message) {
+                self.highlightMessage(at: indexPath, with: .top)
+            }
+        }
+    }
+
+    public func cellDidSelectedReaction(_ reaction: NCChatReaction!, for message: NCChatMessage) {
+        // Do nothing -> override in subclass
+    }
+
+    public func cellWantsToReply(to message: NCChatMessage) {
+        if self.textInputbar.isEditing {
+            return
+        }
+
+        self.didPressReply(for: message)
+    }
+
+    // MARK: - NCChatFileControllerDelegate
+
+    public func fileControllerDidLoadFile(_ fileController: NCChatFileController, with fileStatus: NCChatFileStatus) {
+        if fileController.messageType == kMessageTypeVoiceMessage {
+            if fileController.actionType == actionTypeTranscribeVoiceMessage {
+                self.transcribeVoiceMessage(with: fileStatus)
+            } else {
+                self.setupVoiceMessagePlayer(with: fileStatus)
+            }
+
+            return
+        }
+
+        if self.isPreviewControllerShown {
+            // We are showing a file already, no need to open another one
+            return
+        }
+
+        guard let tableView = self.tableView else { return }
+        var isFileCellStillVisible = false
+
+        if let indexPathsForVisibleRows = tableView.indexPathsForVisibleRows {
+            for indexPath in indexPathsForVisibleRows {
+                guard let message = self.message(for: indexPath), let file = message.file() else { continue }
+
+                if file.parameterId == fileStatus.fileId && file.path == fileStatus.filePath {
+                    isFileCellStillVisible = true
+                    break
+                }
+            }
+        }
+
+        if !isFileCellStillVisible {
+            // Only open file when the corresponding cell is still visible on the screen
+            return
+        }
+
+        DispatchQueue.main.async {
+            self.isPreviewControllerShown = true
+            self.previewControllerFilePath = fileStatus.fileLocalPath
+
+            // When the keyboard is not dismissed, dismissing the previewController might result in a corrupted keyboardView
+            self.dismissKeyboard(false)
+
+            guard let fileLocalPath = fileStatus.fileLocalPath else { return }
+            let fileExtension = URL(fileURLWithPath: fileLocalPath).pathExtension.lowercased()
+
+            // Use VLCKitVideoViewController for file formats unsupported by the native PreviewController
+            if VLCKitVideoViewController.supportedFileExtensions.contains(fileExtension) {
+                let vlcKitViewController = VLCKitVideoViewController(filePath: fileLocalPath)
+                vlcKitViewController.delegate = self
+                vlcKitViewController.modalPresentationStyle = .fullScreen
+                self.present(vlcKitViewController, animated: true)
+
+                return
+            }
+
+            let preview = QLPreviewController()
+            preview.dataSource = self
+            preview.delegate = self
+
+            preview.navigationController?.navigationBar.tintColor = NCAppBranding.themeTextColor()
+            preview.navigationController?.navigationBar.barTintColor = NCAppBranding.themeColor()
+            preview.tabBarController?.tabBar.tintColor = NCAppBranding.themeColor()
+
+            let appearance = UINavigationBarAppearance()
+            appearance.configureWithOpaqueBackground()
+            appearance.titleTextAttributes = [.foregroundColor: NCAppBranding.themeTextColor()]
+            appearance.backgroundColor = NCAppBranding.themeColor()
+            self.navigationItem.standardAppearance = appearance
+            self.navigationItem.compactAppearance = appearance
+            self.navigationItem.scrollEdgeAppearance = appearance
+
+            self.present(preview, animated: true)
+        }
+    }
+
+    public func fileControllerDidFailLoadingFile(_ fileController: NCChatFileController, withErrorDescription errorDescription: String) {
+        let alert = UIAlertController(title: NSLocalizedString("Unable to load file", comment: ""),
+                                      message: errorDescription,
+                                      preferredStyle: .alert)
+
+        alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default))
+        NCUserInterfaceController.sharedInstance().presentAlertViewController(alert)
+    }
+
+    // MARK: - QLPreviewControllerDelegate/DataSource
+
+    public func numberOfPreviewItems(in controller: QLPreviewController) -> Int {
+        return 1
+    }
+
+    public func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {
+        if let filePath = self.previewControllerFilePath {
+            return URL(fileURLWithPath: filePath) as QLPreviewItem
+        }
+
+        return URL(fileURLWithPath: "") as QLPreviewItem
+    }
+
+    public func previewControllerDidDismiss(_ controller: QLPreviewController) {
+        self.isPreviewControllerShown = false
+    }
+
+    // MARK: - VLCVideoViewControllerDelegate
+
+    func vlckitVideoViewControllerDismissed(_ controller: VLCKitVideoViewController) {
+        self.isPreviewControllerShown = false
+    }
+}
+
+extension Sequence where Iterator.Element == NCChatMessage {
+
+    func containsUserMessage() -> Bool {
+        let activeAccount = NCDatabaseManager.sharedInstance().activeAccount()
+        return self.contains(where: { !$0.isSystemMessage && $0.actorId == activeAccount.userId })
+    }
+
+    func containsVisibleMessages() -> Bool {
+        return self.contains(where: { !$0.isUpdateMessage })
+    }
+
+}

+ 29 - 0
NextcloudTalk/CCCertificate.h

@@ -0,0 +1,29 @@
+//
+// SPDX-FileCopyrightText: 2016 Marino Faggiana <m.faggiana@twsweb.it>, TWS
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+
+#import <Foundation/Foundation.h>
+#import <UIKit/UIKit.h>
+
+@protocol CCCertificateDelegate <NSObject>
+
+@optional - (void)trustedCerticateAccepted;
+@optional - (void)trustedCerticateDenied;
+
+@end
+
+@interface CCCertificate : NSObject
+
+@property (weak) id<CCCertificateDelegate> delegate;
+
++ (CCCertificate *)sharedManager;
+
+- (BOOL)checkTrustedChallenge:(NSURLAuthenticationChallenge *)challenge;
+- (BOOL)acceptCertificate;
+- (void)saveCertificate:(SecTrustRef)trust withName:(NSString *)certName;
+
+- (void)presentViewControllerCertificateWithTitle:(NSString *)title viewController:(UIViewController *)viewController delegate:(id)delegate;
+
+@end
+

+ 183 - 0
NextcloudTalk/CCCertificate.m

@@ -0,0 +1,183 @@
+//
+// SPDX-FileCopyrightText: 2016 Marino Faggiana <m.faggiana@twsweb.it>, TWS
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+
+#import "CCCertificate.h"
+
+#import <openssl/x509.h>
+#import <openssl/bio.h>
+#import <openssl/err.h>
+#import <openssl/pem.h>
+
+#import "NCAppBranding.h"
+
+@implementation CCCertificate
+
+NSString *const appCertificates = @"Library/Application Support/Certificates";
+
+//Singleton
++ (CCCertificate *)sharedManager {
+    static CCCertificate *CCCertificate = nil;
+    static dispatch_once_t onceToken;
+    dispatch_once(&onceToken, ^{
+        CCCertificate = [[self alloc] init];
+    });
+    return CCCertificate;
+}
+
+static SecCertificateRef SecTrustGetLeafCertificate(SecTrustRef trust)
+// Returns the leaf certificate from a SecTrust object (that is always the 
+// certificate at index 0).
+{
+    SecCertificateRef   result;
+    
+    assert(trust != NULL);
+    
+    if (SecTrustGetCertificateCount(trust) > 0) {
+        result = SecTrustGetCertificateAtIndex(trust, 0);
+        assert(result != NULL);
+    } else {
+        result = NULL;
+    }
+    return result;
+}
+
+- (BOOL)checkTrustedChallenge:(NSURLAuthenticationChallenge *)challenge
+{
+    BOOL trusted = NO;
+    SecTrustRef trust;
+    NSURLProtectionSpace *protectionSpace;
+    
+    protectionSpace = [challenge protectionSpace];
+    trust = [protectionSpace serverTrust];
+        
+    if(trust != nil) {
+        [self saveCertificate:trust withName:@"tmp.der"];
+        NSString *localCertificatesFolder = [self getDirectoryCerificates];
+        NSArray* listCertificateLocation = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:localCertificatesFolder error:NULL];
+        
+        for (int i = 0 ; i < [listCertificateLocation count] ; i++) {
+            NSString *currentLocalCertLocation = [NSString stringWithFormat:@"%@/%@",localCertificatesFolder,[listCertificateLocation objectAtIndex:i]];
+            NSString *tempCertLocation = [NSString stringWithFormat:@"%@/%@",localCertificatesFolder,@"tmp.der"];
+            NSFileManager *fileManager = [ NSFileManager defaultManager];
+            
+            if(![currentLocalCertLocation isEqualToString:tempCertLocation] &&
+               [fileManager contentsEqualAtPath:tempCertLocation andPath:currentLocalCertLocation]) {
+                
+                NSLog(@"Certificated matched with one saved previously.");
+                trusted = YES;
+            }
+        }
+    } else {
+        trusted = NO;
+    }
+    
+    return trusted;
+}
+
+- (void)saveCertificate:(SecTrustRef)trust withName:(NSString *)certName
+{
+    SecCertificateRef currentServerCert = SecTrustGetLeafCertificate(trust);
+    
+    CFDataRef data = SecCertificateCopyData(currentServerCert);
+    X509 *x509cert = NULL;
+    if (data) {
+        BIO *mem = BIO_new_mem_buf((void *)CFDataGetBytePtr(data), (int)CFDataGetLength(data));
+        x509cert = d2i_X509_bio(mem, NULL);
+        BIO_free(mem);
+        CFRelease(data);
+        
+        if (!x509cert) {
+            
+            NSLog(@"[LOG] OpenSSL couldn't parse X509 Certificate");
+            
+        } else {
+            
+            NSString *localCertificatesFolder = [self getDirectoryCerificates];
+            
+            certName = [NSString stringWithFormat:@"%@/%@",localCertificatesFolder,certName];
+            
+            if ([[NSFileManager defaultManager] fileExistsAtPath:certName]) {
+                NSError *error;
+                [[NSFileManager defaultManager] removeItemAtPath:certName error:&error];
+            }
+            
+            FILE *file;
+            file = fopen([certName UTF8String], "w");
+            if (file) {
+                PEM_write_X509(file, x509cert);
+            }
+            fclose(file);
+        }
+    
+    } else {
+        
+        NSLog(@"[LOG] Failed to retrieve DER data from Certificate Ref");
+    }
+    
+    //Free
+    X509_free(x509cert);
+}
+
+- (void)presentViewControllerCertificateWithTitle:(NSString *)title viewController:(UIViewController *)viewController delegate:(id)delegate
+{
+    if (![viewController isKindOfClass:[UIViewController class]])
+        return;
+    
+    _delegate = delegate;
+    
+    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.5 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
+        
+        UIAlertController *alertController = [UIAlertController alertControllerWithTitle:title message:NSLocalizedString(@"Do you want to connect to the server anyway?", nil)  preferredStyle:UIAlertControllerStyleAlert];
+        
+        [alertController addAction: [UIAlertAction actionWithTitle:NSLocalizedString(@"Yes", nil) style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
+            
+            [[CCCertificate sharedManager] acceptCertificate];
+            
+            if([self.delegate respondsToSelector:@selector(trustedCerticateAccepted)])
+                [self.delegate trustedCerticateAccepted];
+        }]];
+        
+        [alertController addAction: [UIAlertAction actionWithTitle:NSLocalizedString(@"No", nil) style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) {
+            
+            if([self.delegate respondsToSelector:@selector(trustedCerticateDenied)])
+                [self.delegate trustedCerticateDenied];
+        }]];
+        
+        [viewController presentViewController:alertController animated:YES completion:nil];
+    });
+}
+
+- (BOOL)acceptCertificate
+{
+    NSString *localCertificatesFolder = [self getDirectoryCerificates];
+    NSError *error;
+    NSFileManager *fm = [[NSFileManager alloc] init];
+    NSTimeInterval dateCertificate = [[NSDate date] timeIntervalSince1970];
+    NSString *currentCertLocation = [NSString stringWithFormat:@"%@/%f.der",localCertificatesFolder, dateCertificate];
+    
+    NSLog(@"[LOG] currentCertLocation: %@", currentCertLocation);
+    
+    if(![fm moveItemAtPath:[NSString stringWithFormat:@"%@/%@",localCertificatesFolder, @"tmp.der"] toPath:currentCertLocation error:&error]) {
+        
+        NSLog(@"[LOG] Error: %@", [error localizedDescription]);
+        return NO;
+        
+    }
+    
+    return YES;
+}
+
+- (NSString *)getDirectoryCerificates
+{
+    NSURL *dirGroup = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:groupIdentifier];
+    
+    NSString *dir = [[dirGroup URLByAppendingPathComponent:appCertificates] path];
+    if (![[NSFileManager defaultManager] fileExistsAtPath:dir])
+        [[NSFileManager defaultManager] createDirectoryAtPath:dir withIntermediateDirectories:YES attributes:nil error:nil];
+    
+    return dir;
+}
+
+@end

+ 19 - 0
NextcloudTalk/CallConstants.h

@@ -0,0 +1,19 @@
+//
+/**
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#ifndef CallConstants_h
+#define CallConstants_h
+
+typedef NS_ENUM(NSInteger, CallFlag) {
+    CallFlagDisconnected = 0,
+    CallFlagInCall = 1,
+    CallFlagWithAudio = 2,
+    CallFlagWithVideo = 4,
+    CallFlagWithPhone = 8
+};
+
+
+#endif /* CallConstants_h */

+ 205 - 0
NextcloudTalk/CallFlowLayout.swift

@@ -0,0 +1,205 @@
+//
+// SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+// Based on https://stackoverflow.com/a/41409642
+
+import UIKit
+
+@objcMembers
+class CallFlowLayout: UICollectionViewFlowLayout {
+
+    private let targetAspectRatioPortrait = 1.0
+    private let targetAspectRatioLandscape = 1.5
+
+    private var numberOfColumns = 1
+    private var numberOfRows = 1
+    private var targetAspectRatio: Double
+
+    override init() {
+        self.targetAspectRatio = self.targetAspectRatioLandscape
+
+        super.init()
+
+        commonInit()
+    }
+
+    required init?(coder aDecoder: NSCoder) {
+        self.targetAspectRatio = self.targetAspectRatioLandscape
+
+        super.init(coder: aDecoder)
+
+        commonInit()
+    }
+
+    func commonInit() {
+        self.minimumInteritemSpacing = 8
+        self.minimumLineSpacing = 8
+        self.sectionInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
+    }
+
+    func isPortrait() -> Bool {
+        guard let collectionView = collectionView else { return false }
+
+        return collectionView.bounds.size.width < collectionView.bounds.size.height
+    }
+
+    func columnsMax() -> Int {
+        guard let collectionView = collectionView else { return 1 }
+
+        let contentSize = collectionView.bounds.size
+        let cellMinWidth = kCallParticipantCellMinHeight * targetAspectRatio + minimumInteritemSpacing
+
+        if (contentSize.width / cellMinWidth).rounded(.down) < 1 {
+            return 1
+        }
+
+        return Int((contentSize.width / cellMinWidth).rounded(.down))
+    }
+
+    func rowsMax() -> Int {
+        guard let collectionView = collectionView else { return 1 }
+
+        let contentSize = collectionView.bounds.size
+        let cellMinHeight = kCallParticipantCellMinHeight + minimumLineSpacing
+
+        if (contentSize.height / cellMinHeight).rounded(.down) < 1 {
+            return 1
+        }
+
+        return Int((contentSize.height / cellMinHeight).rounded(.down))
+    }
+
+    // Based on the makeGrid method of web:
+    // https://github.com/nextcloud/spreed/blob/5ba554c3f751ba8b8035c7fc8404ca6194d3c16a/src/components/CallView/Grid/Grid.vue#L664
+    func makeGrid() {
+        guard let collectionView = collectionView else { return }
+
+        let numberOfCells = collectionView.numberOfItems(inSection: 0)
+
+        if numberOfCells == 0 {
+            self.numberOfColumns = 0
+            self.numberOfRows = 0
+
+            return
+        }
+
+        if self.isPortrait() {
+            self.targetAspectRatio = self.targetAspectRatioPortrait
+        } else {
+            self.targetAspectRatio = self.targetAspectRatioLandscape
+        }
+
+        // Start with the maximum number of allowed columns/rows
+        self.numberOfColumns = self.columnsMax()
+        self.numberOfRows = self.rowsMax()
+
+        // Try to adjust the number of columns/rows based on the number of cells
+        self.shrinkGrid()
+    }
+
+    func shrinkGrid() {
+        if self.numberOfRows == 1, self.numberOfColumns == 1 {
+            return
+        }
+
+        guard let collectionView = collectionView else { return }
+        let contentSize = collectionView.bounds.size
+
+        var currentColumns = self.numberOfColumns
+        var currentRows = self.numberOfRows
+        var currentSlots = currentColumns * currentRows
+        let numberOfCells = collectionView.numberOfItems(inSection: 0)
+
+        while numberOfCells < currentSlots {
+            let previousColumns = currentColumns
+            let previousRows = currentRows
+
+            let videoWidth = contentSize.width / CGFloat(currentColumns)
+            let videoHeight = contentSize.height / CGFloat(currentRows)
+
+            let videoWidthWithOneColumnLess = contentSize.width / CGFloat(currentColumns - 1)
+            let videoHeightWithOneRowLess = contentSize.height / CGFloat(currentRows - 1)
+
+            let aspectRatioWithOneColumnLess = videoWidthWithOneColumnLess / videoHeight
+            let aspectRatioWithOneRowLess = videoWidth / videoHeightWithOneRowLess
+
+            let deltaAspectRatioWithOneColumnLess = abs(aspectRatioWithOneColumnLess - targetAspectRatio)
+            let deltaAspectRatioWithOneRowLess = abs(aspectRatioWithOneRowLess - targetAspectRatio)
+
+            // Based on the aspect ratio we want to achieve, try to either reduce the number of columns or rows
+            if deltaAspectRatioWithOneColumnLess <= deltaAspectRatioWithOneRowLess {
+                if currentColumns >= 2 {
+                    currentColumns -= 1
+                }
+
+                currentSlots = currentColumns * currentRows
+
+                if numberOfCells > currentSlots {
+                    currentColumns += 1
+
+                    break
+                }
+            } else {
+                if currentRows >= 2 {
+                    currentRows -= 1
+                }
+
+                currentSlots = currentColumns * currentRows
+
+                if numberOfCells > currentSlots {
+                    currentRows += 1
+
+                    break
+                }
+            }
+
+            if previousColumns == currentColumns, previousRows == currentRows {
+                break
+            }
+        }
+
+        self.numberOfColumns = currentColumns
+        self.numberOfRows = currentRows
+    }
+
+    override func prepare() {
+        super.prepare()
+
+        guard let collectionView = collectionView else { return }
+
+        let contentSize = collectionView.bounds.size
+
+        self.makeGrid()
+
+        // Calculate cell width
+        let sectionInsetWidth = sectionInset.left + sectionInset.right
+        let safeAreaInsetWidth = collectionView.safeAreaInsets.left + collectionView.safeAreaInsets.right
+        let marginsAndInsetsWidth = sectionInsetWidth + safeAreaInsetWidth + minimumInteritemSpacing * CGFloat(numberOfColumns - 1)
+        let itemWidth = ((contentSize.width - marginsAndInsetsWidth) / CGFloat(numberOfColumns)).rounded(.down)
+
+        // Calculate cell height
+        let sectionInsetHeight = sectionInset.top + sectionInset.bottom
+        let safeAreaInsetHeight = collectionView.safeAreaInsets.top + collectionView.safeAreaInsets.bottom
+        let marginsAndInsetsHeight = sectionInsetHeight + safeAreaInsetHeight + minimumLineSpacing * CGFloat(numberOfRows - 1)
+        var itemHeight = ((contentSize.height - marginsAndInsetsHeight) / CGFloat(numberOfRows)).rounded(.down)
+
+        // Enfore minimum cell height
+        if itemHeight < kCallParticipantCellMinHeight {
+            itemHeight = kCallParticipantCellMinHeight
+        }
+
+        itemSize = CGSize(width: itemWidth, height: itemHeight)
+    }
+
+    override func invalidationContext(forBoundsChange newBounds: CGRect) -> UICollectionViewLayoutInvalidationContext {
+        let context = super.invalidationContext(forBoundsChange: newBounds)
+
+        if let context = context as? UICollectionViewFlowLayoutInvalidationContext {
+            context.invalidateFlowLayoutDelegateMetrics = newBounds.size != collectionView?.bounds.size
+        }
+
+        return context
+    }
+
+}

+ 49 - 0
NextcloudTalk/CallKitManager.h

@@ -0,0 +1,49 @@
+/**
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#import <Foundation/Foundation.h>
+#import <CallKit/CallKit.h>
+
+extern NSString * const CallKitManagerDidAnswerCallNotification;
+extern NSString * const CallKitManagerDidEndCallNotification;
+extern NSString * const CallKitManagerDidStartCallNotification;
+extern NSString * const CallKitManagerDidChangeAudioMuteNotification;
+extern NSString * const CallKitManagerWantsToUpgradeToVideoCallNotification;
+extern NSString * const CallKitManagerDidFailRequestingCallTransactionNotification;
+
+@interface CallKitCall : NSObject
+
+@property (nonatomic, strong) NSUUID *uuid;
+@property (nonatomic, strong) NSString *token;
+@property (nonatomic, strong) NSString *displayName;
+@property (nonatomic, strong) NSString *accountId;
+@property (nonatomic, strong) CXCallUpdate *update;
+@property (nonatomic, assign) BOOL reportedWhileInCall;
+@property (nonatomic, assign) BOOL isRinging;
+@property (nonatomic, assign) BOOL initiator;
+@property (nonatomic, assign) BOOL silentCall;
+@property (nonatomic, assign) BOOL recordingConsent;
+
+@end
+
+@class NCPushNotification;
+
+@interface CallKitManager : NSObject
+
+@property (nonatomic, strong) NSMutableDictionary *calls; // uuid -> callKitCall
+
++ (instancetype)sharedInstance;
++ (BOOL)isCallKitAvailable;
+- (void)setDefaultProviderConfiguration;
+- (void)reportIncomingCall:(NSString *)token withDisplayName:(NSString *)displayName forAccountId:(NSString *)accountId;
+- (void)reportIncomingCallForNonCallKitDevicesWithPushNotification:(NCPushNotification *)pushNotification;
+- (void)reportIncomingCallForOldAccount;
+- (void)startCall:(NSString *)token withVideoEnabled:(BOOL)videoEnabled andDisplayName:(NSString *)displayName asInitiator:(BOOL)initiator silently:(BOOL)silently recordingConsent:(BOOL)recordingConsent withAccountId:(NSString *)accountId;
+- (void)endCall:(NSString *)token withStatusCode:(NSInteger)statusCode;
+- (void)changeAudioMuted:(BOOL)muted forCall:(NSString *)token;
+- (void)switchCallFrom:(NSString *)from toCall:(NSString *)to;
+
+
+@end

+ 654 - 0
NextcloudTalk/CallKitManager.m

@@ -0,0 +1,654 @@
+/**
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#import "CallKitManager.h"
+#import <CallKit/CXError.h>
+
+#import "CallConstants.h"
+#import "NCAudioController.h"
+#import "NCAPIController.h"
+#import "NCAppBranding.h"
+#import "NCDatabaseManager.h"
+#import "NCNotificationController.h"
+#import "NCRoomsManager.h"
+#import "NCSettingsController.h"
+#import "NCUserInterfaceController.h"
+
+#import "NextcloudTalk-Swift.h"
+
+NSString * const CallKitManagerDidAnswerCallNotification                    = @"CallKitManagerDidAnswerCallNotification";
+NSString * const CallKitManagerDidEndCallNotification                       = @"CallKitManagerDidEndCallNotification";
+NSString * const CallKitManagerDidStartCallNotification                     = @"CallKitManagerDidStartCallNotification";
+NSString * const CallKitManagerDidChangeAudioMuteNotification               = @"CallKitManagerDidChangeAudioMuteNotification";
+NSString * const CallKitManagerWantsToUpgradeToVideoCallNotification        = @"CallKitManagerWantsToUpgradeToVideoCall";
+NSString * const CallKitManagerDidFailRequestingCallTransactionNotification = @"CallKitManagerDidFailRequestingCallTransaction";
+
+NSTimeInterval const kCallKitManagerMaxRingingTimeSeconds       = 45.0;
+NSTimeInterval const kCallKitManagerCheckCallStateEverySeconds  = 5.0;
+
+@interface CallKitManager () <CXProviderDelegate>
+
+@property (nonatomic, strong) CXProvider *provider;
+@property (nonatomic, strong) CXCallController *callController;
+@property (nonatomic, strong) NSMutableDictionary *hangUpTimers; // uuid -> hangUpTimer
+@property (nonatomic, strong) NSMutableDictionary *callStateTimers; // uuid -> callStateTimer
+@property (nonatomic, assign) BOOL startCallRetried;
+
+@end
+
+@implementation CallKitCall
+@end
+
+@implementation CallKitManager
+
++ (CallKitManager *)sharedInstance
+{
+    static CallKitManager *sharedInstance = nil;
+    static dispatch_once_t onceToken;
+    dispatch_once(&onceToken, ^{
+        sharedInstance = [[CallKitManager alloc] init];
+        [sharedInstance provider];
+    });
+    return sharedInstance;
+}
+
+- (id)init
+{
+    self = [super init];
+    if (self) {
+        self.calls = [[NSMutableDictionary alloc] init];
+        self.hangUpTimers = [[NSMutableDictionary alloc] init];
+        self.callStateTimers = [[NSMutableDictionary alloc] init];
+    }
+    return self;
+}
+
++ (BOOL)isCallKitAvailable
+{
+    if ([NCUtils isiOSAppOnMac]) {
+        // There's currently no support for CallKit when running on MacOS.
+        // If this is enabled on MacOS, there's no audio, because we fail to retrieve
+        // the streams from CallKit. Tested with MacOS 12 & 13.
+        return NO;
+    }
+
+    // CallKit should be deactivated in China as requested by Apple
+    return ![NSLocale.currentLocale.countryCode isEqual: @"CN"];
+}
+
+#pragma mark - Getters
+
+- (CXProvider *)provider
+{
+    if (!_provider) {
+        _provider = [[CXProvider alloc] initWithConfiguration:[self defaultProviderConfiguration]];
+        [_provider setDelegate:self queue:nil];
+    }
+    return _provider;
+}
+
+- (CXCallController *)callController
+{
+    if (!_callController) {
+        _callController = [[CXCallController alloc] init];
+    }
+    return _callController;
+}
+
+#pragma mark - Utils
+
+- (CXCallUpdate *)defaultCallUpdate
+{
+    CXCallUpdate *update = [[CXCallUpdate alloc] init];
+    update.supportsHolding = NO;
+    update.supportsGrouping = NO;
+    update.supportsUngrouping = NO;
+    update.supportsDTMF = NO;
+    update.hasVideo = NO;
+    
+    return update;
+}
+
+- (CXProviderConfiguration *)defaultProviderConfiguration
+{
+    CXProviderConfiguration *configuration = [[CXProviderConfiguration alloc] init];
+    configuration.supportsVideo = YES;
+    configuration.maximumCallGroups = 1;
+    configuration.maximumCallsPerCallGroup = 1;
+    configuration.includesCallsInRecents = [NCUserDefaults includeCallsInRecents];
+    configuration.supportedHandleTypes = [NSSet setWithObjects:@(CXHandleTypePhoneNumber), @(CXHandleTypeEmailAddress), @(CXHandleTypeGeneric), nil];
+    configuration.iconTemplateImageData = UIImagePNGRepresentation([UIImage imageNamed:@"app-logo-callkit"]);
+
+    return configuration;
+}
+
+- (CallKitCall *)callForToken:(NSString *)token
+{
+    for (CallKitCall *call in [_calls allValues]) {
+        if ([call.token isEqualToString:token]) {
+            return call;
+        }
+    }
+    
+    return nil;;
+}
+
+#pragma mark - Actions
+
+- (void)setDefaultProviderConfiguration
+{
+    if (_provider) {
+        [_provider setConfiguration:[self defaultProviderConfiguration]];
+    }
+}
+
+- (void)reportIncomingCall:(NSString *)token withDisplayName:(NSString *)displayName forAccountId:(NSString *)accountId
+{
+    NSString *protectedDataAvailable = @"available";
+
+    if (!UIApplication.sharedApplication.isProtectedDataAvailable) {
+        protectedDataAvailable = @"unavailable";
+    }
+
+    [NCUtils log:[NSString stringWithFormat:@"Report incoming call for token %@ for account %@. Protected data is %@", token, accountId, protectedDataAvailable]];
+
+    BOOL ongoingCalls = _calls.count > 0;
+    TalkAccount *activeAccount = [[NCDatabaseManager sharedInstance] activeAccount];
+    
+    // If the app is not active (e.g. in background) and there is an open chat
+    BOOL isAppActive = [[UIApplication sharedApplication] applicationState] == UIApplicationStateActive;
+    ChatViewController *chatViewController = [[NCRoomsManager sharedInstance] chatViewController];
+    if (!isAppActive && chatViewController) {
+        // Leave the chat so it doesn't try to join the chat conversation when the app becomes active.
+        [chatViewController leaveChat];
+        [[NCUserInterfaceController sharedInstance] presentConversationsList];
+    }
+    
+    // If the incoming call is from a different account
+    if (![activeAccount.accountId isEqualToString:accountId]) {
+        // If there is an ongoing call then show a local notification
+        if (ongoingCalls) {
+            [self reportAndCancelIncomingCall:token withDisplayName:displayName forAccountId:accountId];
+            return;
+        // Change accounts if there are no ongoing calls
+        } else {
+            [[NCSettingsController sharedInstance] setActiveAccountWithAccountId:accountId];
+        }
+    }
+    
+    CXCallUpdate *update = [self defaultCallUpdate];
+    update.remoteHandle = [[CXHandle alloc] initWithType:CXHandleTypeGeneric value:token];
+    update.localizedCallerName = displayName;
+    
+    NSUUID *callUUID = [NSUUID new];
+    CallKitCall *call = [[CallKitCall alloc] init];
+    call.uuid = callUUID;
+    call.token = token;
+    call.displayName = displayName;
+    call.accountId = accountId;
+    call.update = update;
+    call.reportedWhileInCall = ongoingCalls;
+    call.isRinging = YES;
+    
+    __weak CallKitManager *weakSelf = self;
+    [self.provider reportNewIncomingCallWithUUID:callUUID update:update completion:^(NSError * _Nullable error) {
+        if (!error) {
+            // Add call to calls array
+            [weakSelf.calls setObject:call forKey:callUUID];
+            
+            // Add hangUpTimer to timers array
+            NSTimer *hangUpTimer = [NSTimer scheduledTimerWithTimeInterval:kCallKitManagerMaxRingingTimeSeconds target:self selector:@selector(endCallWithMissedCallNotification:) userInfo:call repeats:NO];
+            [weakSelf.hangUpTimers setObject:hangUpTimer forKey:callUUID];
+            
+            // Add callStateTimer to timers array
+            NSTimer *callStateTimer = [NSTimer scheduledTimerWithTimeInterval:kCallKitManagerCheckCallStateEverySeconds target:self selector:@selector(checkCallStateForCall:) userInfo:call repeats:NO];
+            [weakSelf.callStateTimers setObject:callStateTimer forKey:callUUID];
+   
+            // Get call info from server
+            [weakSelf getCallInfoForCall:call];
+        } else {
+            NSLog(@"Provider could not present incoming call view.");
+        }
+    }];
+}
+
+- (void)reportAndCancelIncomingCall:(NSString *)token withDisplayName:(NSString *)displayName forAccountId:(NSString *)accountId
+{
+    CXCallUpdate *update = [self defaultCallUpdate];
+    NSUUID *callUUID = [NSUUID new];
+    CallKitCall *call = [[CallKitCall alloc] init];
+    call.uuid = callUUID;
+    call.token = token;
+    call.accountId = accountId;
+    call.update = update;
+    __weak CallKitManager *weakSelf = self;
+    [self.provider reportNewIncomingCallWithUUID:callUUID update:update completion:^(NSError * _Nullable error) {
+        if (!error) {
+            [weakSelf.calls setObject:call forKey:callUUID];
+            NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithObject:call.token forKey:@"roomToken"];
+            [userInfo setValue:@(kNCLocalNotificationTypeCancelledCall) forKey:@"localNotificationType"];
+            [userInfo setObject:call.accountId forKey:@"accountId"];
+            [[NCNotificationController sharedInstance] showLocalNotification:kNCLocalNotificationTypeCancelledCall withUserInfo:userInfo];
+            [weakSelf endCallWithUUID:callUUID];
+        } else {
+            NSLog(@"Provider could not present incoming call view.");
+        }
+    }];
+}
+
+- (void)reportIncomingCallForNonCallKitDevicesWithPushNotification:(NCPushNotification *)pushNotification
+{
+    CXCallUpdate *update = [self defaultCallUpdate];
+    NSUUID *callUUID = [NSUUID new];
+    CallKitCall *call = [[CallKitCall alloc] init];
+    call.uuid = callUUID;
+    call.token = pushNotification.roomToken;
+    call.accountId = pushNotification.accountId;
+    call.update = update;
+    __weak CallKitManager *weakSelf = self;
+    [self.provider reportNewIncomingCallWithUUID:callUUID update:update completion:^(NSError * _Nullable error) {
+        if (!error) {
+            [weakSelf.calls setObject:call forKey:callUUID];
+            [[NCNotificationController sharedInstance] showLocalNotificationForIncomingCallWithPushNotificaion:pushNotification];
+            [weakSelf endCallWithUUID:callUUID];
+        } else {
+            NSLog(@"Provider could not present incoming call view.");
+        }
+    }];
+}
+
+- (void)reportIncomingCallForOldAccount
+{
+    CXCallUpdate *update = [self defaultCallUpdate];
+    update.localizedCallerName = NSLocalizedString(@"Old account", @"Will be used as the caller name when a VoIP notification can't be decrypted");
+
+    NSUUID *callUUID = [NSUUID new];
+    CallKitCall *call = [[CallKitCall alloc] init];
+    call.uuid = callUUID;
+    call.update = update;
+    __weak CallKitManager *weakSelf = self;
+    [self.provider reportNewIncomingCallWithUUID:callUUID update:update completion:^(NSError * _Nullable error) {
+        if (!error) {
+            [weakSelf.calls setObject:call forKey:callUUID];
+            NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithObject:@(kNCLocalNotificationTypeCallFromOldAccount) forKey:@"localNotificationType"];
+            [[NCNotificationController sharedInstance] showLocalNotification:kNCLocalNotificationTypeCallFromOldAccount withUserInfo:userInfo];
+            [weakSelf endCallWithUUID:callUUID];
+        } else {
+            NSLog(@"Provider could not present incoming call view.");
+        }
+    }];
+}
+
+- (void)getCallInfoForCall:(CallKitCall *)call
+{
+    NCRoom *room = [[NCDatabaseManager sharedInstance] roomWithToken:call.token forAccountId:call.accountId];
+    if (room) {
+        [self updateCall:call withDisplayName:room.displayName];
+    }
+    
+    TalkAccount *account = [[NCDatabaseManager sharedInstance] talkAccountForAccountId:call.accountId];
+    [[NCAPIController sharedInstance] getRoomForAccount:account withToken:call.token completionBlock:^(NSDictionary *roomDict, NSError *error) {
+        if (!error) {
+            NCRoom *room = [NCRoom roomWithDictionary:roomDict andAccountId:call.accountId];
+            [self updateCall:call withDisplayName:room.displayName];
+            
+            if ([[NCDatabaseManager sharedInstance] serverHasTalkCapability:kCapabilityCallFlags forAccountId:call.accountId]) {
+                NSInteger callFlag = [[roomDict objectForKey:@"callFlag"] integerValue];
+                if (callFlag == CallFlagDisconnected) {
+                    [self presentMissedCallNotificationForCall:call];
+                    [self endCallWithUUID:call.uuid];
+                } else if ((callFlag & CallFlagWithVideo) != 0) {
+                    [self updateCall:call hasVideo:YES];
+                }
+            }
+        }
+    }];
+}
+
+- (void)updateCall:(CallKitCall *)call withDisplayName:(NSString *)displayName
+{
+    call.displayName = displayName;
+    call.update.localizedCallerName = displayName;
+    
+    [self.provider reportCallWithUUID:call.uuid updated:call.update];
+}
+
+- (void)updateCall:(CallKitCall *)call hasVideo:(BOOL)hasVideo
+{
+    call.update.hasVideo = hasVideo;
+    
+    [self.provider reportCallWithUUID:call.uuid updated:call.update];
+}
+
+- (void)stopHangUpTimerForCallUUID:(NSUUID *)uuid
+{
+    NSTimer *hangUpTimer = [_hangUpTimers objectForKey:uuid];
+    if (hangUpTimer) {
+        [hangUpTimer invalidate];
+        [_hangUpTimers removeObjectForKey:uuid];
+    }
+}
+
+- (void)stopCallStateTimerForCallUUID:(NSUUID *)uuid
+{
+    NSTimer *callStateTimer = [_callStateTimers objectForKey:uuid];
+    if (callStateTimer) {
+        [callStateTimer invalidate];
+        [_callStateTimers removeObjectForKey:uuid];
+    }
+}
+
+- (void)endCallWithMissedCallNotification:(NSTimer*)timer
+{
+    CallKitCall *call = [timer userInfo];
+    [self presentMissedCallNotificationForCall:call];
+    [self endCallWithUUID:call.uuid];
+}
+
+- (void)presentMissedCallNotificationForCall:(CallKitCall *)call
+{
+    if (call) {
+        NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithObject:call.token forKey:@"roomToken"];
+        [userInfo setValue:call.displayName forKey:@"displayName"];
+        [userInfo setValue:@(kNCLocalNotificationTypeMissedCall) forKey:@"localNotificationType"];
+        [userInfo setObject:call.accountId forKey:@"accountId"];
+        [[NCNotificationController sharedInstance] showLocalNotification:kNCLocalNotificationTypeMissedCall withUserInfo:userInfo];
+    }
+}
+
+- (void)checkCallStateForCall:(NSTimer *)timer
+{
+    CallKitCall *call = [timer userInfo];
+    if (!call) {
+        return;
+    }
+
+    __weak CallKitManager *weakSelf = self;
+    TalkAccount *account = [[NCDatabaseManager sharedInstance] talkAccountForAccountId:call.accountId];
+    [[NCAPIController sharedInstance] getPeersForCall:call.token forAccount:account withCompletionBlock:^(NSMutableArray *peers, NSError *error, NSInteger statusCode) {
+        // Make sure call is still ringing at this point to avoid a race-condition between answering the call on this device and the API callback
+        if (!call.isRinging) {
+            return;
+        }
+
+        if (statusCode == 404) {
+            // The conversation was not found for this participant
+            // Mostlikely the conversation was removed while an incoming call was ongoing
+            [self endCallWithUUID:call.uuid];
+            return;
+        }
+
+        if (!error && peers.count == 0) {
+            // No one is in the call, we can hang up and show missed call notification
+            [self presentMissedCallNotificationForCall:call];
+            [self endCallWithUUID:call.uuid];
+            return;
+        }
+        
+        NSInteger callAPIVersion = [[NCAPIController sharedInstance] callAPIVersionForAccount:account];
+        for (NSMutableDictionary *user in peers) {
+            NSString *userId = [user objectForKey:@"userId"];
+            BOOL isUserActorType = YES;
+            if (callAPIVersion >= APIv3) {
+                userId = [user objectForKey:@"actorId"];
+                isUserActorType = [[user objectForKey:@"actorType"] isEqualToString:@"users"];
+            }
+            if ([account.userId isEqualToString:userId] && isUserActorType) {
+                // Account is already in a call (answered the call on a different device) -> no need to keep ringing
+                [self endCallWithUUID:call.uuid];
+                return;
+            }
+        }
+        
+        // Reschedule next check
+        NSTimer *callStateTimer = [NSTimer scheduledTimerWithTimeInterval:kCallKitManagerCheckCallStateEverySeconds target:self selector:@selector(checkCallStateForCall:) userInfo:call repeats:NO];
+        [weakSelf.callStateTimers setObject:callStateTimer forKey:call.uuid];
+    }];
+}
+
+- (void)startCall:(NSString *)token withVideoEnabled:(BOOL)videoEnabled andDisplayName:(NSString *)displayName asInitiator:(BOOL)initiator silently:(BOOL)silently recordingConsent:(BOOL)recordingConsent withAccountId:(NSString *)accountId
+{
+    if (![CallKitManager isCallKitAvailable]) {
+        NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithObject:token forKey:@"roomToken"];
+        [userInfo setValue:@(videoEnabled) forKey:@"isVideoEnabled"];
+        [userInfo setValue:@(initiator) forKey:@"initiator"];
+        [userInfo setValue:@(silently) forKey:@"silentCall"];
+        [userInfo setValue:@(recordingConsent) forKey:@"recordingConsent"];
+        [[NSNotificationCenter defaultCenter] postNotificationName:CallKitManagerDidStartCallNotification
+                                                            object:self
+                                                          userInfo:userInfo];
+        return;
+    }
+    
+    // Start a new call
+    if (_calls.count == 0) {
+        CXCallUpdate *update = [self defaultCallUpdate];
+        CXHandle *handle = [[CXHandle alloc] initWithType:CXHandleTypeGeneric value:token];
+        update.remoteHandle = handle;
+        update.localizedCallerName = displayName;
+        update.hasVideo = videoEnabled;
+        
+        NSUUID *callUUID = [NSUUID new];
+        CallKitCall *call = [[CallKitCall alloc] init];
+        call.uuid = callUUID;
+        call.token = token;
+        call.displayName = displayName;
+        call.accountId = accountId;
+        call.update = update;
+        call.initiator = initiator;
+        call.silentCall = silently;
+        call.recordingConsent = recordingConsent;
+
+        CXStartCallAction *startCallAction = [[CXStartCallAction alloc] initWithCallUUID:callUUID handle:handle];
+        startCallAction.video = videoEnabled;
+        startCallAction.contactIdentifier = displayName;
+        CXTransaction *transaction = [[CXTransaction alloc] init];
+        [transaction addAction:startCallAction];
+        
+        __weak CallKitManager *weakSelf = self;
+        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^(void){
+            [self.callController requestTransaction:transaction completion:^(NSError * _Nullable error) {
+                if (!error) {
+                    self->_startCallRetried = NO;
+                    [weakSelf.calls setObject:call forKey:callUUID];
+                } else {
+                    if (self->_startCallRetried) {
+                        NSLog(@"%@", error.localizedDescription);
+                        self->_startCallRetried = NO;
+                        NSDictionary *userInfo = [NSDictionary dictionaryWithObject:token forKey:@"roomToken"];
+                        [[NSNotificationCenter defaultCenter] postNotificationName:CallKitManagerDidFailRequestingCallTransactionNotification
+                                                                            object:self
+                                                                          userInfo:userInfo];
+                    } else {
+                        self->_startCallRetried = YES;
+                        [self startCall:token withVideoEnabled:videoEnabled andDisplayName:displayName asInitiator:initiator silently:silently recordingConsent:recordingConsent withAccountId:accountId];
+                    }
+                }
+            }];
+        });
+    // Send notification for video call upgrade.
+    // Since we send the token in the notification, it will only ask
+    // for an upgrade if there is an ongoing (audioOnly) call in that room.
+    } else if (videoEnabled) {
+        NSDictionary *userInfo = [NSDictionary dictionaryWithObject:token forKey:@"roomToken"];
+        [[NSNotificationCenter defaultCenter] postNotificationName:CallKitManagerWantsToUpgradeToVideoCallNotification
+                                                            object:self
+                                                          userInfo:userInfo];
+    }
+}
+
+- (void)presentRecordingConsentRequiredNotificationForCall:(CallKitCall *)call
+{
+    if (call) {
+        NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithObject:call.token forKey:@"roomToken"];
+        [userInfo setValue:call.displayName forKey:@"displayName"];
+        [userInfo setValue:@(kNCLocalNotificationTypeRecordingConsentRequired) forKey:@"localNotificationType"];
+        [userInfo setObject:call.accountId forKey:@"accountId"];
+        [[NCNotificationController sharedInstance] showLocalNotification:kNCLocalNotificationTypeRecordingConsentRequired withUserInfo:userInfo];
+    }
+}
+
+- (void)endCall:(NSString *)token withStatusCode:(NSInteger)statusCode
+{
+    [NCUtils log:[NSString stringWithFormat:@"End call for token %@ with statusCode %ld", token, statusCode]];
+
+    CallKitCall *call = [self callForToken:token];
+    if (call) {
+
+        // Check if recording consent is required
+        if (statusCode == 400) {
+            [self presentRecordingConsentRequiredNotificationForCall:call];
+        }
+
+        [self endCallWithUUID:call.uuid];
+    }
+}
+
+- (void)endCallWithUUID:(NSUUID *)uuid
+{
+    CallKitCall *call = [_calls objectForKey:uuid];
+    if (call) {
+        CXEndCallAction *endCallAction = [[CXEndCallAction alloc] initWithCallUUID:call.uuid];
+        CXTransaction *transaction = [[CXTransaction alloc] init];
+        [transaction addAction:endCallAction];
+        [self.callController requestTransaction:transaction completion:^(NSError * _Nullable error) {
+            if (error) {
+                NSLog(@"%@", error.localizedDescription);
+            }
+        }];
+    }
+}
+
+- (void)changeAudioMuted:(BOOL)muted forCall:(NSString *)token
+{
+    CallKitCall *call = [self callForToken:token];
+    if (call) {
+        CXSetMutedCallAction *muteAction = [[CXSetMutedCallAction alloc] initWithCallUUID:call.uuid muted:muted];
+        CXTransaction *transaction = [[CXTransaction alloc] init];
+        [transaction addAction:muteAction];
+        [self.callController requestTransaction:transaction completion:^(NSError * _Nullable error) {
+            if (error) {
+                NSLog(@"%@", error.localizedDescription);
+            }
+        }];
+    }
+}
+
+- (void)switchCallFrom:(NSString *)from toCall:(NSString *)to
+{
+    CallKitCall *call = [self callForToken:from];
+    if (call) {
+        call.token = to;
+    }
+}
+
+#pragma mark - CXProviderDelegate
+
+- (void)providerDidReset:(CXProvider *)provider
+{
+    NSLog(@"Provider:didReset");
+}
+
+- (void)provider:(CXProvider *)provider performStartCallAction:(nonnull CXStartCallAction *)action
+{
+    CallKitCall *call = [_calls objectForKey:action.callUUID];
+    if (call) {
+        // Seems to be needed to display the call name correctly
+        [_provider reportCallWithUUID:call.uuid updated:call.update];
+        
+        // Report outgoing call
+        [provider reportOutgoingCallWithUUID:action.callUUID connectedAtDate:[NSDate new]];
+        NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithObject:action.handle.value forKey:@"roomToken"];
+        [userInfo setValue:@(action.isVideo) forKey:@"isVideoEnabled"];
+        [userInfo setValue:@(call.initiator) forKey:@"initiator"];
+        [userInfo setValue:@(call.silentCall) forKey:@"silentCall"];
+        [userInfo setValue:@(call.recordingConsent) forKey:@"recordingConsent"];
+        [[NSNotificationCenter defaultCenter] postNotificationName:CallKitManagerDidStartCallNotification
+                                                            object:self
+                                                          userInfo:userInfo];
+    }
+    
+    [action fulfill];
+}
+
+- (void)provider:(CXProvider *)provider performAnswerCallAction:(CXAnswerCallAction *)action
+{
+    CallKitCall *call = [_calls objectForKey:action.callUUID];
+    if (call) {
+        [NCUtils log:[NSString stringWithFormat:@"CallKit provider answer call action for token %@", call.token]];
+
+        call.isRinging = NO;
+        [self stopCallStateTimerForCallUUID:call.uuid];
+        
+        [self stopHangUpTimerForCallUUID:call.uuid];
+        NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithObject:call.token forKey:@"roomToken"];
+        [userInfo setValue:@(call.update.hasVideo) forKey:@"hasVideo"];
+        [userInfo setValue:@(call.reportedWhileInCall) forKey:@"waitForCallEnd"];
+        [[NSNotificationCenter defaultCenter] postNotificationName:CallKitManagerDidAnswerCallNotification
+                                                            object:self
+                                                          userInfo:userInfo];
+    }
+    
+    [action fulfill];
+}
+
+- (void)provider:(CXProvider *)provider performEndCallAction:(CXEndCallAction *)action
+{
+    CallKitCall *call = [_calls objectForKey:action.callUUID];
+    if (call) {
+        [NCUtils log:[NSString stringWithFormat:@"CallKit provider end call action for token %@", call.token]];
+
+        call.isRinging = NO;
+        [self stopCallStateTimerForCallUUID:call.uuid];
+        
+        [self stopHangUpTimerForCallUUID:call.uuid];
+        NSString *leaveCallToken = [call.token copy];
+        [_calls removeObjectForKey:action.callUUID];
+
+        if (leaveCallToken) {
+            NSDictionary *userInfo = [NSDictionary dictionaryWithObject:leaveCallToken forKey:@"roomToken"];
+            [[NSNotificationCenter defaultCenter] postNotificationName:CallKitManagerDidEndCallNotification
+                                                                object:self
+                                                              userInfo:userInfo];
+        }
+    }
+
+    [action fulfill];
+}
+
+- (void)provider:(CXProvider *)provider performSetMutedCallAction:(CXSetMutedCallAction *)action
+{
+    CallKitCall *call = [_calls objectForKey:action.callUUID];
+    if (call) {
+        NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithObject:call.token forKey:@"roomToken"];
+        [userInfo setValue:@(action.isMuted) forKey:@"isMuted"];
+        [[NSNotificationCenter defaultCenter] postNotificationName:CallKitManagerDidChangeAudioMuteNotification
+                                                            object:self
+                                                          userInfo:userInfo];
+    }
+    
+    [action fulfill];
+}
+
+- (void)provider:(CXProvider *)provider didActivateAudioSession:(AVAudioSession *)audioSession
+{
+    NSLog(@"Provider:didActivateAudioSession - %@", audioSession);
+
+    [[WebRTCCommon shared] dispatch:^{
+        [[NCAudioController sharedInstance] providerDidActivateAudioSession:audioSession];
+    }];
+}
+
+- (void)provider:(CXProvider *)provider didDeactivateAudioSession:(nonnull AVAudioSession *)audioSession
+{
+    NSLog(@"Provider:didDeactivateAudioSession - %@", audioSession);
+
+    [[WebRTCCommon shared] dispatch:^{
+        [[NCAudioController sharedInstance] providerDidDeactivateAudioSession:audioSession];
+    }];
+}
+
+
+@end

+ 57 - 0
NextcloudTalk/CallParticipantViewCell.h

@@ -0,0 +1,57 @@
+/**
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#import <UIKit/UIKit.h>
+#import <WebRTC/WebRTC.h>
+
+extern NSString *const kCallParticipantCellIdentifier;
+extern NSString *const kCallParticipantCellNibName;
+extern CGFloat const kCallParticipantCellMinHeight;
+
+@class CallParticipantViewCell;
+@class MDCActivityIndicator;
+@class AvatarImageView;
+@class TalkActor;
+
+@protocol CallParticipantViewCellDelegate <NSObject>
+- (void)cellWantsToPresentScreenSharing:(CallParticipantViewCell *)participantCell;
+- (void)cellWantsToChangeZoom:(CallParticipantViewCell *)participantCell showOriginalSize:(BOOL)showOriginalSize;
+@end
+
+@interface CallParticipantViewCell : UICollectionViewCell
+
+@property (nonatomic, weak) id<CallParticipantViewCellDelegate> actionsDelegate;
+
+@property (nonatomic, strong)  NSString *peerIdentifier;
+@property (nonatomic, strong)  NSString *displayName;
+@property (nonatomic, assign)  BOOL audioDisabled;
+@property (nonatomic, assign)  BOOL videoDisabled;
+@property (nonatomic, assign)  BOOL screenShared;
+@property (nonatomic, assign)  BOOL showOriginalSize;
+@property (nonatomic, assign)  RTCIceConnectionState connectionState;
+
+@property (nonatomic, weak) IBOutlet UIView *peerVideoView;
+@property (nonatomic, weak) IBOutlet UILabel *peerNameLabel;
+@property (nonatomic, weak) IBOutlet MDCActivityIndicator *activityIndicator;
+@property (nonatomic, weak) IBOutlet AvatarImageView *peerAvatarImageView;
+@property (nonatomic, weak) IBOutlet UIButton *audioOffIndicator;
+@property (nonatomic, weak) IBOutlet UIButton *screensharingIndicator;
+@property (nonatomic, weak) IBOutlet UIButton *raisedHandIndicator;
+@property (nonatomic, weak) IBOutlet NSLayoutConstraint *stackViewBottomConstraint;
+@property (nonatomic, weak) IBOutlet NSLayoutConstraint *stackViewLeftConstraint;
+@property (nonatomic, weak) IBOutlet NSLayoutConstraint *stackViewRightConstraint;
+@property (nonatomic, weak) IBOutlet NSLayoutConstraint *screensharingIndiciatorRightConstraint;
+@property (nonatomic, weak) IBOutlet NSLayoutConstraint *screensharingIndiciatorTopConstraint;
+
+
+- (void)setVideoView:(RTCMTLVideoView *)videoView;
+- (void)setSpeaking:(BOOL)speaking;
+- (void)setAvatarForActor:(TalkActor *)actor;
+- (CGSize)getRemoteVideoSize;
+- (void)setRemoteVideoSize:(CGSize)size;
+- (void)setRaiseHand:(BOOL)raised;
+- (void)resizeRemoteVideoView;
+
+@end

+ 332 - 0
NextcloudTalk/CallParticipantViewCell.m

@@ -0,0 +1,332 @@
+/**
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#import "CallParticipantViewCell.h"
+
+#import "CallViewController.h"
+#import "NCAPIController.h"
+#import "NCDatabaseManager.h"
+
+#import "NextcloudTalk-Swift.h"
+
+NSString *const kCallParticipantCellIdentifier = @"CallParticipantCellIdentifier";
+NSString *const kCallParticipantCellNibName = @"CallParticipantViewCell";
+CGFloat const kCallParticipantCellMinHeight = 128;
+
+@interface CallParticipantViewCell()
+{
+    UIView<RTCVideoRenderer> *_videoView;
+    CGSize _remoteVideoSize;
+    NSTimer *_disconnectedTimer;
+}
+
+@end
+
+@implementation CallParticipantViewCell
+
+- (void)awakeFromNib
+{
+    [super awakeFromNib];
+    
+    self.audioOffIndicator.hidden = YES;
+    self.screensharingIndicator.hidden = YES;
+    self.raisedHandIndicator.hidden = YES;
+    
+    self.audioOffIndicator.layer.cornerRadius = 4;
+    self.audioOffIndicator.clipsToBounds = YES;
+    self.screensharingIndicator.layer.cornerRadius = 4;
+    self.screensharingIndicator.clipsToBounds = YES;
+
+    self.activityIndicator.radius = 50.0f;
+    self.activityIndicator.cycleColors = @[UIColor.lightGrayColor];
+
+    self.peerAvatarImageView.hidden = YES;
+    self.peerAvatarImageView.layer.cornerRadius = self.peerAvatarImageView.bounds.size.width / 2;
+    self.peerAvatarImageView.layer.masksToBounds = YES;
+
+    self.layer.cornerRadius = 22.0f;
+    [self.layer setMasksToBounds:YES];
+    
+    _showOriginalSize = NO;
+    UITapGestureRecognizer *tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(toggleZoom)];
+    [tapGestureRecognizer setNumberOfTapsRequired:2];
+    [self.contentView addGestureRecognizer:tapGestureRecognizer];
+}
+
+- (void)prepareForReuse
+{
+    [super prepareForReuse];
+
+    [_peerAvatarImageView cancelCurrentRequest];
+    _peerAvatarImageView.image = nil;
+    _peerAvatarImageView.alpha = 1;
+
+    _displayName = nil;
+    _peerNameLabel.text = nil;
+    [_videoView removeFromSuperview];
+    _videoView = nil;
+    _showOriginalSize = NO;
+    self.layer.borderWidth = 0.0f;
+    [self hideLoadingSpinner];
+    [self invalidateDisconnectedTimer];
+}
+
+- (void)applyLayoutAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes
+{
+    [super applyLayoutAttributes:layoutAttributes];
+    
+    [self resizeRemoteVideoView];
+}
+
+- (void)layoutSubviews
+{
+    [super layoutSubviews];
+
+    CGRect bounds = self.bounds;
+
+    // Usually we have a padding to the side of the cell of 22 (= cornerRadius)
+    // But when the cell is really small adjust the padding to be 11 (= cornerRadius / 2)
+    if (bounds.size.width <= 200 || bounds.size.height <= 200) {
+        self.stackViewLeftConstraint.constant = 11;
+        self.stackViewRightConstraint.constant = 11;
+        self.stackViewBottomConstraint.constant = 11;
+        self.screensharingIndiciatorTopConstraint.constant = 11;
+        self.screensharingIndiciatorRightConstraint.constant = 11;
+    } else {
+        self.stackViewLeftConstraint.constant = 22;
+        self.stackViewRightConstraint.constant = 22;
+        self.stackViewBottomConstraint.constant = 22;
+        self.screensharingIndiciatorTopConstraint.constant = 22;
+        self.screensharingIndiciatorRightConstraint.constant = 22;
+    }
+
+    [self.contentView layoutSubviews];
+
+    self.peerAvatarImageView.layer.cornerRadius = self.peerAvatarImageView.bounds.size.width / 2;
+    self.activityIndicator.radius = self.peerAvatarImageView.bounds.size.width / 2;
+}
+
+- (void)toggleZoom
+{
+    _showOriginalSize = !_showOriginalSize;
+    [self.actionsDelegate cellWantsToChangeZoom:self showOriginalSize:_showOriginalSize];
+    [self resizeRemoteVideoView];
+}
+
+- (void)setAvatarForActor:(TalkActor * _Nullable)actor
+{
+    if (actor.id == nil || actor.id.length == 0) {
+        [self setBackgroundColor:[UIColor colorWithWhite:0.5 alpha:1]];
+    } else if (actor.displayName && actor.displayName.length > 0) {
+        [self setBackgroundColor:[[ColorGenerator shared] usernameToColor:actor.displayName]];
+    } else {
+        [self setBackgroundColor:[[ColorGenerator shared] usernameToColor:actor.id]];
+    }
+
+    [self.peerAvatarImageView setActorAvatarForId:actor.id withType:actor.type withDisplayName:actor.displayName withRoomToken:nil];
+}
+
+- (void)setDisplayName:(NSString *)displayName
+{
+    _displayName = displayName;
+    if (!displayName || [displayName isKindOfClass:[NSNull class]] || [displayName isEqualToString:@""]) {
+        _displayName = NSLocalizedString(@"Guest", nil);
+    }
+
+    if ([self.peerNameLabel.text isEqualToString:_displayName]) {
+        return;
+    }
+
+    dispatch_async(dispatch_get_main_queue(), ^{
+        self.peerNameLabel.text = self->_displayName;
+        [self setBackgroundColor:[[ColorGenerator shared] usernameToColor:self->_displayName]];
+    });
+}
+
+- (void)setAudioDisabled:(BOOL)audioDisabled
+{
+    _audioDisabled = audioDisabled;
+    dispatch_async(dispatch_get_main_queue(), ^{
+        [self configureParticipantButtons];
+    });
+}
+
+- (void)setScreenShared:(BOOL)screenShared
+{
+    _screenShared = screenShared;
+    dispatch_async(dispatch_get_main_queue(), ^{
+        [self configureParticipantButtons];
+    });
+}
+
+- (void)setConnectionState:(RTCIceConnectionState)connectionState
+{
+    _connectionState = connectionState;
+
+    [self invalidateDisconnectedTimer];
+    if (connectionState == RTCIceConnectionStateDisconnected) {
+        [self setDisconnectedTimer];
+    } else if (connectionState == RTCIceConnectionStateFailed) {
+        [self setFailedConnectionUI];
+    } else if (connectionState != RTCIceConnectionStateCompleted && connectionState != RTCIceConnectionStateConnected) {
+        [self setConnectingUI];
+    } else {
+        [self setConnectedUI];
+    }
+}
+
+- (void)setDisconnectedTimer
+{
+    _disconnectedTimer = [NSTimer scheduledTimerWithTimeInterval:3.0 target:self selector:@selector(setDisconnectedUI) userInfo:nil repeats:NO];
+}
+
+- (void)invalidateDisconnectedTimer
+{
+    [_disconnectedTimer invalidate];
+    _disconnectedTimer = nil;
+}
+
+- (void)setDisconnectedUI
+{
+    if (_connectionState == RTCIceConnectionStateDisconnected) {
+        dispatch_async(dispatch_get_main_queue(), ^{
+            self.peerNameLabel.text = [NSString stringWithFormat:NSLocalizedString(@"Connecting to %@ …", nil), self->_displayName];
+            self.peerAvatarImageView.alpha = 0.3;
+            [self hideLoadingSpinner];
+        });
+    }
+}
+
+- (void)setConnectingUI
+{
+    dispatch_async(dispatch_get_main_queue(), ^{
+        self.peerAvatarImageView.alpha = 0.3;
+        [self showLoadingSpinner];
+    });
+}
+
+- (void)setFailedConnectionUI
+{
+    dispatch_async(dispatch_get_main_queue(), ^{
+        self.peerNameLabel.text = [NSString stringWithFormat:NSLocalizedString(@"Failed to connect to %@", nil), self->_displayName];
+        self.peerAvatarImageView.alpha = 0.3;
+    });
+}
+
+- (void)setConnectedUI
+{
+    dispatch_async(dispatch_get_main_queue(), ^{
+        self.peerNameLabel.text = self->_displayName;
+        self.peerAvatarImageView.alpha = 1;
+        [self hideLoadingSpinner];
+    });
+}
+
+- (void)showLoadingSpinner
+{
+    [self.activityIndicator startAnimating];
+    [self.activityIndicator setHidden:NO];
+}
+
+- (void)hideLoadingSpinner
+{
+    [self.activityIndicator stopAnimating];
+    [self.activityIndicator setHidden:YES];
+}
+
+- (IBAction)screenSharingButtonPressed:(id)sender
+{
+    [self.actionsDelegate cellWantsToPresentScreenSharing:self];
+}
+
+- (void)configureParticipantButtons
+{    
+    self.audioOffIndicator.hidden = !_audioDisabled;
+    self.screensharingIndicator.hidden = !_screenShared;
+}
+
+- (void)setVideoDisabled:(BOOL)videoDisabled
+{
+    _videoDisabled = videoDisabled;
+    if (videoDisabled) {
+        [_videoView setHidden:YES];
+        [_peerAvatarImageView setHidden:NO];
+    } else {
+        [_peerAvatarImageView setHidden:YES];
+        [_videoView setHidden:NO];
+    }
+}
+
+- (void)setSpeaking:(BOOL)speaking
+{
+    if (speaking) {
+        self.layer.borderColor = [UIColor whiteColor].CGColor;
+        self.layer.borderWidth = 2.0f;
+    } else {
+        self.layer.borderWidth = 0.0f;
+    }
+}
+
+- (void)setRaiseHand:(BOOL)raised
+{
+    dispatch_async(dispatch_get_main_queue(), ^{
+        self.raisedHandIndicator.hidden = !raised;
+    });
+}
+
+- (void)setVideoView:(RTCMTLVideoView *)videoView
+{
+    dispatch_async(dispatch_get_main_queue(), ^{
+        if (videoView == self->_videoView) {
+            return;
+        }
+
+        [self->_videoView removeFromSuperview];
+        self->_videoView = nil;
+        self->_videoView = videoView;
+        [self->_peerVideoView addSubview:self->_videoView];
+        [self->_videoView setHidden:self->_videoDisabled];
+        [self resizeRemoteVideoView];
+    });
+}
+
+- (CGSize)getRemoteVideoSize
+{
+    return self->_remoteVideoSize;
+}
+
+- (void)setRemoteVideoSize:(CGSize)size
+{
+    self->_remoteVideoSize = size;
+    [self resizeRemoteVideoView];
+}
+
+- (void)resizeRemoteVideoView
+{
+    CGRect bounds = self.bounds;
+    CGSize videoSize = _remoteVideoSize;
+    
+    if (videoSize.width > 0 && videoSize.height > 0) {
+        // Aspect fill remote video into bounds.
+        CGRect remoteVideoFrame = AVMakeRectWithAspectRatioInsideRect(videoSize, bounds);
+        CGFloat scale = 1;
+        
+        if (!_showOriginalSize) {
+            CGFloat scaleHeight = bounds.size.height / remoteVideoFrame.size.height;
+            CGFloat scaleWidth = bounds.size.width / remoteVideoFrame.size.width;
+            // Always grab the bigger scale to make video cover the whole cell
+            scale = (scaleHeight > scaleWidth) ? scaleHeight : scaleWidth;
+        }
+        
+        remoteVideoFrame.size.height *= scale;
+        remoteVideoFrame.size.width *= scale;
+        _videoView.frame = remoteVideoFrame;
+        _videoView.center = CGPointMake(CGRectGetMidX(bounds), CGRectGetMidY(bounds));
+    } else {
+        _videoView.frame = bounds;
+    }
+}
+
+@end

+ 141 - 0
NextcloudTalk/CallParticipantViewCell.xib

@@ -0,0 +1,141 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="21701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
+    <device id="retina4_7" orientation="portrait" appearance="light"/>
+    <dependencies>
+        <deployment identifier="iOS"/>
+        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21678"/>
+        <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
+    </dependencies>
+    <objects>
+        <placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
+        <placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
+        <collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" reuseIdentifier="" id="gTV-IL-0wX" customClass="CallParticipantViewCell">
+            <rect key="frame" x="0.0" y="0.0" width="295" height="232"/>
+            <autoresizingMask key="autoresizingMask"/>
+            <view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center">
+                <rect key="frame" x="0.0" y="0.0" width="295" height="232"/>
+                <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
+                <subviews>
+                    <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="2xo-Od-0bZ">
+                        <rect key="frame" x="0.0" y="0.0" width="295" height="232"/>
+                    </view>
+                    <imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="zYx-Of-3tH" customClass="AvatarImageView" customModule="NextcloudTalk" customModuleProvider="target">
+                        <rect key="frame" x="97.5" y="66" width="100" height="100"/>
+                        <constraints>
+                            <constraint firstAttribute="width" secondItem="zYx-Of-3tH" secondAttribute="height" multiplier="1:1" id="8TD-LX-PDg"/>
+                            <constraint firstAttribute="width" priority="750" constant="100" id="E1w-Vk-9vO"/>
+                            <constraint firstAttribute="height" priority="750" constant="100" id="hFb-Kv-gry"/>
+                        </constraints>
+                    </imageView>
+                    <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="eHf-UZ-Ya0" customClass="MDCActivityIndicator">
+                        <rect key="frame" x="97.5" y="66" width="100" height="100"/>
+                        <constraints>
+                            <constraint firstAttribute="height" constant="100" id="TgK-JJ-TAh"/>
+                            <constraint firstAttribute="width" constant="100" id="nsX-XY-5La"/>
+                        </constraints>
+                    </view>
+                    <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="right" contentVerticalAlignment="top" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="bj2-hS-1pF">
+                        <rect key="frame" x="241" y="22" width="32" height="32"/>
+                        <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+                        <constraints>
+                            <constraint firstAttribute="height" constant="32" id="7vp-Kk-0hF"/>
+                            <constraint firstAttribute="width" constant="32" id="Z5s-i5-hgP"/>
+                        </constraints>
+                        <color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+                        <state key="normal" image="display" catalog="system">
+                            <preferredSymbolConfiguration key="preferredSymbolConfiguration" configurationType="pointSize" pointSize="20"/>
+                        </state>
+                        <connections>
+                            <action selector="screenSharingButtonPressed:" destination="gTV-IL-0wX" eventType="touchUpInside" id="DzG-u7-nPx"/>
+                        </connections>
+                    </button>
+                    <stackView opaque="NO" contentMode="scaleToFill" spacing="12" translatesAutoresizingMaskIntoConstraints="NO" id="Tog-ZY-cO4">
+                        <rect key="frame" x="22" y="186" width="251" height="24"/>
+                        <subviews>
+                            <button opaque="NO" contentMode="scaleToFill" horizontalCompressionResistancePriority="1000" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="BPo-EN-rrM" userLabel="Raised Hand Indicator">
+                                <rect key="frame" x="0.0" y="0.0" width="28" height="24"/>
+                                <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+                                <color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+                                <size key="titleShadowOffset" width="1" height="1"/>
+                                <state key="normal" image="hand.raised.fill" catalog="system">
+                                    <color key="titleShadowColor" white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+                                    <preferredSymbolConfiguration key="preferredSymbolConfiguration" configurationType="pointSize" pointSize="20"/>
+                                </state>
+                            </button>
+                            <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Duj-37-SmJ" userLabel="Peer Name Label Container">
+                                <rect key="frame" x="40" y="0.0" width="179" height="24"/>
+                                <subviews>
+                                    <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="248" verticalHuggingPriority="251" fixedFrame="YES" text="ghj" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="D5h-T2-aB9">
+                                        <rect key="frame" x="0.0" y="6" width="179" height="18"/>
+                                        <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+                                        <fontDescription key="fontDescription" type="system" weight="semibold" pointSize="16"/>
+                                        <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+                                        <nil key="highlightedColor"/>
+                                        <color key="shadowColor" red="0.0" green="0.0" blue="0.0" alpha="0.74765580537303389" colorSpace="custom" customColorSpace="sRGB"/>
+                                        <size key="shadowOffset" width="1" height="1"/>
+                                    </label>
+                                </subviews>
+                                <edgeInsets key="layoutMargins" top="12" left="8" bottom="2" right="8"/>
+                            </view>
+                            <button opaque="NO" contentMode="scaleToFill" horizontalCompressionResistancePriority="1000" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="vlO-GV-Y3B">
+                                <rect key="frame" x="231" y="0.0" width="20" height="24"/>
+                                <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+                                <color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+                                <size key="titleShadowOffset" width="1" height="1"/>
+                                <state key="normal" image="mic.slash.fill" catalog="system">
+                                    <color key="titleShadowColor" white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+                                    <preferredSymbolConfiguration key="preferredSymbolConfiguration" configurationType="pointSize" pointSize="20"/>
+                                </state>
+                            </button>
+                        </subviews>
+                        <constraints>
+                            <constraint firstAttribute="height" constant="24" id="eyv-6K-4Fu"/>
+                            <constraint firstAttribute="width" relation="greaterThanOrEqual" id="tc0-yY-uLq"/>
+                        </constraints>
+                    </stackView>
+                </subviews>
+            </view>
+            <constraints>
+                <constraint firstAttribute="trailing" secondItem="bj2-hS-1pF" secondAttribute="trailing" constant="22" id="0vQ-dJ-Ozc"/>
+                <constraint firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="zYx-Of-3tH" secondAttribute="bottom" constant="22" id="25D-wO-Pd3"/>
+                <constraint firstAttribute="bottom" secondItem="2xo-Od-0bZ" secondAttribute="bottom" id="6BS-bd-V7e"/>
+                <constraint firstItem="2xo-Od-0bZ" firstAttribute="leading" secondItem="gTV-IL-0wX" secondAttribute="leading" id="7ax-NA-Mp7"/>
+                <constraint firstItem="zYx-Of-3tH" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="gTV-IL-0wX" secondAttribute="leading" constant="22" id="Cpi-Oj-lKf"/>
+                <constraint firstItem="zYx-Of-3tH" firstAttribute="centerY" secondItem="gTV-IL-0wX" secondAttribute="centerY" id="Ejn-6A-Tbm"/>
+                <constraint firstItem="2xo-Od-0bZ" firstAttribute="top" secondItem="gTV-IL-0wX" secondAttribute="top" id="KOw-nZ-G8t"/>
+                <constraint firstItem="eHf-UZ-Ya0" firstAttribute="centerY" secondItem="gTV-IL-0wX" secondAttribute="centerY" id="QeX-QA-hWf"/>
+                <constraint firstItem="Tog-ZY-cO4" firstAttribute="leading" secondItem="gTV-IL-0wX" secondAttribute="leading" constant="22" id="RH0-ZW-O57"/>
+                <constraint firstAttribute="bottom" secondItem="Tog-ZY-cO4" secondAttribute="bottom" constant="22" id="UXP-Gm-lYX"/>
+                <constraint firstItem="eHf-UZ-Ya0" firstAttribute="centerX" secondItem="gTV-IL-0wX" secondAttribute="centerX" id="ZNb-4N-V6e"/>
+                <constraint firstItem="bj2-hS-1pF" firstAttribute="top" secondItem="gTV-IL-0wX" secondAttribute="top" constant="22" id="f5w-bM-AbI"/>
+                <constraint firstItem="zYx-Of-3tH" firstAttribute="centerX" secondItem="gTV-IL-0wX" secondAttribute="centerX" id="fL4-Jy-Q1U"/>
+                <constraint firstAttribute="trailing" secondItem="2xo-Od-0bZ" secondAttribute="trailing" id="gTd-Es-Nku"/>
+                <constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="zYx-Of-3tH" secondAttribute="trailing" constant="22" id="h85-nY-ag4"/>
+                <constraint firstAttribute="trailing" secondItem="Tog-ZY-cO4" secondAttribute="trailing" constant="22" id="k7o-jB-7o3"/>
+                <constraint firstItem="Tog-ZY-cO4" firstAttribute="top" relation="greaterThanOrEqual" secondItem="zYx-Of-3tH" secondAttribute="bottom" constant="-2" id="rpk-9a-BBU"/>
+                <constraint firstItem="zYx-Of-3tH" firstAttribute="top" relation="greaterThanOrEqual" secondItem="gTV-IL-0wX" secondAttribute="top" constant="22" id="wL8-E7-J74"/>
+            </constraints>
+            <size key="customSize" width="295" height="202"/>
+            <connections>
+                <outlet property="activityIndicator" destination="eHf-UZ-Ya0" id="KEG-cC-sSn"/>
+                <outlet property="audioOffIndicator" destination="vlO-GV-Y3B" id="cIm-dZ-peB"/>
+                <outlet property="peerAvatarImageView" destination="zYx-Of-3tH" id="fkR-av-PMY"/>
+                <outlet property="peerNameLabel" destination="D5h-T2-aB9" id="m82-2d-znc"/>
+                <outlet property="peerVideoView" destination="2xo-Od-0bZ" id="YvZ-QP-fUW"/>
+                <outlet property="raisedHandIndicator" destination="BPo-EN-rrM" id="Dwq-96-Waf"/>
+                <outlet property="screensharingIndicator" destination="bj2-hS-1pF" id="VFt-FC-Oum"/>
+                <outlet property="screensharingIndiciatorRightConstraint" destination="0vQ-dJ-Ozc" id="kx1-q2-1Sk"/>
+                <outlet property="screensharingIndiciatorTopConstraint" destination="f5w-bM-AbI" id="d5y-18-ICg"/>
+                <outlet property="stackViewBottomConstraint" destination="UXP-Gm-lYX" id="UJp-3p-DeZ"/>
+                <outlet property="stackViewLeftConstraint" destination="RH0-ZW-O57" id="sks-d4-NES"/>
+                <outlet property="stackViewRightConstraint" destination="k7o-jB-7o3" id="17X-EJ-Ekm"/>
+            </connections>
+            <point key="canvasLocation" x="32.799999999999997" y="133.13343328335833"/>
+        </collectionViewCell>
+    </objects>
+    <resources>
+        <image name="display" catalog="system" width="128" height="101"/>
+        <image name="hand.raised.fill" catalog="system" width="128" height="128"/>
+        <image name="mic.slash.fill" catalog="system" width="108" height="128"/>
+    </resources>
+</document>

+ 40 - 0
NextcloudTalk/CallReactionView.swift

@@ -0,0 +1,40 @@
+//
+// SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+
+import Foundation
+
+@objcMembers class CallReactionView: UIView {
+
+    @IBOutlet var contentView: UIView!
+    @IBOutlet weak var reactionLabel: UILabel!
+    @IBOutlet weak var actorLabelView: UIView!
+    @IBOutlet weak var actorLabel: UILabel!
+
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        commonInit()
+    }
+
+    required init?(coder aDecoder: NSCoder) {
+        super.init(coder: aDecoder)
+        commonInit()
+    }
+
+    func commonInit() {
+        Bundle.main.loadNibNamed("CallReactionView", owner: self, options: nil)
+        addSubview(contentView)
+        contentView.frame = frame
+        contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
+        actorLabelView.layer.cornerRadius = 4.0
+        actorLabelView.layer.shadowOpacity = 0.8
+        actorLabelView.layer.shadowOffset = CGSize(width: 2.0, height: 2.0)
+    }
+
+    func setReaction(reaction: String, actor: String) {
+        reactionLabel.text = reaction
+        actorLabel.text = actor
+        actorLabelView.backgroundColor = ColorGenerator.shared.usernameToColor(actor)
+    }
+}

+ 66 - 0
NextcloudTalk/CallReactionView.xib

@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="21701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
+    <device id="retina6_12" orientation="portrait" appearance="light"/>
+    <dependencies>
+        <deployment identifier="iOS"/>
+        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21678"/>
+        <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
+    </dependencies>
+    <objects>
+        <placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="CallReactionView" customModule="NextcloudTalk" customModuleProvider="target">
+            <connections>
+                <outlet property="actorLabel" destination="baK-5w-yBh" id="KPi-q7-cLI"/>
+                <outlet property="actorLabelView" destination="bdL-Go-Q3E" id="jYc-Ly-74d"/>
+                <outlet property="contentView" destination="CYW-lL-z7e" id="90l-rX-zxJ"/>
+                <outlet property="reactionLabel" destination="LOU-Zb-7yV" id="q8j-ks-oNB"/>
+            </connections>
+        </placeholder>
+        <placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
+        <view contentMode="scaleToFill" id="CYW-lL-z7e" userLabel="ContentView">
+            <rect key="frame" x="0.0" y="0.0" width="150" height="50"/>
+            <autoresizingMask key="autoresizingMask" flexibleMinX="YES" widthSizable="YES" flexibleMaxX="YES" flexibleMinY="YES" flexibleMaxY="YES"/>
+            <subviews>
+                <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="👌" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="LOU-Zb-7yV">
+                    <rect key="frame" x="10" y="10" width="30" height="30"/>
+                    <constraints>
+                        <constraint firstAttribute="width" constant="30" id="36m-jH-nwO"/>
+                        <constraint firstAttribute="height" constant="30" id="kak-BO-NOd"/>
+                    </constraints>
+                    <fontDescription key="fontDescription" type="system" pointSize="30"/>
+                    <nil key="textColor"/>
+                    <nil key="highlightedColor"/>
+                </label>
+                <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="bdL-Go-Q3E">
+                    <rect key="frame" x="50" y="10" width="16" height="30"/>
+                    <subviews>
+                        <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="baK-5w-yBh">
+                            <rect key="frame" x="8" y="4" width="0.0" height="22"/>
+                            <fontDescription key="fontDescription" type="system" weight="semibold" pointSize="16"/>
+                            <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+                            <nil key="highlightedColor"/>
+                        </label>
+                    </subviews>
+                    <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+                    <constraints>
+                        <constraint firstAttribute="trailing" secondItem="baK-5w-yBh" secondAttribute="trailing" constant="8" id="2hg-5o-sVf"/>
+                        <constraint firstAttribute="bottom" secondItem="baK-5w-yBh" secondAttribute="bottom" constant="4" id="A6I-ry-bHd"/>
+                        <constraint firstItem="baK-5w-yBh" firstAttribute="top" secondItem="bdL-Go-Q3E" secondAttribute="top" constant="4" id="ASc-Jw-Ima"/>
+                        <constraint firstItem="baK-5w-yBh" firstAttribute="leading" secondItem="bdL-Go-Q3E" secondAttribute="leading" constant="8" id="XKH-tm-rIA"/>
+                    </constraints>
+                </view>
+            </subviews>
+            <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+            <constraints>
+                <constraint firstItem="LOU-Zb-7yV" firstAttribute="leading" secondItem="CYW-lL-z7e" secondAttribute="leading" constant="10" id="4xn-Th-4Jb"/>
+                <constraint firstItem="bdL-Go-Q3E" firstAttribute="leading" secondItem="LOU-Zb-7yV" secondAttribute="trailing" constant="10" id="Xge-jX-KgT"/>
+                <constraint firstItem="LOU-Zb-7yV" firstAttribute="top" secondItem="CYW-lL-z7e" secondAttribute="top" constant="10" id="c9M-bQ-ok1"/>
+                <constraint firstItem="bdL-Go-Q3E" firstAttribute="top" secondItem="CYW-lL-z7e" secondAttribute="top" constant="10" id="ks2-we-SWW"/>
+                <constraint firstAttribute="bottom" secondItem="LOU-Zb-7yV" secondAttribute="bottom" constant="10" id="wnm-H5-xwv"/>
+                <constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="bdL-Go-Q3E" secondAttribute="trailing" constant="10" id="xKF-K3-8uF"/>
+                <constraint firstAttribute="bottom" secondItem="bdL-Go-Q3E" secondAttribute="bottom" constant="10" id="xks-df-MCN"/>
+            </constraints>
+            <freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
+            <point key="canvasLocation" x="-341.98473282442745" y="-2.1126760563380285"/>
+        </view>
+    </objects>
+</document>

+ 53 - 0
NextcloudTalk/CallViewController.h

@@ -0,0 +1,53 @@
+/**
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#import <UIKit/UIKit.h>
+#import <MetalKit/MetalKit.h>
+
+#import <WebRTC/RTCCameraPreviewView.h>
+#import "AvatarBackgroundImageView.h"
+#import "NCRoom.h"
+#import "NCChatTitleView.h"
+
+@class CallViewController;
+@class NCZoomableView;
+@protocol CallViewControllerDelegate <NSObject>
+
+- (void)callViewControllerWantsToBeDismissed:(CallViewController *)viewController;
+- (void)callViewControllerWantsVideoCallUpgrade:(CallViewController *)viewController;
+- (void)callViewControllerDidFinish:(CallViewController *)viewController;
+- (void)callViewController:(CallViewController *)viewController wantsToSwitchCallFromCall:(NSString *)from toRoom:(NSString *)to;
+
+@end
+
+@interface CallViewController : UIViewController
+
+@property (nonatomic, weak) id<CallViewControllerDelegate> delegate;
+@property (nonatomic, strong) NCRoom *room;
+@property (nonatomic, assign) BOOL audioDisabledAtStart;
+@property (nonatomic, assign) BOOL videoDisabledAtStart;
+@property (nonatomic, assign) BOOL voiceChatModeAtStart;
+@property (nonatomic, assign) BOOL initiator;
+@property (nonatomic, assign) BOOL silentCall;
+@property (nonatomic, assign) BOOL recordingConsent;
+
+@property (nonatomic, strong) IBOutlet MTKView *localVideoView;
+@property (nonatomic, strong) IBOutlet NCZoomableView *screensharingView;
+@property (nonatomic, strong) IBOutlet UIButton *closeScreensharingButton;
+@property (nonatomic, strong) IBOutlet UIButton *toggleChatButton;
+@property (nonatomic, strong) IBOutlet UIView *waitingView;
+@property (nonatomic, strong) IBOutlet AvatarBackgroundImageView *avatarBackgroundImageView;
+@property (nonatomic, strong) IBOutlet UILabel *waitingLabel;
+@property (nonatomic, strong) IBOutlet NCChatTitleView *titleView;
+@property (nonatomic, strong) IBOutlet UILabel *callTimeLabel;
+@property (nonatomic, strong) IBOutlet UIView *screenshareLabelContainer;
+@property (nonatomic, strong) IBOutlet UILabel *screenshareLabel;
+@property (nonatomic, strong) IBOutlet UIView *participantsLabelContainer;
+@property (nonatomic, strong) IBOutlet UILabel *participantsLabel;
+
+- (instancetype)initCallInRoom:(NCRoom *)room asUser:(NSString*)displayName audioOnly:(BOOL)audioOnly;
+- (void)toggleChatView;
+
+@end

+ 2654 - 0
NextcloudTalk/CallViewController.m

@@ -0,0 +1,2654 @@
+/**
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#import "CallViewController.h"
+
+#import <AVKit/AVKit.h>
+#import <ReplayKit/ReplayKit.h>
+
+#import <WebRTC/RTCCameraVideoCapturer.h>
+#import <WebRTC/RTCMediaStream.h>
+#import <WebRTC/RTCMTLVideoView.h>
+#import <WebRTC/RTCVideoTrack.h>
+
+#import "JDStatusBarNotification.h"
+
+#import "CallKitManager.h"
+#import "CallParticipantViewCell.h"
+#import "NCAPIController.h"
+#import "NCAppBranding.h"
+#import "NCAudioController.h"
+#import "NCCallController.h"
+#import "NCDatabaseManager.h"
+#import "NCRoomsManager.h"
+#import "NCSettingsController.h"
+#import "NCSignalingMessage.h"
+#import "RoomInfoTableViewController.h"
+#import "NCScreensharingController.h"
+
+#import "NextcloudTalk-Swift.h"
+
+typedef NS_ENUM(NSInteger, CallState) {
+    CallStateJoining,
+    CallStateWaitingParticipants,
+    CallStateReconnecting,
+    CallStateInCall,
+    CallStateSwitchingToAnotherRoom
+};
+
+CGFloat const kSidebarWidth                     = 350;
+CGFloat const kReactionViewAnimationDuration    = 2.0;
+CGFloat const kReactionViewHidingDuration       = 1.0;
+CGFloat const kMaxReactionsInScreen             = 5.0;
+
+typedef void (^UpdateCallParticipantViewCellBlock)(CallParticipantViewCell *cell);
+
+@interface PendingCellUpdate : NSObject
+
+@property (nonatomic, strong) NCPeerConnection *peer;
+@property (nonatomic, strong) UpdateCallParticipantViewCellBlock block;
+
+@end
+
+@implementation PendingCellUpdate
+@end
+
+@interface CallViewController () <NCCallControllerDelegate, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout, UICollectionViewDataSource, RTCVideoViewDelegate, CallParticipantViewCellDelegate, UIGestureRecognizerDelegate, NCChatTitleViewDelegate>
+{
+    CallState _callState;
+    NSMutableArray *_peersInCall;
+    NSMutableArray *_screenPeersInCall;
+    NSMutableDictionary *_videoRenderersDict; // peerIdentifier -> renderer
+    NSMutableDictionary *_screenRenderersDict; // peerId -> renderer
+    NSString *_presentedScreenPeerId;
+    NCCallController *_callController;
+    ChatViewController *_chatViewController;
+    UINavigationController *_chatNavigationController;
+    CGSize _screensharingSize;
+    UITapGestureRecognizer *_tapGestureForDetailedView;
+    NSTimer *_detailedViewTimer;
+    NSTimer *_proximityTimer;
+    NSString *_displayName;
+    BOOL _isAudioOnly;
+    BOOL _isDetailedViewVisible;
+    BOOL _userDisabledVideo;
+    BOOL _userDisabledSpeaker;
+    BOOL _videoCallUpgrade;
+    BOOL _hangingUp;
+    BOOL _pushToTalkActive;
+    BOOL _isHandRaised;
+    BOOL _proximityState;
+    BOOL _showChatAfterRoomSwitch;
+    BOOL _connectingSoundAlreadyPlayed;
+    UIImpactFeedbackGenerator *_buttonFeedbackGenerator;
+    CGPoint _localVideoDragStartingPosition;
+    CGPoint _localVideoOriginPosition;
+    AVRoutePickerView *_airplayView;
+    NSMutableArray *_pendingPeerInserts;
+    NSMutableArray *_pendingPeerDeletions;
+    NSMutableArray *_pendingPeerUpdates;
+    NSTimer *_batchUpdateTimer;
+    UIImageSymbolConfiguration *_barButtonsConfiguration;
+    CGFloat _lastScheduledReaction;
+    NSTimer *_callDurationTimer;
+    AVAudioPlayer *_soundsPlayer;
+}
+
+@property (nonatomic, strong) IBOutlet UIButton *audioMuteButton;
+@property (nonatomic, strong) IBOutlet UIButton *speakerButton;
+@property (nonatomic, strong) IBOutlet UIButton *videoDisableButton;
+@property (nonatomic, strong) IBOutlet UIButton *switchCameraButton;
+@property (nonatomic, strong) IBOutlet UIButton *hangUpButton;
+@property (nonatomic, strong) IBOutlet UIButton *videoCallButton;
+@property (nonatomic, strong) IBOutlet UIButton *recordingButton;
+@property (nonatomic, strong) IBOutlet UIButton *lowerHandButton;
+@property (nonatomic, strong) IBOutlet UIButton *moreMenuButton;
+@property (nonatomic, strong) IBOutlet UICollectionView *collectionView;
+@property (nonatomic, strong) IBOutlet UIView *topBarView;
+@property (nonatomic, strong) IBOutlet UIStackView *topBarButtonStackView;
+@property (nonatomic, strong) IBOutlet UIView *sideBarView;
+@property (nonatomic, strong) IBOutlet NSLayoutConstraint *collectionViewLeftConstraint;
+@property (nonatomic, strong) IBOutlet NSLayoutConstraint *collectionViewBottomConstraint;
+@property (nonatomic, strong) IBOutlet NSLayoutConstraint *collectionViewRightConstraint;
+@property (nonatomic, strong) IBOutlet NSLayoutConstraint *topBarViewRightContraint;
+@property (nonatomic, strong) IBOutlet NSLayoutConstraint *screenshareViewRightContraint;
+@property (nonatomic, strong) IBOutlet NSLayoutConstraint *sideBarViewRightConstraint;
+@property (nonatomic, strong) IBOutlet NSLayoutConstraint *sideBarViewBottomConstraint;
+@property (nonatomic, strong) IBOutlet NSLayoutConstraint *sideBarWidthConstraint;
+@property (nonatomic, strong) IBOutlet NSLayoutConstraint *stackViewToTitleViewConstraint;
+
+@end
+
+@implementation CallViewController
+
+@synthesize delegate = _delegate;
+
+- (instancetype)initCallInRoom:(NCRoom *)room asUser:(NSString *)displayName audioOnly:(BOOL)audioOnly
+{
+    self = [super init];
+    if (!self) {
+        return nil;
+    }
+    
+    self.modalPresentationStyle = UIModalPresentationFullScreen;
+    
+    _room = room;
+    _displayName = displayName;
+    _isAudioOnly = audioOnly;
+    _peersInCall = [[NSMutableArray alloc] init];
+    _screenPeersInCall = [[NSMutableArray alloc] init];
+    _videoRenderersDict = [[NSMutableDictionary alloc] init];
+    _screenRenderersDict = [[NSMutableDictionary alloc] init];
+    _buttonFeedbackGenerator = [[UIImpactFeedbackGenerator alloc] initWithStyle:(UIImpactFeedbackStyleLight)];
+    _pendingPeerInserts = [[NSMutableArray alloc] init];
+    _pendingPeerDeletions = [[NSMutableArray alloc] init];
+    _pendingPeerUpdates = [[NSMutableArray alloc] init];
+    _lastScheduledReaction = 0.0;
+
+    _barButtonsConfiguration = [UIImageSymbolConfiguration configurationWithPointSize:20];
+    
+    // Use image downloader without cache so I can get 200 or 201 from the avatar requests.
+    [AvatarBackgroundImageView setSharedImageDownloader:[[NCAPIController sharedInstance] imageDownloaderNoCache]];
+    
+    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didJoinRoom:) name:NCRoomsManagerDidJoinRoomNotification object:nil];
+    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(providerDidEndCall:) name:CallKitManagerDidEndCallNotification object:nil];
+    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(providerDidChangeAudioMute:) name:CallKitManagerDidChangeAudioMuteNotification object:nil];
+    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(providerWantsToUpgradeToVideoCall:) name:CallKitManagerWantsToUpgradeToVideoCallNotification object:nil];
+    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(audioSessionDidChangeRoute:) name:AudioSessionDidChangeRouteNotification object:nil];
+    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(audioSessionDidActivate:) name:AudioSessionWasActivatedByProviderNotification object:nil];
+    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(audioSessionDidChangeRoutingInformation:) name:AudioSessionDidChangeRoutingInformationNotification object:nil];
+    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(appDidBecomeActive:) name:UIApplicationDidBecomeActiveNotification object:nil];
+    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(appWillResignActive:) name:UIApplicationWillResignActiveNotification object:nil];
+
+    [[AllocationTracker shared] addAllocation:@"CallViewController"];
+
+    return self;
+}
+
+- (void)startCallWithSessionId:(NSString *)sessionId
+{
+    _callController = [[NCCallController alloc] initWithDelegate:self inRoom:_room forAudioOnlyCall:_isAudioOnly withSessionId:sessionId andVoiceChatMode:_voiceChatModeAtStart];
+    _callController.userDisplayName = _displayName;
+    _callController.disableAudioAtStart = _audioDisabledAtStart;
+    _callController.disableVideoAtStart = _videoDisabledAtStart;
+    _callController.silentCall = _silentCall;
+    _callController.recordingConsent = _recordingConsent;
+
+    [_callController startCall];
+}
+
+- (void)viewDidLoad
+{
+    [super viewDidLoad];
+    [self setCallState:CallStateJoining];
+    
+    _tapGestureForDetailedView = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(showDetailedViewWithTimer)];
+    [_tapGestureForDetailedView setNumberOfTapsRequired:1];
+
+    UILongPressGestureRecognizer *pushToTalkRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handlePushToTalk:)];
+    [self.audioMuteButton addGestureRecognizer:pushToTalkRecognizer];
+
+    [_participantsLabelContainer setHidden:YES];
+
+    [_screensharingView setHidden:YES];
+    [_screensharingView setClipsToBounds:YES];
+
+    [self.hangUpButton.layer setCornerRadius:self.hangUpButton.frame.size.height / 2];
+    [self.closeScreensharingButton.layer setCornerRadius:16.0f];
+
+    [self.collectionView.layer setCornerRadius:22.0f];
+    [self.collectionView setContentInsetAdjustmentBehavior:UIScrollViewContentInsetAdjustmentAlways];
+
+    [self.sideBarView setClipsToBounds:YES];
+    [self.sideBarView.layer setCornerRadius:22.0f];
+
+    _airplayView = [[AVRoutePickerView alloc] initWithFrame:CGRectMake(0, 0, 48, 56)];
+    _airplayView.tintColor = [UIColor whiteColor];
+    _airplayView.activeTintColor = [UIColor whiteColor];
+        
+    self.audioMuteButton.accessibilityLabel = NSLocalizedString(@"Microphone", nil);
+    self.audioMuteButton.accessibilityValue = NSLocalizedString(@"Microphone enabled", nil);
+    self.audioMuteButton.accessibilityHint = NSLocalizedString(@"Double tap to enable or disable the microphone", nil);
+    self.speakerButton.accessibilityLabel = NSLocalizedString(@"Speaker", nil);
+    self.speakerButton.accessibilityValue = NSLocalizedString(@"Speaker disabled", nil);
+    self.speakerButton.accessibilityHint = NSLocalizedString(@"Double tap to enable or disable the speaker", nil);
+    self.videoDisableButton.accessibilityLabel = NSLocalizedString(@"Camera", nil);
+    self.videoDisableButton.accessibilityValue = NSLocalizedString(@"Camera enabled", nil);
+    self.videoDisableButton.accessibilityHint = NSLocalizedString(@"Double tap to enable or disable the camera", nil);
+    self.hangUpButton.accessibilityLabel = NSLocalizedString(@"Hang up", nil);
+    self.hangUpButton.accessibilityHint = NSLocalizedString(@"Double tap to hang up the call", nil);
+    self.videoCallButton.accessibilityLabel = NSLocalizedString(@"Camera", nil);
+    self.videoCallButton.accessibilityHint = NSLocalizedString(@"Double tap to upgrade this voice call to a video call", nil);
+    self.toggleChatButton.accessibilityLabel = NSLocalizedString(@"Chat", nil);
+    self.toggleChatButton.accessibilityHint = NSLocalizedString(@"Double tap to show or hide chat view", nil);
+    self.recordingButton.accessibilityLabel = NSLocalizedString(@"Recording", nil);
+    self.recordingButton.accessibilityHint = NSLocalizedString(@"Double tap to stop recording", nil);
+    self.lowerHandButton.accessibilityLabel = NSLocalizedString(@"Lower hand", nil);
+    self.lowerHandButton.accessibilityHint = NSLocalizedString(@"Double tap to lower hand", nil);
+    self.moreMenuButton.accessibilityLabel = NSLocalizedString(@"More actions", nil);
+    self.moreMenuButton.accessibilityHint = NSLocalizedString(@"Double tap to show more actions", nil);
+
+    self.moreMenuButton.showsMenuAsPrimaryAction = YES;
+
+    // Text color should be always white in the call view
+    [self.titleView setTitleTextColor:UIColor.whiteColor];
+    [self.titleView updateForRoom:_room];
+
+    // The titleView uses the themeColor as a background for the userStatusImage
+    // As we always have a black background, we need to change that
+    [self.titleView setUserStatusBackgroundColor:UIColor.blackColor];
+
+    self.titleView.delegate = self;
+    
+    self.collectionView.delegate = self;
+    
+    [self createWaitingScreen];
+
+    // We hide localVideoView until we receive it from cameraController
+    [self setLocalVideoViewHidden:YES];
+    
+    // We disableLocalVideo here even if the call controller has not been created just to show the video button as disabled
+    // also we set _userDisabledVideo = YES so the proximity sensor doesn't enable it.
+    if (_videoDisabledAtStart) {
+        _userDisabledVideo = YES;
+        [self disableLocalVideo];
+    }
+    
+    if (_voiceChatModeAtStart) {
+        _userDisabledSpeaker = YES;
+    }
+
+    TalkAccount *activeAccount = [[NCDatabaseManager sharedInstance] activeAccount];
+    // 'conversation-permissions' capability was not added in Talk 13 release, so we check for 'direct-mention-flag' capability
+    // as a workaround.
+    BOOL serverSupportsConversationPermissions =
+    [[NCDatabaseManager sharedInstance] serverHasTalkCapability:kCapabilityConversationPermissions forAccountId:activeAccount.accountId] ||
+    [[NCDatabaseManager sharedInstance] serverHasTalkCapability:kCapabilityDirectMentionFlag forAccountId:activeAccount.accountId];
+    if (serverSupportsConversationPermissions) {
+        [self setAudioMuteButtonEnabled:(_room.permissions & NCPermissionCanPublishAudio)];
+        [self setVideoDisableButtonEnabled:(_room.permissions & NCPermissionCanPublishVideo)];
+    }
+    
+    [self.collectionView registerNib:[UINib nibWithNibName:kCallParticipantCellNibName bundle:nil] forCellWithReuseIdentifier:kCallParticipantCellIdentifier];
+    [self.collectionView setContentInsetAdjustmentBehavior:UIScrollViewContentInsetAdjustmentNever];
+    
+    UIPanGestureRecognizer *localVideoDragGesturure = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(localVideoDragged:)];
+    [self.localVideoView addGestureRecognizer:localVideoDragGesturure];
+
+    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(sensorStateChange:)
+                                                 name:UIDeviceProximityStateDidChangeNotification object:nil];
+
+    // callStartTime is only available if we have the "recording-v1" capability
+    if ([[NCDatabaseManager sharedInstance] serverHasTalkCapability:kCapabilityRecordingV1]) {
+        _callDurationTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(callDurationTimerUpdate) userInfo:nil repeats:YES];
+    }
+}
+
+- (void)viewDidLayoutSubviews
+{
+    [super viewDidLayoutSubviews];
+
+    [self.screenshareLabelContainer.layer setCornerRadius:self.screenshareLabelContainer.frame.size.height / 2];
+    [self.participantsLabelContainer.layer setCornerRadius:self.participantsLabelContainer.frame.size.height / 2];
+}
+
+- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
+{
+    [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
+
+    [self adjustConstraints];
+    [self.collectionView.collectionViewLayout invalidateLayout];
+    
+    [coordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext>  _Nonnull context) {
+        [self setLocalVideoRect];
+        [self->_screensharingView resizeContentView];
+        [self adjustTopBar];
+    } completion:^(id<UIViewControllerTransitionCoordinatorContext>  _Nonnull context) {
+    }];
+}
+
+- (void)viewSafeAreaInsetsDidChange
+{
+    [super viewSafeAreaInsetsDidChange];
+    [self adjustConstraints];
+    [self setLocalVideoRect];
+    [self adjustTopBar];
+}
+
+- (void)viewWillAppear:(BOOL)animated
+{
+    [super viewWillAppear:animated];
+
+    [self setSideBarVisible:NO animated:NO withCompletion:nil];
+    [self adjustConstraints];
+    [self setLocalVideoRect];
+    [self adjustSpeakerButton];
+    [self adjustTopBar];
+}
+
+- (void)viewWillDisappear:(BOOL)animated
+{
+    [super viewWillDisappear:animated];
+    [[UIDevice currentDevice] setProximityMonitoringEnabled:NO];
+    [UIApplication sharedApplication].idleTimerDisabled = NO;
+}
+
+- (void)viewDidAppear:(BOOL)animated
+{
+    [super viewDidAppear:animated];
+
+    [[UIDevice currentDevice] setProximityMonitoringEnabled:YES];
+    [UIApplication sharedApplication].idleTimerDisabled = YES;
+}
+
+- (UIStatusBarStyle)preferredStatusBarStyle
+{
+    return UIStatusBarStyleLightContent;
+}
+
+- (void)dealloc
+{
+    NSLog(@"CallViewController dealloc");
+    [[AllocationTracker shared] removeAllocation:@"CallViewController"];
+    [[NSNotificationCenter defaultCenter] removeObserver:self];
+}
+
+- (void)didReceiveMemoryWarning {
+    [super didReceiveMemoryWarning];
+    // Dispose of any resources that can be recreated.
+}
+
+- (void)pressesBegan:(NSSet<UIPress *> *)presses withEvent:(UIPressesEvent *)event
+{
+    // No push-to-talk while in chat
+    if (!_chatNavigationController) {
+        for (UIPress* press in presses) {
+            if (press.key.keyCode == UIKeyboardHIDUsageKeyboardSpacebar) {
+                [self pushToTalkStart];
+                
+                return;
+            }
+        }
+    }
+    
+    [super pressesBegan:presses withEvent:event];
+}
+
+- (void)pressesEnded:(NSSet<UIPress *> *)presses withEvent:(UIPressesEvent *)event
+{
+    // No push-to-talk while in chat
+    if (!_chatNavigationController) {
+        for (UIPress* press in presses) {
+            if (press.key.keyCode == UIKeyboardHIDUsageKeyboardSpacebar) {
+                [self pushToTalkEnd];
+                
+                return;
+            }
+        }
+    }
+    
+    [super pressesEnded:presses withEvent:event];
+}
+
+#pragma mark - App lifecycle notifications
+
+-(void)appDidBecomeActive:(NSNotification*)notification
+{
+    if (!_isAudioOnly && _callController && !_userDisabledVideo) {
+        // Only enable video if it was not disabled by the user.
+        [self enableLocalVideo];
+    }
+}
+
+-(void)appWillResignActive:(NSNotification*)notification
+{
+    if (!_isAudioOnly && _callController) {
+        [_callController getVideoEnabledStateWithCompletionBlock:^(BOOL isEnabled) {
+            if (isEnabled) {
+                // Disable video when the app moves to the background as we can't access the camera anymore.
+                [self disableLocalVideo];
+            }
+        }];
+    }
+}
+
+#pragma mark - Rooms manager notifications
+
+- (void)didJoinRoom:(NSNotification *)notification
+{
+    NSString *token = [notification.userInfo objectForKey:@"token"];
+    if (![token isEqualToString:_room.token]) {
+        return;
+    }
+    
+    NSError *error = [notification.userInfo objectForKey:@"error"];
+    if (error) {
+        [self presentJoinError:[notification.userInfo objectForKey:@"errorReason"]];
+        return;
+    }
+    
+    NCRoomController *roomController = [notification.userInfo objectForKey:@"roomController"];
+    if (!_callController) {
+        [self startCallWithSessionId:roomController.userSessionId];
+    }
+
+    [self.titleView updateForRoom:_room];
+}
+
+- (void)providerDidChangeAudioMute:(NSNotification *)notification
+{
+    NSString *roomToken = [notification.userInfo objectForKey:@"roomToken"];
+    if (![roomToken isEqualToString:_room.token]) {
+        return;
+    }
+    
+    BOOL isMuted = [[notification.userInfo objectForKey:@"isMuted"] boolValue];
+    [self setAudioMuted:isMuted];
+}
+
+- (void)providerDidEndCall:(NSNotification *)notification
+{
+    NSString *roomToken = [notification.userInfo objectForKey:@"roomToken"];
+    if (![roomToken isEqualToString:_room.token]) {
+        return;
+    }
+    
+    [self hangupForAll:NO];
+}
+
+- (void)providerWantsToUpgradeToVideoCall:(NSNotification *)notification
+{
+    NSString *roomToken = [notification.userInfo objectForKey:@"roomToken"];
+    if (![roomToken isEqualToString:_room.token]) {
+        return;
+    }
+    
+    if (_isAudioOnly) {
+        [self showUpgradeToVideoCallDialog];
+    }
+}
+
+#pragma mark - Audio controller notifications
+
+- (void)audioSessionDidChangeRoute:(NSNotification *)notification
+{
+    [self adjustSpeakerButton];
+}
+
+- (void)audioSessionDidActivate:(NSNotification *)notification
+{
+    [self adjustSpeakerButton];
+}
+
+- (void)audioSessionDidChangeRoutingInformation:(NSNotification *)notification
+{
+    [self adjustSpeakerButton];
+
+    dispatch_async(dispatch_get_main_queue(), ^{
+        [self adjustMoreButtonMenu];
+    });
+}
+
+#pragma mark - Local video
+
+- (void)setLocalVideoRect
+{
+    CGSize localVideoSize;
+    
+    CGFloat width = [UIScreen mainScreen].bounds.size.width / 6;
+    CGFloat height = [UIScreen mainScreen].bounds.size.height / 6;
+    
+    NSString *videoResolution = [[[NCSettingsController sharedInstance] videoSettingsModel] currentVideoResolutionSettingFromStore];
+    NSString *localVideoRes = [[[NCSettingsController sharedInstance] videoSettingsModel] readableResolution:videoResolution];
+
+    // When running on MacOS the camera will always be in portrait mode
+    if ([localVideoRes isEqualToString:@"Low"] || [localVideoRes isEqualToString:@"Normal"]) {
+        if (width < height || [NCUtils isiOSAppOnMac]) {
+            localVideoSize = CGSizeMake(height * 3/4, height);
+        } else {
+            localVideoSize = CGSizeMake(width, width * 3/4);
+        }
+    } else {
+        if (width < height || [NCUtils isiOSAppOnMac]) {
+            localVideoSize = CGSizeMake(height * 9/16, height);
+        } else {
+            localVideoSize = CGSizeMake(width, width * 9/16);
+        }
+    }
+
+    UIEdgeInsets safeAreaInsets = self.view.safeAreaInsets;
+    CGSize viewSize = self.view.frame.size;
+    CGFloat defaultPadding = 16;
+    CGFloat extraPadding = 60; // Padding to not cover  participant name or mute indicator when there is only one other participant in the call
+    _localVideoOriginPosition = CGPointMake(viewSize.width - localVideoSize.width - _collectionViewRightConstraint.constant - safeAreaInsets.right - defaultPadding,
+                                            viewSize.height - localVideoSize.height - _collectionViewBottomConstraint.constant - safeAreaInsets.bottom - extraPadding);
+
+    CGRect localVideoRect = CGRectMake(_localVideoOriginPosition.x, _localVideoOriginPosition.y, localVideoSize.width, localVideoSize.height);
+    
+    dispatch_async(dispatch_get_main_queue(), ^{
+        self->_localVideoView.frame = localVideoRect;
+        self->_localVideoView.layer.cornerRadius = 15.0f;
+        self->_localVideoView.layer.masksToBounds = YES;
+    });
+}
+
+#pragma mark - Proximity sensor
+
+- (void)sensorStateChange:(NSNotificationCenter *)notification
+{
+    dispatch_async(dispatch_get_main_queue(), ^{
+        [self->_proximityTimer invalidate];
+        self->_proximityTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(adjustProximityState) userInfo:nil repeats:NO];
+    });
+}
+
+- (void)adjustProximityState
+{
+    BOOL currentProximityState = [[UIDevice currentDevice] proximityState];
+
+    if (currentProximityState == _proximityState) {
+        return;
+    }
+
+    _proximityState = currentProximityState;
+
+    if (!_isAudioOnly) {
+        if (_proximityState == YES) {
+            [self disableLocalVideo];
+            [self disableSpeaker];
+        } else {
+            // Only enable video if it was not disabled by the user.
+            if (!_userDisabledVideo) {
+                [self enableLocalVideo];
+            }
+            if (!_userDisabledSpeaker) {
+                [self enableSpeaker];
+            }
+        }
+    }
+
+    [self pushToTalkEnd];
+}
+
+#pragma mark - User Interface
+
+- (void)setCallState:(CallState)state
+{
+    _callState = state;
+    switch (state) {
+        case CallStateJoining:
+        case CallStateWaitingParticipants:
+        case CallStateReconnecting:
+        {
+            [self startPlayingConnectingSound];
+            [self showWaitingScreen];
+            [self invalidateDetailedViewTimer];
+            [self showDetailedView];
+            [self removeTapGestureForDetailedView];
+        }
+            break;
+            
+        case CallStateInCall:
+        {
+            [self stopPlayingConnectingSound];
+            [self hideWaitingScreen];
+            if (!_isAudioOnly) {
+                [self addTapGestureForDetailedView];
+                [self showDetailedViewWithTimer];
+            }
+        }
+            break;
+
+        case CallStateSwitchingToAnotherRoom:
+        {
+            [self showWaitingScreen];
+            [self invalidateDetailedViewTimer];
+            [self showDetailedView];
+            [self removeTapGestureForDetailedView];
+        }
+            break;
+
+        default:
+            break;
+    }
+}
+
+- (void)setCallStateForPeersInCall
+{
+    if ([_peersInCall count] > 0) {
+        if (_callState != CallStateInCall) {
+            [self setCallState:CallStateInCall];
+        }
+    } else {
+        if (_callState == CallStateInCall) {
+            [self setCallState:CallStateWaitingParticipants];
+        }
+    }
+
+    if (_room.type != kNCRoomTypeOneToOne) {
+        dispatch_async(dispatch_get_main_queue(), ^{
+            NSTextAttachment *participantsAttachment = [[NSTextAttachment alloc] init];
+            participantsAttachment.image = [[UIImage systemImageNamed:@"person.2"] imageWithTintColor:self.participantsLabel.textColor];
+
+            NSMutableAttributedString *resultString = [[NSMutableAttributedString alloc] initWithAttributedString:[NSAttributedString attributedStringWithAttachment:participantsAttachment]];
+            [resultString appendAttributedString:[[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@"  %ld", [self->_peersInCall count] + 1]]];
+
+            NSRange range = NSMakeRange(0, [resultString length]);
+            [resultString addAttribute:NSFontAttributeName value:self.participantsLabel.font range:range];
+
+            self.participantsLabel.attributedText = resultString;
+
+            [self.participantsLabelContainer setHidden:NO];
+        });
+    }
+}
+
+- (void)createWaitingScreen
+{
+    self.avatarBackgroundImageView.backgroundColor = [NCAppBranding themeColor];
+
+    if (_room.type == kNCRoomTypeOneToOne) {
+        UIColor *bgColor = [[ColorGenerator shared] usernameToColor:self.room.displayName];
+        [self.avatarBackgroundImageView setBackgroundColor:bgColor];
+        self.avatarBackgroundImageView.backgroundColor = [self.avatarBackgroundImageView.backgroundColor colorWithAlphaComponent:0.8];
+    }
+    
+    [self setWaitingScreenText];
+}
+
+- (void)setWaitingScreenText
+{
+    NSString *waitingMessage = NSLocalizedString(@"Waiting for others to join call …", nil);
+    if (_room.type == kNCRoomTypeOneToOne) {
+        waitingMessage = [NSString stringWithFormat:NSLocalizedString(@"Waiting for %@ to join call …", nil), _room.displayName];
+    }
+    
+    if (_callState == CallStateReconnecting) {
+        waitingMessage = NSLocalizedString(@"Connecting to the call …", nil);
+    }
+    
+    if (_callState == CallStateSwitchingToAnotherRoom) {
+        waitingMessage = NSLocalizedString(@"Switching to another conversation …", nil);
+    }
+
+    dispatch_async(dispatch_get_main_queue(), ^{
+        self.waitingLabel.text = waitingMessage;
+    });
+}
+
+- (void)showWaitingScreen
+{
+    [self setWaitingScreenText];
+    dispatch_async(dispatch_get_main_queue(), ^{
+        self.collectionView.backgroundView = self.waitingView;
+    });
+}
+
+- (void)hideWaitingScreen
+{
+    dispatch_async(dispatch_get_main_queue(), ^{
+        self.collectionView.backgroundView = nil;
+    });
+}
+
+- (void)startPlayingConnectingSound
+{
+    if (!_initiator || _connectingSoundAlreadyPlayed) {
+        return;
+    }
+
+    NSString *soundFilePath = [[NSBundle mainBundle] pathForResource:@"connecting" ofType:@"mp3"];
+    NSURL *soundFileURL = [NSURL fileURLWithPath:soundFilePath];
+
+    _soundsPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:soundFileURL error:nil];
+    _soundsPlayer.numberOfLoops = -1;
+
+    [_soundsPlayer play];
+
+    _connectingSoundAlreadyPlayed = YES;
+}
+
+- (void)stopPlayingConnectingSound
+{
+    [_soundsPlayer stop];
+}
+
+- (void)addTapGestureForDetailedView
+{
+    dispatch_async(dispatch_get_main_queue(), ^{
+        [self.view addGestureRecognizer:self->_tapGestureForDetailedView];
+    });
+}
+
+- (void)removeTapGestureForDetailedView
+{
+    dispatch_async(dispatch_get_main_queue(), ^{
+        [self.view removeGestureRecognizer:self->_tapGestureForDetailedView];
+    });
+}
+
+- (void)showDetailedView
+{
+    _isDetailedViewVisible = YES;
+    [self showPeersInfo];
+}
+
+- (void)showDetailedViewWithTimer
+{
+    if (_isDetailedViewVisible) {
+        [self hideDetailedView];
+    } else {
+        [self showDetailedView];
+        [self setDetailedViewTimer];
+    }
+}
+
+- (void)hideDetailedView
+{
+    // Keep detailed view visible while push to talk is active
+    if (_pushToTalkActive) {
+        [self setDetailedViewTimer];
+        return;
+    }
+    
+    _isDetailedViewVisible = NO;
+    [self hidePeersInfo];
+    [self invalidateDetailedViewTimer];
+}
+
+- (void)setAudioMuteButtonActive:(BOOL)active
+{
+    dispatch_async(dispatch_get_main_queue(), ^{
+        NSString *micStatusString = nil;
+
+        if (active) {
+            micStatusString = NSLocalizedString(@"Microphone enabled", nil);
+            [self->_audioMuteButton setImage:[UIImage systemImageNamed:@"mic.fill" withConfiguration:self->_barButtonsConfiguration] forState:UIControlStateNormal];
+        } else {
+            micStatusString = NSLocalizedString(@"Microphone disabled", nil);
+            [self->_audioMuteButton setImage:[UIImage systemImageNamed:@"mic.slash.fill" withConfiguration:self->_barButtonsConfiguration] forState:UIControlStateNormal];
+        }
+
+        self->_audioMuteButton.accessibilityValue = micStatusString;
+    });
+}
+
+- (void)setAudioMuteButtonEnabled:(BOOL)enabled
+{
+    dispatch_async(dispatch_get_main_queue(), ^{
+        self->_audioMuteButton.enabled = enabled;
+    });
+}
+
+- (void)setVideoDisableButtonActive:(BOOL)active
+{
+    dispatch_async(dispatch_get_main_queue(), ^{
+        NSString *cameraStatusString = nil;
+
+        if (active) {
+            cameraStatusString = NSLocalizedString(@"Camera enabled", nil);
+            [self->_videoDisableButton setImage:[UIImage systemImageNamed:@"video.fill" withConfiguration:self->_barButtonsConfiguration] forState:UIControlStateNormal];
+        } else {
+            cameraStatusString = NSLocalizedString(@"Camera disabled", nil);
+            [self->_videoDisableButton setImage:[UIImage systemImageNamed:@"video.slash.fill" withConfiguration:self->_barButtonsConfiguration] forState:UIControlStateNormal];
+        }
+
+        self->_videoDisableButton.accessibilityValue = cameraStatusString;
+    });
+}
+
+- (void)setVideoDisableButtonEnabled:(BOOL)enabled
+{
+    dispatch_async(dispatch_get_main_queue(), ^{
+        self->_videoDisableButton.enabled = enabled;
+    });
+}
+
+- (void)setLocalVideoViewHidden:(BOOL)hidden
+{
+    dispatch_async(dispatch_get_main_queue(), ^{
+        [self->_localVideoView setHidden:hidden];
+    });
+}
+
+- (void)adjustTopBar
+{
+    dispatch_async(dispatch_get_main_queue(), ^{
+        // Enable/Disable video buttons
+        self->_videoDisableButton.hidden = self->_isAudioOnly;
+        self->_switchCameraButton.hidden = self->_isAudioOnly;
+        self->_videoCallButton.hidden = !self->_isAudioOnly;
+
+        self->_lowerHandButton.hidden = !self->_isHandRaised;
+
+        // Only when the server supports recording-v1 we have access to callStartTime, otherwise hide the label
+        self->_callTimeLabel.hidden = ![[NCDatabaseManager sharedInstance] serverHasTalkCapability:kCapabilityRecordingV1];
+
+        NCAudioController *audioController = [NCAudioController sharedInstance];
+        self->_speakerButton.hidden = ![audioController isAudioRouteChangeable];
+
+        BOOL hideRecordingButton = ![self->_room callRecordingIsInActiveState];
+        self->_recordingButton.hidden = hideRecordingButton;
+
+        // Differ between starting a call recording and an actual running call recording
+        if (self->_room.callRecording == NCCallRecordingStateVideoStarting || self->_room.callRecording == NCCallRecordingStateAudioStarting) {
+            self->_recordingButton.tintColor = UIColor.systemGrayColor;
+        } else {
+            self->_recordingButton.tintColor = UIColor.systemRedColor;
+        }
+
+        // When the horizontal size is compact (e.g. iPhone portrait) we don't show the 'End call' text on the button
+        // Don't make assumptions about the device here, because with split screen even an iPad can have a compact width
+        if (self.traitCollection.horizontalSizeClass == UIUserInterfaceSizeClassCompact) {
+            [self setHangUpButtonWithTitle:NO];
+        } else {
+            [self setHangUpButtonWithTitle:YES];
+        }
+        
+        // Make sure we get the correct frame for the stack view, after changing the visibility of buttons
+        [self->_topBarView setNeedsLayout];
+        [self->_topBarView layoutIfNeeded];
+
+        // Hide titleView if we don't have enough space
+        // Don't do it in one go, as then we will have some jumping
+        if (self->_topBarButtonStackView.frame.origin.x < 200) {
+            [self setHangUpButtonWithTitle:NO];
+            [self->_titleView setHidden:YES];
+            [self->_stackViewToTitleViewConstraint setActive:NO];
+        } else {
+            [self->_titleView setHidden:NO];
+            [self->_stackViewToTitleViewConstraint setActive:YES];
+        }
+
+        // Need to update the layout again, if we changed it here
+        [self->_topBarView setNeedsLayout];
+        [self->_topBarView layoutIfNeeded];
+
+        // Hide the speaker button to make some more room for higher priority buttons
+        // This should only be the case for iPhone SE (1st Gen) when recording is active and/or hand is raised
+        if (self->_topBarButtonStackView.frame.origin.x < 0) {
+            self->_speakerButton.hidden = YES;
+        }
+
+        [self->_topBarView setNeedsLayout];
+        [self->_topBarView layoutIfNeeded];
+
+        if (self->_topBarButtonStackView.frame.origin.x < 0) {
+            self->_callTimeLabel.hidden = YES;
+        }
+
+        [self adjustMoreButtonMenu];
+
+        if (([self->_room canModerate] || self->_room.type == kNCRoomTypeOneToOne) &&
+            [[NCDatabaseManager sharedInstance] serverHasTalkCapability:kCapabilityPublishingPermissions]) {
+            __weak typeof(self) weakSelf = self;
+            UIAction *hangupForAllAction = [UIAction actionWithTitle:NSLocalizedString(@"End call for everyone", @"") image:[UIImage systemImageNamed:@"phone.down.fill"] identifier:nil handler:^(UIAction *action) {
+                [weakSelf hangupForAll:YES];
+            }];
+
+            hangupForAllAction.attributes = UIMenuElementAttributesDestructive;
+
+            self.hangUpButton.menu = [UIMenu menuWithTitle:@"" children:@[hangupForAllAction]];
+        }
+    });
+}
+
+- (void)setHangUpButtonWithTitle:(BOOL)title
+{
+    if (title) {
+        [_hangUpButton setTitle:NSLocalizedString(@"End call", nil) forState:UIControlStateNormal];
+        [_hangUpButton setTitleColor:[UIColor grayColor] forState:UIControlStateHighlighted];
+        [_hangUpButton setContentEdgeInsets:UIEdgeInsetsMake(0, 16, 0, 24)];
+        [_hangUpButton setTitleEdgeInsets:UIEdgeInsetsMake(0, 8, 0, -8)];
+    } else {
+        [_hangUpButton setTitle:@"" forState:UIControlStateNormal];
+        [_hangUpButton setContentEdgeInsets:UIEdgeInsetsZero];
+        [_hangUpButton setTitleEdgeInsets:UIEdgeInsetsZero];
+    }
+}
+
+- (void)adjustConstraints
+{
+    CGFloat rightConstraintConstant = [self getRightSideConstraintConstant];
+    [self->_collectionViewRightConstraint setConstant:rightConstraintConstant];
+
+    if (self.traitCollection.horizontalSizeClass == UIUserInterfaceSizeClassCompact) {
+        [self->_collectionViewLeftConstraint setConstant:0.0f];
+    } else {
+        [self->_collectionViewLeftConstraint setConstant:8.0f];
+    }
+
+    if (self.traitCollection.verticalSizeClass == UIUserInterfaceSizeClassCompact) {
+        [self->_collectionViewBottomConstraint setConstant:0.0f];
+        [self->_sideBarViewBottomConstraint setConstant:0.0f];
+    } else {
+        [self->_collectionViewBottomConstraint setConstant:8.0f];
+        [self->_sideBarViewBottomConstraint setConstant:8.0f];
+    }
+}
+
+- (void)showScreensharingPicker
+{
+    RPSystemBroadcastPickerView *broadcastPicker = [[RPSystemBroadcastPickerView alloc] initWithFrame:CGRectMake(0, 0, 10, 10)];
+    broadcastPicker.preferredExtension = [NSString stringWithFormat:@"%@.BroadcastUploadExtension", NSBundle.mainBundle.bundleIdentifier];
+    broadcastPicker.showsMicrophoneButton = NO;
+
+    UIButton *btn = nil;
+
+    for (UIView *subview in broadcastPicker.subviews) {
+        if ([subview isKindOfClass:[UIButton class]]) {
+            btn = (UIButton *)subview;
+        }
+    }
+    if (btn != nil) {
+        [btn sendActionsForControlEvents:UIControlEventTouchUpInside];
+    } else {
+        NSLog(@"RPSystemBroadcastPickerView button not found");
+    }
+}
+
+- (void)adjustMoreButtonMenu
+{
+    // When we target iOS 15, we might want to use an uncached UIDeferredMenuElement
+
+    NSMutableArray *items = [[NSMutableArray alloc] init];
+    __weak typeof(self) weakSelf = self;
+
+    // Add speaker button to menu if it was hidden from topbar
+    NCAudioController *audioController = [NCAudioController sharedInstance];
+    if ([self.speakerButton isHidden] && [audioController isAudioRouteChangeable]) {
+        UIImage *speakerImage = [UIImage systemImageNamed:@"speaker.slash.fill"];
+        NSString *speakerActionTitle = NSLocalizedString(@"Disable speaker", nil);
+
+        if (![NCAudioController sharedInstance].isSpeakerActive) {
+            speakerImage = [UIImage systemImageNamed:@"speaker.wave.3.fill"];
+            speakerActionTitle = NSLocalizedString(@"Enable speaker", nil);
+        }
+
+        BOOL shouldShowAirPlayButton = audioController.numberOfAvailableInputs > 1;
+        if (shouldShowAirPlayButton) {
+            speakerImage = [UIImage systemImageNamed:@"airplayaudio"];
+            speakerActionTitle = NSLocalizedString(@"Audio options", nil);
+        }
+
+        void (^speakerBlock)(UIAction *action) = ^void(UIAction *action) {
+            [weakSelf speakerButtonPressed:nil];
+        };
+
+        void (^airplayBlock)(UIAction *action) = ^void(UIAction *action) {
+            __strong typeof(self) strongSelf = weakSelf;
+
+            for (id subview in strongSelf->_airplayView.subviews) {
+                if ([subview isKindOfClass:[UIButton class]]) {
+                    [subview sendActionsForControlEvents:UIControlEventTouchUpInside];
+                }
+            }
+        };
+
+        UIAction *speakerAction = [UIAction actionWithTitle:speakerActionTitle image:speakerImage identifier:nil handler:shouldShowAirPlayButton ? airplayBlock : speakerBlock];
+        [items addObject:speakerAction];
+    }
+
+    // Raise hand
+    if ([[NCDatabaseManager sharedInstance] serverHasTalkCapability:kCapabilityRaiseHand]) {
+        NSString *raiseHandTitel = NSLocalizedString(@"Raise hand", nil);
+
+        if (_isHandRaised) {
+            raiseHandTitel = NSLocalizedString(@"Lower hand", nil);
+        }
+
+        UIAction *raiseHandAction = [UIAction actionWithTitle:raiseHandTitel image:[UIImage systemImageNamed:@"hand.raised.fill"] identifier:nil handler:^(UIAction *action) {
+            __strong typeof(self) strongSelf = weakSelf;
+
+            [strongSelf->_callController raiseHand:!strongSelf->_isHandRaised];
+            strongSelf->_isHandRaised = !strongSelf->_isHandRaised;
+            [strongSelf adjustTopBar];
+        }];
+
+        [items addObject:raiseHandAction];
+    }
+
+    // Send a reaction
+    TalkAccount *activeAccount = [[NCDatabaseManager sharedInstance] activeAccount];
+    ServerCapabilities *serverCapabilities = [[NCDatabaseManager sharedInstance] serverCapabilitiesForAccountId:activeAccount.accountId];
+
+    if (serverCapabilities.callReactions.count > 0) {
+        NSMutableArray *reactionItems = [[NSMutableArray alloc] init];
+
+        for (NSString *reaction in serverCapabilities.callReactions) {
+            UIAction *reactionAction = [UIAction actionWithTitle:reaction image:nil identifier:nil handler:^(UIAction *action) {
+                __strong typeof(self) strongSelf = weakSelf;
+
+                [strongSelf->_callController sendReaction:reaction];
+                [strongSelf addReaction:reaction fromUser:activeAccount.userDisplayName];
+            }];
+
+            [reactionItems addObject:reactionAction];
+        }
+
+        UIMenu *reactionMenu;
+
+        if (@available(iOS 16.0, *)) {
+            NSInteger currentItemsCount = 0;
+            NSMutableArray *temporaryReactionItems = [[NSMutableArray alloc] init];
+            NSMutableArray *temporaryReactionMenus = [[NSMutableArray alloc] init];
+
+            for (UIAction *reactionAction in reactionItems) {
+                currentItemsCount += 1;
+
+                [temporaryReactionItems addObject:reactionAction];
+
+                if (currentItemsCount >= 2) {
+                    UIMenu *inlineReactionMenu = [UIMenu menuWithTitle:@""
+                                                                 image:nil
+                                                            identifier:nil
+                                                               options:UIMenuOptionsDisplayInline
+                                                              children:temporaryReactionItems];
+
+                    inlineReactionMenu.preferredElementSize = UIMenuElementSizeSmall;
+
+                    [temporaryReactionMenus addObject:inlineReactionMenu];
+                    temporaryReactionItems = [[NSMutableArray alloc] init];
+                    currentItemsCount = 0;
+                }
+            }
+
+            if (currentItemsCount > 0) {
+                UIMenu *inlineReactionMenu = [UIMenu menuWithTitle:@""
+                                                             image:nil
+                                                        identifier:nil
+                                                           options:UIMenuOptionsDisplayInline
+                                                          children:temporaryReactionItems];
+
+                inlineReactionMenu.preferredElementSize = UIMenuElementSizeSmall;
+
+                [temporaryReactionMenus addObject:inlineReactionMenu];
+            }
+
+            reactionMenu = [UIMenu menuWithTitle:NSLocalizedString(@"Send a reaction", nil)
+                                           image:[UIImage systemImageNamed:@"face.smiling"]
+                                      identifier:nil
+                                         options:0
+                                        children:temporaryReactionMenus];
+
+        } else {
+            // Show the menu as one long list on devices < iOS 16
+            reactionMenu = [UIMenu menuWithTitle:NSLocalizedString(@"Send a reaction", nil)
+                                           image:[UIImage systemImageNamed:@"face.smiling"]
+                                      identifier:nil
+                                         options:0
+                                        children:reactionItems];
+        }
+
+        [items addObject:reactionMenu];
+    }
+
+    // Start/Stop recording
+    if ([self->_room isUserOwnerOrModerator] && [[NCSettingsController sharedInstance] isRecordingEnabled]) {
+        UIImage *recordingImage = [UIImage systemImageNamed:@"record.circle.fill"];
+        NSString *recordingActionTitle = NSLocalizedString(@"Start recording", nil);
+
+        if ([self->_room callRecordingIsInActiveState]) {
+            recordingImage = [UIImage systemImageNamed:@"stop.circle.fill"];
+            recordingActionTitle = NSLocalizedString(@"Stop recording", nil);
+        }
+
+        UIAction *recordingAction = [UIAction actionWithTitle:recordingActionTitle image:recordingImage identifier:nil handler:^(UIAction *action) {
+            __strong typeof(self) strongSelf = weakSelf;
+
+            if ([strongSelf->_room callRecordingIsInActiveState]) {
+                [strongSelf showStopRecordingConfirmationDialog];
+            } else {
+                [strongSelf->_callController startRecording];
+            }
+        }];
+
+        [items addObject:recordingAction];
+    }
+
+    // Background blur
+    if (!_isAudioOnly) {
+        UIImage *blurActionImage = [UIImage systemImageNamed:@"person.crop.rectangle.fill"];
+        NSString *blurActionTitle = NSLocalizedString(@"Enable blur", nil);
+
+        if (@available(iOS 16.0, *)) {
+            blurActionImage = [UIImage systemImageNamed:@"person.and.background.dotted"];
+        }
+
+        if ([self->_callController isBackgroundBlurEnabled]) {
+            blurActionImage = [UIImage systemImageNamed:@"person.crop.rectangle"];
+            blurActionTitle = NSLocalizedString(@"Disable blur", nil);
+        }
+
+        UIAction *toggleBackgroundBlur = [UIAction actionWithTitle:blurActionTitle image:blurActionImage identifier:nil handler:^(UIAction *action) {
+            __strong typeof(self) strongSelf = weakSelf;
+            [strongSelf->_callController enableBackgroundBlur:![strongSelf->_callController isBackgroundBlurEnabled]];
+            [strongSelf adjustTopBar];
+        }];
+
+        [items addObject:toggleBackgroundBlur];
+    }
+
+    // Screensharing
+    UIImage *screensharingImage = [UIImage systemImageNamed:@"rectangle.inset.filled.on.rectangle"];
+    NSString *screensharingActionTitle = NSLocalizedString(@"Enable screensharing", nil);
+
+    if ([self->_callController screensharingActive]) {
+        screensharingImage = [UIImage systemImageNamed:@"rectangle.on.rectangle.slash"];
+        screensharingActionTitle = NSLocalizedString(@"Stop screensharing", nil);
+    }
+
+    UIAction *screenshareAction = [UIAction actionWithTitle:screensharingActionTitle image:screensharingImage identifier:nil handler:^(UIAction *action) {
+        [weakSelf showScreensharingPicker];
+    }];
+
+    [items addObject:screenshareAction];
+
+    self.moreMenuButton.menu = [UIMenu menuWithTitle:@"" children:items];
+}
+
+- (void)adjustSpeakerButton
+{
+    dispatch_async(dispatch_get_main_queue(), ^{
+        NCAudioController *audioController = [NCAudioController sharedInstance];
+        [self setSpeakerButtonActive:audioController.isSpeakerActive];
+
+        // If the visibility of the speaker button does not reflect the route changeability
+        // we need to try and adjust the top bar
+        if (self->_speakerButton.isHidden == [audioController isAudioRouteChangeable]) {
+            [self adjustTopBar];
+        }
+
+        // Show AirPlay button if there are more audio routes available
+        if (audioController.numberOfAvailableInputs > 1) {
+            [self setSpeakerButtonWithAirplayButton];
+        } else {
+            [self->_airplayView removeFromSuperview];
+        }
+    });
+}
+
+- (void)setDetailedViewTimer
+{
+    [self invalidateDetailedViewTimer];
+    _detailedViewTimer = [NSTimer scheduledTimerWithTimeInterval:5.0 target:self selector:@selector(hideDetailedView) userInfo:nil repeats:NO];
+}
+
+- (void)invalidateDetailedViewTimer
+{
+    [_detailedViewTimer invalidate];
+    _detailedViewTimer = nil;
+}
+
+- (void)presentJoinError:(NSString *)alertMessage
+{
+    NSString *alertTitle = [NSString stringWithFormat:NSLocalizedString(@"Could not join %@ call", nil), _room.displayName];
+    if (_room.type == kNCRoomTypeOneToOne) {
+        alertTitle = [NSString stringWithFormat:NSLocalizedString(@"Could not join call with %@", nil), _room.displayName];
+    }
+    
+    UIAlertController * alert = [UIAlertController alertControllerWithTitle:alertTitle
+                                                                    message:alertMessage
+                                                             preferredStyle:UIAlertControllerStyleAlert];
+    
+    UIAlertAction* okButton = [UIAlertAction actionWithTitle:NSLocalizedString(@"OK", nil)
+                                                       style:UIAlertActionStyleDefault
+                                                     handler:^(UIAlertAction * _Nonnull action) {
+                                                         [self hangupForAll:NO];
+                                                     }];
+    [alert addAction:okButton];
+    dispatch_async(dispatch_get_main_queue(), ^{
+        [self presentViewController:alert animated:YES completion:nil];
+    });
+}
+
+- (void)adjustLocalVideoPositionFromOriginPosition:(CGPoint)position
+{
+    UIEdgeInsets safeAreaInsets = _localVideoView.superview.safeAreaInsets;
+
+    CGFloat edgeInsetTop = 16 + _topBarView.frame.origin.y + _topBarView.frame.size.height;
+    CGFloat edgeInsetLeft = 16 + safeAreaInsets.left + _collectionViewLeftConstraint.constant;
+    CGFloat edgeInsetBottom = 16 + safeAreaInsets.bottom + _collectionViewBottomConstraint.constant;
+    CGFloat edgeInsetRight = 16 + safeAreaInsets.right + _collectionViewRightConstraint.constant;
+
+    UIEdgeInsets edgeInsets = UIEdgeInsetsMake(edgeInsetTop, edgeInsetLeft, edgeInsetBottom, edgeInsetRight);
+
+    CGSize parentSize = _localVideoView.superview.bounds.size;
+    CGSize viewSize = _localVideoView.bounds.size;
+
+    // Adjust left
+    if (position.x < edgeInsets.left) {
+        position = CGPointMake(edgeInsets.left, position.y);
+    }
+
+    // Adjust top
+    if (position.y < edgeInsets.top) {
+        position = CGPointMake(position.x, edgeInsets.top);
+    }
+
+    // Adjust right
+    if (position.x > parentSize.width - viewSize.width - edgeInsets.right) {
+        position = CGPointMake(parentSize.width - viewSize.width - edgeInsets.right, position.y);
+    }
+
+    // Adjust bottom
+    if (position.y > parentSize.height - viewSize.height - edgeInsets.bottom) {
+        position = CGPointMake(position.x, parentSize.height - viewSize.height - edgeInsets.bottom);
+    }
+
+    CGRect frame = _localVideoView.frame;
+    frame.origin.x = position.x;
+    frame.origin.y = position.y;
+
+    [UIView animateWithDuration:0.3 animations:^{
+        self->_localVideoView.frame = frame;
+    }];
+}
+
+- (void)localVideoDragged:(UIPanGestureRecognizer *)gesture
+{
+    if (gesture.view == _localVideoView) {
+        if (gesture.state == UIGestureRecognizerStateBegan) {
+            _localVideoDragStartingPosition = gesture.view.center;
+        } else if (gesture.state == UIGestureRecognizerStateChanged) {
+            CGPoint translation = [gesture translationInView:gesture.view];
+            _localVideoView.center = CGPointMake(_localVideoDragStartingPosition.x + translation.x, _localVideoDragStartingPosition.y + translation.y);
+        } else if (gesture.state == UIGestureRecognizerStateEnded) {
+            _localVideoOriginPosition = gesture.view.frame.origin;
+            [self adjustLocalVideoPositionFromOriginPosition:_localVideoOriginPosition];
+        }
+    }
+}
+
+- (void)callDurationTimerUpdate
+{
+    NSTimeInterval currentTimestamp = [[NSDate date] timeIntervalSince1970];
+
+    // In case we are the ones who start the call, we don't have the server-side callStartTime, so we set it locally
+    if (self.room.callStartTime == 0) {
+        self.room.callStartTime = currentTimestamp;
+    }
+
+    // Make sure that the remote callStartTime is not in the future
+    NSInteger callStartTime = MIN(self.room.callStartTime, currentTimestamp);
+
+    long callDuration = currentTimestamp - callStartTime;
+    long oneHourInSeconds = 60 * 60;
+
+    long hours = callDuration / 3600;
+    long minutes = (callDuration / 60) % 60;
+    long seconds = callDuration % 60;
+
+    if (hours > 0) {
+        [self.callTimeLabel setText:[NSString stringWithFormat:@"%lu:%02lu:%02lu", hours, minutes, seconds]];
+    } else {
+        [self.callTimeLabel setText:[NSString stringWithFormat:@"%02lu:%02lu", minutes, seconds]];
+    }
+
+    if (self->_topBarButtonStackView.frame.origin.x < 0) {
+        [self adjustTopBar];
+    }
+
+    if (callDuration == oneHourInSeconds) {
+        NSString *callRunningFor1h = NSLocalizedString(@"The call has been running for one hour", nil);
+        [[JDStatusBarNotificationPresenter sharedPresenter] presentWithText:callRunningFor1h dismissAfterDelay:7.0 includedStyle:JDStatusBarNotificationIncludedStyleDark];
+    }
+}
+
+#pragma mark - Call actions
+
+-(void)handlePushToTalk:(UILongPressGestureRecognizer *)gestureRecognizer
+{
+    if (gestureRecognizer.state == UIGestureRecognizerStateBegan) {
+        [self pushToTalkStart];
+    } else if (gestureRecognizer.state == UIGestureRecognizerStateEnded) {
+        [self pushToTalkEnd];
+    }
+}
+
+- (void)pushToTalkStart
+{
+    if (!_callController) {
+        return;
+    }
+
+    [_callController getAudioEnabledStateWithCompletionBlock:^(BOOL isEnabled) {
+        if (!isEnabled) {
+            [self setAudioMuted:NO];
+
+            dispatch_async(dispatch_get_main_queue(), ^{
+                [self->_buttonFeedbackGenerator impactOccurred];
+                self->_pushToTalkActive = YES;
+            });
+        }
+    }];
+}
+
+- (void)pushToTalkEnd
+{
+    if (_pushToTalkActive) {
+        [self setAudioMuted:YES];
+
+        _pushToTalkActive = NO;
+    }
+}
+
+- (IBAction)audioButtonPressed:(id)sender
+{
+    if (!_callController) {
+        return;
+    }
+
+    [_callController getAudioEnabledStateWithCompletionBlock:^(BOOL isEnabled) {
+        if ([CallKitManager isCallKitAvailable]) {
+            dispatch_async(dispatch_get_main_queue(), ^{
+                [[CallKitManager sharedInstance] changeAudioMuted:isEnabled forCall:self->_room.token];
+            });
+        } else {
+            [self setAudioMuted:isEnabled];
+        }
+    }];
+}
+
+- (void)forceMuteAudio
+{
+    if (!_callController) {
+        return;
+    }
+
+    [_callController getAudioEnabledStateWithCompletionBlock:^(BOOL isEnabled) {
+        if (!isEnabled) {
+            // We are already muted, no need to mute again
+            return;
+        }
+
+        [self setAudioMuted:YES];
+
+        NSString *micDisabledString = NSLocalizedString(@"Microphone disabled", nil);
+        NSString *forceMutedString = NSLocalizedString(@"You have been muted by a moderator", nil);
+
+        dispatch_async(dispatch_get_main_queue(), ^{
+            [[JDStatusBarNotificationPresenter sharedPresenter] presentWithTitle:micDisabledString subtitle:forceMutedString includedStyle:JDStatusBarNotificationIncludedStyleDark completion:nil];
+            [[JDStatusBarNotificationPresenter sharedPresenter] dismissAfterDelay:7.0];
+        });
+    }];
+}
+
+- (void)setAudioMuted:(BOOL)isMuted
+{
+    [_callController enableAudio:!isMuted];
+    [self setAudioMuteButtonActive:!isMuted];
+}
+
+- (IBAction)videoButtonPressed:(id)sender
+{
+    if (!_callController) {
+        return;
+    }
+    
+    [_callController getVideoEnabledStateWithCompletionBlock:^(BOOL isEnabled) {
+        [self setLocalVideoEnabled:!isEnabled];
+        self->_userDisabledVideo = isEnabled;
+    }];
+}
+
+- (void)disableLocalVideo
+{
+    [self setLocalVideoEnabled:NO];
+}
+
+- (void)enableLocalVideo
+{
+    [self setLocalVideoEnabled:YES];
+}
+
+- (void)setLocalVideoEnabled:(BOOL)enabled
+{
+    [_callController enableVideo:enabled];
+
+    [self setLocalVideoViewHidden:!enabled];
+    [self setVideoDisableButtonActive:enabled];
+}
+
+- (IBAction)switchCameraButtonPressed:(id)sender
+{
+    [_callController switchCamera];
+    [self flipLocalVideoView];
+}
+
+- (void)flipLocalVideoView
+{
+    CATransition *animation = [CATransition animation];
+    animation.duration = .5f;
+    animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
+    animation.type = @"oglFlip";
+    animation.subtype = kCATransitionFromRight;
+    
+    [self.localVideoView.layer addAnimation:animation forKey:nil];
+}
+
+- (IBAction)speakerButtonPressed:(id)sender
+{
+    if ([NCAudioController sharedInstance].isSpeakerActive) {
+        [self disableSpeaker];
+        _userDisabledSpeaker = YES;
+    } else {
+        [self enableSpeaker];
+        _userDisabledSpeaker = NO;
+    }
+
+    [self adjustMoreButtonMenu];
+}
+
+- (void)disableSpeaker
+{
+    [self setSpeakerButtonActive:NO];
+
+    [[WebRTCCommon shared] dispatch:^{
+        [[NCAudioController sharedInstance] setAudioSessionToVoiceChatMode];
+    }];
+}
+
+- (void)enableSpeaker
+{
+    [self setSpeakerButtonActive:YES];
+
+    [[WebRTCCommon shared] dispatch:^{
+        [[NCAudioController sharedInstance] setAudioSessionToVideoChatMode];
+    }];
+}
+
+- (void)setSpeakerButtonActive:(BOOL)active
+{
+    dispatch_async(dispatch_get_main_queue(), ^{
+        NSString *speakerStatusString = nil;
+
+        if (active) {
+            speakerStatusString = NSLocalizedString(@"Speaker enabled", nil);
+            [self.speakerButton setImage:[UIImage systemImageNamed:@"speaker.wave.3.fill" withConfiguration:self->_barButtonsConfiguration] forState:UIControlStateNormal];
+        } else {
+            speakerStatusString = NSLocalizedString(@"Speaker disabled", nil);
+            [self.speakerButton setImage:[UIImage systemImageNamed:@"speaker.slash.fill" withConfiguration:self->_barButtonsConfiguration] forState:UIControlStateNormal];
+        }
+
+        self.speakerButton.accessibilityValue = speakerStatusString;
+        self.speakerButton.accessibilityHint = NSLocalizedString(@"Double tap to enable or disable the speaker", nil);
+    });
+}
+
+- (void)setSpeakerButtonWithAirplayButton
+{
+    dispatch_async(dispatch_get_main_queue(), ^{
+        [self.speakerButton setImage:nil forState:UIControlStateNormal];
+        self.speakerButton.accessibilityValue = NSLocalizedString(@"AirPlay button", nil);
+        self.speakerButton.accessibilityHint = NSLocalizedString(@"Double tap to select different audio routes", nil);
+        [self.speakerButton addSubview:self->_airplayView];
+    });
+}
+
+- (IBAction)hangupButtonPressed:(id)sender
+{
+    [self hangupForAll:NO];
+}
+
+- (void)hangupForAll:(BOOL)allParticipants
+{
+    if (!_hangingUp) {
+        _hangingUp = YES;
+        // Dismiss possible notifications
+        [[NSNotificationCenter defaultCenter] removeObserver:self];
+
+        // Make sure we don't try to receive messages while hanging up
+        if (_chatViewController) {
+            [_chatViewController leaveChat];
+            _chatViewController = nil;
+        }
+
+        // Make sure there's no menu interfering with our dismissal
+        [self.moreMenuButton.contextMenuInteraction dismissMenu];
+        [self.hangUpButton.contextMenuInteraction dismissMenu];
+
+        [self.delegate callViewControllerWantsToBeDismissed:self];
+        
+        [_callController stopCapturing];
+        [_localVideoView setHidden:YES];
+
+        dispatch_async(dispatch_get_main_queue(), ^{
+            for (NCPeerConnection *peerConnection in self->_peersInCall) {
+                // Video renderers
+                RTCMTLVideoView *videoRenderer = [self->_videoRenderersDict objectForKey:peerConnection.peerIdentifier];
+                [self->_videoRenderersDict removeObjectForKey:peerConnection.peerIdentifier];
+
+                [[WebRTCCommon shared] dispatch:^{
+                    [[[peerConnection getRemoteStream].videoTracks firstObject] removeRenderer:videoRenderer];
+                }];
+            }
+
+            for (NCPeerConnection *peerConnection in self->_screenPeersInCall) {
+                // Screen renderers
+                RTCMTLVideoView *screenRenderer = [self->_screenRenderersDict objectForKey:peerConnection.peerId];
+                [self->_screenRenderersDict removeObjectForKey:peerConnection.peerId];
+
+                [[WebRTCCommon shared] dispatch:^{
+                    [[[peerConnection getRemoteStream].videoTracks firstObject] removeRenderer:screenRenderer];
+                }];
+            }
+
+            [self->_callDurationTimer invalidate];
+        });
+        
+        if (_callController) {
+            [_callController leaveCallForAll:allParticipants];
+        } else {
+            [self finishCall];
+        }
+    }
+}
+
+- (IBAction)videoCallButtonPressed:(id)sender
+{
+    [self showUpgradeToVideoCallDialog];
+}
+
+- (void)showUpgradeToVideoCallDialog
+{
+    UIAlertController *confirmDialog =
+    [UIAlertController alertControllerWithTitle:NSLocalizedString(@"Do you want to enable your camera?", nil)
+                                        message:NSLocalizedString(@"If you enable your camera, this call will be interrupted for a few seconds.", nil)
+                                 preferredStyle:UIAlertControllerStyleAlert];
+    UIAlertAction *confirmAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"Enable", nil) style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
+        [self upgradeToVideoCall];
+    }];
+    [confirmDialog addAction:confirmAction];
+    UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"Cancel", nil) style:UIAlertActionStyleCancel handler:nil];
+    [confirmDialog addAction:cancelAction];
+    [self presentViewController:confirmDialog animated:YES completion:nil];
+}
+
+- (void)upgradeToVideoCall
+{
+    _videoCallUpgrade = YES;
+    [self hangupForAll:NO];
+}
+
+- (IBAction)toggleChatButtonPressed:(id)sender
+{
+    [self toggleChatView];
+}
+
+- (CGFloat)getRightSideConstraintConstant
+{
+    CGFloat constant = 0;
+
+    if (self.sideBarWidthConstraint.constant > 0) {
+        // Take sidebar width into account
+        constant += self.sideBarWidthConstraint.constant;
+
+        // Add padding between the element and the sidebar
+        constant += 8;
+    }
+
+    if (self.traitCollection.horizontalSizeClass == UIUserInterfaceSizeClassRegular) {
+        // On regular size classes, we also have a padding of 8 to the safe area
+        constant += 8;
+    }
+
+    return constant;
+}
+
+- (void)setSideBarVisible:(BOOL)visible animated:(BOOL)animated withCompletion:(void (^ __nullable)(void))block
+{
+    [self.view layoutIfNeeded];
+
+    if (visible) {
+        [self.sideBarView setHidden:NO];
+        [self.sideBarWidthConstraint setConstant:kSidebarWidth];
+    } else {
+        [self.sideBarWidthConstraint setConstant:0];
+    }
+
+    CGFloat rightConstraintConstant = [self getRightSideConstraintConstant];
+    [self.topBarViewRightContraint setConstant:rightConstraintConstant];
+    [self.screenshareViewRightContraint setConstant:rightConstraintConstant];
+    [self.collectionViewRightConstraint setConstant:rightConstraintConstant];
+    [self adjustTopBar];
+
+    CGPoint localVideoViewOrigin = self.localVideoView.frame.origin;
+    // Check if localVideoView needs to be moved to the right when sidebar is being closed
+    if (!visible) {
+        CGFloat sideBarWidthGap = self.collectionView.frame.size.width - kSidebarWidth;
+        if (localVideoViewOrigin.x > sideBarWidthGap) {
+            localVideoViewOrigin.x = self.localVideoView.superview.frame.size.width;
+        }
+    }
+
+    void (^animations)(void) = ^void() {
+        [self.titleView layoutIfNeeded];
+        [self.view layoutIfNeeded];
+        [self adjustLocalVideoPositionFromOriginPosition:localVideoViewOrigin];
+    };
+
+    void (^afterAnimations)(void) = ^void() {
+        if (!visible) {
+            [self.sideBarView setHidden:YES];
+        }
+
+        if (block) {
+            block();
+        }
+    };
+
+    if (animated) {
+        [UIView animateWithDuration:0.3f animations:^{
+            animations();
+        } completion:^(BOOL finished) {
+            afterAnimations();
+        }];
+    } else {
+        animations();
+        afterAnimations();
+    }
+}
+
+- (void)adjustChatLocation
+{
+    if (!_chatNavigationController) {
+        return;
+    }
+
+    if (self.traitCollection.horizontalSizeClass == UIUserInterfaceSizeClassCompact && [_chatNavigationController.view isDescendantOfView:_sideBarView]) {
+        // Chat is displayed in the sidebar, but needs to move to full screen
+
+        // Remove chat from the sidebar and add to call view
+        [_chatNavigationController.view removeFromSuperview];
+        [self.view addSubview:_chatNavigationController.view];
+
+        // Show the navigationbar in case of fullscreen and adjust the frame
+        [_chatNavigationController setNavigationBarHidden:NO];
+        _chatNavigationController.view.frame = self.view.bounds;
+
+        // Finally hide the sidebar
+        [self setSideBarVisible:NO animated:NO withCompletion:nil];
+    } else if (self.traitCollection.horizontalSizeClass == UIUserInterfaceSizeClassRegular && [_chatNavigationController.view isDescendantOfView:self.view]) {
+        // Chat is fullscreen, but should move to the sidebar
+
+        // Remove chat from the call view and move it to the sidebar
+        [_chatNavigationController.view removeFromSuperview];
+        [self.sideBarView addSubview:_chatNavigationController.view];
+
+        // Show the sidebar to have the correct bounds
+        [self setSideBarVisible:YES animated:NO withCompletion:nil];
+
+        CGRect sideBarViewBounds = self.sideBarView.bounds;
+        _chatNavigationController.view.frame = CGRectMake(sideBarViewBounds.origin.x, sideBarViewBounds.origin.y, kSidebarWidth, sideBarViewBounds.size.height);
+
+        // Don't show the navigation bar when we show the chat in the sidebar
+        [_chatNavigationController setNavigationBarHidden:YES];
+    }
+}
+
+- (void)showChat
+{
+    if (!_chatNavigationController) {
+        // Create new chat controller
+        TalkAccount *activeAccount = [[NCDatabaseManager sharedInstance] activeAccount];
+        NCRoom *room = [[NCDatabaseManager sharedInstance] roomWithToken:_room.token forAccountId:activeAccount.accountId];
+        _chatViewController = [[ChatViewController alloc] initFor:room];
+        _chatViewController.presentedInCall = YES;
+        _chatNavigationController = [[UINavigationController alloc] initWithRootViewController:_chatViewController];
+    }
+
+    [self addChildViewController:_chatNavigationController];
+
+    if (self.traitCollection.horizontalSizeClass == UIUserInterfaceSizeClassCompact) {
+        // Show chat fullscreen
+        [self.view addSubview:_chatNavigationController.view];
+
+        _chatNavigationController.view.frame = self.view.bounds;
+        _chatNavigationController.view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
+    } else {
+        // Show chat in sidebar
+
+        [self.sideBarView addSubview:_chatNavigationController.view];
+
+        CGRect sideBarViewBounds = self.sideBarView.bounds;
+        _chatNavigationController.view.frame = CGRectMake(sideBarViewBounds.origin.x, sideBarViewBounds.origin.y, kSidebarWidth, sideBarViewBounds.size.height);
+
+        // Make sure the width does not change when collapsing the side bar (weird animation)
+        _chatNavigationController.view.autoresizingMask = UIViewAutoresizingFlexibleHeight;
+
+        [_chatNavigationController setNavigationBarHidden:YES];
+
+        __weak typeof(self) weakSelf = self;
+
+        [self setSideBarVisible:YES animated:YES withCompletion:^{
+            __strong typeof(self) strongSelf = weakSelf;
+
+            strongSelf->_chatNavigationController.view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
+        }];
+    }
+
+    [_chatNavigationController didMoveToParentViewController:self];
+}
+
+- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection
+{
+    if (!_chatNavigationController) {
+        return;
+    }
+
+    if (previousTraitCollection.horizontalSizeClass != self.traitCollection.horizontalSizeClass) {
+        // Need to adjust the position of the chat, either sidebar -> fullscreen or fullscreen -> sidebar
+        [self adjustChatLocation];
+    }
+}
+
+- (void)toggleChatView
+{
+    if (!_chatNavigationController) {
+        [self showChat];
+
+        if (!_isAudioOnly) {
+            [self.view bringSubviewToFront:_localVideoView];
+        }
+
+        [self removeTapGestureForDetailedView];
+    } else {
+        [self.view layoutIfNeeded];
+
+        // Make sure we have a nice animation when closing the side bar and the chat is not squished
+        _chatNavigationController.view.autoresizingMask = UIViewAutoresizingFlexibleHeight;
+
+        __weak typeof(self) weakSelf = self;
+
+        [self setSideBarVisible:NO animated:YES withCompletion:^{
+            __strong typeof(self) strongSelf = weakSelf;
+
+            [strongSelf->_chatViewController leaveChat];
+            strongSelf->_chatViewController = nil;
+
+            [strongSelf->_chatNavigationController willMoveToParentViewController:nil];
+            [strongSelf->_chatNavigationController.view removeFromSuperview];
+            [strongSelf->_chatNavigationController removeFromParentViewController];
+
+            strongSelf->_chatNavigationController = nil;
+
+            if (!strongSelf->_isAudioOnly && strongSelf->_callState == CallStateInCall) {
+                [strongSelf addTapGestureForDetailedView];
+                [strongSelf showDetailedViewWithTimer];
+            }
+        }];
+    }
+}
+
+- (void)finishCall
+{
+    _callController = nil;
+    if (_videoCallUpgrade) {
+        _videoCallUpgrade = NO;
+        [self.delegate callViewControllerWantsVideoCallUpgrade:self];
+    } else {
+        [self.delegate callViewControllerDidFinish:self];
+    }
+}
+
+- (IBAction)lowerHandButtonPressed:(id)sender
+{
+    self->_isHandRaised = NO;
+    [self->_callController raiseHand:NO];
+    [self adjustTopBar];
+}
+
+- (IBAction)videoRecordingButtonPressed:(id)sender
+{
+    if (![_room canModerate]) {
+        NSString *notificationText = NSLocalizedString(@"This call is being recorded", nil);
+        [[JDStatusBarNotificationPresenter sharedPresenter] presentWithText:notificationText dismissAfterDelay:7.0 includedStyle:JDStatusBarNotificationIncludedStyleDark];
+        
+        return;
+    }
+
+    [self showStopRecordingConfirmationDialog];
+}
+
+- (void)showStopRecordingConfirmationDialog
+{
+    UIAlertController *confirmDialog =
+    [UIAlertController alertControllerWithTitle:NSLocalizedString(@"Stop recording", nil)
+                                        message:NSLocalizedString(@"Do you want to stop the recording?", nil)
+                                 preferredStyle:UIAlertControllerStyleAlert];
+
+    UIAlertAction *stopAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"Stop", @"Action to 'Stop' a recording") style:UIAlertActionStyleDestructive handler:^(UIAlertAction * _Nonnull action) {
+        [self->_callController stopRecording];
+    }];
+    [confirmDialog addAction:stopAction];
+
+    UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"Cancel", nil) style:UIAlertActionStyleCancel handler:nil];
+    [confirmDialog addAction:cancelAction];
+
+    [self presentViewController:confirmDialog animated:YES completion:nil];
+}
+
+#pragma mark - Call Reactions
+
+- (void)addReaction:(NSString *)reaction fromUser:(NSString *)user
+{
+    CallReactionView *callReactionView = [[CallReactionView alloc] initWithFrame:CGRectZero];
+    [callReactionView setReactionWithReaction:reaction actor:user];
+
+    // Schedule when to show reaction
+    CGFloat delayBetweenReactions = kReactionViewAnimationDuration / kMaxReactionsInScreen;
+    CGFloat now = [[NSDate date] timeIntervalSince1970];
+
+    if (_lastScheduledReaction < now) {
+        delayBetweenReactions = (now - _lastScheduledReaction > delayBetweenReactions) ? 0 : delayBetweenReactions;
+        _lastScheduledReaction = now;
+    }
+
+    _lastScheduledReaction += delayBetweenReactions;
+
+    __weak typeof(self) weakSelf = self;
+    CGFloat delay = _lastScheduledReaction - now;
+    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), dispatch_get_main_queue(), ^(void){
+        [weakSelf showReaction:callReactionView];
+    });
+}
+
+- (void)showReaction:(CallReactionView *)callReactionView
+{
+    CGSize callViewSize = self.view.bounds.size;
+    CGSize callReactionSize = [callReactionView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
+
+    CGFloat minLeftPosition = callViewSize.width * 0.05;
+    CGFloat maxLeftPosition = callViewSize.width * 0.2;
+    CGFloat randomLeftPosition = minLeftPosition + arc4random_uniform(maxLeftPosition - minLeftPosition + 1);
+
+    CGFloat startPosition = callViewSize.height - self.view.safeAreaInsets.bottom - callReactionSize.height;
+    CGFloat minTopPosition = startPosition / 2;
+    CGFloat maxTopPosition = minTopPosition * 1.2;
+    CGFloat randomTopPosition = minTopPosition + arc4random_uniform(maxTopPosition - minTopPosition + 1);
+
+    if (callViewSize.width - callReactionSize.width < 0) {
+        randomLeftPosition = minLeftPosition;
+    }
+
+    CGRect reactionInitialPosition = CGRectMake(randomLeftPosition,
+                                                startPosition,
+                                                callReactionSize.width,
+                                                callReactionSize.height);
+
+    callReactionView.frame = reactionInitialPosition;
+
+    [self.view addSubview:callReactionView];
+    [self.view bringSubviewToFront:callReactionView];
+
+    [UIView animateWithDuration:2.0 animations:^{
+        callReactionView.frame = CGRectMake(reactionInitialPosition.origin.x,
+                                            randomTopPosition,
+                                            reactionInitialPosition.size.width,
+                                            reactionInitialPosition.size.height);
+    }];
+
+    [UIView animateWithDuration:1.0 delay:1.0 options:0 animations:^{
+        callReactionView.alpha = 0;
+    } completion:^(BOOL finished) {
+        [callReactionView removeFromSuperview];
+    }];
+}
+
+#pragma mark - CallParticipantViewCell delegate
+
+- (void)cellWantsToPresentScreenSharing:(CallParticipantViewCell *)participantCell
+{
+    NCPeerConnection *peerConnection = [self peerConnectionForPeerIdentifier:participantCell.peerIdentifier];
+    [self showScreenOfPeer:peerConnection];
+}
+
+- (void)cellWantsToChangeZoom:(CallParticipantViewCell *)participantCell showOriginalSize:(BOOL)showOriginalSize
+{
+    NCPeerConnection *peer = [self peerConnectionForPeerIdentifier:participantCell.peerIdentifier];
+    
+    if (peer) {
+        [peer setShowRemoteVideoInOriginalSize:showOriginalSize];
+    }
+}
+
+#pragma mark - UICollectionView Datasource
+
+- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
+{
+    [self setCallStateForPeersInCall];
+    return [_peersInCall count];
+}
+
+- (void)updateParticipantCell:(CallParticipantViewCell *)cell withPeerConnection:(NCPeerConnection *)peerConnection
+{
+    BOOL isVideoDisabled = peerConnection.isRemoteVideoDisabled;
+
+    if (_isAudioOnly || ![peerConnection hasRemoteStream]) {
+        isVideoDisabled = YES;
+    }
+    
+    RTCMTLVideoView *videoView = [_videoRenderersDict objectForKey:peerConnection.peerIdentifier];
+    [cell setVideoView:videoView];
+
+    // It is possible that we receive a `didChangeVideoSize` call, while the participant cell was not yet shown,
+    // therefore the remote video size will never be set. In case we have a videoView here, use the frame size
+    if (videoView) {
+        CGSize videoSize = videoView.frame.size;
+        CGSize currentSize = [cell getRemoteVideoSize];
+
+        // Only set it, when there's no size set yet
+        if (CGSizeEqualToSize(CGSizeZero, currentSize) && !CGSizeEqualToSize(CGSizeZero, videoSize)) {
+            [cell setRemoteVideoSize:videoView.frame.size];
+        }
+    }
+
+    [cell setDisplayName:peerConnection.peerName];
+    [cell setAudioDisabled:peerConnection.isRemoteAudioDisabled];
+    [cell setScreenShared:[_screenRenderersDict objectForKey:peerConnection.peerId]];
+    [cell setVideoDisabled: isVideoDisabled];
+    [cell setShowOriginalSize:peerConnection.showRemoteVideoInOriginalSize];
+    [cell setRaiseHand:peerConnection.isHandRaised];
+    [cell.peerNameLabel setAlpha:_isDetailedViewVisible ? 1.0 : 0.0];
+    [cell.audioOffIndicator setAlpha:_isDetailedViewVisible ? 1.0 : 0.0];
+
+    [[WebRTCCommon shared] dispatch:^{
+        TalkActor *actor = [self->_callController getActorFromSessionId:peerConnection.peerId];
+
+        if ([actor.rawDisplayName isEqualToString:@""] && peerConnection.peerName && ![peerConnection.peerName isEqualToString:@""]) {
+            actor.rawDisplayName = peerConnection.peerName;
+        }
+
+        RTCIceConnectionState connectionState = peerConnection.isDummyPeer ?
+        RTCIceConnectionStateConnected : [peerConnection getPeerConnection].iceConnectionState;
+
+        dispatch_async(dispatch_get_main_queue(), ^{
+            [cell setAvatarForActor:actor];
+            [cell setConnectionState:connectionState];
+        });
+    }];
+}
+
+- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
+{
+    CallParticipantViewCell *cell = (CallParticipantViewCell *)[collectionView dequeueReusableCellWithReuseIdentifier:kCallParticipantCellIdentifier forIndexPath:indexPath];
+    NCPeerConnection *peerConnection = [_peersInCall objectAtIndex:indexPath.row];
+    cell.peerIdentifier = peerConnection.peerIdentifier;
+    cell.actionsDelegate = self;
+        
+    return cell;
+}
+
+-(void)collectionView:(UICollectionView *)collectionView willDisplayCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath
+{
+    CallParticipantViewCell *participantCell = (CallParticipantViewCell *)cell;
+    NCPeerConnection *peerConnection = [_peersInCall objectAtIndex:indexPath.row];
+    
+    [self updateParticipantCell:participantCell withPeerConnection:peerConnection];
+}
+
+#pragma mark - Call Controller delegate
+
+- (void)callControllerDidJoinCall:(NCCallController *)callController
+{
+    [self setCallStateForPeersInCall];
+
+    // Show chat if it was visible before room switch
+    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^(void){
+        if (self->_showChatAfterRoomSwitch && !self->_chatViewController) {
+            self->_showChatAfterRoomSwitch = NO;
+            [self toggleChatView];
+        }
+    });
+}
+
+- (void)callControllerDidFailedJoiningCall:(NCCallController *)callController statusCode:(NSInteger)statusCode errorReason:(NSString *) errorReason
+{
+    dispatch_async(dispatch_get_main_queue(), ^{
+        BOOL isAppActive = [[UIApplication sharedApplication] applicationState] == UIApplicationStateActive;
+
+        if (isAppActive) {
+            [self presentJoinError:errorReason];
+        } else {
+            [[CallKitManager sharedInstance] endCall:self->_room.token withStatusCode:statusCode];
+        }
+    });
+}
+
+- (void)callControllerDidEndCall:(NCCallController *)callController
+{
+    [self finishCall];
+}
+
+- (void)callController:(NCCallController *)callController peerJoined:(NCPeerConnection *)peer
+{
+    // Always add a joined peer, even if the peer doesn't publish any streams (yet)
+    [self addPeer:peer];
+}
+
+- (void)callController:(NCCallController *)callController peerLeft:(NCPeerConnection *)peer
+{
+    [self removePeer:peer];
+}
+
+- (void)callController:(NCCallController *)callController didCreateCameraController:(NCCameraController *)cameraController
+{
+    dispatch_async(dispatch_get_main_queue(), ^{
+        cameraController.localView = self->_localVideoView;
+    });
+}
+
+- (void)callControllerDidDrawFirstLocalFrame:(NCCallController *)callController
+{
+    [_callController getVideoEnabledStateWithCompletionBlock:^(BOOL isEnabled) {
+        dispatch_async(dispatch_get_main_queue(), ^{
+            [self setLocalVideoViewHidden:!isEnabled];
+        });
+    }];
+}
+
+- (void)callController:(NCCallController *)callController userPermissionsChanged:(NSInteger)permissions
+{
+    [self setAudioMuteButtonEnabled:(permissions & NCPermissionCanPublishAudio) && [callController isMicrophoneAccessAvailable]];
+    [self setVideoDisableButtonEnabled:((permissions & NCPermissionCanPublishVideo) && [callController isCameraAccessAvailable])];
+}
+
+- (void)callController:(NCCallController *)callController didCreateLocalAudioTrack:(RTCAudioTrack *)audioTrack
+{
+    if (!audioTrack) {
+        // No audio track was created, probably because there are no publishing rights or microphone access was denied
+        [self setAudioMuteButtonEnabled:NO];
+        [self setAudioMuteButtonActive:NO];
+
+        return;
+    }
+
+    [self setAudioMuteButtonActive:audioTrack.isEnabled];
+}
+
+- (void)callController:(NCCallController *)callController didCreateLocalVideoTrack:(RTCVideoTrack *)videoTrack
+{
+    if (!videoTrack && !self->_isAudioOnly) {
+        // No video track was created, probably because there are no publishing rights or camera access was denied
+        [self setVideoDisableButtonEnabled:NO];
+        [self setVideoDisableButtonActive:NO];
+        _userDisabledVideo = YES;
+
+        return;
+    }
+
+    [self setVideoDisableButtonActive:videoTrack.isEnabled];
+
+    // We set _userDisabledVideo = YES so the proximity sensor doesn't enable it.
+    if (!videoTrack.isEnabled) {
+        _userDisabledVideo = YES;
+    }
+}
+
+- (void)callController:(NCCallController *)callController didAddStream:(RTCMediaStream *)remoteStream ofPeer:(NCPeerConnection *)remotePeer
+{
+    [[WebRTCCommon shared] assertQueue];
+
+    dispatch_async(dispatch_get_main_queue(), ^{
+        RTCMTLVideoView *renderView = [[RTCMTLVideoView alloc] initWithFrame:CGRectZero];
+
+        [[WebRTCCommon shared] dispatch:^{
+            RTCVideoTrack *remoteVideoTrack = [[remotePeer getRemoteStream].videoTracks firstObject];
+            renderView.delegate = self;
+            [remoteVideoTrack addRenderer:renderView];
+        }];
+
+        if ([remotePeer.roomType isEqualToString:kRoomTypeVideo]) {
+            [self->_videoRenderersDict setObject:renderView forKey:remotePeer.peerIdentifier];
+            NSIndexPath *indexPath = [self indexPathForPeerIdentifier:remotePeer.peerIdentifier];
+
+            if (!indexPath) {
+                // This is a new peer, add it
+
+                [self addPeer:remotePeer];
+            } else {
+                // This peer already exists in the collection view, so we can just update its cell
+
+                BOOL isVideoDisabled = (self->_isAudioOnly || remotePeer.isRemoteVideoDisabled);
+
+                [self updatePeer:remotePeer block:^(CallParticipantViewCell *cell) {
+                    [cell setVideoView:renderView];
+                    [cell setVideoDisabled:isVideoDisabled];
+                }];
+            }
+        } else if ([remotePeer.roomType isEqualToString:kRoomTypeScreen]) {
+            [self->_screenRenderersDict setObject:renderView forKey:remotePeer.peerId];
+            [self->_screenPeersInCall addObject:remotePeer];
+            [self showScreenOfPeer:remotePeer];
+            [self updatePeer:remotePeer block:^(CallParticipantViewCell *cell) {
+                [cell setScreenShared:YES];
+            }];
+        }
+    });
+}
+
+- (void)callController:(NCCallController *)callController didRemoveStream:(RTCMediaStream *)remoteStream ofPeer:(NCPeerConnection *)remotePeer
+{
+    
+}
+
+- (void)callController:(NCCallController *)callController iceStatusChanged:(RTCIceConnectionState)state ofPeer:(NCPeerConnection *)peer
+{
+    if (state == RTCIceConnectionStateClosed) {
+        dispatch_async(dispatch_get_main_queue(), ^{
+            if ([peer.roomType isEqualToString:kRoomTypeVideo]) {
+                [self removePeer:peer];
+            } else if ([peer.roomType isEqualToString:kRoomTypeScreen]) {
+                [self removeScreensharingOfPeer:peer];
+            }
+        });
+    } else if ([peer.roomType isEqualToString:kRoomTypeVideo]) {
+        [self updatePeer:peer block:^(CallParticipantViewCell *cell) {
+            [cell setConnectionState:state];
+        }];
+    }
+}
+
+- (void)callController:(NCCallController *)callController didAddDataChannel:(RTCDataChannel *)dataChannel
+{
+}
+
+- (void)callController:(NCCallController *)callController didReceiveDataChannelMessage:(NSString *)message fromPeer:(NCPeerConnection *)peer
+{
+    if ([message isEqualToString:@"audioOn"] || [message isEqualToString:@"audioOff"]) {
+        [self updatePeer:peer block:^(CallParticipantViewCell *cell) {
+            [cell setAudioDisabled:peer.isRemoteAudioDisabled];
+        }];
+    } else if ([message isEqualToString:@"videoOn"] || [message isEqualToString:@"videoOff"]) {
+        if (!_isAudioOnly) {
+            [self updatePeer:peer block:^(CallParticipantViewCell *cell) {
+                [cell setVideoDisabled:peer.isRemoteVideoDisabled];
+            }];
+        }
+    } else if ([message isEqualToString:@"speaking"] || [message isEqualToString:@"stoppedSpeaking"]) {
+        if ([_peersInCall count] > 1) {
+            [self updatePeer:peer block:^(CallParticipantViewCell *cell) {
+                [cell setSpeaking:peer.isPeerSpeaking];
+            }];
+        }
+    } else if ([message isEqualToString:@"raiseHand"]) {
+        [self updatePeer:peer block:^(CallParticipantViewCell *cell) {
+            [cell setRaiseHand:peer.isHandRaised];
+        }];
+    }
+}
+
+- (void)callController:(NCCallController *)callController didReceiveNick:(NSString *)nick fromPeer:(NCPeerConnection *)peer
+{
+    [self updatePeer:peer block:^(CallParticipantViewCell *cell) {
+        [cell setDisplayName:nick];
+    }];
+
+    if ([peer.peerId isEqualToString:_presentedScreenPeerId]) {
+        dispatch_async(dispatch_get_main_queue(), ^{
+            [self->_screenshareLabel setText:nick];
+        });
+    }
+}
+
+- (void)callController:(NCCallController *)callController didReceiveUnshareScreenFromPeer:(NCPeerConnection *)peer
+{
+    [self removeScreensharingOfPeer:peer];
+}
+
+- (void)callController:(NCCallController *)callController didReceiveForceMuteActionForPeerId:(NSString *)peerId
+{
+    if ([peerId isEqualToString:callController.signalingSessionId]) {
+        [self forceMuteAudio];
+    } else {
+        NSLog(@"Peer was force muted: %@", peerId);
+    }
+}
+
+- (void)callController:(NCCallController *)callController didReceiveReaction:(NSString *)reaction fromPeer:(NCPeerConnection *)peer
+{
+    dispatch_async(dispatch_get_main_queue(), ^{
+        if (reaction.length == 0) {
+            return;
+        }
+        NSString *user = peer.peerName;
+        if (user.length == 0) {
+            user = NSLocalizedString(@"Guest", nil);
+        }
+
+        [self addReaction:reaction fromUser:user];
+    });
+}
+
+- (void)callControllerIsReconnectingCall:(NCCallController *)callController
+{
+    dispatch_async(dispatch_get_main_queue(), ^{
+        // Cancel any pending operations
+        _pendingPeerInserts = [[NSMutableArray alloc] init];
+        _pendingPeerDeletions = [[NSMutableArray alloc] init];
+        _pendingPeerUpdates = [[NSMutableArray alloc] init];
+        _peersInCall = [[NSMutableArray alloc] init];
+
+        // Reset a potential queued batch update
+        [self->_batchUpdateTimer invalidate];
+        self->_batchUpdateTimer = nil;
+
+        // Force the collectionView to reload all data
+        [self.collectionView reloadData];
+        [self.collectionView.collectionViewLayout invalidateLayout];
+        [self.collectionView layoutSubviews];
+
+        [self setCallState:CallStateReconnecting];
+    });
+}
+
+- (void)callControllerWantsToHangUpCall:(NCCallController *)callController
+{
+    dispatch_async(dispatch_get_main_queue(), ^{
+        [self hangupForAll:NO];
+    });
+}
+
+- (void)callControllerDidChangeRecording:(NCCallController *)callController
+{
+    [self adjustTopBar];
+
+    dispatch_async(dispatch_get_main_queue(), ^{
+        NSString *notificationText = NSLocalizedString(@"Call recording stopped", nil);
+
+        if (self->_room.callRecording == NCCallRecordingStateVideoStarting || self->_room.callRecording == NCCallRecordingStateAudioStarting) {
+            notificationText = NSLocalizedString(@"Call recording is starting", nil);
+        } else if (self->_room.callRecording == NCCallRecordingStateVideoRunning || self->_room.callRecording == NCCallRecordingStateAudioRunning) {
+            notificationText = NSLocalizedString(@"Call recording started", nil);
+        } else if (self->_room.callRecording == NCCallRecordingStateFailed && self->_room.isUserOwnerOrModerator) {
+            notificationText = NSLocalizedString(@"Call recording failed. Please contact your administrator", nil);
+        }
+
+        [[JDStatusBarNotificationPresenter sharedPresenter] presentWithText:notificationText dismissAfterDelay:7.0 includedStyle:JDStatusBarNotificationIncludedStyleDark];
+    });
+}
+
+- (void)callControllerDidChangeScreenrecording:(NCCallController *)callController
+{
+    [self adjustTopBar];
+}
+
+- (void)callController:(NCCallController *)callController isSwitchingToCall:(NSString *)token withAudioEnabled:(BOOL)audioEnabled andVideoEnabled:(BOOL)videoEnabled
+{
+    [self setCallState:CallStateSwitchingToAnotherRoom];
+
+    // Close chat before switching to another room
+    if (_chatViewController) {
+        _showChatAfterRoomSwitch = YES;
+        [self toggleChatView];
+    }
+
+    // Connect to new call
+    TalkAccount *activeAccount = [[NCDatabaseManager sharedInstance] activeAccount];
+    [[NCRoomsManager sharedInstance] updateRoom:token withCompletionBlock:^(NSDictionary *roomDict, NSError *error) {
+        if (error) {
+            NSLog(@"Error getting room to switch");
+            return;
+        }
+        // Prepare rooms manager to switch to another room
+        [[NCRoomsManager sharedInstance] prepareSwitchToAnotherRoomFromRoom:self->_room.token withCompletionBlock:^(NSError *error) {
+            // Notify callkit about room switch
+            [self.delegate callViewController:self wantsToSwitchCallFromCall:self->_room.token toRoom:token];
+            // Assign new room as current room
+            self->_room = [NCRoom roomWithDictionary:roomDict andAccountId:activeAccount.accountId];
+            // Save current audio and video state
+            self->_audioDisabledAtStart = !audioEnabled;
+            self->_videoDisabledAtStart = !videoEnabled;
+            // Forget current call controller
+            self->_callController = nil;
+            // Join new room
+            [[NCRoomsManager sharedInstance] joinRoom:token forCall:YES];
+        }];
+    }];
+}
+
+#pragma mark - Screensharing
+
+- (void)showScreenOfPeer:(NCPeerConnection *)peer
+{
+    dispatch_async(dispatch_get_main_queue(), ^{
+        RTCMTLVideoView *renderView = [self->_screenRenderersDict objectForKey:peer.peerId];
+
+        [self->_screensharingView replaceContentView:renderView];
+        [self->_screensharingView bringSubviewToFront:self->_closeScreensharingButton];
+
+        // The screenPeer does not have a name associated to it, try to get the nonScreenPeer
+        NCPeerConnection *nonScreenPeer = [self peerConnectionForPeerId:peer.peerId];
+        NSString *peerDisplayName = nonScreenPeer.peerName;
+        if (!peerDisplayName || [peerDisplayName isKindOfClass:[NSNull class]] || [peerDisplayName isEqualToString:@""]) {
+            peerDisplayName = NSLocalizedString(@"Guest", nil);
+        }
+
+        self->_presentedScreenPeerId = peer.peerId;
+        [self->_screenshareLabel setText:peerDisplayName];
+        [self->_screensharingView bringSubviewToFront:self->_screenshareLabelContainer];
+
+        [UIView transitionWithView:self->_screensharingView duration:0.4
+                           options:UIViewAnimationOptionTransitionCrossDissolve
+                        animations:^{self->_screensharingView.hidden = NO;}
+                        completion:nil];
+    });
+
+    // Enable/Disable detailed view with tap gesture
+    // in voice only call when screensharing is enabled
+    if (_isAudioOnly) {
+        [self addTapGestureForDetailedView];
+        [self showDetailedViewWithTimer];
+    }
+}
+
+- (void)removeScreensharingOfPeer:(NCPeerConnection *)peer
+{
+    dispatch_async(dispatch_get_main_queue(), ^{
+        RTCMTLVideoView *screenRenderer = [self->_screenRenderersDict objectForKey:peer.peerId];
+        NCPeerConnection *screenPeerConnection = [self screenPeerConnectionForPeerId:peer.peerId];
+        self->_presentedScreenPeerId = nil;
+        [self->_screenRenderersDict removeObjectForKey:peer.peerId];
+        [self updatePeer:screenPeerConnection block:^(CallParticipantViewCell *cell) {
+            [cell setScreenShared:NO];
+        }];
+
+        if (self->_screensharingView.contentView == screenRenderer) {
+            [self closeScreensharingButtonPressed:self];
+        }
+
+        [[WebRTCCommon shared] dispatch:^{
+            [[[screenPeerConnection getRemoteStream].videoTracks firstObject] removeRenderer:screenRenderer];
+        }];
+
+        [_screenPeersInCall removeObject:screenPeerConnection];
+    });
+}
+
+- (IBAction)closeScreensharingButtonPressed:(id)sender
+{
+    dispatch_async(dispatch_get_main_queue(), ^{
+        [UIView transitionWithView:self->_screensharingView duration:0.4
+                           options:UIViewAnimationOptionTransitionCrossDissolve
+                        animations:^{self->_screensharingView.hidden = YES;}
+                        completion:nil];
+    });
+
+    // Back to normal voice only UI
+    if (_isAudioOnly) {
+        [self invalidateDetailedViewTimer];
+        [self showDetailedView];
+        [self removeTapGestureForDetailedView];
+    }
+}
+
+#pragma mark - GestureDelegate
+
+- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
+{
+    return YES;
+}
+
+#pragma mark - RTCVideoViewDelegate
+
+- (void)videoView:(RTCMTLVideoView*)videoView didChangeVideoSize:(CGSize)size
+{
+    dispatch_async(dispatch_get_main_queue(), ^{
+        for (RTCMTLVideoView *rendererView in [self->_videoRenderersDict allValues]) {
+            if ([videoView isEqual:rendererView]) {
+                rendererView.frame = CGRectMake(0, 0, size.width, size.height);
+                NSArray *keys = [self->_videoRenderersDict allKeysForObject:videoView];
+                if (keys.count) {
+                    NSIndexPath *indexPath = [self indexPathForPeerIdentifier:keys[0]];
+                    if (indexPath) {
+                        CallParticipantViewCell *participantCell = (CallParticipantViewCell *) [self.collectionView cellForItemAtIndexPath:indexPath];
+                        [participantCell setRemoteVideoSize:size];
+                    }
+                }
+            }
+        }
+        for (RTCMTLVideoView *rendererView in [self->_screenRenderersDict allValues]) {
+            if ([videoView isEqual:rendererView]) {
+                rendererView.frame = CGRectMake(0, 0, size.width, size.height);
+                if ([self.screensharingView.contentView isEqual:rendererView]) {
+                    self->_screensharingSize = rendererView.frame.size;
+                    [self->_screensharingView setContentViewSize:rendererView.frame.size];
+                    [self->_screensharingView resizeContentView];
+                }
+            }
+        }
+    });
+}
+
+#pragma mark - Cell updates
+
+- (NSIndexPath *)indexPathForPeerIdentifier:(NSString *)peerIdentifier
+{
+    NSIndexPath *indexPath = nil;
+    for (int i = 0; i < _peersInCall.count; i ++) {
+        NCPeerConnection *peer = [_peersInCall objectAtIndex:i];
+        if ([peer.peerIdentifier isEqualToString:peerIdentifier]) {
+            indexPath = [NSIndexPath indexPathForRow:i inSection:0];
+        }
+    }
+    
+    return indexPath;
+}
+
+- (NSIndexPath *)indexPathForPeerId:(NSString *)peerId
+{
+    NSIndexPath *indexPath = nil;
+    for (int i = 0; i < _peersInCall.count; i ++) {
+        NCPeerConnection *peer = [_peersInCall objectAtIndex:i];
+        if ([peer.peerId isEqualToString:peerId]) {
+            indexPath = [NSIndexPath indexPathForRow:i inSection:0];
+        }
+    }
+
+    return indexPath;
+}
+
+- (void)updatePeer:(NCPeerConnection *)peer block:(UpdateCallParticipantViewCellBlock)block
+{
+    dispatch_async(dispatch_get_main_queue(), ^{
+        NSIndexPath *indexPath = [self indexPathForPeerId:peer.peerId];
+        if (indexPath) {
+            CallParticipantViewCell *cell = (id)[self.collectionView cellForItemAtIndexPath:indexPath];
+            block(cell);
+        } else {
+            // The participant might not be added at this point -> delay the update
+
+            PendingCellUpdate *pendingUpdate = [[PendingCellUpdate alloc] init];
+            pendingUpdate.peer = peer;
+            pendingUpdate.block = block;
+
+            [self->_pendingPeerUpdates addObject:pendingUpdate];
+        }
+    });
+}
+
+- (NCPeerConnection *)peerConnectionForPeerIdentifier:(NSString *)peerIdentifier {
+    for (NCPeerConnection *peerConnection in self->_peersInCall) {
+        if ([peerConnection.peerIdentifier isEqualToString:peerIdentifier]) {
+            return peerConnection;
+        }
+    }
+
+    for (NCPeerConnection *peerConnection in self->_screenPeersInCall) {
+        if ([peerConnection.peerIdentifier isEqualToString:peerIdentifier]) {
+            return peerConnection;
+        }
+    }
+    
+    return nil;
+}
+
+- (NCPeerConnection *)peerConnectionForPeerId:(NSString *)peerId {
+    for (NCPeerConnection *peerConnection in self->_peersInCall) {
+        if ([peerConnection.peerId isEqualToString:peerId]) {
+            return peerConnection;
+        }
+    }
+
+    return nil;
+}
+
+- (NCPeerConnection *)screenPeerConnectionForPeerId:(NSString *)peerId {
+    for (NCPeerConnection *peerConnection in self->_screenPeersInCall) {
+        if ([peerConnection.peerId isEqualToString:peerId]) {
+            return peerConnection;
+        }
+    }
+
+    return nil;
+}
+
+- (void)showPeersInfo
+{
+    dispatch_async(dispatch_get_main_queue(), ^{
+        NSArray *visibleCells = [self->_collectionView visibleCells];
+        for (CallParticipantViewCell *cell in visibleCells) {
+            [UIView animateWithDuration:0.3f animations:^{
+                [cell.peerNameLabel setAlpha:1.0f];
+                [cell.audioOffIndicator setAlpha:1.0f];
+                [cell layoutIfNeeded];
+            }];
+        }
+    });
+}
+
+- (void)hidePeersInfo
+{
+    dispatch_async(dispatch_get_main_queue(), ^{
+        NSArray *visibleCells = [self->_collectionView visibleCells];
+        for (CallParticipantViewCell *cell in visibleCells) {
+            [UIView animateWithDuration:0.3f animations:^{
+                // Don't hide raise hand indicator, that should always be visible
+                [cell.peerNameLabel setAlpha:0.0f];
+                [cell.audioOffIndicator setAlpha:0.0f];
+                [cell layoutIfNeeded];
+            }];
+        }
+    });
+}
+
+- (void)addPeer:(NCPeerConnection *)peer
+{
+    dispatch_async(dispatch_get_main_queue(), ^{
+        if (self->_peersInCall.count == 0) {
+            // Don't delay adding the first peer
+
+            [self->_peersInCall addObject:peer];
+            NSIndexPath *insertionIndexPath = [NSIndexPath indexPathForRow:0 inSection:0];
+            [self.collectionView insertItemsAtIndexPaths:@[insertionIndexPath]];
+        } else {
+            // Delay updating the collection view a bit to allow batch updating
+
+            [self->_pendingPeerInserts addObject:peer];
+            [self scheduleBatchCollectionViewUpdate];
+        }
+    });
+}
+
+- (void)removePeer:(NCPeerConnection *)peer
+{
+    dispatch_async(dispatch_get_main_queue(), ^{
+        if ([self->_pendingPeerInserts containsObject:peer]) {
+            // The peer is a pending insert, but was removed before the batch update
+            // In this case we can just remove the pending insert
+            [self->_pendingPeerInserts removeObject:peer];
+        } else {
+            [self->_pendingPeerDeletions addObject:peer];
+            [self scheduleBatchCollectionViewUpdate];
+        }
+    });
+}
+
+- (void)scheduleBatchCollectionViewUpdate
+{
+    // Make sure to call this only from the main queue
+
+    if (self->_batchUpdateTimer == nil) {
+        self->_batchUpdateTimer = [NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:@selector(batchCollectionViewUpdate) userInfo:nil repeats:NO];
+    }
+}
+
+- (void)batchCollectionViewUpdate
+{
+    self->_batchUpdateTimer = nil;
+
+    if (_pendingPeerInserts.count == 0 && _pendingPeerDeletions.count == 0) {
+        return;
+    }
+
+    [_collectionView performBatchUpdates:^{
+        // Perform deletes before inserts according to apples docs
+        NSMutableArray *indexPathsToDelete = [[NSMutableArray alloc] init];
+
+        // Determine all indexPaths we want to delete and remove the renderers
+        for (NCPeerConnection *peer in _pendingPeerDeletions) {
+            // Video renderers
+            RTCMTLVideoView *videoRenderer = [self->_videoRenderersDict objectForKey:peer.peerIdentifier];
+            [self->_videoRenderersDict removeObjectForKey:peer.peerIdentifier];
+
+            [[WebRTCCommon shared] dispatch:^{
+                [[[peer getRemoteStream].videoTracks firstObject] removeRenderer:videoRenderer];
+            }];
+
+            NSIndexPath *indexPath = [self indexPathForPeerIdentifier:peer.peerIdentifier];
+
+            // Make sure we remove every index path only once
+            if (indexPath && ![indexPathsToDelete containsObject:indexPath]) {
+                [indexPathsToDelete addObject:indexPath];
+            }
+        }
+
+        // Deletes should be done in descending order
+        NSSortDescriptor *rowSortDescending = [[NSSortDescriptor alloc] initWithKey:@"row" ascending:NO];
+        NSArray *indexPathsToDeleteSorted = [indexPathsToDelete sortedArrayUsingDescriptors:@[rowSortDescending]];
+
+        for (NSIndexPath *indexPath in indexPathsToDeleteSorted) {
+            [self->_peersInCall removeObjectAtIndex:indexPath.row];
+            [_collectionView deleteItemsAtIndexPaths:@[indexPath]];
+        }
+
+        // Add all new peers
+        for (NCPeerConnection *peer in _pendingPeerInserts) {
+            NSIndexPath *indexPath = [self indexPathForPeerIdentifier:peer.peerIdentifier];
+            if (!indexPath) {
+                [self->_peersInCall addObject:peer];
+                NSIndexPath *insertionIndexPath = [NSIndexPath indexPathForRow:self->_peersInCall.count - 1 inSection:0];
+                [self.collectionView insertItemsAtIndexPaths:@[insertionIndexPath]];
+            }
+        }
+
+        // Process pending updates
+        for (PendingCellUpdate *pendingUpdate in _pendingPeerUpdates) {
+            [self updatePeer:pendingUpdate.peer block:pendingUpdate.block];
+        }
+
+        _pendingPeerInserts = [[NSMutableArray alloc] init];
+        _pendingPeerDeletions = [[NSMutableArray alloc] init];
+        _pendingPeerUpdates = [[NSMutableArray alloc] init];
+    } completion:^(BOOL finished) {
+
+    }];
+}
+
+#pragma mark - NCChatTitleViewDelegate
+
+- (void)chatTitleViewTapped:(NCChatTitleView *)titleView
+{
+    RoomInfoTableViewController *roomInfoVC = [[RoomInfoTableViewController alloc] initForRoom:_room];
+    roomInfoVC.hideDestructiveActions = YES;
+
+    roomInfoVC.modalPresentationStyle = UIModalPresentationPageSheet;
+    UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:roomInfoVC];
+    [self presentViewController:navController animated:YES completion:nil];
+}
+
+
+@end

+ 406 - 0
NextcloudTalk/CallViewController.xib

@@ -0,0 +1,406 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="32700.99.1234" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
+    <device id="ipad12_9rounded" orientation="portrait" layout="fullscreen" appearance="light"/>
+    <dependencies>
+        <deployment identifier="iOS"/>
+        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22685"/>
+        <capability name="Safe area layout guides" minToolsVersion="9.0"/>
+        <capability name="System colors in document resources" minToolsVersion="11.0"/>
+        <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
+    </dependencies>
+    <objects>
+        <placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="CallViewController">
+            <connections>
+                <outlet property="audioMuteButton" destination="kDR-Ds-I7B" id="9NJ-iO-JMY"/>
+                <outlet property="avatarBackgroundImageView" destination="H1v-6g-V21" id="ASI-wy-zPa"/>
+                <outlet property="callTimeLabel" destination="0Nb-8F-OJ2" id="uzu-FJ-A0H"/>
+                <outlet property="closeScreensharingButton" destination="N0N-Ny-Aeg" id="NrD-JV-Hec"/>
+                <outlet property="collectionView" destination="aUh-Z0-hO6" id="jmc-BV-dTa"/>
+                <outlet property="collectionViewBottomConstraint" destination="TOU-Pk-3pi" id="R4U-x4-psd"/>
+                <outlet property="collectionViewLeftConstraint" destination="XPT-Wv-kZY" id="daX-Mt-ctm"/>
+                <outlet property="collectionViewRightConstraint" destination="MON-Nu-TFX" id="9Gf-Mr-91m"/>
+                <outlet property="hangUpButton" destination="Rl8-bS-FJ5" id="jNg-Ly-6wz"/>
+                <outlet property="localVideoView" destination="TXj-7E-NAa" id="nXn-uK-PDD"/>
+                <outlet property="lowerHandButton" destination="RpQ-sH-1qf" id="g5K-YY-Va1"/>
+                <outlet property="moreMenuButton" destination="Gjp-FF-fc2" id="qXV-GI-Uuu"/>
+                <outlet property="participantsLabel" destination="jKC-M1-4EZ" id="NIv-kp-5HO"/>
+                <outlet property="participantsLabelContainer" destination="cRK-6e-FFN" id="lRs-B4-zOS"/>
+                <outlet property="recordingButton" destination="aQX-QQ-eLT" id="Jh5-wa-IyR"/>
+                <outlet property="screenshareLabel" destination="jS8-GI-SJT" id="l5i-yo-wU9"/>
+                <outlet property="screenshareLabelContainer" destination="o1l-BI-9Qm" id="w1I-1t-qFm"/>
+                <outlet property="screenshareViewRightContraint" destination="IS9-3G-cQz" id="QGY-es-LX7"/>
+                <outlet property="screensharingView" destination="Zzc-Pq-hMC" id="kaT-G4-IxS"/>
+                <outlet property="sideBarView" destination="fSY-ZT-xIC" id="4UK-JE-3Md"/>
+                <outlet property="sideBarViewBottomConstraint" destination="cnF-xe-Mjs" id="3zi-x2-dCx"/>
+                <outlet property="sideBarViewRightConstraint" destination="grQ-FV-QY8" id="dTS-zj-Qdo"/>
+                <outlet property="sideBarWidthConstraint" destination="8kc-f3-P81" id="D7w-hK-6Hk"/>
+                <outlet property="speakerButton" destination="dgz-bL-PRr" id="WdG-PS-8Qa"/>
+                <outlet property="stackViewToTitleViewConstraint" destination="uUY-lE-iwc" id="RG2-12-HAc"/>
+                <outlet property="switchCameraButton" destination="Mf3-yk-Olo" id="bJn-Fu-bqF"/>
+                <outlet property="titleView" destination="NY0-IT-mMr" id="TTN-Ws-hgl"/>
+                <outlet property="toggleChatButton" destination="vBI-Mz-P4p" id="PVu-uW-pOJ"/>
+                <outlet property="topBarButtonStackView" destination="FHx-ks-wEy" id="dZG-YF-m0q"/>
+                <outlet property="topBarView" destination="reh-cY-1Qv" id="rBs-Qj-M1A"/>
+                <outlet property="topBarViewRightContraint" destination="8ND-zt-NSC" id="iqG-dd-cZy"/>
+                <outlet property="videoCallButton" destination="YDj-MO-jIc" id="RwW-bN-mi1"/>
+                <outlet property="videoDisableButton" destination="5zQ-it-ujU" id="n3u-2y-uqi"/>
+                <outlet property="view" destination="i5M-Pr-FkT" id="sfx-zR-JGt"/>
+                <outlet property="waitingLabel" destination="ihe-9I-8ts" id="0Ur-3H-2PC"/>
+                <outlet property="waitingView" destination="BF4-kz-lxP" id="vLf-wm-y8K"/>
+            </connections>
+        </placeholder>
+        <placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
+        <view clearsContextBeforeDrawing="NO" contentMode="scaleToFill" id="i5M-Pr-FkT" userLabel="CallView">
+            <rect key="frame" x="0.0" y="0.0" width="1024" height="1366"/>
+            <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+            <subviews>
+                <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="reh-cY-1Qv" userLabel="TopBar">
+                    <rect key="frame" x="0.0" y="0.0" width="1024" height="88"/>
+                    <subviews>
+                        <button opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="1000" horizontalCompressionResistancePriority="1000" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="tailTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Rl8-bS-FJ5" userLabel="HangupButton">
+                            <rect key="frame" x="915" y="32" width="48" height="48"/>
+                            <color key="backgroundColor" systemColor="systemRedColor"/>
+                            <constraints>
+                                <constraint firstAttribute="width" relation="greaterThanOrEqual" constant="48" id="aZ2-oa-X4M"/>
+                            </constraints>
+                            <fontDescription key="fontDescription" type="system" weight="semibold" pointSize="16"/>
+                            <color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+                            <state key="normal" image="phone.down.fill" catalog="system">
+                                <preferredSymbolConfiguration key="preferredSymbolConfiguration" configurationType="pointSize" pointSize="20"/>
+                            </state>
+                            <connections>
+                                <action selector="hangupButtonPressed:" destination="-1" eventType="touchUpInside" id="S0I-zJ-AFf"/>
+                            </connections>
+                        </button>
+                        <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="sX5-HH-Bl6" userLabel="Separator">
+                            <rect key="frame" x="971" y="40" width="1" height="32"/>
+                            <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+                            <constraints>
+                                <constraint firstAttribute="height" constant="32" id="BU7-Cl-egd"/>
+                                <constraint firstAttribute="width" constant="1" id="SAl-Ea-9sk"/>
+                            </constraints>
+                            <fontDescription key="fontDescription" type="system" pointSize="17"/>
+                            <nil key="textColor"/>
+                            <nil key="highlightedColor"/>
+                        </label>
+                        <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="vBI-Mz-P4p" userLabel="ToggleChatButton">
+                            <rect key="frame" x="976" y="28" width="44" height="56"/>
+                            <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+                            <constraints>
+                                <constraint firstAttribute="width" constant="44" id="9DJ-Sj-p8R"/>
+                            </constraints>
+                            <color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+                            <state key="normal" image="bubble.left.fill" catalog="system">
+                                <preferredSymbolConfiguration key="preferredSymbolConfiguration" configurationType="pointSize" pointSize="20"/>
+                            </state>
+                            <connections>
+                                <action selector="toggleChatButtonPressed:" destination="-1" eventType="touchUpInside" id="zJP-fn-GWH"/>
+                            </connections>
+                        </button>
+                        <stackView opaque="NO" contentMode="scaleToFill" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="FHx-ks-wEy">
+                            <rect key="frame" x="496.5" y="28" width="410.5" height="56"/>
+                            <subviews>
+                                <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" horizontalCompressionResistancePriority="1000" text="00:00" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="0Nb-8F-OJ2" userLabel="CallTimeLabel">
+                                    <rect key="frame" x="0.0" y="0.0" width="46.5" height="56"/>
+                                    <fontDescription key="fontDescription" type="system" weight="semibold" pointSize="16"/>
+                                    <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+                                    <nil key="highlightedColor"/>
+                                </label>
+                                <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="aQX-QQ-eLT" userLabel="RecordingButton">
+                                    <rect key="frame" x="54.5" y="0.0" width="44" height="56"/>
+                                    <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+                                    <constraints>
+                                        <constraint firstAttribute="width" constant="44" id="q2u-LZ-1pc"/>
+                                    </constraints>
+                                    <color key="tintColor" systemColor="systemRedColor"/>
+                                    <state key="normal" image="record.circle.fill" catalog="system">
+                                        <preferredSymbolConfiguration key="preferredSymbolConfiguration" configurationType="pointSize" pointSize="20"/>
+                                    </state>
+                                    <connections>
+                                        <action selector="videoRecordingButtonPressed:" destination="-1" eventType="touchUpInside" id="Bx0-Nb-pcQ"/>
+                                    </connections>
+                                </button>
+                                <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="kDR-Ds-I7B" userLabel="AudioButton">
+                                    <rect key="frame" x="106.5" y="0.0" width="44" height="56"/>
+                                    <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+                                    <constraints>
+                                        <constraint firstAttribute="width" constant="44" id="Ar2-um-gQl"/>
+                                    </constraints>
+                                    <color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+                                    <state key="normal" image="mic.fill" catalog="system">
+                                        <preferredSymbolConfiguration key="preferredSymbolConfiguration" configurationType="pointSize" pointSize="20"/>
+                                    </state>
+                                    <connections>
+                                        <action selector="audioButtonPressed:" destination="-1" eventType="touchUpInside" id="pXz-DO-93v"/>
+                                    </connections>
+                                </button>
+                                <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="5zQ-it-ujU" userLabel="VideoDisableButton">
+                                    <rect key="frame" x="158.5" y="0.0" width="44" height="56"/>
+                                    <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+                                    <constraints>
+                                        <constraint firstAttribute="width" constant="44" id="sGL-hj-uAT"/>
+                                    </constraints>
+                                    <color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+                                    <state key="normal" image="video.fill" catalog="system">
+                                        <preferredSymbolConfiguration key="preferredSymbolConfiguration" configurationType="pointSize" pointSize="20"/>
+                                    </state>
+                                    <connections>
+                                        <action selector="videoButtonPressed:" destination="-1" eventType="touchUpInside" id="5Q5-4w-o5q"/>
+                                    </connections>
+                                </button>
+                                <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="YDj-MO-jIc" userLabel="VideoCallButton">
+                                    <rect key="frame" x="210.5" y="0.0" width="44" height="56"/>
+                                    <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+                                    <constraints>
+                                        <constraint firstAttribute="width" constant="44" id="350-WN-M1a"/>
+                                    </constraints>
+                                    <color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+                                    <state key="normal" image="video.slash.fill" catalog="system">
+                                        <preferredSymbolConfiguration key="preferredSymbolConfiguration" configurationType="pointSize" pointSize="20"/>
+                                    </state>
+                                    <connections>
+                                        <action selector="videoCallButtonPressed:" destination="-1" eventType="touchUpInside" id="YxF-Ew-WqM"/>
+                                    </connections>
+                                </button>
+                                <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="dgz-bL-PRr" userLabel="SpeakerButton">
+                                    <rect key="frame" x="262.5" y="0.0" width="44" height="56"/>
+                                    <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+                                    <constraints>
+                                        <constraint firstAttribute="width" constant="44" id="tg2-YD-INe"/>
+                                    </constraints>
+                                    <color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+                                    <state key="normal" image="speaker.slash.fill" catalog="system">
+                                        <preferredSymbolConfiguration key="preferredSymbolConfiguration" configurationType="pointSize" pointSize="20"/>
+                                    </state>
+                                    <connections>
+                                        <action selector="speakerButtonPressed:" destination="-1" eventType="touchUpInside" id="PPl-wW-y2T"/>
+                                    </connections>
+                                </button>
+                                <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="RpQ-sH-1qf" userLabel="LowerHandButton">
+                                    <rect key="frame" x="314.5" y="0.0" width="44" height="56"/>
+                                    <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+                                    <constraints>
+                                        <constraint firstAttribute="width" constant="44" id="77m-11-7t8"/>
+                                    </constraints>
+                                    <color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+                                    <state key="normal" image="hand.raised.fill" catalog="system">
+                                        <preferredSymbolConfiguration key="preferredSymbolConfiguration" configurationType="pointSize" pointSize="20"/>
+                                    </state>
+                                    <connections>
+                                        <action selector="lowerHandButtonPressed:" destination="-1" eventType="touchUpInside" id="vrz-Ss-1iM"/>
+                                    </connections>
+                                </button>
+                                <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Gjp-FF-fc2" userLabel="MoreButton">
+                                    <rect key="frame" x="366.5" y="0.0" width="44" height="56"/>
+                                    <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+                                    <constraints>
+                                        <constraint firstAttribute="width" constant="44" id="yxW-nz-Toe"/>
+                                    </constraints>
+                                    <color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+                                    <state key="normal" image="ellipsis" catalog="system">
+                                        <preferredSymbolConfiguration key="preferredSymbolConfiguration" configurationType="pointSize" pointSize="20"/>
+                                    </state>
+                                </button>
+                            </subviews>
+                        </stackView>
+                        <view contentMode="left" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="NY0-IT-mMr" userLabel="TitleView" customClass="NCChatTitleView">
+                            <rect key="frame" x="8" y="32" width="480.5" height="48"/>
+                            <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+                            <constraints>
+                                <constraint firstAttribute="height" constant="48" id="TeG-Qy-adB"/>
+                                <constraint firstAttribute="width" relation="greaterThanOrEqual" id="eXn-Ro-wDA"/>
+                            </constraints>
+                            <variation key="default">
+                                <mask key="constraints">
+                                    <exclude reference="eXn-Ro-wDA"/>
+                                </mask>
+                            </variation>
+                        </view>
+                    </subviews>
+                    <viewLayoutGuide key="safeArea" id="U6B-V9-PR8"/>
+                    <color key="backgroundColor" white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+                    <constraints>
+                        <constraint firstItem="vBI-Mz-P4p" firstAttribute="top" secondItem="U6B-V9-PR8" secondAttribute="top" constant="4" id="0QU-Sz-S3Y"/>
+                        <constraint firstItem="Rl8-bS-FJ5" firstAttribute="leading" secondItem="FHx-ks-wEy" secondAttribute="trailing" constant="8" id="2FZ-aE-UFv"/>
+                        <constraint firstItem="NY0-IT-mMr" firstAttribute="leading" secondItem="reh-cY-1Qv" secondAttribute="leading" constant="8" id="Fg4-8S-ufG"/>
+                        <constraint firstItem="vBI-Mz-P4p" firstAttribute="leading" secondItem="sX5-HH-Bl6" secondAttribute="trailing" constant="4" id="Hwz-5s-8r3"/>
+                        <constraint firstItem="NY0-IT-mMr" firstAttribute="centerY" secondItem="reh-cY-1Qv" secondAttribute="centerYWithinMargins" id="Km8-3x-pGw"/>
+                        <constraint firstItem="U6B-V9-PR8" firstAttribute="bottom" secondItem="FHx-ks-wEy" secondAttribute="bottom" constant="4" id="Ml0-KX-CA3"/>
+                        <constraint firstItem="FHx-ks-wEy" firstAttribute="top" secondItem="U6B-V9-PR8" secondAttribute="top" constant="4" id="RS5-Em-m6g"/>
+                        <constraint firstItem="sX5-HH-Bl6" firstAttribute="top" secondItem="U6B-V9-PR8" secondAttribute="top" constant="16" id="U0s-29-FWP"/>
+                        <constraint firstItem="U6B-V9-PR8" firstAttribute="bottom" secondItem="Rl8-bS-FJ5" secondAttribute="bottom" constant="8" id="XVa-1k-FRv"/>
+                        <constraint firstItem="Rl8-bS-FJ5" firstAttribute="top" secondItem="U6B-V9-PR8" secondAttribute="top" constant="8" id="cCq-ee-a3F"/>
+                        <constraint firstItem="U6B-V9-PR8" firstAttribute="bottom" secondItem="vBI-Mz-P4p" secondAttribute="bottom" constant="4" id="e6u-T7-4Mi"/>
+                        <constraint firstItem="sX5-HH-Bl6" firstAttribute="leading" secondItem="Rl8-bS-FJ5" secondAttribute="trailing" constant="8" id="rU0-Bc-0MT"/>
+                        <constraint firstItem="FHx-ks-wEy" firstAttribute="leading" secondItem="NY0-IT-mMr" secondAttribute="trailing" constant="8" id="uUY-lE-iwc"/>
+                        <constraint firstItem="U6B-V9-PR8" firstAttribute="trailing" secondItem="vBI-Mz-P4p" secondAttribute="trailing" constant="4" id="yGf-lp-FfM"/>
+                    </constraints>
+                </view>
+                <collectionView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" dataMode="none" translatesAutoresizingMaskIntoConstraints="NO" id="aUh-Z0-hO6">
+                    <rect key="frame" x="0.0" y="88" width="1024" height="1258"/>
+                    <color key="backgroundColor" white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+                    <collectionViewFlowLayout key="collectionViewLayout" minimumLineSpacing="0.0" minimumInteritemSpacing="0.0" sectionInsetReference="safeArea" id="iSf-tu-VMU" customClass="CallFlowLayout" customModule="NextcloudTalk" customModuleProvider="target">
+                        <size key="itemSize" width="50" height="50"/>
+                        <size key="headerReferenceSize" width="0.0" height="0.0"/>
+                        <size key="footerReferenceSize" width="0.0" height="0.0"/>
+                        <inset key="sectionInset" minX="0.0" minY="0.0" maxX="0.0" maxY="0.0"/>
+                    </collectionViewFlowLayout>
+                    <connections>
+                        <outlet property="dataSource" destination="-1" id="tQD-40-kmq"/>
+                        <outlet property="delegate" destination="-1" id="rTW-Ir-7cH"/>
+                    </connections>
+                </collectionView>
+                <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="cRK-6e-FFN" userLabel="ParticipantsLabelContainer">
+                    <rect key="frame" x="8" y="96" width="34.5" height="35.5"/>
+                    <subviews>
+                        <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="0" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="jKC-M1-4EZ" userLabel="ParticipantsLabel">
+                            <rect key="frame" x="12" y="8" width="10.5" height="19.5"/>
+                            <constraints>
+                                <constraint firstAttribute="width" relation="greaterThanOrEqual" id="bvS-tX-y9s"/>
+                            </constraints>
+                            <fontDescription key="fontDescription" type="system" weight="semibold" pointSize="16"/>
+                            <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+                            <nil key="highlightedColor"/>
+                        </label>
+                    </subviews>
+                    <color key="backgroundColor" red="0.1175515647" green="0.1187154416" blue="0.1187154416" alpha="0.80000000000000004" colorSpace="custom" customColorSpace="displayP3"/>
+                    <constraints>
+                        <constraint firstAttribute="bottom" secondItem="jKC-M1-4EZ" secondAttribute="bottom" constant="8" id="2X5-nV-O2N"/>
+                        <constraint firstItem="jKC-M1-4EZ" firstAttribute="top" secondItem="cRK-6e-FFN" secondAttribute="top" constant="8" id="F3Z-XM-Igo"/>
+                        <constraint firstItem="jKC-M1-4EZ" firstAttribute="leading" secondItem="cRK-6e-FFN" secondAttribute="leading" constant="12" id="mb0-zM-Fqk"/>
+                        <constraint firstAttribute="trailing" secondItem="jKC-M1-4EZ" secondAttribute="trailing" constant="12" id="zhd-Ya-rRk"/>
+                    </constraints>
+                </view>
+                <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Zzc-Pq-hMC" userLabel="ScreenshareView" customClass="NCZoomableView" customModule="NextcloudTalk" customModuleProvider="target">
+                    <rect key="frame" x="0.0" y="88" width="1024" height="1258"/>
+                    <subviews>
+                        <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="N0N-Ny-Aeg" userLabel="CloseScreenshareView">
+                            <rect key="frame" x="980" y="12" width="32" height="32"/>
+                            <color key="backgroundColor" red="0.11764705882352941" green="0.11764705882352941" blue="0.11764705882352941" alpha="0.80088553523385764" colorSpace="custom" customColorSpace="displayP3"/>
+                            <constraints>
+                                <constraint firstAttribute="height" constant="32" id="uMi-V0-B3H"/>
+                                <constraint firstAttribute="width" constant="32" id="xCZ-QS-yDb"/>
+                            </constraints>
+                            <color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+                            <state key="normal" image="xmark" catalog="system"/>
+                            <connections>
+                                <action selector="closeScreensharingButtonPressed:" destination="-1" eventType="touchUpInside" id="Pio-Dr-GaB"/>
+                            </connections>
+                        </button>
+                        <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="o1l-BI-9Qm" userLabel="ScreenshareLabelContainer">
+                            <rect key="frame" x="480" y="1216" width="64.5" height="34"/>
+                            <subviews>
+                                <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="User" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="jS8-GI-SJT" userLabel="ScreenshareLabel">
+                                    <rect key="frame" x="16" y="8" width="32.5" height="18"/>
+                                    <constraints>
+                                        <constraint firstAttribute="width" relation="greaterThanOrEqual" id="fl4-VY-OBo"/>
+                                    </constraints>
+                                    <fontDescription key="fontDescription" style="UICTFontTextStyleSubhead"/>
+                                    <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+                                    <nil key="highlightedColor"/>
+                                </label>
+                            </subviews>
+                            <color key="backgroundColor" red="0.1175515647" green="0.1187154416" blue="0.1187154416" alpha="0.80000000000000004" colorSpace="custom" customColorSpace="displayP3"/>
+                            <constraints>
+                                <constraint firstItem="jS8-GI-SJT" firstAttribute="leading" secondItem="o1l-BI-9Qm" secondAttribute="leading" constant="16" id="L7w-HG-xVj"/>
+                                <constraint firstAttribute="bottom" secondItem="jS8-GI-SJT" secondAttribute="bottom" constant="8" id="TMQ-6H-QNh"/>
+                                <constraint firstAttribute="trailing" secondItem="jS8-GI-SJT" secondAttribute="trailing" constant="16" id="dnd-lP-phb"/>
+                                <constraint firstItem="jS8-GI-SJT" firstAttribute="top" secondItem="o1l-BI-9Qm" secondAttribute="top" constant="8" id="jNW-9o-AJP"/>
+                            </constraints>
+                        </view>
+                    </subviews>
+                    <viewLayoutGuide key="safeArea" id="j53-tu-e0T"/>
+                    <color key="backgroundColor" white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+                    <constraints>
+                        <constraint firstItem="j53-tu-e0T" firstAttribute="trailing" secondItem="N0N-Ny-Aeg" secondAttribute="trailing" constant="12" id="Pxi-mX-3dC"/>
+                        <constraint firstItem="j53-tu-e0T" firstAttribute="bottom" secondItem="o1l-BI-9Qm" secondAttribute="bottom" constant="8" id="bNY-pf-A4h"/>
+                        <constraint firstItem="N0N-Ny-Aeg" firstAttribute="top" secondItem="j53-tu-e0T" secondAttribute="top" constant="12" id="pDR-Pt-2K8"/>
+                        <constraint firstItem="o1l-BI-9Qm" firstAttribute="centerX" secondItem="Zzc-Pq-hMC" secondAttribute="centerX" id="yHO-ZB-x8k"/>
+                    </constraints>
+                </view>
+                <view contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="TXj-7E-NAa" userLabel="LocalVideo" customClass="MTKView">
+                    <rect key="frame" x="16" y="80" width="90" height="120"/>
+                    <autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxX="YES" heightSizable="YES" flexibleMaxY="YES"/>
+                    <subviews>
+                        <button opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Mf3-yk-Olo">
+                            <rect key="frame" x="25" y="80" width="40" height="40"/>
+                            <autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMinY="YES"/>
+                            <color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+                            <state key="normal" image="camera.rotate" catalog="system"/>
+                            <connections>
+                                <action selector="switchCameraButtonPressed:" destination="-1" eventType="touchUpInside" id="SJb-T1-tkb"/>
+                            </connections>
+                        </button>
+                    </subviews>
+                </view>
+                <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="fSY-ZT-xIC" userLabel="Sidebar View">
+                    <rect key="frame" x="666" y="32" width="350" height="1306"/>
+                    <color key="backgroundColor" systemColor="systemBackgroundColor"/>
+                    <constraints>
+                        <constraint firstAttribute="width" constant="350" id="8kc-f3-P81"/>
+                    </constraints>
+                </view>
+            </subviews>
+            <viewLayoutGuide key="safeArea" id="bbs-K4-b33"/>
+            <color key="backgroundColor" white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+            <constraints>
+                <constraint firstItem="bbs-K4-b33" firstAttribute="trailing" secondItem="reh-cY-1Qv" secondAttribute="trailing" id="8ND-zt-NSC"/>
+                <constraint firstItem="reh-cY-1Qv" firstAttribute="leading" secondItem="bbs-K4-b33" secondAttribute="leading" id="AhA-F1-rqp"/>
+                <constraint firstItem="fSY-ZT-xIC" firstAttribute="top" secondItem="bbs-K4-b33" secondAttribute="top" constant="8" id="ENd-Oe-5YE"/>
+                <constraint firstItem="aUh-Z0-hO6" firstAttribute="top" secondItem="reh-cY-1Qv" secondAttribute="bottom" id="GtW-pl-edb"/>
+                <constraint firstItem="bbs-K4-b33" firstAttribute="trailing" secondItem="Zzc-Pq-hMC" secondAttribute="trailing" id="IS9-3G-cQz"/>
+                <constraint firstItem="bbs-K4-b33" firstAttribute="trailing" secondItem="aUh-Z0-hO6" secondAttribute="trailing" id="MON-Nu-TFX"/>
+                <constraint firstItem="Zzc-Pq-hMC" firstAttribute="top" secondItem="reh-cY-1Qv" secondAttribute="bottom" id="SEj-Nc-c8l"/>
+                <constraint firstItem="bbs-K4-b33" firstAttribute="bottom" secondItem="aUh-Z0-hO6" secondAttribute="bottom" id="TOU-Pk-3pi"/>
+                <constraint firstItem="aUh-Z0-hO6" firstAttribute="leading" secondItem="bbs-K4-b33" secondAttribute="leading" id="XPT-Wv-kZY"/>
+                <constraint firstItem="cRK-6e-FFN" firstAttribute="top" secondItem="reh-cY-1Qv" secondAttribute="bottom" constant="8" id="XUL-j4-h0o"/>
+                <constraint firstItem="reh-cY-1Qv" firstAttribute="top" secondItem="i5M-Pr-FkT" secondAttribute="top" id="c1h-3M-GBu"/>
+                <constraint firstItem="cRK-6e-FFN" firstAttribute="leading" secondItem="aUh-Z0-hO6" secondAttribute="leading" constant="8" id="ch5-E2-gZj"/>
+                <constraint firstItem="bbs-K4-b33" firstAttribute="bottom" secondItem="fSY-ZT-xIC" secondAttribute="bottom" constant="8" id="cnF-xe-Mjs"/>
+                <constraint firstItem="bbs-K4-b33" firstAttribute="trailing" secondItem="fSY-ZT-xIC" secondAttribute="trailing" constant="8" id="grQ-FV-QY8"/>
+                <constraint firstItem="Zzc-Pq-hMC" firstAttribute="bottom" secondItem="bbs-K4-b33" secondAttribute="bottom" id="nY6-7m-HGN"/>
+                <constraint firstItem="Zzc-Pq-hMC" firstAttribute="leading" secondItem="bbs-K4-b33" secondAttribute="leading" id="pzB-Lo-vzV"/>
+                <constraint firstItem="reh-cY-1Qv" firstAttribute="bottom" secondItem="bbs-K4-b33" secondAttribute="top" constant="64" id="urh-LB-Bha"/>
+            </constraints>
+            <point key="canvasLocation" x="29.296874999999996" y="49.194729136163978"/>
+        </view>
+        <view contentMode="scaleToFill" id="BF4-kz-lxP">
+            <rect key="frame" x="0.0" y="0.0" width="1024" height="1366"/>
+            <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
+            <subviews>
+                <imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="H1v-6g-V21" customClass="AvatarBackgroundImageView">
+                    <rect key="frame" x="0.0" y="0.0" width="1024" height="1366"/>
+                    <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+                </imageView>
+                <label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="center" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Waiting for others to the call..." textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" minimumFontSize="11" translatesAutoresizingMaskIntoConstraints="NO" id="ihe-9I-8ts">
+                    <rect key="frame" x="44" y="44" width="936" height="42"/>
+                    <autoresizingMask key="autoresizingMask" flexibleMinX="YES" widthSizable="YES" flexibleMaxX="YES" flexibleMaxY="YES"/>
+                    <fontDescription key="fontDescription" type="system" weight="semibold" pointSize="16"/>
+                    <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+                    <nil key="highlightedColor"/>
+                </label>
+            </subviews>
+            <viewLayoutGuide key="safeArea" id="ikW-9Y-8vD"/>
+            <point key="canvasLocation" x="757.03125" y="52.26939970717423"/>
+        </view>
+    </objects>
+    <resources>
+        <image name="bubble.left.fill" catalog="system" width="128" height="110"/>
+        <image name="camera.rotate" catalog="system" width="128" height="93"/>
+        <image name="ellipsis" catalog="system" width="128" height="37"/>
+        <image name="hand.raised.fill" catalog="system" width="128" height="128"/>
+        <image name="mic.fill" catalog="system" width="110" height="128"/>
+        <image name="phone.down.fill" catalog="system" width="128" height="55"/>
+        <image name="record.circle.fill" catalog="system" width="128" height="123"/>
+        <image name="speaker.slash.fill" catalog="system" width="115" height="128"/>
+        <image name="video.fill" catalog="system" width="128" height="81"/>
+        <image name="video.slash.fill" catalog="system" width="128" height="114"/>
+        <image name="xmark" catalog="system" width="128" height="113"/>
+        <systemColor name="systemBackgroundColor">
+            <color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+        </systemColor>
+        <systemColor name="systemRedColor">
+            <color red="1" green="0.23137254901960785" blue="0.18823529411764706" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
+        </systemColor>
+    </resources>
+</document>

+ 57 - 0
NextcloudTalk/CallsFromOldAccountViewController.swift

@@ -0,0 +1,57 @@
+//
+// SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+
+import UIKit
+
+@objc protocol CallsFromOldAccountViewControllerDelegate: AnyObject {
+    func callsFromOldAccountWarningAcknowledged()
+}
+
+class CallsFromOldAccountViewController: UIViewController {
+
+    weak var delegate: CallsFromOldAccountViewControllerDelegate?
+
+    @IBOutlet weak var warningTextLabel: UILabel!
+    @IBOutlet weak var acknowledgeWarningButton: NCButton!
+
+    override func viewDidLoad() {
+        super.viewDidLoad()
+
+        self.navigationController?.navigationBar.titleTextAttributes = [NSAttributedString.Key.foregroundColor: NCAppBranding.themeTextColor()]
+        self.navigationController?.navigationBar.tintColor = NCAppBranding.themeTextColor()
+        self.navigationController?.navigationBar.barTintColor = NCAppBranding.themeColor()
+        self.navigationController?.navigationBar.isTranslucent = false
+        self.navigationItem.title = NSLocalizedString("Calls from old accounts", comment: "")
+
+        let appearance = UINavigationBarAppearance()
+        appearance.configureWithOpaqueBackground()
+        appearance.titleTextAttributes = [.foregroundColor: NCAppBranding.themeTextColor()]
+        appearance.backgroundColor = NCAppBranding.themeColor()
+        self.navigationItem.standardAppearance = appearance
+        self.navigationItem.compactAppearance = appearance
+        self.navigationItem.scrollEdgeAppearance = appearance
+
+        acknowledgeWarningButton.setTitle(NSLocalizedString("Confirm and hide warning", comment: ""), for: .normal)
+        acknowledgeWarningButton.setButtonStyle(style: .primary)
+
+        let warning1 = NSLocalizedString("Calls from an old account were received.", comment: "")
+        let warning2 = NSLocalizedString("This usually indicates that this device was previously used for an account, which was not properly removed from the server.", comment: "")
+        let warning3 = NSLocalizedString("To resolve this issue, use the web interface and go to \"Settings -> Security\".", comment: "")
+        let warning4 = NSLocalizedString("Under \"Devices & sessions\" check if there are duplicate entries for the same device.", comment: "")
+        let warning5 = NSLocalizedString("Remove old duplicate entries and leave only the most recent entries.", comment: "")
+        let warning6 = NSLocalizedString("If you're using multiple servers, you need to check all of them.", comment: "")
+
+        let warningTextComplete = warning1 + " " + warning2 + "\n\n" + warning3 + "\n\n" + warning4 + "\n\n" + warning5 + "\n\n" + warning6
+
+        warningTextLabel.text = warningTextComplete
+    }
+
+    @IBAction func acknowledgeWarningButtonPressed(_ sender: Any) {
+        NCSettingsController.sharedInstance().setDidReceiveCallsFromOldAccount(false)
+        self.navigationController?.popViewController(animated: true)
+        self.delegate?.callsFromOldAccountWarningAcknowledged()
+    }
+
+}

+ 74 - 0
NextcloudTalk/CallsFromOldAccountViewController.xib

@@ -0,0 +1,74 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="21225" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
+    <device id="retina6_1" orientation="portrait" appearance="light"/>
+    <dependencies>
+        <deployment identifier="iOS"/>
+        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21207"/>
+        <capability name="Safe area layout guides" minToolsVersion="9.0"/>
+        <capability name="System colors in document resources" minToolsVersion="11.0"/>
+        <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
+    </dependencies>
+    <objects>
+        <placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="CallsFromOldAccountViewController" customModule="NextcloudTalk" customModuleProvider="target">
+            <connections>
+                <outlet property="acknowledgeWarningButton" destination="JBD-4r-MQ9" id="ghn-B9-JTK"/>
+                <outlet property="view" destination="i5M-Pr-FkT" id="sfx-zR-JGt"/>
+                <outlet property="warningTextLabel" destination="1Re-sk-ErK" id="cyq-ye-37V"/>
+            </connections>
+        </placeholder>
+        <placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
+        <view clearsContextBeforeDrawing="NO" contentMode="scaleToFill" id="i5M-Pr-FkT">
+            <rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
+            <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+            <subviews>
+                <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="1Re-sk-ErK">
+                    <rect key="frame" x="20" y="112" width="374" height="21"/>
+                    <constraints>
+                        <constraint firstAttribute="height" relation="greaterThanOrEqual" constant="21" id="otU-oy-cnr"/>
+                        <constraint firstAttribute="width" relation="greaterThanOrEqual" constant="10" id="wEz-so-ou3"/>
+                    </constraints>
+                    <fontDescription key="fontDescription" type="system" pointSize="16"/>
+                    <nil key="textColor"/>
+                    <nil key="highlightedColor"/>
+                </label>
+                <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="JBD-4r-MQ9" customClass="NCButton" customModule="NextcloudTalk" customModuleProvider="target">
+                    <rect key="frame" x="158" y="163" width="98" height="40"/>
+                    <constraints>
+                        <constraint firstAttribute="width" relation="greaterThanOrEqual" id="J0b-me-Meq"/>
+                        <constraint firstAttribute="height" constant="40" id="pn7-f2-CaS"/>
+                    </constraints>
+                    <fontDescription key="fontDescription" type="boldSystem" pointSize="15"/>
+                    <inset key="contentEdgeInsets" minX="24" minY="0.0" maxX="24" maxY="0.0"/>
+                    <inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/>
+                    <state key="normal" title="Button">
+                        <color key="titleColor" systemColor="labelColor"/>
+                    </state>
+                    <connections>
+                        <action selector="acknowledgeWarningButtonPressed:" destination="-2" eventType="touchUpInside" id="ze0-sz-Jfl"/>
+                    </connections>
+                </button>
+            </subviews>
+            <viewLayoutGuide key="safeArea" id="fnl-2z-Ty3"/>
+            <color key="backgroundColor" systemColor="systemBackgroundColor"/>
+            <constraints>
+                <constraint firstItem="fnl-2z-Ty3" firstAttribute="trailing" secondItem="1Re-sk-ErK" secondAttribute="trailing" constant="20" id="3BR-hb-od8"/>
+                <constraint firstItem="JBD-4r-MQ9" firstAttribute="centerX" secondItem="i5M-Pr-FkT" secondAttribute="centerX" id="CUY-aC-JlA"/>
+                <constraint firstItem="JBD-4r-MQ9" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="fnl-2z-Ty3" secondAttribute="leading" constant="20" id="ELA-6W-Vhe"/>
+                <constraint firstItem="fnl-2z-Ty3" firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="JBD-4r-MQ9" secondAttribute="trailing" constant="20" id="YAf-Gp-ax1"/>
+                <constraint firstItem="JBD-4r-MQ9" firstAttribute="top" secondItem="1Re-sk-ErK" secondAttribute="bottom" constant="30" id="bpq-Kk-BgI"/>
+                <constraint firstItem="1Re-sk-ErK" firstAttribute="top" secondItem="fnl-2z-Ty3" secondAttribute="top" constant="20" id="lFh-YF-diq"/>
+                <constraint firstItem="1Re-sk-ErK" firstAttribute="leading" secondItem="fnl-2z-Ty3" secondAttribute="leading" constant="20" id="nHJ-ID-H47"/>
+            </constraints>
+            <simulatedNavigationBarMetrics key="simulatedTopBarMetrics" prompted="NO"/>
+            <point key="canvasLocation" x="82.608695652173921" y="91.741071428571431"/>
+        </view>
+    </objects>
+    <resources>
+        <systemColor name="labelColor">
+            <color red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
+        </systemColor>
+        <systemColor name="systemBackgroundColor">
+            <color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+        </systemColor>
+    </resources>
+</document>

+ 16 - 0
NextcloudTalk/CapturerEventsDelegate.h

@@ -0,0 +1,16 @@
+// From https://github.com/react-native-webrtc/react-native-webrtc (MIT License)
+// SPDX-FileCopyrightText: 2023 React-Native-WebRTC authors
+// SPDX-License-Identifier: MIT
+
+#import <WebRTC/RTCVideoCapturer.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@protocol CapturerEventsDelegate
+
+/** Called when the capturer is ended and in an irrecoverable state. */
+- (void)capturerDidEnd:(RTCVideoCapturer *)capturer;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 39 - 0
NextcloudTalk/ChatTableViewCell.h

@@ -0,0 +1,39 @@
+/**
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#import <UIKit/UIKit.h>
+
+#import "DRCellSlideGestureRecognizer.h"
+#import "NCChatMessage.h"
+
+static CGFloat kChatCellStatusViewHeight    = 20.0;
+static CGFloat kChatCellDateLabelWidth      = 40.0;
+static CGFloat kChatCellAvatarHeight        = 30.0;
+
+typedef NS_ENUM(NSInteger, ChatMessageDeliveryState) {
+    ChatMessageDeliveryStateSent = 0,
+    ChatMessageDeliveryStateRead,
+    ChatMessageDeliveryStateSending,
+    ChatMessageDeliveryStateDeleting,
+    ChatMessageDeliveryStateFailed,
+    ChatMessageDeliveryStateSilent
+};
+
+@protocol ChatTableViewCellDelegate <NSObject>
+
+- (void)cellDidSelectedReaction:(NCChatReaction *)reaction forMessage:(NCChatMessage *)message;
+- (void)cellWantsToReplyToMessage:(NCChatMessage *)message;
+
+@end
+
+@interface ChatTableViewCell : UITableViewCell
+
+@property (nonatomic, assign) NSInteger messageId;
+@property (nonatomic, strong) NCChatMessage *message;
+
+- (UIMenu *)getDeferredUserMenuForMessage:(NCChatMessage *)message;
+- (void)addReplyGestureWithActionBlock:(DRCellSlideActionBlock)block;
+
+@end

+ 158 - 0
NextcloudTalk/ChatTableViewCell.m

@@ -0,0 +1,158 @@
+/**
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#import "ChatTableViewCell.h"
+#import "NextcloudTalk-Swift.h"
+
+typedef void (^GetMenuUserActionsForMessageCompletionBlock)(NSArray *menuItems);
+
+@interface ChatTableViewCell () <UITextFieldDelegate>
+@property (nonatomic, strong) DRCellSlideGestureRecognizer *replyGestureRecognizer;
+@end
+
+@implementation ChatTableViewCell
+
+- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
+{
+    self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
+    if (self) {
+        // Initialization
+    }
+    return self;
+}
+
+- (void)prepareForReuse
+{
+    [super prepareForReuse];
+    self.messageId = -1;
+    self.message = nil;
+    [self removeGestureRecognizer:self.replyGestureRecognizer];
+}
+
+- (void)addReplyGestureWithActionBlock:(DRCellSlideActionBlock)block
+{
+    self.replyGestureRecognizer = [DRCellSlideGestureRecognizer new];
+    self.replyGestureRecognizer.leftActionStartPosition = 80;
+    DRCellSlideAction *action = [DRCellSlideAction actionForFraction:0.2];
+    action.behavior = DRCellSlideActionPullBehavior;
+    action.activeColor = [UIColor labelColor];
+    action.inactiveColor = [UIColor placeholderTextColor];
+    action.activeBackgroundColor = self.backgroundColor;
+    action.inactiveBackgroundColor = self.backgroundColor;
+    action.icon = [UIImage systemImageNamed:@"arrowshape.turn.up.left"];
+
+    [action setWillTriggerBlock:^(UITableView *tableView, NSIndexPath *indexPath) {
+        block(tableView, indexPath);
+    }];
+
+    [action setDidChangeStateBlock:^(DRCellSlideAction *action, BOOL active) {
+        if (active) {
+            // Actuate `Peek` feedback (weak boom)
+            AudioServicesPlaySystemSound(1519);
+        }
+    }];
+
+    [self.replyGestureRecognizer addActions:action];
+    [self addGestureRecognizer:self.replyGestureRecognizer];
+}
+
+- (UIMenu *)getDeferredUserMenuForMessage:(NCChatMessage *)message
+{
+    TalkAccount *activeAccount = [[NCDatabaseManager sharedInstance] activeAccount];
+    if (![message.actorType isEqualToString:@"users"] || [message.actorId isEqualToString:activeAccount.userId]) {
+        return nil;
+    }
+
+    UIDeferredMenuElement *deferredMenuElement;
+
+    // Use an uncached provider so local time is not cached
+    deferredMenuElement = [UIDeferredMenuElement elementWithUncachedProvider:^(void (^ _Nonnull completion)(NSArray<UIMenuElement *> * _Nonnull)) {
+        [self getMenuUserActionsForMessage:message withCompletionBlock:^(NSArray *menuItems) {
+            completion(menuItems);
+        }];
+    }];
+
+    return [UIMenu menuWithTitle:message.actorDisplayName children:@[deferredMenuElement]];
+}
+
+- (void)getMenuUserActionsForMessage:(NCChatMessage *)message withCompletionBlock:(GetMenuUserActionsForMessageCompletionBlock)block
+{
+    TalkAccount *activeAccount = [[NCDatabaseManager sharedInstance] activeAccount];
+    [[NCAPIController sharedInstance] getUserActionsForUser:message.actorId usingAccount:activeAccount withCompletionBlock:^(NSDictionary *userActions, NSError *error) {
+        if (error) {
+            if (block) {
+                UIAction *errorAction = [UIAction actionWithTitle:NSLocalizedString(@"No actions available", nil) image:nil identifier:nil handler:^(UIAction *action) {}];
+                errorAction.attributes = UIMenuElementAttributesDisabled;
+                block(@[errorAction]);
+            }
+
+            return;
+        }
+
+        NSArray *actions = [userActions objectForKey:@"actions"];
+        if (![actions isKindOfClass:[NSArray class]]) {
+            if (block) {
+                UIAction *errorAction = [UIAction actionWithTitle:NSLocalizedString(@"No actions available", nil) image:nil identifier:nil handler:^(UIAction *action) {}];
+                errorAction.attributes = UIMenuElementAttributesDisabled;
+                block(@[errorAction]);
+            }
+
+            return;
+        }
+
+        NSMutableArray *items = [[NSMutableArray alloc] init];
+
+        for (NSDictionary *action in actions) {
+            NSString *appId = [action objectForKey:@"appId"];
+            NSString *title = [action objectForKey:@"title"];
+            NSString *link = [action objectForKey:@"hyperlink"];
+
+            // Talk to user action
+            if ([appId isEqualToString:@"spreed"]) {
+                UIAction *talkAction = [UIAction actionWithTitle:title
+                                                           image:[[UIImage imageNamed:@"talk-20"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]
+                                                      identifier:nil
+                                                         handler:^(UIAction *action) {
+                    NSMutableDictionary *userInfo = [[NSMutableDictionary alloc] init];
+                    NSString *userId = [userActions objectForKey:@"userId"];
+                    [userInfo setObject:userId forKey:@"actorId"];
+                    [[NSNotificationCenter defaultCenter] postNotificationName:NSNotification.NCChatViewControllerTalkToUserNotification
+                                                                        object:self
+                                                                      userInfo:userInfo];
+                }];
+
+                [items addObject:talkAction];
+                continue;
+            }
+
+            // Other user actions
+            UIAction *otherAction = [UIAction actionWithTitle:title
+                                                        image:nil
+                                                   identifier:nil
+                                                      handler:^(UIAction *action) {
+                NSURL *actionURL = [NSURL URLWithString:[link stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]]];
+                [[UIApplication sharedApplication] openURL:actionURL options:@{} completionHandler:nil];
+            }];
+
+            if ([appId isEqualToString:@"profile"]) {
+                [otherAction setImage:[UIImage systemImageNamed:@"person"]];
+            } else if ([appId isEqualToString:@"email"]) {
+                [otherAction setImage:[UIImage systemImageNamed:@"envelope"]];
+            } else if ([appId isEqualToString:@"timezone"]) {
+                [otherAction setImage:[UIImage systemImageNamed:@"clock"]];
+            } else if ([appId isEqualToString:@"social"]) {
+                [otherAction setImage:[UIImage systemImageNamed:@"heart"]];
+            }
+
+            [items addObject:otherAction];
+        }
+
+        if (block) {
+            block(items);
+        }
+    }];
+}
+
+@end

+ 1813 - 0
NextcloudTalk/ChatViewController.swift

@@ -0,0 +1,1813 @@
+//
+// SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+
+import Foundation
+import NextcloudKit
+import PhotosUI
+import UIKit
+
+@objcMembers public class ChatViewController: BaseChatViewController {
+
+    // MARK: - Public var
+    public var presentedInCall = false
+    public var chatController: NCChatController
+    public var highlightMessageId = 0
+
+    // MARK: - Private var
+    private var hasPresentedLobby = false
+    private var hasRequestedInitialHistory = false
+    private var hasReceiveInitialHistory = false
+    private var retrievingHistory = false
+
+    private var hasJoinedRoom = false
+    private var startReceivingMessagesAfterJoin = false
+    private var offlineMode = false
+    private var hasStoredHistory = true
+    private var hasStopped = false
+
+    private var chatViewPresentedTimestamp = Date().timeIntervalSince1970
+
+    private lazy var unreadMessagesSeparator: NCChatMessage = {
+        let message = NCChatMessage()
+        message.messageId = kUnreadMessagesSeparatorIdentifier
+        return message
+    }()
+
+    private lazy var lastReadMessage: Int = {
+        return self.room.lastReadMessage
+    }()
+
+    private var lobbyCheckTimer: Timer?
+
+    // MARK: - Call buttons in NavigationBar
+
+    func getBarButton(forVideo video: Bool) -> BarButtonItemWithActivity {
+        let symbolConfiguration = UIImage.SymbolConfiguration(pointSize: 20)
+        let buttonImage = UIImage(systemName: video ? "video" : "phone", withConfiguration: symbolConfiguration)
+
+        let button = BarButtonItemWithActivity(width: 50, with: buttonImage)
+        button.innerButton.addAction { [unowned self] in
+            button.showIndicator()
+            startCall(withVideo: video, silently: false, button: button)
+        }
+
+        if NCDatabaseManager.sharedInstance().roomHasTalkCapability(kCapabilitySilentCall, for: self.room) {
+            let silentCall = UIAction(title: NSLocalizedString("Call without notification", comment: ""), image: UIImage(systemName: "bell.slash")) { [unowned self] _ in
+                button.showIndicator()
+                startCall(withVideo: video, silently: true, button: button)
+            }
+
+            button.innerButton.menu = UIMenu(children: [silentCall])
+        }
+
+        return button
+    }
+
+    func startCall(withVideo video: Bool, silently: Bool, button: BarButtonItemWithActivity) {
+        if self.room.recordingConsent {
+            let alert = UIAlertController(title: "⚠️" + NSLocalizedString("The call might be recorded", comment: ""),
+                                          message: NSLocalizedString("The recording might include your voice, video from camera, and screen share. Your consent is required before joining the call.", comment: ""),
+                                          preferredStyle: .alert)
+
+            alert.addAction(.init(title: NSLocalizedString("Give consent and join call", comment: "Give consent to the recording of the call and join that call"), style: .default) { _ in
+                CallKitManager.sharedInstance().startCall(self.room.token, withVideoEnabled: video, andDisplayName: self.room.displayName, asInitiator: !self.room.hasCall, silently: silently, recordingConsent: true, withAccountId: self.room.accountId)
+            })
+
+            alert.addAction(.init(title: NSLocalizedString("Cancel", comment: ""), style: .cancel) { _ in
+                button.hideIndicator()
+            })
+
+            NCUserInterfaceController.sharedInstance().presentAlertViewController(alert)
+
+        } else {
+            CallKitManager.sharedInstance().startCall(self.room.token, withVideoEnabled: video, andDisplayName: self.room.displayName, asInitiator: !self.room.hasCall, silently: silently, recordingConsent: false, withAccountId: self.room.accountId)
+        }
+    }
+
+    private lazy var videoCallButton: BarButtonItemWithActivity = {
+        let videoCallButton = self.getBarButton(forVideo: true)
+
+        videoCallButton.accessibilityLabel = NSLocalizedString("Video call", comment: "")
+        videoCallButton.accessibilityHint = NSLocalizedString("Double tap to start a video call", comment: "")
+
+        return videoCallButton
+    }()
+
+    private lazy var voiceCallButton: BarButtonItemWithActivity = {
+        let voiceCallButton = self.getBarButton(forVideo: false)
+
+        voiceCallButton.accessibilityLabel = NSLocalizedString("Voice call", comment: "")
+        voiceCallButton.accessibilityHint = NSLocalizedString("Double tap to start a voice call", comment: "")
+
+        return voiceCallButton
+    }()
+
+    private var messageExpirationTimer: Timer?
+
+    public override init?(for room: NCRoom) {
+        self.chatController = NCChatController(for: room)
+
+        super.init(for: room)
+
+        NotificationCenter.default.addObserver(self, selector: #selector(didUpdateRoom(notification:)), name: NSNotification.Name.NCRoomsManagerDidUpdateRoom, object: nil)
+        NotificationCenter.default.addObserver(self, selector: #selector(didJoinRoom(notification:)), name: NSNotification.Name.NCRoomsManagerDidJoinRoom, object: nil)
+        NotificationCenter.default.addObserver(self, selector: #selector(didLeaveRoom(notification:)), name: NSNotification.Name.NCRoomsManagerDidLeaveRoom, object: nil)
+        NotificationCenter.default.addObserver(self, selector: #selector(didReceiveInitialChatHistory(notification:)), name: NSNotification.Name.NCChatControllerDidReceiveInitialChatHistory, object: nil)
+        NotificationCenter.default.addObserver(self, selector: #selector(didReceiveInitialChatHistoryOffline(notification:)), name: NSNotification.Name.NCChatControllerDidReceiveInitialChatHistoryOffline, object: nil)
+        NotificationCenter.default.addObserver(self, selector: #selector(didReceiveChatHistory(notification:)), name: NSNotification.Name.NCChatControllerDidReceiveChatHistory, object: nil)
+        NotificationCenter.default.addObserver(self, selector: #selector(didReceiveChatMessages(notification:)), name: NSNotification.Name.NCChatControllerDidReceiveChatMessages, object: nil)
+        NotificationCenter.default.addObserver(self, selector: #selector(didSendChatMessage(notification:)), name: NSNotification.Name.NCChatControllerDidSendChatMessage, object: nil)
+        NotificationCenter.default.addObserver(self, selector: #selector(didReceiveChatBlocked(notification:)), name: NSNotification.Name.NCChatControllerDidReceiveChatBlocked, object: nil)
+        NotificationCenter.default.addObserver(self, selector: #selector(didReceiveNewerCommonReadMessage(notification:)), name: NSNotification.Name.NCChatControllerDidReceiveNewerCommonReadMessage, object: nil)
+        NotificationCenter.default.addObserver(self, selector: #selector(didReceiveCallStartedMessage(notification:)), name: NSNotification.Name.NCChatControllerDidReceiveCallStartedMessage, object: nil)
+        NotificationCenter.default.addObserver(self, selector: #selector(didReceiveCallEndedMessage(notification:)), name: NSNotification.Name.NCChatControllerDidReceiveCallEndedMessage, object: nil)
+        NotificationCenter.default.addObserver(self, selector: #selector(didReceiveUpdateMessage(notification:)), name: NSNotification.Name.NCChatControllerDidReceiveUpdateMessage, object: nil)
+        NotificationCenter.default.addObserver(self, selector: #selector(didReceiveHistoryCleared(notification:)), name: NSNotification.Name.NCChatControllerDidReceiveHistoryCleared, object: nil)
+        NotificationCenter.default.addObserver(self, selector: #selector(didReceiveMessagesInBackground(notification:)), name: NSNotification.Name.NCChatControllerDidReceiveMessagesInBackground, object: nil)
+        NotificationCenter.default.addObserver(self, selector: #selector(didChangeRoomCapabilities(notification:)), name: NSNotification.Name.NCDatabaseManagerRoomCapabilitiesChanged, object: nil)
+
+        NotificationCenter.default.addObserver(self, selector: #selector(didReceiveParticipantJoin(notification:)), name: NSNotification.Name.NCExternalSignalingControllerDidReceiveJoinOfParticipant, object: nil)
+        NotificationCenter.default.addObserver(self, selector: #selector(didReceiveParticipantLeave(notification:)), name: NSNotification.Name.NCExternalSignalingControllerDidReceiveLeaveOfParticipant, object: nil)
+        NotificationCenter.default.addObserver(self, selector: #selector(didReceiveStartedTyping(notification:)), name: NSNotification.Name.NCExternalSignalingControllerDidReceiveStartedTyping, object: nil)
+        NotificationCenter.default.addObserver(self, selector: #selector(didReceiveStoppedTyping(notification:)), name: NSNotification.Name.NCExternalSignalingControllerDidReceiveStoppedTyping, object: nil)
+        NotificationCenter.default.addObserver(self, selector: #selector(didFailRequestingCallTransaction(notification:)), name: NSNotification.Name.CallKitManagerDidFailRequestingCallTransaction, object: nil)
+        NotificationCenter.default.addObserver(self, selector: #selector(didUpdateParticipants(notification:)), name: NSNotification.Name.NCExternalSignalingControllerDidUpdateParticipants, object: nil)
+
+        NotificationCenter.default.addObserver(self, selector: #selector(appDidBecomeActive(notification:)), name: UIApplication.didBecomeActiveNotification, object: nil)
+        NotificationCenter.default.addObserver(self, selector: #selector(appWillResignActive(notification:)), name: UIApplication.willResignActiveNotification, object: nil)
+        NotificationCenter.default.addObserver(self, selector: #selector(connectionStateHasChanged(notification:)), name: NSNotification.Name.NCConnectionStateHasChanged, object: nil)
+        NotificationCenter.default.addObserver(self, selector: #selector(maintenanceModeActive(notification:)), name: NSNotification.Name.NCServerMaintenanceMode, object: nil)
+
+        // Notifications when runing on Mac
+        NotificationCenter.default.addObserver(self, selector: #selector(appDidBecomeActive(notification:)), name: NSNotification.Name(rawValue: "NSApplicationDidBecomeActiveNotification"), object: nil)
+        NotificationCenter.default.addObserver(self, selector: #selector(appWillResignActive(notification:)), name: NSNotification.Name(rawValue: "NSApplicationDidResignActiveNotification"), object: nil)
+    }
+
+    deinit {
+        NotificationCenter.default.removeObserver(self)
+        print("Dealloc NewChatViewController")
+    }
+
+    // MARK: - View lifecycle
+
+    public override func viewDidLoad() {
+        super.viewDidLoad()
+
+        if room.supportsCalling {
+            self.navigationItem.rightBarButtonItems = [videoCallButton, voiceCallButton]
+        }
+
+        // No sharing options in federation v1
+        if room.isFederated {
+            // When hiding the button it is still respected in the layout constraints
+            // So we need to remove the image to remove the button for now
+            self.leftButton.setImage(nil, for: .normal)
+        }
+
+        // Disable room info, input bar and call buttons until joining room
+        self.disableRoomControls()
+    }
+
+    public override func viewWillAppear(_ animated: Bool) {
+        super.viewWillAppear(animated)
+
+        self.checkLobbyState()
+        self.checkRoomControlsAvailability()
+
+        self.startObservingExpiredMessages()
+
+        // Workaround for open conversations:
+        // We can't get initial chat history until we join the conversation (since we are not a participant until then)
+        // So for rooms that we don't know the last read message we wait until we join the room to get the initial chat history.
+        if !self.hasReceiveInitialHistory, !self.hasRequestedInitialHistory, self.room.lastReadMessage > 0 {
+            self.hasRequestedInitialHistory = true
+            self.chatController.getInitialChatHistory()
+        }
+
+        if !self.offlineMode {
+            if self.room.token == nil {
+                fatalTokenError()
+            }
+
+            NCRoomsManager.sharedInstance().joinRoom(self.room.token, forCall: false)
+        }
+    }
+
+    private func fatalTokenError() {
+        let capabilities = NCDatabaseManager.sharedInstance().serverCapabilities()
+
+        switch capabilities.versionMajor {
+        case 19:
+            fatalError()
+        case 20:
+            fatalError()
+        case 21:
+            fatalError()
+        case 22:
+            fatalError()
+        case 23:
+            fatalError()
+        case 24:
+            fatalError()
+        case 25:
+            fatalError()
+        case 26:
+            fatalError()
+        case 27:
+            fatalError()
+        case 28:
+            fatalError()
+        case 29:
+            fatalError()
+        case 30:
+            fatalError()
+        default:
+            fatalError()
+        }
+    }
+
+    public override func viewWillDisappear(_ animated: Bool) {
+        super.viewWillDisappear(animated)
+
+        self.saveLastReadMessage()
+        self.stopVoiceMessagePlayer()
+    }
+
+    public override func viewDidDisappear(_ animated: Bool) {
+        super.viewDidDisappear(animated)
+
+        if self.isMovingFromParent {
+            self.leaveChat()
+        }
+
+        self.videoCallButton.hideIndicator()
+        self.voiceCallButton.hideIndicator()
+    }
+
+    required init?(coder decoder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+
+    // MARK: - App lifecycle
+
+    func appDidBecomeActive(notification: Notification) {
+        // Don't handle this event if the view is not loaded yet.
+        // Otherwise we try to join the room and receive new messages while
+        // viewDidLoad wasn't called, resulting in uninitialized dictionaries and crashes
+        if !self.isViewLoaded {
+            return
+        }
+
+        // If we stopped the chat, we don't want to resume it here
+        if self.hasStopped {
+            return
+        }
+
+        // Check if new messages were added while the app was inactive (eg. via background-refresh)
+        self.checkForNewStoredMessages()
+
+        if !self.offlineMode {
+            if self.room.token == nil {
+                fatalTokenError()
+            }
+            
+            NCRoomsManager.sharedInstance().joinRoom(self.room.token, forCall: false)
+        }
+
+        self.startObservingExpiredMessages()
+    }
+
+    func appWillResignActive(notification: Notification) {
+        // If we stopped the chat, we don't want to change anything here
+        if self.hasStopped {
+            return
+        }
+
+        self.startReceivingMessagesAfterJoin = true
+        self.removeUnreadMessagesSeparator()
+        self.savePendingMessage()
+        self.chatController.stop()
+        self.messageExpirationTimer?.invalidate()
+        self.stopTyping(force: false)
+        NCRoomsManager.sharedInstance().leaveChat(inRoom: self.room.token)
+    }
+
+    func connectionStateHasChanged(notification: Notification) {
+        guard let rawConnectionState = notification.userInfo?["connectionState"] as? Int, let connectionState = ConnectionState(rawValue: rawConnectionState) else {
+            return
+        }
+
+        switch connectionState {
+        case .connected:
+            if offlineMode {
+                offlineMode = false
+                startReceivingMessagesAfterJoin = true
+                self.removeOfflineFooterView()
+                NCRoomsManager.sharedInstance().joinRoom(self.room.token, forCall: false)
+            }
+        default:
+            break
+        }
+    }
+
+    func maintenanceModeActive(notification: Notification) {
+        self.setOfflineMode()
+    }
+
+    // MARK: - User Interface
+
+    func disableRoomControls() {
+        self.titleView?.isUserInteractionEnabled = false
+
+        self.videoCallButton.hideIndicator()
+        self.videoCallButton.isEnabled = false
+        self.voiceCallButton.hideIndicator()
+        self.voiceCallButton.isEnabled = false
+
+        self.rightButton.isEnabled = false
+        self.leftButton.isEnabled = false
+    }
+
+    func checkRoomControlsAvailability() {
+        if hasJoinedRoom, !offlineMode {
+            // Enable room info and call buttons when we joined a room
+            self.titleView?.isUserInteractionEnabled = true
+            self.videoCallButton.isEnabled = true
+            self.voiceCallButton.isEnabled = true
+        }
+
+        // Files/objects can only be send when we're not offline
+        self.leftButton.isEnabled = !offlineMode
+
+        // Always allow to start writing a message, even if we didn't join the room (yet)
+        self.rightButton.isEnabled = self.canPressRightButton()
+        self.textInputbar.isUserInteractionEnabled = true
+
+        if !room.userCanStartCall, !room.hasCall {
+            // Disable call buttons
+            self.videoCallButton.isEnabled = false
+            self.voiceCallButton.isEnabled = false
+        }
+
+        if room.readOnlyState == .readOnly || self.shouldPresentLobbyView() {
+            // Hide text input
+            self.setTextInputbarHidden(true, animated: self.isVisible)
+
+            // Disable call buttons
+            self.videoCallButton.isEnabled = false
+            self.voiceCallButton.isEnabled = false
+        } else if NCDatabaseManager.sharedInstance().roomHasTalkCapability(kCapabilityChatPermission, for: room), (room.permissions & NCPermission.chat.rawValue) == 0 {
+            // Hide text input
+            self.setTextInputbarHidden(true, animated: isVisible)
+        } else if self.isTextInputbarHidden {
+            // Show text input if it was hidden in a previous state
+            self.setTextInputbarHidden(false, animated: isVisible)
+
+            if self.tableView?.slk_isAtBottom ?? false {
+                self.tableView?.slk_scrollToBottom(animated: true)
+            }
+
+            // Make sure the textinput has the correct height
+            self.setChatMessage(self.textInputbar.textView.text)
+        }
+
+        if self.presentedInCall {
+            // Create a close button and remove the call buttons
+            let barButtonItem = UIBarButtonItem(title: nil, style: .plain, target: nil, action: nil)
+            barButtonItem.primaryAction = UIAction(title: NSLocalizedString("Close", comment: ""), handler: { _ in
+                NCRoomsManager.sharedInstance().callViewController?.toggleChatView()
+            })
+            self.navigationItem.rightBarButtonItems = [barButtonItem]
+        }
+    }
+
+    func checkLobbyState() {
+        if self.shouldPresentLobbyView() {
+            self.hasPresentedLobby = true
+
+            var placeholderText = NSLocalizedString("You are currently waiting in the lobby", comment: "")
+
+            // Lobby timer
+            if self.room.lobbyTimer > 0 {
+                let date = Date(timeIntervalSince1970: TimeInterval(self.room.lobbyTimer))
+                let meetingStart = NCUtils.readableDateTime(fromDate: date)
+                let meetingStartPlaceholder = NSLocalizedString("This meeting is scheduled for", comment: "The meeting start time will be displayed after this text e.g (This meeting is scheduled for tomorrow at 10:00)")
+                placeholderText += "\n\n\(meetingStartPlaceholder)\n\(meetingStart)"
+            }
+
+            // Room description
+            if NCDatabaseManager.sharedInstance().roomHasTalkCapability(kCapabilityRoomDescription, for: room), !self.room.roomDescription.isEmpty {
+                placeholderText += "\n\n" + self.room.roomDescription
+            }
+
+            // Only set it when text changes to avoid flickering in links
+            if chatBackgroundView.placeholderTextView.text != placeholderText {
+                chatBackgroundView.placeholderTextView.text = placeholderText
+            }
+
+            self.chatBackgroundView.setImage(UIImage(named: "lobby-placeholder"))
+            self.chatBackgroundView.placeholderView.isHidden = false
+            self.chatBackgroundView.loadingView.stopAnimating()
+            self.chatBackgroundView.loadingView.isHidden = true
+
+            // Clear current chat since chat history will be retrieved when lobby is disabled
+            self.cleanChat()
+        } else {
+            self.chatBackgroundView.setImage(UIImage(named: "chat-placeholder"))
+            self.chatBackgroundView.placeholderTextView.text = NSLocalizedString("No messages yet, start the conversation!", comment: "")
+            self.chatBackgroundView.placeholderView.isHidden = true
+            self.chatBackgroundView.loadingView.startAnimating()
+            self.chatBackgroundView.loadingView.isHidden = false
+
+            // Stop checking lobby flag
+            self.lobbyCheckTimer?.invalidate()
+
+            // Retrieve initial chat history if lobby was enabled and we didn't retrieve it before
+            if !hasReceiveInitialHistory, !hasRequestedInitialHistory, hasPresentedLobby {
+                self.hasRequestedInitialHistory = true
+                self.chatController.getInitialChatHistory()
+            }
+
+            self.hasPresentedLobby = false
+        }
+    }
+
+    func setOfflineFooterView() {
+        let isAtBottom = self.shouldScrollOnNewMessages()
+
+        let footerLabel = UILabel(frame: .init(x: 0, y: 0, width: 350, height: 24))
+        footerLabel.textAlignment = .center
+        footerLabel.textColor = .label
+        footerLabel.font = .systemFont(ofSize: 12)
+        footerLabel.backgroundColor = .clear
+        footerLabel.text = NSLocalizedString("Offline, only showing downloaded messages", comment: "")
+
+        self.tableView?.tableFooterView = footerLabel
+        self.tableView?.tableFooterView?.backgroundColor = .secondarySystemBackground
+
+        if isAtBottom {
+            self.tableView?.slk_scrollToBottom(animated: true)
+        }
+    }
+
+    func removeOfflineFooterView() {
+        DispatchQueue.main.async {
+            self.tableView?.tableFooterView?.removeFromSuperview()
+            self.tableView?.tableFooterView = nil
+
+            // Scrolling after removing the tableFooterView won't scroll all the way to the bottom therefore just keep the current position
+            // And don't try to call scrollToBottom
+        }
+    }
+
+    func setOfflineMode() {
+        self.offlineMode = true
+        self.setOfflineFooterView()
+        self.chatController.stopReceivingNewChatMessages()
+        self.disableRoomControls()
+        self.checkRoomControlsAvailability()
+    }
+
+    // MARK: - Message expiration
+
+    func startObservingExpiredMessages() {
+        self.messageExpirationTimer?.invalidate()
+        self.removeExpiredMessages()
+        self.messageExpirationTimer = Timer.scheduledTimer(withTimeInterval: 30.0, repeats: true, block: { [weak self] _ in
+            self?.removeExpiredMessages()
+        })
+    }
+
+    func removeExpiredMessages() {
+        DispatchQueue.main.async {
+            let currentTimestamp = Int(Date().timeIntervalSince1970)
+
+            // Iterate backwards in case we need to delete multiple sections in one go
+            for sectionIndex in self.dateSections.indices.reversed() {
+                let section = self.dateSections[sectionIndex]
+
+                guard let messages = self.messages[section] else { continue }
+
+                let deleteMessages = messages.filter { message in
+                    return message.expirationTimestamp > 0 && message.expirationTimestamp <= currentTimestamp
+                }
+
+                if !deleteMessages.isEmpty {
+                    self.tableView?.beginUpdates()
+
+                    let filteredMessages = messages.filter { !deleteMessages.contains($0) }
+                    self.messages[section] = filteredMessages
+
+                    if !filteredMessages.isEmpty {
+                        self.tableView?.reloadSections(IndexSet(integer: sectionIndex), with: .top)
+                    } else {
+                        self.messages.removeValue(forKey: section)
+                        self.sortDateSections()
+                        self.tableView?.deleteSections(IndexSet(integer: sectionIndex), with: .top)
+                    }
+
+                    self.tableView?.endUpdates()
+                }
+            }
+
+            self.chatController.removeExpiredMessages()
+        }
+    }
+
+    // MARK: - Utils
+
+    func presentJoinError(_ subtitle: String) {
+        NotificationPresenter.shared().present(title: NSLocalizedString("Could not join conversation", comment: ""), subtitle: subtitle, includedStyle: .warning)
+        NotificationPresenter.shared().dismiss(afterDelay: 8.0)
+    }
+
+    // MARK: - Action methods
+
+    override func sendChatMessage(message: String, withParentMessage parentMessage: NCChatMessage?, messageParameters: String, silently: Bool) {
+        // Create temporary message
+        let temporaryMessage = self.createTemporaryMessage(message: message, replyTo: parentMessage, messageParameters: messageParameters, silently: silently)
+
+        if NCDatabaseManager.sharedInstance().roomHasTalkCapability(kCapabilityChatReferenceId, for: room) {
+            self.appendTemporaryMessage(temporaryMessage: temporaryMessage)
+        }
+
+        // Send message
+        self.chatController.send(temporaryMessage)
+    }
+
+    public override func canPressRightButton() -> Bool {
+        let canPress = super.canPressRightButton()
+
+        if self.textInputbar.isEditing {
+            // When we're editing, we can use the default implementation, as we don't want to save an empty message
+            return canPress
+        }
+
+        // If in offline mode, we don't want to show the voice button
+        if !offlineMode, !canPress, !presentedInCall,
+           NCDatabaseManager.sharedInstance().roomHasTalkCapability(kCapabilityVoiceMessage, for: room),
+           !room.isFederated {
+
+            self.showVoiceMessageRecordButton()
+            return true
+        }
+
+        self.showSendMessageButton()
+
+        return canPress
+    }
+
+    // MARK: - Voice message player
+    // Override the default implementation to don't hijack the audio session when presented in a call
+
+    override func playVoiceMessagePlayer() {
+        if !self.presentedInCall {
+            self.setSpeakerAudioSession()
+            self.enableProximitySensor()
+        }
+
+        self.startVoiceMessagePlayerTimer()
+        self.voiceMessagesPlayer?.play()
+    }
+
+    override func pauseVoiceMessagePlayer() {
+        if !self.presentedInCall {
+            self.disableProximitySensor()
+        }
+
+        self.stopVoiceMessagePlayerTimer()
+        self.voiceMessagesPlayer?.pause()
+        self.checkVisibleCellAudioPlayers()
+    }
+
+    override func stopVoiceMessagePlayer() {
+        if !self.presentedInCall {
+            self.disableProximitySensor()
+        }
+
+        self.stopVoiceMessagePlayerTimer()
+        self.voiceMessagesPlayer?.stop()
+    }
+
+    override func sensorStateChange(notification: Notification) {
+        if self.presentedInCall {
+            return
+        }
+
+        if UIDevice.current.proximityState {
+            self.setVoiceChatAudioSession()
+        } else {
+            self.pauseVoiceMessagePlayer()
+            self.setSpeakerAudioSession()
+            self.disableProximitySensor()
+        }
+    }
+
+    // MARK: - UIScrollViewDelegate methods
+
+    public override func scrollViewDidScroll(_ scrollView: UIScrollView) {
+        super.scrollViewDidScroll(scrollView)
+
+        guard scrollView == self.tableView,
+              scrollView.contentOffset.y < 0,
+              self.couldRetrieveHistory()
+        else { return }
+
+        if let firstMessage = self.getFirstRealMessage()?.message,
+            self.chatController.hasHistory(fromMessageId: firstMessage.messageId) {
+
+            self.retrievingHistory = true
+            self.showLoadingHistoryView()
+
+            if self.offlineMode {
+                self.chatController.getHistoryBatchOffline(fromMessagesId: firstMessage.messageId)
+            } else {
+                self.chatController.getHistoryBatch(fromMessagesId: firstMessage.messageId)
+            }
+        }
+    }
+
+    public func stopChat() {
+        self.hasStopped = true
+        self.chatController.stop()
+        self.cleanChat()
+    }
+
+    public func resumeChat() {
+        self.hasStopped = false
+
+        if !self.hasReceiveInitialHistory, !self.hasRequestedInitialHistory {
+            self.hasRequestedInitialHistory = true
+            self.chatController.getInitialChatHistory()
+        }
+    }
+
+    public func leaveChat() {
+        self.lobbyCheckTimer?.invalidate()
+        self.messageExpirationTimer?.invalidate()
+        self.chatController.stop()
+
+        // Dismiss possible notifications
+        // swiftlint:disable:next notification_center_detachment
+        NotificationCenter.default.removeObserver(self)
+
+        // In case we're typing when we leave the chat, make sure we notify everyone
+        // The 'stopTyping' method makes sure to only send signaling messages when we were typing before
+        self.stopTyping(force: false)
+
+        // If this chat view controller is for the same room as the one owned by the rooms manager
+        // then we should not try to leave the chat. Since we will leave the chat when the
+        // chat view controller owned by rooms manager moves from parent view controller.
+        if NCRoomsManager.sharedInstance().chatViewController?.room.token == self.room.token,
+           NCRoomsManager.sharedInstance().chatViewController !== self {
+            return
+        }
+
+        NCRoomsManager.sharedInstance().leaveChat(inRoom: self.room.token)
+
+        // Remove chat view controller pointer if this chat is owned by rooms manager
+        // and the chat view is moving from parent view controller
+        if NCRoomsManager.sharedInstance().chatViewController === self {
+            NCRoomsManager.sharedInstance().chatViewController = nil
+        }
+    }
+
+    public override func cleanChat() {
+        super.cleanChat()
+
+        self.hasReceiveInitialHistory = false
+        self.hasRequestedInitialHistory = false
+        self.chatController.hasReceivedMessagesFromServer = false
+    }
+
+    func saveLastReadMessage() {
+        NCRoomsManager.sharedInstance().updateLastReadMessage(self.lastReadMessage, for: self.room)
+    }
+
+    // MARK: - Room Manager notifications
+
+    func didUpdateRoom(notification: Notification) {
+        guard let room = notification.userInfo?["room"] as? NCRoom else { return }
+
+        if room.token != self.room.token {
+            return
+        }
+
+        self.room = room
+        self.setTitleView()
+
+        if !self.hasStopped {
+            self.checkLobbyState()
+            self.checkRoomControlsAvailability()
+        }
+    }
+
+    func didJoinRoom(notification: Notification) {
+        guard let token = notification.userInfo?["token"] as? String else { return }
+
+        if token != self.room.token {
+            return
+        }
+
+        if self.isVisible,
+            notification.userInfo?["error"] != nil,
+            let errorReason = notification.userInfo?["errorReason"] as? String {
+
+            self.setOfflineMode()
+            self.presentJoinError(errorReason)
+
+            if let isBanned = notification.userInfo?["isBanned"] as? Bool, isBanned {
+                // Usually we remove all notifications when the view disappears, but in this case, we want to keep it
+                self.dismissNotificationsOnViewWillDisappear = false
+
+                // We are not allowed to join this conversation -> Move back to the conversation list
+                NCUserInterfaceController.sharedInstance().presentConversationsList()
+            }
+
+            return
+        }
+
+        if let room = notification.userInfo?["room"] as? NCRoom {
+            self.room = room
+        }
+
+        self.hasJoinedRoom = true
+        self.checkRoomControlsAvailability()
+
+        if self.hasStopped {
+            return
+        }
+
+        if self.startReceivingMessagesAfterJoin, self.hasReceiveInitialHistory {
+            self.startReceivingMessagesAfterJoin = false
+            self.chatController.startReceivingNewChatMessages()
+        } else if !self.hasReceiveInitialHistory, !self.hasRequestedInitialHistory {
+            self.hasRequestedInitialHistory = true
+            self.chatController.getInitialChatHistory()
+        }
+
+        DispatchQueue.main.asyncAfter(deadline: .now() + 4) { [capturedToken = self.room.token] in
+            // After we joined a room, check if there are offline messages for this particular room which need to be send
+            NCRoomsManager.sharedInstance().resendOfflineMessages(forToken: capturedToken, withCompletionBlock: nil)
+        }
+    }
+
+    func didLeaveRoom(notification: Notification) {
+        guard let token = notification.userInfo?["token"] as? String else { return }
+
+        if token != self.room.token {
+            return
+        }
+
+        if notification.userInfo?["error"] != nil {
+            // In case an error occurred when leaving the room, we assume we are still joined
+            return
+        }
+
+        self.hasJoinedRoom = false
+        self.disableRoomControls()
+        self.checkRoomControlsAvailability()
+    }
+
+    // MARK: - CallKit Manager notifications
+
+    func didFailRequestingCallTransaction(notification: Notification) {
+        guard let token = notification.userInfo?["roomToken"] as? String else { return }
+
+        if token != self.room.token {
+            return
+        }
+
+        DispatchQueue.main.async {
+            self.videoCallButton.hideIndicator()
+            self.voiceCallButton.hideIndicator()
+        }
+    }
+
+    // MARK: - Chat Controller notifications
+
+    // swiftlint:disable:next cyclomatic_complexity
+    func didReceiveInitialChatHistory(notification: Notification) {
+        DispatchQueue.main.async {
+            if notification.object as? NCChatController != self.chatController {
+                return
+            }
+
+            if self.shouldPresentLobbyView() {
+                self.hasRequestedInitialHistory = false
+                self.startObservingRoomLobbyFlag()
+
+                return
+            }
+
+            if let messages = notification.userInfo?["messages"] as? [NCChatMessage], !messages.isEmpty {
+
+                var indexPathUnreadMessageSeparator: IndexPath?
+                let lastMessage = messages.reversed().first(where: { !$0.isUpdateMessage })
+
+                self.appendMessages(messages: messages)
+
+                if let lastMessage, lastMessage.messageId > self.lastReadMessage {
+                    // Iterate backwards to find the correct location for the unread message separator
+                    for sectionIndex in self.dateSections.indices.reversed() {
+                        let dateSection: Date = self.dateSections[sectionIndex]
+
+                        guard var messages = self.messages[dateSection] else { continue }
+
+                        for messageIndex in messages.indices.reversed() {
+                            let message = messages[messageIndex]
+
+                            if message.messageId > self.lastReadMessage {
+                                continue
+                            }
+
+                            messages.insert(self.unreadMessagesSeparator, at: messageIndex + 1)
+                            self.messages[dateSection] = messages
+                            indexPathUnreadMessageSeparator = IndexPath(row: messageIndex + 1, section: sectionIndex)
+
+                            break
+                        }
+
+                        if indexPathUnreadMessageSeparator != nil {
+                            break
+                        }
+                    }
+
+                    self.lastReadMessage = lastMessage.messageId
+                }
+
+                let storedTemporaryMessages = self.chatController.getTemporaryMessages()
+
+                if !storedTemporaryMessages.isEmpty {
+                    self.insertMessages(messages: storedTemporaryMessages)
+
+                    if indexPathUnreadMessageSeparator != nil {
+                        // It is possible that temporary messages are added which add new sections
+                        // In this case the indexPath of the unreadMessageSeparator would be invalid and could lead to a crash
+                        // Therefore we need to make sure we got the correct indexPath here
+                        indexPathUnreadMessageSeparator = self.indexPathForUnreadMessageSeparator()
+                    }
+                }
+
+                self.tableView?.reloadData()
+
+                if let indexPathUnreadMessageSeparator {
+                    self.tableView?.scrollToRow(at: indexPathUnreadMessageSeparator, at: .middle, animated: false)
+                } else {
+                    self.tableView?.slk_scrollToBottom(animated: false)
+                }
+
+                self.updateToolbar(animated: false)
+            } else {
+                self.chatBackgroundView.placeholderView.isHidden = false
+            }
+
+            self.hasReceiveInitialHistory = true
+
+            if notification.userInfo?["error"] == nil {
+                self.chatController.startReceivingNewChatMessages()
+            } else {
+                self.offlineMode = true
+                self.chatController.getInitialChatHistoryForOfflineMode()
+            }
+        }
+    }
+
+    func didReceiveInitialChatHistoryOffline(notification: Notification) {
+        DispatchQueue.main.async {
+            if notification.object as? NCChatController != self.chatController {
+                return
+            }
+
+            if let messages = notification.userInfo?["messages"] as? [NCChatMessage], !messages.isEmpty {
+                self.appendMessages(messages: messages)
+                self.setOfflineFooterView()
+                self.tableView?.reloadData()
+                self.tableView?.slk_scrollToBottom(animated: false)
+                self.updateToolbar(animated: false)
+            } else {
+                self.chatBackgroundView.placeholderView.isHidden = false
+            }
+
+            let storedTemporaryMessages = self.chatController.getTemporaryMessages()
+
+            if !storedTemporaryMessages.isEmpty {
+
+                self.insertMessages(messages: storedTemporaryMessages)
+                self.tableView?.reloadData()
+                self.tableView?.slk_scrollToBottom(animated: false)
+                self.updateToolbar(animated: false)
+            }
+        }
+    }
+
+    func didReceiveChatHistory(notification: Notification) {
+        DispatchQueue.main.async {
+            if notification.object as? NCChatController != self.chatController {
+                return
+            }
+
+            if let messages = notification.userInfo?["messages"] as? [NCChatMessage], !messages.isEmpty {
+
+                let shouldAddBlockSeparator = notification.userInfo?["shouldAddBlockSeparator"] as? Bool ?? false
+
+                if let lastHistoryMessageIP = self.prependMessages(historyMessages: messages, addingBlockSeparator: shouldAddBlockSeparator),
+                   let tableView = self.tableView {
+
+                    self.tableView?.reloadData()
+
+                    if NCUtils.isValid(indexPath: lastHistoryMessageIP, forTableView: tableView) {
+                        self.tableView?.scrollToRow(at: lastHistoryMessageIP, at: .top, animated: false)
+                    }
+                }
+            }
+
+            if notification.userInfo?["noMoreStoredHistory"] as? Bool == true {
+                self.hasStoredHistory = false
+            }
+
+            self.retrievingHistory = false
+            self.hideLoadingHistoryView()
+        }
+    }
+
+    // swiftlint:disable:next cyclomatic_complexity
+    func didReceiveChatMessages(notification: Notification) {
+        // If we receive messages in the background, we should make sure that our update here completely run
+        let bgTask = BGTaskHelper.startBackgroundTask(withName: "didReceiveChatMessages")
+
+        DispatchQueue.main.async {
+            if notification.object as? NCChatController != self.chatController || notification.userInfo?["error"] != nil {
+                return
+            }
+
+            let firstNewMessagesAfterHistory = notification.userInfo?["firstNewMessagesAfterHistory"] as? Bool ?? false
+
+            if let messages = notification.userInfo?["messages"] as? [NCChatMessage], let tableView = self.tableView, !messages.isEmpty {
+                // Detect if we should scroll to new messages before we issue a reloadData
+                // Otherwise longer messages will prevent scrolling
+                let shouldScrollOnNewMessages = self.shouldScrollOnNewMessages()
+                let newMessagesContainVisibleMessages = messages.containsVisibleMessages()
+
+                // Use a Set here so we don't have to deal with duplicates
+                var insertIndexPaths: Set<IndexPath> = []
+                var insertSections: IndexSet = []
+                var reloadIndexPaths: Set<IndexPath> = []
+
+                var addedUnreadMessageSeparator = false
+
+                // Check if unread messages separator should be added (only if it's not already shown)
+                if firstNewMessagesAfterHistory, self.getLastRealMessage() != nil, self.indexPathForUnreadMessageSeparator() == nil, newMessagesContainVisibleMessages,
+                   let lastDateSection = self.dateSections.last, var messagesBeforeUpdate = self.messages[lastDateSection] {
+
+                    messagesBeforeUpdate.append(self.unreadMessagesSeparator)
+                    self.messages[lastDateSection] = messagesBeforeUpdate
+                    insertIndexPaths.insert(IndexPath(row: messagesBeforeUpdate.count - 1, section: self.dateSections.count - 1))
+                    addedUnreadMessageSeparator = true
+                }
+
+                self.appendMessages(messages: messages)
+
+                for newMessage in messages {
+                    // If we don't get an indexPath here, something is wrong with our appendMessages function
+                    let indexPath = self.indexPath(for: newMessage)!
+
+                    if indexPath.section >= tableView.numberOfSections {
+                        // New section -> insert the section
+                        insertSections.insert(indexPath.section)
+                    }
+
+                    if indexPath.section < tableView.numberOfSections, indexPath.row < tableView.numberOfRows(inSection: indexPath.section) {
+                        // This is a already known indexPath, so we want to reload the cell
+                        reloadIndexPaths.insert(indexPath)
+                    } else {
+                        // New indexPath -> insert it
+                        insertIndexPaths.insert(indexPath)
+                    }
+
+                    if newMessage.isUpdateMessage, let parentMessage = newMessage.parent, let parentPath = self.indexPath(for: parentMessage) {
+                        if parentPath.section < tableView.numberOfSections, parentPath.row < tableView.numberOfRows(inSection: parentPath.section) {
+                            // We received an update message to a message which is already part of our current data, therefore we need to reload it
+                            reloadIndexPaths.insert(parentPath)
+                        }
+                    }
+
+                    if let collapsedByMessage = newMessage.collapsedBy, let collapsedPath = self.indexPath(for: collapsedByMessage) {
+                        if collapsedPath.section < tableView.numberOfSections, collapsedPath.row < tableView.numberOfRows(inSection: collapsedPath.section) {
+                            // The current message is collapsed, so we need to make sure that the collapsedBy message is reloaded
+                            reloadIndexPaths.insert(collapsedPath)
+                        }
+                    }
+                }
+
+                tableView.performBatchUpdates {
+                    if !insertSections.isEmpty {
+                        tableView.insertSections(insertSections, with: .automatic)
+                    }
+
+                    if !insertIndexPaths.isEmpty {
+                        tableView.insertRows(at: Array(insertIndexPaths), with: .automatic)
+                    }
+
+                    if !reloadIndexPaths.isEmpty {
+                        tableView.reloadRows(at: Array(reloadIndexPaths), with: .none)
+                    }
+
+                } completion: { _ in
+                    // Remove unread messages separator when user writes a message
+                    if messages.containsUserMessage() {
+                        self.removeUnreadMessagesSeparator()
+                    }
+
+                    // Only scroll to unread message separator if we added it while processing the received messages
+                    // Otherwise we would scroll whenever a unread message separator is available
+                    if addedUnreadMessageSeparator, let indexPathUnreadMessageSeparator = self.indexPathForUnreadMessageSeparator() {
+                        tableView.scrollToRow(at: indexPathUnreadMessageSeparator, at: .middle, animated: true)
+                    } else if (shouldScrollOnNewMessages || messages.containsUserMessage()), let lastIndexPath = self.getLastRealMessage()?.indexPath {
+                        tableView.scrollToRow(at: lastIndexPath, at: .none, animated: true)
+                    } else if self.firstUnreadMessage == nil, newMessagesContainVisibleMessages, let firstNewMessage = messages.first {
+                        // This check is needed since several calls to receiveMessages API might be needed
+                        // (if the number of unread messages is bigger than the "limit" in receiveMessages request)
+                        // to receive all the unread messages.
+                        if firstNewMessage.timestamp >= Int(self.chatViewPresentedTimestamp) {
+                            self.showNewMessagesView(until: firstNewMessage)
+                        }
+                    }
+
+                    // Set last received message as last read message
+                    if let lastReceivedMessage = messages.last {
+                        self.lastReadMessage = lastReceivedMessage.messageId
+                    }
+                }
+
+                if firstNewMessagesAfterHistory {
+                    self.chatBackgroundView.loadingView.stopAnimating()
+                    self.chatBackgroundView.loadingView.isHidden = true
+                }
+
+                if self.highlightMessageId > 0, let indexPath = self.indexPathAndMessage(forMessageId: self.highlightMessageId)?.indexPath {
+                    self.highlightMessage(at: indexPath, with: .middle)
+                    self.highlightMessageId = 0
+                }
+            }
+
+            bgTask.stopBackgroundTask()
+        }
+    }
+
+    func didSendChatMessage(notification: Notification) {
+        DispatchQueue.main.async {
+            if notification.object as? NCChatController != self.chatController {
+                return
+            }
+
+            if notification.userInfo?["error"] == nil {
+                return
+            }
+
+            guard let message = notification.userInfo?["message"] as? String else { return }
+
+            if let referenceId = notification.userInfo?["referenceId"] as? String {
+                // Got a referenceId -> update the corresponding message
+                let isOfflineMessage = notification.userInfo?["isOfflineMessage"] as? Bool ?? false
+
+                self.modifyMessageWith(referenceId: referenceId) { message in
+                    message.sendingFailed = !isOfflineMessage
+                    message.isOfflineMessage = isOfflineMessage
+                }
+
+            } else {
+                // No referenceId -> show generic error
+                self.textView.text = message
+
+                let alert = UIAlertController(title: NSLocalizedString("Could not send the message", comment: ""),
+                                              message: NSLocalizedString("An error occurred while sending the message", comment: ""),
+                                              preferredStyle: .alert)
+
+                alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default))
+                NCUserInterfaceController.sharedInstance().presentAlertViewController(alert)
+            }
+        }
+    }
+
+    func didReceiveChatBlocked(notification: Notification) {
+        if notification.object as? NCChatController != self.chatController {
+            return
+        }
+
+        self.startObservingRoomLobbyFlag()
+    }
+
+    func didReceiveNewerCommonReadMessage(notification: Notification) {
+        if notification.object as? NCChatController != self.chatController {
+            return
+        }
+
+        guard let lastCommonReadMessage = notification.userInfo?["lastCommonReadMessage"] as? Int else { return }
+
+        if lastCommonReadMessage > self.room.lastCommonReadMessage {
+            self.room.lastCommonReadMessage = lastCommonReadMessage
+        }
+
+        self.checkLastCommonReadMessage()
+    }
+
+    func didReceiveCallStartedMessage(notification: Notification) {
+        if notification.object as? NCChatController != self.chatController {
+            return
+        }
+
+        self.room.hasCall = true
+        self.checkRoomControlsAvailability()
+    }
+
+    func didReceiveCallEndedMessage(notification: Notification) {
+        if notification.object as? NCChatController != self.chatController {
+            return
+        }
+
+        self.room.hasCall = false
+        self.checkRoomControlsAvailability()
+    }
+
+    func didReceiveUpdateMessage(notification: Notification) {
+        if notification.object as? NCChatController != self.chatController {
+            return
+        }
+
+        guard let message = notification.userInfo?["updateMessage"] as? NCChatMessage,
+              let updateMessage = message.parent
+        else { return }
+
+        self.updateMessage(withMessageId: updateMessage.messageId, updatedMessage: updateMessage)
+    }
+
+    func didReceiveHistoryCleared(notification: Notification) {
+        if notification.object as? NCChatController != self.chatController {
+            return
+        }
+
+        guard let message = notification.userInfo?["historyCleared"] as? NCChatMessage
+        else { return }
+
+        if self.chatController.hasOlderStoredMessagesThanMessageId(message.messageId) {
+            self.cleanChat()
+            self.chatController.clearHistoryAndResetChatController()
+            self.hasRequestedInitialHistory = false
+            self.chatController.getInitialChatHistory()
+        }
+    }
+
+    func didReceiveMessagesInBackground(notification: Notification) {
+        if notification.object as? NCChatController != self.chatController {
+            return
+        }
+
+        print("didReceiveMessagesInBackground")
+        self.checkForNewStoredMessages()
+    }
+
+    // MARK: - Database controller notifications
+
+    func didChangeRoomCapabilities(notification: Notification) {
+        guard let token = notification.userInfo?["roomToken"] as? String else { return }
+
+        if token != self.room.token {
+            return
+        }
+
+        self.tableView?.reloadData()
+        self.checkRoomControlsAvailability()
+    }
+
+    // MARK: - External signaling controller notifications
+
+    func didUpdateParticipants(notification: Notification) {
+        guard let token = notification.userInfo?["roomToken"] as? String else { return }
+
+        if token != self.room.token {
+            return
+        }
+
+        let serverSupportsConversationPermissions = NCDatabaseManager.sharedInstance().roomHasTalkCapability(kCapabilityConversationPermissions, for: room) ||
+                                                    NCDatabaseManager.sharedInstance().roomHasTalkCapability(kCapabilityDirectMentionFlag, for: room)
+
+        guard serverSupportsConversationPermissions else { return }
+
+        let activeAccount = NCDatabaseManager.sharedInstance().activeAccount()
+
+        // Retrieve the information about ourselves
+        guard let userDict = notification.userInfo?["users"] as? [[String: String]],
+              let appUserDict = userDict.first(where: { $0["userId"] == activeAccount.userId })
+        else { return }
+
+        // Check if we still have the same permissions
+
+        if let permissionsString = appUserDict["participantPermissions"],
+           let permissions = Int(permissionsString),
+           permissions != self.room.permissions {
+
+            // Need to update the room from the api because otherwise "canStartCall" is not updated correctly
+            NCRoomsManager.sharedInstance().updateRoom(self.room.token, withCompletionBlock: nil)
+        }
+    }
+
+    func processTypingNotification(notification: Notification, startedTyping started: Bool) {
+        guard let token = notification.userInfo?["roomToken"] as? String,
+              let sessionId = notification.userInfo?["sessionId"] as? String,
+              token == self.room.token
+        else { return }
+
+        // Waiting for https://github.com/nextcloud/spreed/issues/9726 to receive the correct displayname for guests
+        let displayName = notification.userInfo?["displayName"] as? String ?? NSLocalizedString("Guest", comment: "")
+
+        // Don't show a typing indicator for ourselves or if typing indicator setting is disabled
+        // Workaround: TypingPrivacy should be checked locally, not from the remote server, use serverCapabilities for now
+        // TODO: Remove workaround for federated typing indicators.
+        guard let serverCapabilities = NCDatabaseManager.sharedInstance().serverCapabilities(forAccountId: self.room.accountId)
+        else { return }
+
+        let activeAccount = NCDatabaseManager.sharedInstance().activeAccount()
+        let userId = notification.userInfo?["userId"] as? String
+        let isFederated = notification.userInfo?["isFederated"] as? Bool ?? false
+
+        // Since our own userId can exist on other servers, only suppress the notification if it's not federated
+        if (userId == activeAccount.userId && !isFederated) || serverCapabilities.typingPrivacy {
+            return
+        }
+
+        // For guests we use the sessionId as an identifier, for users we use the userId
+        var userIdentifier = sessionId
+
+        if let userId, !userId.isEmpty {
+            userIdentifier = userId
+        }
+
+        if started {
+            self.addTypingIndicator(withUserIdentifier: userIdentifier, andDisplayName: displayName)
+        } else {
+            self.removeTypingIndicator(withUserIdentifier: userIdentifier)
+        }
+    }
+
+    func didReceiveStartedTyping(notification: Notification) {
+        self.processTypingNotification(notification: notification, startedTyping: true)
+    }
+
+    func didReceiveStoppedTyping(notification: Notification) {
+        self.processTypingNotification(notification: notification, startedTyping: false)
+    }
+
+    func didReceiveParticipantJoin(notification: Notification) {
+        guard let token = notification.userInfo?["roomToken"] as? String,
+              let sessionId = notification.userInfo?["sessionId"] as? String,
+              token == self.room.token
+        else { return }
+
+        DispatchQueue.main.async {
+            if self.isTyping {
+                self.sendStartedTypingMessage(to: sessionId)
+            }
+        }
+    }
+
+    func didReceiveParticipantLeave(notification: Notification) {
+        guard let token = notification.userInfo?["roomToken"] as? String,
+              let sessionId = notification.userInfo?["sessionId"] as? String,
+              token == self.room.token
+        else { return }
+
+        // For guests we use the sessionId as an identifier, for users we use the userId
+        var userIdentifier = sessionId
+
+        if let userId = notification.userInfo?["userId"] as? String, !userId.isEmpty {
+            userIdentifier = userId
+        }
+
+        self.removeTypingIndicator(withUserIdentifier: userIdentifier)
+    }
+
+    // MARK: - Lobby functions
+
+    func startObservingRoomLobbyFlag() {
+        self.updateRoomInformation()
+
+        DispatchQueue.main.async {
+            self.lobbyCheckTimer?.invalidate()
+            self.lobbyCheckTimer = Timer.scheduledTimer(timeInterval: 5.0, target: self, selector: #selector(self.updateRoomInformation), userInfo: nil, repeats: true)
+        }
+    }
+
+    func updateRoomInformation() {
+        NCRoomsManager.sharedInstance().updateRoom(self.room.token, withCompletionBlock: nil)
+    }
+
+    func shouldPresentLobbyView() -> Bool {
+        let serverSupportsConversationPermissions =
+        NCDatabaseManager.sharedInstance().roomHasTalkCapability(kCapabilityConversationPermissions, for: room) ||
+        NCDatabaseManager.sharedInstance().roomHasTalkCapability(kCapabilityDirectMentionFlag, for: room)
+
+        if serverSupportsConversationPermissions, (self.room.permissions & NCPermission.canIgnoreLobby.rawValue) != 0 {
+            return false
+        }
+
+        return self.room.lobbyState == .moderatorsOnly && !self.room.canModerate
+    }
+
+    // MARK: - Chat functions
+
+    func couldRetrieveHistory() -> Bool {
+        return self.hasReceiveInitialHistory &&
+                !self.retrievingHistory &&
+                !self.dateSections.isEmpty
+                && self.hasStoredHistory
+    }
+
+    func checkLastCommonReadMessage() {
+        DispatchQueue.main.async {
+            guard let tableView = self.tableView,
+                  let indexPathsForVisibleRows = tableView.indexPathsForVisibleRows
+            else { return }
+
+            var reloadCells: [IndexPath] = []
+
+            for visibleIndexPath in indexPathsForVisibleRows {
+                if let message = self.message(for: visibleIndexPath),
+                   message.messageId > 0,
+                   message.messageId <= self.room.lastCommonReadMessage {
+
+                    reloadCells.append(visibleIndexPath)
+                }
+            }
+
+            if !reloadCells.isEmpty {
+                self.tableView?.beginUpdates()
+                self.tableView?.reloadRows(at: reloadCells, with: .none)
+                self.tableView?.endUpdates()
+            }
+        }
+    }
+
+    func checkForNewStoredMessages() {
+        // Get the last "real" message. For temporary messages the messageId would be 0
+        // which would load all stored messages of the current conversation
+        if let lastMessage = self.getLastRealMessage()?.message {
+            self.chatController.checkForNewMessages(fromMessageId: lastMessage.messageId)
+            self.checkLastCommonReadMessage()
+        }
+    }
+
+    // MARK: - Editing support
+
+    public override func didCommitTextEditing(_ sender: Any) {
+        if let editingMessage {
+            let activeAccount = NCDatabaseManager.sharedInstance().activeAccount()
+
+            let messageParametersJSONString = NCMessageParameter.messageParametersJSONString(from: self.mentionsDict) ?? ""
+            editingMessage.message = self.replaceMentionsDisplayNamesWithMentionsKeysInMessage(message: self.textView.text, parameters: messageParametersJSONString)
+            editingMessage.messageParametersJSONString = messageParametersJSONString
+
+            NCAPIController.sharedInstance().editChatMessage(inRoom: editingMessage.token, withMessageId: editingMessage.messageId, withMessage: editingMessage.sendingMessage, for: activeAccount) { messageDict, error, _ in
+                if error != nil {
+                    NotificationPresenter.shared().present(text: NSLocalizedString("Error occurred while editing a message", comment: ""), dismissAfterDelay: 5.0, includedStyle: .error)
+                    return
+                }
+
+                guard let messageDict,
+                      let parent = messageDict["parent"] as? [AnyHashable: Any],
+                      let updatedMessage = NCChatMessage(dictionary: parent, andAccountId: activeAccount.accountId)
+                else { return }
+
+                self.updateMessage(withMessageId: editingMessage.messageId, updatedMessage: updatedMessage)
+            }
+        }
+
+        super.didCommitTextEditing(sender)
+    }
+
+    // MARK: - ChatMessageTableViewCellDelegate delegate
+
+    override public func cellDidSelectedReaction(_ reaction: NCChatReaction!, for message: NCChatMessage!) {
+        self.addOrRemoveReaction(reaction: reaction, in: message)
+    }
+
+    // MARK: - ContextMenu (Long press on message)
+
+    func isMessageReplyable(message: NCChatMessage) -> Bool {
+        return message.isReplyable && !message.isDeleting
+    }
+
+    func isMessageReactable(message: NCChatMessage) -> Bool {
+        var isReactable = NCDatabaseManager.sharedInstance().roomHasTalkCapability(kCapabilityReactions, for: room)
+        isReactable = isReactable && !self.offlineMode
+        isReactable = isReactable && self.room.readOnlyState != .readOnly
+        isReactable = isReactable && !message.isDeletedMessage && !message.isCommandMessage && !message.sendingFailed && !message.isTemporary
+
+        return isReactable
+    }
+
+    func getSetReminderOptions(for message: NCChatMessage) -> [UIMenuElement] {
+        var reminderOptions: [UIMenuElement] = []
+        let now = Date()
+
+        let sunday = 1
+        let monday = 2
+        let friday = 6
+        let saturday = 7
+
+        let formatter = DateFormatter()
+        formatter.dateFormat = "EEE"
+
+        let setReminderCompletion: SetReminderForMessage = { (error: Error?) in
+            if error != nil {
+                NotificationPresenter.shared().present(text: NSLocalizedString("Error occurred when creating a reminder", comment: ""), dismissAfterDelay: 5.0, includedStyle: .error)
+            } else {
+                NotificationPresenter.shared().present(text: NSLocalizedString("Reminder was successfully set", comment: ""), dismissAfterDelay: 5.0, includedStyle: .success)
+            }
+        }
+
+        // Today
+        let laterTodayTime = NCUtils.today(withHour: 18, withMinute: 0, withSecond: 0)!
+        let laterToday = UIAction(title: NSLocalizedString("Later today", comment: "Remind me later today about that message"), subtitle: NCUtils.getTime(fromDate: laterTodayTime)) { _ in
+            let timestamp = String(Int(laterTodayTime.timeIntervalSince1970))
+            NCAPIController.sharedInstance().setReminderFor(message, withTimestamp: timestamp, withCompletionBlock: setReminderCompletion)
+        }
+
+        // Tomorrow
+        var tomorrowTime = NCUtils.today(withHour: 8, withMinute: 0, withSecond: 0)!
+        tomorrowTime = Calendar.current.date(byAdding: .day, value: 1, to: tomorrowTime)!
+        let tomorrow = UIAction(title: NSLocalizedString("Tomorrow", comment: "Remind me tomorrow about that message")) { _ in
+            let timestamp = String(Int(tomorrowTime.timeIntervalSince1970))
+            NCAPIController.sharedInstance().setReminderFor(message, withTimestamp: timestamp, withCompletionBlock: setReminderCompletion)
+        }
+        tomorrow.subtitle = "\(formatter.string(from: tomorrowTime)), \(NCUtils.getTime(fromDate: tomorrowTime))"
+
+        // This weekend
+        var weekendTime = NCUtils.today(withHour: 8, withMinute: 0, withSecond: 0)!
+        weekendTime = NCUtils.setWeekday(saturday, withDate: weekendTime)
+        let thisWeekend = UIAction(title: NSLocalizedString("This weekend", comment: "Remind me this weekend about that message")) { _ in
+            let timestamp = String(Int(weekendTime.timeIntervalSince1970))
+            NCAPIController.sharedInstance().setReminderFor(message, withTimestamp: timestamp, withCompletionBlock: setReminderCompletion)
+        }
+        thisWeekend.subtitle = "\(formatter.string(from: weekendTime)), \(NCUtils.getTime(fromDate: weekendTime))"
+
+        // Next week
+        var nextWeekTime = NCUtils.today(withHour: 8, withMinute: 0, withSecond: 0)!
+        nextWeekTime = Calendar.current.date(byAdding: .weekOfYear, value: 1, to: nextWeekTime)!
+        nextWeekTime = NCUtils.setWeekday(monday, withDate: nextWeekTime)
+        let nextWeek = UIAction(title: NSLocalizedString("Next week", comment: "Remind me next week about that message")) { _ in
+            let timestamp = String(Int(nextWeekTime.timeIntervalSince1970))
+            NCAPIController.sharedInstance().setReminderFor(message, withTimestamp: timestamp, withCompletionBlock: setReminderCompletion)
+        }
+        nextWeek.subtitle = "\(formatter.string(from: nextWeekTime)), \(NCUtils.getTime(fromDate: nextWeekTime))"
+
+        // Custom reminder
+        let customReminderAction = UIAction(title: NSLocalizedString("Pick date & time", comment: ""), image: .init(systemName: "calendar.badge.clock")) { [weak self] _ in
+            DispatchQueue.main.async {
+                guard let self else { return }
+                self.interactingMessage = message
+                self.lastMessageBeforeInteraction = self.tableView?.indexPathsForVisibleRows?.last
+
+                let startingDate = Calendar.current.date(byAdding: .hour, value: 1, to: now)
+                let minimumDate = Calendar.current.date(byAdding: .minute, value: 15, to: now)
+
+                self.datePickerTextField.getDate(startingDate: startingDate, minimumDate: minimumDate) { selectedDate in
+                    let timestamp = String(Int(selectedDate.timeIntervalSince1970))
+                    NCAPIController.sharedInstance().setReminderFor(message, withTimestamp: timestamp, withCompletionBlock: setReminderCompletion)
+                }
+            }
+        }
+
+        let customReminder = UIMenu(options: .displayInline, children: [customReminderAction])
+
+        // Hide "Later today" when it's past 5pm
+        if Calendar.current.component(.hour, from: now) < 17 {
+            reminderOptions.append(laterToday)
+        }
+
+        reminderOptions.append(tomorrow)
+
+        // Only show "This weekend" for Mon-Tue
+        let nowWeekday = Calendar.current.component(.weekday, from: now)
+        if nowWeekday != friday && nowWeekday != saturday && nowWeekday != sunday {
+            reminderOptions.append(thisWeekend)
+        }
+
+        // "Next week" should be hidden on sunday
+        if nowWeekday != sunday {
+            reminderOptions.append(nextWeek)
+        }
+
+        reminderOptions.append(customReminder)
+
+        return reminderOptions
+    }
+
+    override func getContextMenuAccessoryView(forMessage message: NCChatMessage, forIndexPath indexPath: IndexPath, withCellHeight cellHeight: CGFloat) -> UIView? {
+        let hasChatPermissions = !NCDatabaseManager.sharedInstance().roomHasTalkCapability(kCapabilityChatPermission, for: room) || (self.room.permissions & NCPermission.chat.rawValue) != 0
+
+        guard hasChatPermissions && self.isMessageReactable(message: message) else { return nil }
+
+        let reactionViewPadding = 10
+        let emojiButtonPadding = 10
+        let emojiButtonSize = 48
+        let frequentlyUsedEmojis = NCDatabaseManager.sharedInstance().activeAccount().frequentlyUsedEmojis
+
+        let totalEmojiButtonWidth = frequentlyUsedEmojis.count * emojiButtonSize
+        let totalEmojiButtonPadding = frequentlyUsedEmojis.count * emojiButtonPadding
+        let addButtonWidth = emojiButtonSize + emojiButtonPadding
+
+        // We need to add an extra padding to the right so the buttons are correctly padded
+        let reactionViewWidth = totalEmojiButtonWidth + totalEmojiButtonPadding + addButtonWidth + emojiButtonPadding
+        let reactionView = UIView(frame: .init(x: 0, y: Int(cellHeight) + reactionViewPadding, width: reactionViewWidth, height: emojiButtonSize))
+
+        var positionX = emojiButtonPadding
+
+        for emoji in frequentlyUsedEmojis {
+            let emojiShortcutButton = UIButton(type: .system)
+            emojiShortcutButton.frame = CGRect(x: positionX, y: 0, width: emojiButtonSize, height: emojiButtonSize)
+            emojiShortcutButton.layer.cornerRadius = CGFloat(emojiButtonSize) / 2
+
+            emojiShortcutButton.titleLabel?.font = .systemFont(ofSize: 20)
+            emojiShortcutButton.setTitle(emoji, for: .normal)
+            emojiShortcutButton.backgroundColor = .systemBackground
+
+            emojiShortcutButton.addAction { [weak self] in
+                guard let self else { return }
+                self.tableView?.contextMenuInteraction?.dismissMenu()
+
+                self.contextMenuActionBlock = {
+                    self.addReaction(reaction: emoji, to: message)
+                }
+            }
+
+            // Disable shortcuts, if we already reacted with that emoji
+            for reaction in message.reactionsArray() {
+                if reaction.reaction == emoji && reaction.userReacted {
+                    emojiShortcutButton.isEnabled = false
+                    emojiShortcutButton.alpha = 0.4
+                    break
+                }
+            }
+
+            reactionView.addSubview(emojiShortcutButton)
+            positionX += emojiButtonSize + emojiButtonPadding
+        }
+
+        let addReactionButton = UIButton(type: .system)
+        addReactionButton.frame = CGRect(x: positionX, y: 0, width: emojiButtonSize, height: emojiButtonSize)
+        addReactionButton.layer.cornerRadius = CGFloat(emojiButtonSize) / 2
+
+        addReactionButton.titleLabel?.font = .systemFont(ofSize: 22)
+        addReactionButton.setImage(.init(systemName: "plus"), for: .normal)
+        addReactionButton.tintColor = .label
+        addReactionButton.backgroundColor = .systemBackground
+        addReactionButton.addAction { [weak self] in
+            guard let self else { return }
+            self.tableView?.contextMenuInteraction?.dismissMenu()
+
+            self.contextMenuActionBlock = {
+                self.didPressAddReaction(for: message, at: indexPath)
+            }
+        }
+
+        reactionView.addSubview(addReactionButton)
+
+        // The reactionView will be shown after the animation finishes, otherwise we see the view already when animating and this looks odd
+        reactionView.alpha = 0
+        reactionView.layer.cornerRadius = CGFloat(emojiButtonSize) / 2
+        reactionView.backgroundColor = .systemBackground
+
+        return reactionView
+    }
+
+    // swiftlint:disable:next cyclomatic_complexity
+    public override func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
+        if tableView == self.autoCompletionView {
+            return nil
+        }
+
+        let cell = tableView.cellForRow(at: indexPath)
+
+        // Show reactionSummary for legacy cells
+        if let cell = cell as? ChatTableViewCell {
+            let pointInCell = tableView.convert(point, to: cell)
+            let reactionView = cell.contentView.subviews.first(where: { $0 is ReactionsView && $0.frame.contains(pointInCell) })
+
+            if reactionView != nil {
+                self.showReactionsSummary(of: cell.message)
+                return nil
+            }
+        }
+
+        if let cell = cell as? BaseChatTableViewCell {
+            let pointInCell = tableView.convert(point, to: cell)
+            let pointInReactionPart = cell.convert(pointInCell, to: cell.reactionPart)
+            let reactionView = cell.reactionPart.subviews.first(where: { $0 is ReactionsView && $0.frame.contains(pointInReactionPart) })
+
+            if reactionView != nil, let message = cell.message {
+                self.showReactionsSummary(of: message)
+                return nil
+            }
+        }
+
+        guard let message = self.message(for: indexPath) else { return nil }
+
+        if message.isSystemMessage || message.isDeletedMessage || message.messageId == kUnreadMessagesSeparatorIdentifier {
+            return nil
+        }
+
+        var actions: [UIMenuElement] = []
+        var informationalActions: [UIMenuElement] = []
+        let activeAccount = NCDatabaseManager.sharedInstance().activeAccount()
+        let hasChatPermissions = !NCDatabaseManager.sharedInstance().roomHasTalkCapability(kCapabilityChatPermission, for: room) || (self.room.permissions & NCPermission.chat.rawValue) != 0
+
+        // Show edit information
+        if let lastEditActorDisplayName = message.lastEditActorDisplayName, message.lastEditTimestamp > 0 {
+            let timestampDate = Date(timeIntervalSince1970: TimeInterval(message.lastEditTimestamp))
+
+            let editInfo = UIAction(title: NSLocalizedString("Edited by", comment: "A message was edited by ...") + " " + lastEditActorDisplayName, attributes: [.disabled], handler: {_ in })
+            editInfo.subtitle = NCUtils.readableTimeAndDate(fromDate: timestampDate)
+
+            informationalActions.append(editInfo)
+        }
+
+        // Show silent send information
+        if message.isSilent {
+            let silentInfo = UIAction(title: NSLocalizedString("Sent without notification", comment: "A message has been sent without notifications"), attributes: [.disabled], handler: {_ in })
+            silentInfo.image = UIImage(systemName: "bell.slash")
+
+            informationalActions.append(silentInfo)
+        }
+
+        if !informationalActions.isEmpty {
+            actions.append(UIMenu(options: [.displayInline], children: informationalActions))
+        }
+
+        // Reply option
+        if self.isMessageReplyable(message: message), hasChatPermissions, !self.textInputbar.isEditing {
+            actions.append(UIAction(title: NSLocalizedString("Reply", comment: ""), image: .init(systemName: "arrowshape.turn.up.left")) { _ in
+                self.didPressReply(for: message)
+            })
+        }
+
+        // Show "Add reaction" when running on MacOS because we don't have an accessory view
+        if self.isMessageReactable(message: message), hasChatPermissions, NCUtils.isiOSAppOnMac() {
+            actions.append(UIAction(title: NSLocalizedString("Add reaction", comment: ""), image: .init(systemName: "face.smiling")) { _ in
+                self.didPressAddReaction(for: message, at: indexPath)
+            })
+        }
+
+        // Reply-privately option (only to other users and not in one-to-one)
+        if self.isMessageReplyable(message: message), self.room.type != .oneToOne, message.actorType == "users", message.actorId != activeAccount.userId {
+            actions.append(UIAction(title: NSLocalizedString("Reply privately", comment: ""), image: .init(systemName: "person")) { _ in
+                self.didPressReplyPrivately(for: message)
+            })
+        }
+
+        // Forward option (only normal messages for now)
+        if message.file() == nil, message.poll == nil, !message.isDeletedMessage {
+            actions.append(UIAction(title: NSLocalizedString("Forward", comment: ""), image: .init(systemName: "arrowshape.turn.up.right")) { _ in
+                self.didPressForward(for: message)
+            })
+        }
+
+        // Note to self
+        if message.file() == nil, message.poll == nil, !message.isDeletedMessage, room.type != .noteToSelf,
+           NCDatabaseManager.sharedInstance().roomHasTalkCapability(kCapabilityNoteToSelf, for: room) {
+            actions.append(UIAction(title: NSLocalizedString("Note to self", comment: ""), image: .init(systemName: "square.and.pencil")) { _ in
+                self.didPressNoteToSelf(for: message)
+            })
+        }
+
+        // Remind me later
+        if !message.sendingFailed, !message.isOfflineMessage, NCDatabaseManager.sharedInstance().roomHasTalkCapability(kCapabilityRemindMeLater, for: room) {
+            let deferredMenuElement = UIDeferredMenuElement.uncached { [weak self] completion in
+                NCAPIController.sharedInstance().getReminderFor(message) { [weak self] response, error in
+                    guard let self else { return }
+
+                    var menuOptions: [UIMenuElement] = []
+                    menuOptions.append(contentsOf: self.getSetReminderOptions(for: message))
+
+                    if error == nil,
+                       let responseDict = response as? [String: Any],
+                       let timestamp = responseDict["timestamp"] as? Int {
+
+                        // There's already an existing reminder set for this message
+                        // -> offer a delete option
+                        let timestampDate = Date(timeIntervalSince1970: TimeInterval(timestamp))
+
+                        let clearAction = UIAction(title: NSLocalizedString("Clear reminder", comment: ""), image: .init(systemName: "trash")) { _ in
+                            NCAPIController.sharedInstance().deleteReminder(for: message) { error in
+                                if error == nil {
+                                    NotificationPresenter.shared().present(text: NSLocalizedString("Reminder was successfully cleared", comment: ""), dismissAfterDelay: 5.0, includedStyle: .success)
+                                } else {
+                                    NotificationPresenter.shared().present(text: NSLocalizedString("Failed to clear reminder", comment: ""), dismissAfterDelay: 5.0, includedStyle: .error)
+                                }
+                            }
+                        }
+                        clearAction.subtitle = NCUtils.readableDateTime(fromDate: timestampDate)
+                        clearAction.attributes = .destructive
+
+                        menuOptions.append(UIMenu(options: .displayInline, children: [clearAction]))
+                    }
+
+                    completion(menuOptions)
+                }
+            }
+
+            actions.append(UIMenu(title: NSLocalizedString("Set reminder", comment: "Remind me later about that message"),
+                                  image: .init(systemName: "alarm"),
+                                  children: [deferredMenuElement]))
+        }
+
+        // Re-send option
+        if (message.sendingFailed || message.isOfflineMessage) && hasChatPermissions {
+            actions.append(UIAction(title: NSLocalizedString("Resend", comment: ""), image: .init(systemName: "arrow.clockwise")) { _ in
+                self.didPressResend(for: message)
+            })
+        }
+
+        // Copy option
+        actions.append(UIAction(title: NSLocalizedString("Copy", comment: ""), image: .init(systemName: "square.on.square")) { _ in
+            self.didPressCopy(for: message)
+        })
+
+        // Translate
+        if !self.offlineMode, NCDatabaseManager.sharedInstance().hasAvailableTranslations(forAccountId: activeAccount.accountId) {
+            actions.append(UIAction(title: NSLocalizedString("Translate", comment: ""), image: .init(systemName: "character.book.closed")) { _ in
+                self.didPressTranslate(for: message)
+            })
+        }
+
+        // Open in nextcloud option
+        if !self.offlineMode, message.file() != nil {
+            let openInNextcloudTitle = String(format: NSLocalizedString("Open in %@", comment: ""), filesAppName)
+            actions.append(UIAction(title: openInNextcloudTitle, image: .init(named: "logo-action")?.withRenderingMode(.alwaysTemplate)) { _ in
+                self.didPressOpenInNextcloud(for: message)
+            })
+        }
+
+        // Transcribe voice-message
+        if message.messageType == kMessageTypeVoiceMessage {
+            let transcribeTitle = NSLocalizedString("Transcribe", comment: "TRANSLATORS this is for transcribing a voice message to text")
+            actions.append(UIAction(title: transcribeTitle, image: .init(systemName: "text.bubble")) { _ in
+                self.didPressTranscribeVoiceMessage(for: message)
+            })
+        }
+
+        var destructiveMenuActions: [UIMenuElement] = []
+
+        // Edit option
+        if message.isEditable(for: activeAccount, in: self.room) && hasChatPermissions {
+            destructiveMenuActions.append(UIAction(title: NSLocalizedString("Edit", comment: "Edit a message or room participants"), image: .init(systemName: "pencil")) { _ in
+                self.didPressEdit(for: message)
+            })
+        }
+
+        // Delete option
+        if message.sendingFailed || message.isOfflineMessage || (message.isDeletable(for: activeAccount, in: self.room) && hasChatPermissions) {
+            destructiveMenuActions.append(UIAction(title: NSLocalizedString("Delete", comment: ""), image: .init(systemName: "trash"), attributes: .destructive) { _ in
+                self.didPressDelete(for: message)
+            })
+        }
+
+        if !destructiveMenuActions.isEmpty {
+            actions.append(UIMenu(options: [.displayInline], children: destructiveMenuActions))
+        }
+
+        let menu = UIMenu(children: actions)
+
+        let configuration = UIContextMenuConfiguration(identifier: indexPath as NSIndexPath) {
+            return nil
+        } actionProvider: { _ in
+            return menu
+        }
+
+        return configuration
+    }
+
+    // MARK: - NCChatTitleViewDelegate
+
+    public override func chatTitleViewTapped(_ titleView: NCChatTitleView!) {
+        guard let roomInfoVC = RoomInfoTableViewController(for: self.room, from: self) else { return }
+        roomInfoVC.hideDestructiveActions = self.presentedInCall
+
+        if let splitViewController = NCUserInterfaceController.sharedInstance().mainViewController {
+            if !splitViewController.isCollapsed {
+                roomInfoVC.modalPresentationStyle = .pageSheet
+                let navController = UINavigationController(rootViewController: roomInfoVC)
+                self.present(navController, animated: true)
+            } else {
+                self.navigationController?.pushViewController(roomInfoVC, animated: true)
+            }
+        } else {
+            self.navigationController?.pushViewController(roomInfoVC, animated: true)
+        }
+
+        // When returning from RoomInfoTableViewController the default keyboard will be shown, so the height might be wrong -> make sure the keyboard is hidden
+        self.dismissKeyboard(true)
+    }
+}

+ 18 - 0
NextcloudTalk/ChatViewControllerExtension.swift

@@ -0,0 +1,18 @@
+//
+// SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+
+import Foundation
+
+extension Notification.Name {
+    static let NCChatViewControllerReplyPrivatelyNotification = Notification.Name(rawValue: "NCChatViewControllerReplyPrivatelyNotification")
+    static let NCChatViewControllerForwardNotification = Notification.Name(rawValue: "NCChatViewControllerForwardNotification")
+    static let NCChatViewControllerTalkToUserNotification = Notification.Name(rawValue: "NCChatViewControllerTalkToUserNotification")
+}
+
+@objc extension NSNotification {
+    public static let NCChatViewControllerReplyPrivatelyNotification = Notification.Name.NCChatViewControllerReplyPrivatelyNotification
+    public static let NCChatViewControllerForwardNotification = Notification.Name.NCChatViewControllerForwardNotification
+    public static let NCChatViewControllerTalkToUserNotification = Notification.Name.NCChatViewControllerTalkToUserNotification
+}

+ 100 - 0
NextcloudTalk/ColorGenerator.swift

@@ -0,0 +1,100 @@
+//
+// SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+
+// See https://github.com/nextcloud/nextcloud-vue/blob/56b79afae93f4701a0cb933bfeb7b4a2fbd590fb/src/functions/usernameToColor/usernameToColor.js
+// and https://github.com/nextcloud/nextcloud-vue/blob/56b79afae93f4701a0cb933bfeb7b4a2fbd590fb/src/utils/GenColors.js
+
+import Foundation
+import CryptoKit
+
+@objcMembers class ColorGenerator: NSObject {
+
+    public static let shared = ColorGenerator()
+
+    private let steps = 6
+    private let finalPalette: [UIColor]
+
+    // See: https://stackoverflow.com/a/22334560
+    private static let multiplier = CGFloat(255.999999)
+
+    private override init() {
+        finalPalette = ColorGenerator.genColors(steps)
+
+        super.init()
+    }
+
+    private static func stepCalc(_ steps: Int, _ ends: [UIColor]) -> [CGFloat] {
+        var step: [CGFloat] = [0, 0, 0]
+
+        var red0: CGFloat = 0
+        var green0: CGFloat = 0
+        var blue0: CGFloat = 0
+
+        var red1: CGFloat = 0
+        var green1: CGFloat = 0
+        var blue1: CGFloat = 0
+
+        ends[0].getRed(&red0, green: &green0, blue: &blue0, alpha: nil)
+        ends[1].getRed(&red1, green: &green1, blue: &blue1, alpha: nil)
+
+        step[0] = (red1 - red0) / CGFloat(steps)
+        step[1] = (green1 - green0) / CGFloat(steps)
+        step[2] = (blue1 - blue0) / CGFloat(steps)
+
+        return step
+    }
+
+    private static func mixPalette(_ steps: Int, _ color1: UIColor, _ color2: UIColor) -> [UIColor] {
+        var palette: [UIColor] = [color1]
+
+        let step = stepCalc(steps, [color1, color2])
+
+        for i in 1...steps - 1 {
+            var red: CGFloat = 0
+            var green: CGFloat = 0
+            var blue: CGFloat = 0
+
+            color1.getRed(&red, green: &green, blue: &blue, alpha: nil)
+            let r = abs(red + step[0] * CGFloat(i))
+            let g = abs(green + step[1] * CGFloat(i))
+            let b = abs(blue + step[2] * CGFloat(i))
+
+            palette.append(UIColor(red: r, green: g, blue: b, alpha: 1))
+        }
+
+        return palette
+    }
+
+    public static func genColors(_ steps: Int) -> [UIColor] {
+        let red = UIColor(red: 182 / multiplier, green: 70 / multiplier, blue: 157 / multiplier, alpha: 1)
+        let yellow = UIColor(red: 221 / multiplier, green: 203 / multiplier, blue: 85 / multiplier, alpha: 1)
+        let blue = UIColor(red: 0, green: 130 / multiplier, blue: 201 / multiplier, alpha: 1)
+
+        var palette1 = mixPalette(steps, red, yellow)
+        let palette2 = mixPalette(steps, yellow, blue)
+        let palette3 = mixPalette(steps, blue, red)
+
+        palette1.append(contentsOf: palette2)
+        palette1.append(contentsOf: palette3)
+
+        return palette1
+    }
+
+    public func usernameToColor(_ username: String) -> UIColor {
+        let hash = username.lowercased()
+        var hashInt = 0
+
+        if let usernameData = hash.data(using: .utf8) {
+            let md5Hash = Insecure.MD5.hash(data: usernameData)
+            let t = md5Hash.map { String(format: "%02hhx", $0) }.joined()
+            hashInt = t.map { Int(String($0), radix: 16)! % 16}.reduce(0, +)
+        }
+
+        let maximum = steps * 3
+        hashInt = hashInt % maximum
+
+        return finalPalette[hashInt]
+    }
+}

+ 93 - 0
NextcloudTalk/ContactsSearchResultTableViewContoller.swift

@@ -0,0 +1,93 @@
+//
+// SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+
+import UIKit
+
+@objcMembers class ContactsSearchResultTableViewController: UITableViewController {
+
+    var indexes: [String] = []
+    var contacts: [String: [NCUser]] = [:]
+
+    let tableBackgroundView = PlaceholderView(for: .insetGrouped)!
+
+    override init(style: UITableView.Style) {
+        super.init(style: style)
+    }
+
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+
+    override func viewDidLoad() {
+        super.viewDidLoad()
+
+        self.tableView.register(UINib(nibName: kContactsTableCellNibName, bundle: nil), forCellReuseIdentifier: kContactCellIdentifier)
+
+        tableBackgroundView.setImage(UIImage(named: "contacts-placeholder"))
+        tableBackgroundView.placeholderTextView.text = NSLocalizedString("No results found", comment: "")
+        self.showSearchingUI()
+        self.tableView.backgroundView = tableBackgroundView
+
+    }
+
+    // MARK: - UI
+
+    func showSearchingUI() {
+        tableBackgroundView.placeholderView.isHidden = true
+        tableBackgroundView.loadingView.startAnimating()
+        tableBackgroundView.loadingView.isHidden = false
+    }
+
+    func hideSearchingUI() {
+        tableBackgroundView.loadingView.stopAnimating()
+        tableBackgroundView.loadingView.isHidden = true
+    }
+
+    func setContacts(_ contacts:[String: [NCUser]], indexes:[String]) {
+        self.contacts = contacts
+        self.indexes = indexes
+        self.tableView.reloadData()
+    }
+
+    func setSearchResultContacts(_ contacts:[String: [NCUser]], indexes:[String]) {
+        self.hideSearchingUI()
+        self.tableBackgroundView.placeholderView.isHidden = !contacts.isEmpty
+        self.setContacts(contacts, indexes: indexes)
+    }
+
+    // MARK: - TableView
+
+    override func numberOfSections(in tableView: UITableView) -> Int {
+        return indexes.count
+    }
+
+    override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
+        return indexes[section]
+    }
+
+    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+        let index = indexes[section]
+        return contacts[index]?.count ?? 0
+    }
+
+    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
+
+        let index = indexes[indexPath.section]
+        let contactsForIndex = contacts[index]
+        guard let contact = contactsForIndex?[indexPath.row] else {
+            return UITableViewCell()
+        }
+
+        let contactCell = tableView.dequeueReusableCell(withIdentifier: kContactCellIdentifier) as? ContactsTableViewCell ??
+        ContactsTableViewCell(style: .default, reuseIdentifier: kContactCellIdentifier)
+
+        contactCell.labelTitle.text = contact.name
+
+        let contactType = contact.source as String
+        contactCell.contactImage.setActorAvatar(forId: contact.userId, withType: contactType, withDisplayName: contact.name, withRoomToken: nil)
+
+        return contactCell
+    }
+}

+ 27 - 0
NextcloudTalk/ContactsTableViewCell.h

@@ -0,0 +1,27 @@
+/**
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#import <UIKit/UIKit.h>
+
+@class AvatarImageView;
+
+extern NSString *const kContactCellIdentifier;
+extern NSString *const kContactsTableCellNibName;
+
+extern CGFloat const kContactsTableCellHeight;
+extern CGFloat const kContactsTableCellTitleFontSize;
+
+@interface ContactsTableViewCell : UITableViewCell
+
+@property(nonatomic, weak) IBOutlet AvatarImageView *contactImage;
+@property(nonatomic, weak) IBOutlet UILabel *labelTitle;
+@property (weak, nonatomic) IBOutlet UIImageView *userStatusImageView;
+@property (weak, nonatomic) IBOutlet UILabel *userStatusMessageLabel;
+
+- (void)setUserStatus:(NSString *)userStatus;
+- (void)setUserStatusMessage:(NSString * _Nullable)userStatusMessage withIcon:(NSString * _Nullable)userStatusIcon;
+- (void)setUserStatusIconWithImage:(UIImage * _Nullable)image;
+
+@end

+ 94 - 0
NextcloudTalk/ContactsTableViewCell.m

@@ -0,0 +1,94 @@
+/**
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#import "ContactsTableViewCell.h"
+#import "NCAppBranding.h"
+
+#import "NextcloudTalk-Swift.h"
+
+NSString *const kContactCellIdentifier = @"ContactCellIdentifier";
+NSString *const kContactsTableCellNibName = @"ContactsTableViewCell";
+
+CGFloat const kContactsTableCellHeight = 72.0f;
+CGFloat const kContactsTableCellTitleFontSize = 17.0f;
+
+@implementation ContactsTableViewCell
+
+- (void)awakeFromNib {
+    [super awakeFromNib];
+    
+    self.contactImage.layer.cornerRadius = 24.0;
+    self.contactImage.layer.masksToBounds = YES;
+    self.contactImage.backgroundColor = [NCAppBranding placeholderColor];
+    self.contactImage.contentMode = UIViewContentModeScaleToFill;
+}
+
+- (void)setSelected:(BOOL)selected animated:(BOOL)animated {
+    [super setSelected:selected animated:animated];
+
+    // Configure the view for the selected state
+}
+
+- (void)prepareForReuse
+{
+    [super prepareForReuse];
+    
+    // Fix problem of rendering downloaded image in a reused cell
+    [self.contactImage cancelCurrentRequest];
+    self.contactImage.image = nil;
+    
+    self.userStatusImageView.image = nil;
+    self.userStatusImageView.backgroundColor = [UIColor clearColor];
+    
+    self.userStatusMessageLabel.text = @"";
+    self.userStatusMessageLabel.hidden = YES;
+    
+    self.labelTitle.text = @"";
+    self.labelTitle.textColor = [UIColor labelColor];
+    
+    self.labelTitle.font = [UIFont systemFontOfSize:kContactsTableCellTitleFontSize weight:UIFontWeightRegular];
+}
+
+- (void)setUserStatus:(NSString *)userStatus
+{
+    UIImage *statusImage = nil;
+    if ([userStatus isEqualToString:@"online"]) {
+        statusImage = [UIImage imageNamed:@"user-status-online"];
+    } else if ([userStatus isEqualToString:@"away"]) {
+        statusImage = [UIImage imageNamed:@"user-status-away"];
+    } else if ([userStatus isEqualToString:@"dnd"]) {
+        statusImage = [UIImage imageNamed:@"user-status-dnd"];
+    }
+    
+    if (statusImage) {
+        [self setUserStatusIconWithImage:statusImage];
+    }
+}
+
+- (void)setUserStatusIconWithImage:(UIImage *)image
+{
+    [_userStatusImageView setImage:image];
+    _userStatusImageView.contentMode = UIViewContentModeCenter;
+    _userStatusImageView.layer.cornerRadius = 10;
+    _userStatusImageView.clipsToBounds = YES;
+
+    // When a background color is set directly to the cell it seems that there is no background configuration.
+    _userStatusImageView.backgroundColor = (self.backgroundColor) ? self.backgroundColor : [[self backgroundConfiguration] backgroundColor];
+}
+
+- (void)setUserStatusMessage:(NSString *)userStatusMessage withIcon:(NSString *)userStatusIcon
+{
+    if (userStatusMessage && ![userStatusMessage isEqualToString:@""]) {
+        self.userStatusMessageLabel.text = userStatusMessage;
+        if (userStatusIcon && ![userStatusIcon isEqualToString:@""]) {
+            self.userStatusMessageLabel.text = [NSString stringWithFormat:@"%@ %@", userStatusIcon, userStatusMessage];
+        }
+        self.userStatusMessageLabel.hidden = NO;
+    } else {
+        self.userStatusMessageLabel.hidden = YES;
+    }
+}
+
+@end

+ 87 - 0
NextcloudTalk/ContactsTableViewCell.xib

@@ -0,0 +1,87 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="21701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
+    <device id="retina4_7" orientation="portrait" appearance="light"/>
+    <dependencies>
+        <deployment identifier="iOS"/>
+        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21678"/>
+        <capability name="Safe area layout guides" minToolsVersion="9.0"/>
+        <capability name="System colors in document resources" minToolsVersion="11.0"/>
+        <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
+    </dependencies>
+    <objects>
+        <placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
+        <placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
+        <tableViewCell contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" rowHeight="72" id="KGk-i7-Jjw" customClass="ContactsTableViewCell">
+            <rect key="frame" x="0.0" y="0.0" width="320" height="72"/>
+            <autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxX="YES" flexibleMaxY="YES"/>
+            <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="KGk-i7-Jjw" id="H2p-sc-9uM">
+                <rect key="frame" x="0.0" y="0.0" width="320" height="72"/>
+                <autoresizingMask key="autoresizingMask"/>
+                <subviews>
+                    <imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="5eI-x8-VQH" customClass="AvatarImageView" customModule="NextcloudTalk" customModuleProvider="target">
+                        <rect key="frame" x="12" y="12" width="48" height="48"/>
+                        <color key="backgroundColor" red="0.83529411760000005" green="0.83529411760000005" blue="0.83529411760000005" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
+                        <constraints>
+                            <constraint firstAttribute="height" constant="48" id="9HH-HQ-ST0"/>
+                            <constraint firstAttribute="width" constant="48" id="Ic4-Hj-oK9"/>
+                        </constraints>
+                    </imageView>
+                    <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="vfg-pq-jRm">
+                        <rect key="frame" x="45" y="48" width="20" height="20"/>
+                        <constraints>
+                            <constraint firstAttribute="width" constant="20" id="Bil-ZR-myw"/>
+                            <constraint firstAttribute="height" constant="20" id="b9y-Pf-33a"/>
+                        </constraints>
+                    </imageView>
+                    <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" distribution="fillProportionally" translatesAutoresizingMaskIntoConstraints="NO" id="ifa-Ff-Eac">
+                        <rect key="frame" x="75" y="12" width="230" height="48"/>
+                        <subviews>
+                            <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumFontSize="17" adjustsLetterSpacingToFitWidth="YES" translatesAutoresizingMaskIntoConstraints="NO" id="nOA-bG-zXZ">
+                                <rect key="frame" x="0.0" y="0.0" width="230" height="14"/>
+                                <fontDescription key="fontDescription" type="system" pointSize="17"/>
+                                <nil key="textColor"/>
+                                <nil key="highlightedColor"/>
+                            </label>
+                            <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumFontSize="13" adjustsLetterSpacingToFitWidth="YES" translatesAutoresizingMaskIntoConstraints="NO" id="27H-pY-tFT">
+                                <rect key="frame" x="0.0" y="14" width="230" height="34"/>
+                                <fontDescription key="fontDescription" type="system" pointSize="15"/>
+                                <color key="textColor" systemColor="secondaryLabelColor"/>
+                                <nil key="highlightedColor"/>
+                            </label>
+                        </subviews>
+                        <constraints>
+                            <constraint firstAttribute="height" constant="48" id="ADo-md-pcR"/>
+                            <constraint firstAttribute="width" relation="greaterThanOrEqual" constant="230" id="jaj-pL-S4K"/>
+                        </constraints>
+                    </stackView>
+                </subviews>
+                <viewLayoutGuide key="safeArea" id="Pye-fe-c7r"/>
+                <constraints>
+                    <constraint firstItem="ifa-Ff-Eac" firstAttribute="bottom" secondItem="H2p-sc-9uM" secondAttribute="bottom" constant="-12" id="83b-zt-HfV"/>
+                    <constraint firstItem="5eI-x8-VQH" firstAttribute="bottom" secondItem="H2p-sc-9uM" secondAttribute="bottom" constant="-12" id="AVW-ML-8YO"/>
+                    <constraint firstItem="ifa-Ff-Eac" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="top" constant="12" id="W2o-1a-DHQ"/>
+                    <constraint firstItem="5eI-x8-VQH" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="top" constant="12" id="XyY-WJ-Sov"/>
+                    <constraint firstItem="ifa-Ff-Eac" firstAttribute="trailing" secondItem="H2p-sc-9uM" secondAttribute="trailing" constant="-15" id="ZFF-EU-3yR"/>
+                    <constraint firstItem="5eI-x8-VQH" firstAttribute="leading" secondItem="H2p-sc-9uM" secondAttribute="leading" constant="12" id="fQR-A3-gQb"/>
+                    <constraint firstItem="vfg-pq-jRm" firstAttribute="leading" secondItem="H2p-sc-9uM" secondAttribute="leading" constant="45" id="hXM-ju-rXF"/>
+                    <constraint firstItem="vfg-pq-jRm" firstAttribute="bottom" secondItem="H2p-sc-9uM" secondAttribute="bottom" constant="-4" id="pqA-H1-JwO"/>
+                    <constraint firstItem="ifa-Ff-Eac" firstAttribute="leading" secondItem="vfg-pq-jRm" secondAttribute="trailing" constant="10" id="sVo-dR-gTF"/>
+                    <constraint firstItem="vfg-pq-jRm" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="top" constant="48" id="xJ5-xb-XdK"/>
+                </constraints>
+            </tableViewCellContentView>
+            <viewLayoutGuide key="safeArea" id="Hom-0H-2YQ"/>
+            <connections>
+                <outlet property="contactImage" destination="5eI-x8-VQH" id="9Dl-Ae-9dw"/>
+                <outlet property="labelTitle" destination="nOA-bG-zXZ" id="0dX-jq-5N3"/>
+                <outlet property="userStatusImageView" destination="vfg-pq-jRm" id="mS5-WO-e7h"/>
+                <outlet property="userStatusMessageLabel" destination="27H-pY-tFT" id="kAG-y7-hW7"/>
+            </connections>
+            <point key="canvasLocation" x="33.600000000000001" y="45.877061469265371"/>
+        </tableViewCell>
+    </objects>
+    <resources>
+        <systemColor name="secondaryLabelColor">
+            <color red="0.23529411764705882" green="0.23529411764705882" blue="0.2627450980392157" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
+        </systemColor>
+    </resources>
+</document>

+ 15 - 0
NextcloudTalk/ContextChatViewController.swift

@@ -0,0 +1,15 @@
+//
+// SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+
+import Foundation
+
+@objcMembers public class ContextChatViewController: BaseChatViewController {
+
+    public override func viewDidLoad() {
+        super.viewDidLoad()
+
+        self.titleView?.longPressGestureRecognizer.isEnabled = false
+    }
+}

+ 48 - 0
NextcloudTalk/CustomPresentable.swift

@@ -0,0 +1,48 @@
+// MIT License
+//
+// Copyright (c) 2021 Daniel Gauthier
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+// From https://github.com/danielmgauthier/ViewControllerTransitionExample
+// SPDX-License-Identifier: MIT
+
+import UIKit
+
+protocol CustomPresentable: UIViewController {
+    var transitionManager: UIViewControllerTransitioningDelegate? { get set }
+    var dismissalHandlingScrollView: UIScrollView? { get }
+    var dismissalGestureEnabled: Bool { get set }
+    func updatePresentationLayout(animated: Bool)
+}
+
+extension CustomPresentable {
+    var dismissalHandlingScrollView: UIScrollView? { nil }
+
+    func updatePresentationLayout(animated: Bool = false) {
+        presentationController?.containerView?.setNeedsLayout()
+        if animated {
+            UIView.animate(withDuration: 0.3, delay: 0.0, usingSpringWithDamping: 1.0, initialSpringVelocity: 0.0, options: .allowUserInteraction, animations: {
+                self.presentationController?.containerView?.layoutIfNeeded()
+            }, completion: nil)
+        } else {
+            presentationController?.containerView?.layoutIfNeeded()
+        }
+    }
+}

+ 9 - 0
NextcloudTalk/CustomPresentableNavigationController.swift

@@ -0,0 +1,9 @@
+//
+// SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+
+class CustomPresentableNavigationController: UINavigationController, CustomPresentable {
+    var dismissalGestureEnabled: Bool = true
+    var transitionManager: UIViewControllerTransitioningDelegate?
+}

+ 14 - 0
NextcloudTalk/DateHeaderView.h

@@ -0,0 +1,14 @@
+/**
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#import <UIKit/UIKit.h>
+
+static CGFloat kDateHeaderViewHeight = 34.0;
+
+@interface DateHeaderView : UIView
+
+@property (weak, nonatomic) IBOutlet UILabel *dateLabel;
+
+@end

+ 31 - 0
NextcloudTalk/DateHeaderView.m

@@ -0,0 +1,31 @@
+/**
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#import "DateHeaderView.h"
+
+@interface DateHeaderView ()
+
+@property (strong, nonatomic) IBOutlet UIView *contentView;
+
+@end
+
+@implementation DateHeaderView
+
+- (instancetype)init
+{
+    self = [super init];
+    
+    if (self) {
+        [[NSBundle mainBundle] loadNibNamed:@"DateHeaderView" owner:self options:nil];
+        
+        [self addSubview:self.contentView];
+        
+        self.contentView.frame = self.bounds;
+    }
+    
+    return self;
+}
+
+@end

+ 46 - 0
NextcloudTalk/DateHeaderView.xib

@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="17506" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
+    <device id="retina4_7" orientation="portrait" appearance="light"/>
+    <dependencies>
+        <deployment identifier="iOS"/>
+        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17505"/>
+        <capability name="Safe area layout guides" minToolsVersion="9.0"/>
+        <capability name="System colors in document resources" minToolsVersion="11.0"/>
+        <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
+    </dependencies>
+    <objects>
+        <placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="DateHeaderView">
+            <connections>
+                <outlet property="contentView" destination="iN0-l3-epB" id="eDv-e4-iV2"/>
+                <outlet property="dateLabel" destination="PTc-Tu-Ciu" id="jrR-rl-w4b"/>
+            </connections>
+        </placeholder>
+        <placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
+        <view contentMode="scaleToFill" id="iN0-l3-epB">
+            <rect key="frame" x="0.0" y="0.0" width="360" height="34"/>
+            <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+            <subviews>
+                <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Label" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="PTc-Tu-Ciu" customClass="DateLabelCustom" customModule="NextcloudTalk" customModuleProvider="target">
+                    <rect key="frame" x="135" y="5" width="90" height="24"/>
+                    <autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMaxY="YES"/>
+                    <color key="backgroundColor" systemColor="secondarySystemBackgroundColor"/>
+                    <gestureRecognizers/>
+                    <fontDescription key="fontDescription" type="system" pointSize="12"/>
+                    <color key="textColor" systemColor="secondaryLabelColor"/>
+                    <nil key="highlightedColor"/>
+                </label>
+            </subviews>
+            <viewLayoutGuide key="safeArea" id="vUN-kp-3ea"/>
+            <freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
+            <point key="canvasLocation" x="25.600000000000001" y="-268.06596701649175"/>
+        </view>
+    </objects>
+    <resources>
+        <systemColor name="secondaryLabelColor">
+            <color red="0.23529411764705882" green="0.23529411764705882" blue="0.2627450980392157" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
+        </systemColor>
+        <systemColor name="secondarySystemBackgroundColor">
+            <color red="0.94901960784313721" green="0.94901960784313721" blue="0.96862745098039216" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
+        </systemColor>
+    </resources>
+</document>

この差分においてかなりの量のファイルが変更されているため、一部のファイルを表示していません